/* +------------------------------------+
* | Inspire Internet Relay Chat Daemon |
* +------------------------------------+
*
* InspIRCd: (C) 2002-2011 InspIRCd Development Team
* See: http://wiki.inspircd.org/Credits
*
* This program is free but copyrighted software; see
* the file COPYING for details.
*
* ---------------------------------------------------
*/
#include "inspircd.h"
#include "protocol.h"
#include "account.h"
#include "hash.h"
/* $ModDesc: Allow users to register their own accounts */
static dynamic_reference<AccountProvider> accounts("account");
static dynamic_reference<AccountDBProvider> db("accountdb");
static bool IsValidEmail(const std::string& emailaddr)
{
// We consider an email valid if it contains exactly one @ sign that isn't at the very beginning or end.
// We don't check for a dot because some email addresses don't have one, such as root@localhost
std::string::size_type pos = emailaddr.find_first_of('@');
return pos != std::string::npos && pos != 0 && pos != emailaddr.length() - 1 && pos == emailaddr.find_last_of('@');
}
/** Handle /REGISTER
*/
class CommandRegister : public Command
{
const std::string& hashtype;
const std::set<irc::string>& recentlydropped;
const unsigned int& maxregcount;
TSStringExtItem& email;
public:
LocalIntExt regcount;
CommandRegister(Module* Creator, const std::string& hashtype_ref, const std::set<irc::string>& recentlydropped_ref, TSStringExtItem& email_ref, const unsigned int& maxregcount_ref) :
Command(Creator,"REGISTER", 1, 2), hashtype(hashtype_ref), recentlydropped(recentlydropped_ref), maxregcount(maxregcount_ref), email(email_ref), regcount(EXTENSIBLE_USER, "regcount", Creator)
{
syntax = "<password> [email]";
}
CmdResult Handle (const std::vector<std::string>& parameters, User *user)
{
if (!ServerInstance->IsNick(user->nick.c_str(), ServerInstance->Config->Limits.NickMax))
{
user->WriteServ("NOTICE %s :You may not register your UID", user->nick.c_str());
return CMD_FAILURE;
}
if (accounts && accounts->IsRegistered(user))
{
user->WriteServ("NOTICE %s :You are already logged in to an account", user->nick.c_str());
return CMD_FAILURE;
}
if(recentlydropped.find(user->nick) != recentlydropped.end())
{
user->WriteServ("NOTICE %s :Account %s was dropped less than an hour ago and may not yet be re-registered", user->nick.c_str(), user->nick.c_str());
return CMD_FAILURE;
}
unsigned int user_regcount = regcount.get(user);
if(maxregcount && user_regcount >= maxregcount && !user->HasPrivPermission("accounts/no-registration-limit"))
{
user->WriteServ("NOTICE %s :You have already registered the maximum number of accounts for this session", user->nick.c_str());
return CMD_FAILURE;
}
if(parameters.size() == 2 && !IsValidEmail(parameters[1]))
{
user->WriteServ("NOTICE %s :The email address provided is invalid", user->nick.c_str());
return CMD_FAILURE;
}
// Don't send this now. Wait until we have the password set.
AccountDBEntry* entry = db->AddAccount(false, user->nick, ServerInstance->Time(), hashtype);
if(!entry)
{
user->WriteServ("NOTICE %s :Account %s already exists", user->nick.c_str(), user->nick.c_str());
return CMD_FAILURE;
}
entry->hash_password_ts = entry->ts;
// XXX: The code to generate a hash was copied from m_password_hash. We may want a better way to do this.
if (hashtype == "plaintext" || hashtype.empty())
entry->password = parameters[0];
else if (hashtype.substr(0,5) == "hmac-")
{
std::string type = hashtype.substr(5);
HashProvider* hp = ServerInstance->Modules->FindDataService<HashProvider>("hash/" + type);
if (!hp)
{
entry->hash = "plaintext";
entry->password = parameters[0];
ServerInstance->Logs->Log ("MODULE", DEFAULT, "unknown hash type in m_account_register, not using a hash");
}
else
{
std::string salt = ServerInstance->GenRandomStr(6, false);
entry->password = BinToBase64(salt) + "$" + BinToBase64(hp->hmac(salt, parameters[0]), NULL, 0);
}
}
else
{
HashProvider* hp = ServerInstance->Modules->FindDataService<HashProvider>("hash/" + hashtype);
if (hp)
{
entry->password = hp->hexsum(parameters[0]);
}
else
{
entry->hash = "plaintext";
entry->password = parameters[0];
ServerInstance->Logs->Log ("MODULE", DEFAULT, "unknown hash type in m_account_register, not using a hash");
}
}
if(parameters.size() == 2)
email.set(entry, parameters[1]);
db->SendAccount(entry);
regcount.set(user, user_regcount + 1);
if(parameters.size() == 2)
ServerInstance->SNO->WriteGlobalSno('u', "%s used REGISTER to register a new account with email %s", user->nick.c_str(), parameters[1].c_str());
else
ServerInstance->SNO->WriteGlobalSno('u', "%s used REGISTER to register a new account", user->nick.c_str());
if(accounts) accounts->DoLogin(user, entry->name, "");
return CMD_SUCCESS;
}
};
/** Handle /SETEMAIL
*/
class CommandSetemail : public Command
{
TSStringExtItem& email;
public:
CommandSetemail(Module* Creator, TSStringExtItem& email_ref) : Command(Creator,"SETEMAIL", 0, 1), email(email_ref)
{
syntax = "[new email]";
}
CmdResult Handle (const std::vector<std::string>& parameters, User *user)
{
AccountDBEntry* entry;
if(!accounts || !accounts->IsRegistered(user) || !(entry = db->GetAccount(accounts->GetAccountName(user), false)))
{
user->WriteServ("NOTICE %s :You are not logged in", user->nick.c_str());
return CMD_FAILURE;
}
if(parameters.empty() || parameters[0].empty())
{
if(min_params)
{
// We could get here if we got "SETEMAIL :"
user->WriteServ("NOTICE %s :An email address is required", user->nick.c_str());
return CMD_FAILURE;
}
email.set(entry, "");
db->SendUpdate(entry, "Email_address");
ServerInstance->SNO->WriteGlobalSno('u', "%s cleared the email address of account %s", user->nick.c_str(), entry->name.c_str());
user->WriteServ("NOTICE %s :Account %s email removed", user->nick.c_str(), entry->name.c_str());
return CMD_SUCCESS;
}
else if(!IsValidEmail(parameters[0]))
{
user->WriteServ("NOTICE %s :The email address provided is invalid", user->nick.c_str());
return CMD_FAILURE;
}
else
{
email.set(entry, parameters[0]);
db->SendUpdate(entry, "Email_address");
ServerInstance->SNO->WriteGlobalSno('u', "%s set the email address of account %s to %s", user->nick.c_str(), entry->name.c_str(), parameters[0].c_str());
user->WriteServ("NOTICE %s :Account %s email set to %s", user->nick.c_str(), entry->name.c_str(), parameters[0].c_str());
return CMD_SUCCESS;
}
}
};
/** Handle /SETPASS
*/
class CommandSetpass : public Command
{
const std::string& hashtype;
public:
CommandSetpass(Module* Creator, const std::string& hashtype_ref) : Command(Creator,"SETPASS", 2, 3), hashtype(hashtype_ref)
{
syntax = "[username] <old password> <new password>";
}
CmdResult Handle (const std::vector<std::string>& parameters, User *user)
{
irc::string username;
std::string oldpass, newpass;
if(parameters.size() == 2)
{
if(accounts && accounts->IsRegistered(user))
username = accounts->GetAccountName(user);
else
username = user->nick;
oldpass = parameters[0];
newpass = parameters[1];
}
else
{
username = parameters[0];
oldpass = parameters[1];
newpass = parameters[2];
}
if(newpass.empty())
{
user->WriteServ("NOTICE %s :You must specify a new password", user->nick.c_str());
return CMD_FAILURE;
}
AccountDBEntry* entry = db->GetAccount(username, false);
if(!entry || entry->password.empty() || ServerInstance->PassCompare(user, entry->password, oldpass, entry->hash))
{
user->WriteServ("NOTICE %s :Invalid username or password", user->nick.c_str());
return CMD_FAILURE;
}
entry->hash = hashtype;
// XXX: The code to generate a hash was copied from m_password_hash. We may want a better way to do this.
if (hashtype == "plaintext" || hashtype.empty())
entry->password = newpass;
else if (hashtype.substr(0,5) == "hmac-")
{
std::string type = hashtype.substr(5);
HashProvider* hp = ServerInstance->Modules->FindDataService<HashProvider>("hash/" + type);
if (!hp)
{
entry->hash = "plaintext";
entry->password = newpass;
ServerInstance->Logs->Log ("MODULE", DEFAULT, "unknown hash type in m_account_register, not using a hash");
}
else
{
std::string salt = ServerInstance->GenRandomStr(6, false);
entry->password = BinToBase64(salt) + "$" + BinToBase64(hp->hmac(salt, newpass), NULL, 0);
}
}
else
{
HashProvider* hp = ServerInstance->Modules->FindDataService<HashProvider>("hash/" + hashtype);
if (hp)
{
entry->password = hp->hexsum(newpass);
}
else
{
entry->hash = "plaintext";
entry->password = newpass;
ServerInstance->Logs->Log ("MODULE", DEFAULT, "unknown hash type in m_account_register, not using a hash");
}
}
entry->hash_password_ts = ServerInstance->Time();
db->SendUpdate(entry, "hash_password");
user->WriteServ("NOTICE %s :Account %s password changed successfully", user->nick.c_str(), entry->name.c_str());
return CMD_SUCCESS;
}
};
/** Handle /FSETPASS
*/
class CommandFsetpass : public Command
{
const std::string& hashtype;
public:
CommandFsetpass(Module* Creator, const std::string& hashtype_ref) : Command(Creator,"FSETPASS", 2, 2), hashtype(hashtype_ref)
{
flags_needed = 'o'; syntax = "<username> <new password>";
}
CmdResult Handle (const std::vector<std::string>& parameters, User *user)
{
AccountDBEntry* entry = db->GetAccount(parameters[0], false);
if(!entry)
{
user->WriteServ("NOTICE %s :No such account", user->nick.c_str());
return CMD_FAILURE;
}
if(parameters[1].empty())
{
user->WriteServ("NOTICE %s :You must specify a new password", user->nick.c_str());
return CMD_FAILURE;
}
entry->hash = hashtype;
// XXX: The code to generate a hash was copied from m_password_hash. We may want a better way to do this.
if (hashtype == "plaintext" || hashtype.empty())
entry->password = parameters[1];
else if (hashtype.substr(0,5) == "hmac-")
{
std::string type = hashtype.substr(5);
HashProvider* hp = ServerInstance->Modules->FindDataService<HashProvider>("hash/" + type);
if (!hp)
{
entry->hash = "plaintext";
entry->password = parameters[1];
ServerInstance->Logs->Log ("MODULE", DEFAULT, "unknown hash type in m_account_register, not using a hash");
}
else
{
std::string salt = ServerInstance->GenRandomStr(6, false);
entry->password = BinToBase64(salt) + "$" + BinToBase64(hp->hmac(salt, parameters[1]), NULL, 0);
}
}
else
{
HashProvider* hp = ServerInstance->Modules->FindDataService<HashProvider>("hash/" + hashtype);
if (hp)
{
entry->password = hp->hexsum(parameters[1]);
}
else
{
entry->hash = "plaintext";
entry->password = parameters[1];
ServerInstance->Logs->Log ("MODULE", DEFAULT, "unknown hash type in m_account_register, not using a hash");
}
}
entry->hash_password_ts = ServerInstance->Time();
db->SendUpdate(entry, "hash_password");
ServerInstance->SNO->WriteGlobalSno('a', "%s used FSETPASS to force password change of account '%s'", user->nick.c_str(), entry->name.c_str());
user->WriteServ("NOTICE %s :Account %s force-password changed successfully", user->nick.c_str(), entry->name.c_str());
return CMD_SUCCESS;
}
};
/** Handle /DROP
*/
class CommandDrop : public Command
{
std::set<irc::string>& recentlydropped;
public:
CommandDrop(Module* Creator, std::set<irc::string>& recentlydropped_ref) : Command(Creator,"DROP", 1, 2), recentlydropped(recentlydropped_ref)
{
syntax = "[account name] <password>";
}
CmdResult Handle (const std::vector<std::string>& parameters, User *user)
{
irc::string username;
std::string password;
if(parameters.size() == 1)
{
if(accounts && accounts->IsRegistered(user))
username = accounts->GetAccountName(user);
else
username = user->nick;
password = parameters[0];
}
else
{
username = parameters[0];
password = parameters[1];
}
AccountDBEntry* entry = db->GetAccount(username, false);
if(!entry || entry->password.empty() || ServerInstance->PassCompare(user, entry->password, password, entry->hash))
{
user->WriteServ("NOTICE %s :Invalid username or password", user->nick.c_str());
return CMD_FAILURE;
}
if(!accounts || username != accounts->GetAccountName(user))
user->WriteServ("NOTICE %s :Account %s has been dropped", user->nick.c_str(), username.c_str());
recentlydropped.insert(entry->name);
std::vector<std::string> params;
params.push_back("*");
params.push_back("RECENTLYDROPPED");
params.push_back(entry->name);
ServerInstance->PI->SendEncapsulatedData(params);
ServerInstance->SNO->WriteGlobalSno('u', "%s used DROP to drop account %s", user->nick.c_str(), entry->name.c_str());
db->RemoveAccount(true, entry);
return CMD_SUCCESS;
}
};
/** Handle /FDROP
*/
class CommandFdrop : public Command
{
std::set<irc::string>& recentlydropped;
public:
CommandFdrop(Module* Creator, std::set<irc::string>& recentlydropped_ref) : Command(Creator,"FDROP", 1, 1), recentlydropped(recentlydropped_ref)
{
flags_needed = 'o'; syntax = "<account name>";
}
CmdResult Handle (const std::vector<std::string>& parameters, User *user)
{
AccountDBEntry* entry = db->GetAccount(parameters[0], false);
if(!entry)
{
user->WriteServ("NOTICE %s :No such account", user->nick.c_str());
return CMD_FAILURE;
}
ServerInstance->SNO->WriteGlobalSno('a', "%s used FDROP to force drop of account '%s'", user->nick.c_str(), entry->name.c_str());
user->WriteServ("NOTICE %s :Account %s force-dropped successfully", user->nick.c_str(), entry->name.c_str());
recentlydropped.insert(entry->name);
std::vector<std::string> params;
params.push_back("*");
params.push_back("RECENTLYDROPPED");
params.push_back(entry->name);
ServerInstance->PI->SendEncapsulatedData(params);
db->RemoveAccount(true, entry);
return CMD_SUCCESS;
}
};
/** Handle /HOLD
*/
class CommandHold : public Command
{
public:
TSBoolExtItem held;
CommandHold(Module* Creator) : Command(Creator,"HOLD", 2, 2), held("Held", false, true, Creator)
{
flags_needed = 'o'; syntax = "<account name> OFF|ON";
}
CmdResult Handle (const std::vector<std::string>& parameters, User *user)
{
bool newsetting;
if(irc::string(parameters[1]) == "ON")
newsetting = true;
else if(irc::string(parameters[1]) == "OFF")
newsetting = false;
else
{
user->WriteServ("NOTICE %s :Unknown setting", user->nick.c_str());
return CMD_FAILURE;
}
AccountDBEntry* entry = db->GetAccount(parameters[0], false);
if(!entry)
{
user->WriteServ("NOTICE %s :No such account", user->nick.c_str());
return CMD_FAILURE;
}
held.set(entry, newsetting);
db->SendUpdate(entry, "Held");
ServerInstance->SNO->WriteGlobalSno('a', "%s used HOLD to %sable hold of account '%s'", user->nick.c_str(), newsetting ? "en" : "dis", entry->name.c_str());
user->WriteServ("NOTICE %s :Account %s %sheld successfully", user->nick.c_str(), entry->name.c_str(), newsetting ? "" : "un");
return CMD_SUCCESS;
}
};
/** Handle /RECENTLYDROPPED
*/
class CommandRecentlydropped : public Command
{
std::set<irc::string>& recentlydropped;
public:
CommandRecentlydropped(Module* Creator, std::set<irc::string>& recentlydropped_ref) : Command(Creator,"RECENTLYDROPPED", 1, 1), recentlydropped(recentlydropped_ref)
{
flags_needed = FLAG_SERVERONLY; syntax = "<account name>";
}
CmdResult Handle (const std::vector<std::string>& parameters, User*)
{
recentlydropped.insert(parameters[0]);
return CMD_SUCCESS;
}
};
class ModuleAccountRegister : public Module
{
time_t expiretime;
std::string hashtype;
std::set<irc::string> recentlydropped;
unsigned int maxregcount;
TSStringExtItem email;
CommandRegister cmd_register;
CommandSetemail cmd_setemail;
CommandSetpass cmd_setpass;
CommandFsetpass cmd_fsetpass;
CommandDrop cmd_drop;
CommandFdrop cmd_fdrop;
CommandHold cmd_hold;
CommandRecentlydropped cmd_recentlydropped;
TSExtItem last_used;
public:
ModuleAccountRegister() : email("Email_address", "", this), cmd_register(this, hashtype, recentlydropped, email, maxregcount),
cmd_setemail(this, email), cmd_setpass(this, hashtype), cmd_fsetpass(this, hashtype),
cmd_drop(this, recentlydropped), cmd_fdrop(this, recentlydropped), cmd_hold(this),
cmd_recentlydropped(this, recentlydropped), last_used("Last_used", this)
{
}
void init()
{
if(!db) throw ModuleException("m_account_register requires that m_account be loaded");
ServerInstance->Modules->AddService(email);
ServerInstance->Modules->AddService(cmd_register);
ServerInstance->Modules->AddService(cmd_register.regcount);
ServerInstance->Modules->AddService(cmd_setemail);
ServerInstance->Modules->AddService(cmd_setpass);
ServerInstance->Modules->AddService(cmd_fsetpass);
ServerInstance->Modules->AddService(cmd_drop);
ServerInstance->Modules->AddService(cmd_fdrop);
ServerInstance->Modules->AddService(cmd_hold);
ServerInstance->Modules->AddService(cmd_hold.held);
ServerInstance->Modules->AddService(cmd_recentlydropped);
ServerInstance->Modules->AddService(last_used);
Implementation eventlist[] = { I_OnEvent, I_OnSyncNetwork, I_OnGarbageCollect };
ServerInstance->Modules->Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation));
}
void ReadConfig(ConfigReadStatus&)
{
ConfigTag* conf = ServerInstance->Config->GetTag("acctregister");
hashtype = conf->getString("hashtype", "plaintext");
expiretime = ServerInstance->Duration (conf->getString ("expiretime", "21d"));
maxregcount = conf->getInt("maxregcount", 3);
if(expiretime && expiretime < 7200)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "account expiration times of under 2 hours are unsafe, setting to 2 hours");
expiretime = 7200;
}
if(conf->getBool("emailrequired"))
{
cmd_register.min_params = 2;
cmd_register.syntax = "<password> <email>";
cmd_setemail.min_params = 1;
cmd_setemail.syntax = "<new email>";
}
else
{
cmd_register.min_params = 1;
cmd_register.syntax = "<password> [email]";
cmd_setemail.min_params = 0;
cmd_setemail.syntax = "[new email]";
}
}
void OnEvent(Event& event)
{
if(event.id == "account_login"){
AccountEvent& acct_event = static_cast<AccountEvent&>(event);
if(!IS_LOCAL(acct_event.user))
return;
AccountDBEntry* entry = db->GetAccount(acct_event.account, false);
if(!entry)
return;
last_used.set(entry, ServerInstance->Time());
db->SendUpdate(entry, "Last_used");
}
}
virtual void OnSyncNetwork(SyncTarget* target)
{
for (std::set<irc::string>::const_iterator i = recentlydropped.begin(); i != recentlydropped.end(); ++i)
target->SendCommand("ENCAP * RECENTLYDROPPED " + i->value);
}
void OnGarbageCollect()
{
std::string account_name;
AccountDBEntry* entry;
time_t threshold = ServerInstance->Time() - expiretime;
time_t* last_used_time;
bool* held;
recentlydropped.clear();
for (user_hash::const_iterator i = ServerInstance->Users->clientlist->begin(); i != ServerInstance->Users->clientlist->end(); ++i)
{
if(!IS_LOCAL(i->second))
continue;
account_name = accounts->GetAccountName(i->second);
if(account_name.empty())
continue;
entry = db->GetAccount(account_name, false);
if(!entry)
continue;
last_used.set(entry, ServerInstance->Time()); // ServerInstance->Time() is inlined, so we would gain nothing by using a temporary variable
db->SendUpdate(entry, "Last_used");
}
if(!expiretime) return;
for (AccountDB::const_iterator i = db->GetDB().begin(); i != db->GetDB().end();)
{
entry = i++->second;
last_used_time = last_used.get(entry);
held = cmd_hold.held.get_value(entry);
if((!last_used_time || *last_used_time < threshold) && !(held && *held))
{
ServerInstance->SNO->WriteGlobalSno('u', "Account %s has expired", entry->name.c_str());
db->RemoveAccount(true, entry);
}
}
}
void Prioritize()
{
ServerInstance->Modules->SetPriority(this, I_ModuleInit, PRIORITY_AFTER, ServerInstance->Modules->Find("m_account.so"));
}
Version GetVersion()
{
return Version("Allow users to register their own accounts", VF_VENDOR);
}
};
MODULE_INIT(ModuleAccountRegister)