/* +------------------------------------+ * | 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 accounts("account"); static dynamic_reference 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& recentlydropped; const unsigned int& maxregcount; TSStringExtItem& email; public: LocalIntExt regcount; CommandRegister(Module* Creator, const std::string& hashtype_ref, const std::set& 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 = " [email]"; } CmdResult Handle (const std::vector& 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("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("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& 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] "; } CmdResult Handle (const std::vector& 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("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("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 = " "; } CmdResult Handle (const std::vector& 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("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("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& recentlydropped; public: CommandDrop(Module* Creator, std::set& recentlydropped_ref) : Command(Creator,"DROP", 1, 2), recentlydropped(recentlydropped_ref) { syntax = "[account name] "; } CmdResult Handle (const std::vector& 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 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& recentlydropped; public: CommandFdrop(Module* Creator, std::set& recentlydropped_ref) : Command(Creator,"FDROP", 1, 1), recentlydropped(recentlydropped_ref) { flags_needed = 'o'; syntax = ""; } CmdResult Handle (const std::vector& 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 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 = " OFF|ON"; } CmdResult Handle (const std::vector& 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& recentlydropped; public: CommandRecentlydropped(Module* Creator, std::set& recentlydropped_ref) : Command(Creator,"RECENTLYDROPPED", 1, 1), recentlydropped(recentlydropped_ref) { flags_needed = FLAG_SERVERONLY; syntax = ""; } CmdResult Handle (const std::vector& parameters, User*) { recentlydropped.insert(parameters[0]); return CMD_SUCCESS; } }; class ModuleAccountRegister : public Module { time_t expiretime; std::string hashtype; std::set 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 = " "; cmd_setemail.min_params = 1; cmd_setemail.syntax = ""; } else { cmd_register.min_params = 1; cmd_register.syntax = " [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(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::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)