diff --git a/cockatrice/src/window_main.cpp b/cockatrice/src/window_main.cpp index 4b6cd9d4..703183d6 100644 --- a/cockatrice/src/window_main.cpp +++ b/cockatrice/src/window_main.cpp @@ -510,9 +510,11 @@ void MainWindow::registerError(Response::ResponseCode r, QString reasonStr, quin tr("It's mandatory to specify a valid email address when registering.")); break; case Response::RespEmailBlackListed: - QMessageBox::critical( - this, tr("Registration denied"), - tr("The email address provider used during registration has been blacklisted for use on this server.")); + if (reasonStr.isEmpty()) { + reasonStr = + "The email address provider used during registration has been blocked from use on this server."; + } + QMessageBox::critical(this, tr("Registration denied"), reasonStr); break; case Response::RespTooManyRequests: QMessageBox::critical( diff --git a/common/pb/response.proto b/common/pb/response.proto index e2b3a71a..a4de4cb9 100644 --- a/common/pb/response.proto +++ b/common/pb/response.proto @@ -37,11 +37,11 @@ message Response { RespActivationAccepted = 31; // Server accepted a reg user activation token RespActivationFailed = 32; // Server didn't accept a reg user activation token RespRegistrationAcceptedNeedsActivation = - 33; // Server accepted cient registration, but it will need token activation + 33; // Server accepted client registration, but it will need token activation RespClientIdRequired = 34; // Server requires client to generate and send its client id before allowing access RespClientUpdateRequired = 35; // Client is missing features that the server is requiring RespServerFull = 36; // Server user limit reached - RespEmailBlackListed = 37; // Server has blacklisted the email address provided for registration + RespEmailBlackListed = 37; // Server has blocked the email address provided for registration for some reason } enum ResponseType { JOIN_ROOM = 1000; diff --git a/servatrice/servatrice.ini.example b/servatrice/servatrice.ini.example index 2b89c473..6a728e26 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -164,6 +164,14 @@ minpasswordlength = 6 ; Example: "10minutemail.com,gmail.com" ;emailproviderblacklist="" +; You can require users to only use certain email domains for registration. This setting is a +; comma-separated list of email provider domains that you have explicitly audited and require +; the use of in order to create an account. Comparison's are explicit, so you must specify the +; domain in completion, such as gmail.com and hotmail.com. Email whitelist is checked before +; Email blacklist is checked, so an email cannot be in both setting configurations. +; Example: "gmail.com,hotmail.com,icloud.com" +;emailproviderwhitelist="" + [forgotpassword] ; Servatrice can process reset password requests allowing users to reset their account diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index 02aaa2db..bfb8bc2f 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -267,10 +267,13 @@ bool Servatrice::initServer() if (getRegistrationEnabled()) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList emailBlackListFilters = getEmailBlackList().split(",", Qt::SkipEmptyParts); + QStringList emailWhiteListFilters = getEmailWhiteList().split(",", Qt::SkipEmptyParts); #else QStringList emailBlackListFilters = getEmailBlackList().split(",", QString::SkipEmptyParts); + QStringList emailWhiteListFilters = getEmailWhiteList().split(",", QString::SkipEmptyParts); #endif qDebug() << "Email blacklist: " << emailBlackListFilters; + qDebug() << "Email whitelist: " << emailWhiteListFilters; qDebug() << "Require email address to register: " << getRequireEmailForRegistrationEnabled(); qDebug() << "Require email activation via token: " << getRequireEmailActivationEnabled(); if (getMaxAccountsPerEmail()) { @@ -1074,6 +1077,11 @@ QString Servatrice::getEmailBlackList() const return settingsCache->value("registration/emailproviderblacklist").toString(); } +QString Servatrice::getEmailWhiteList() const +{ + return settingsCache->value("registration/emailproviderwhitelist").toString(); +} + bool Servatrice::getEnableAudit() const { return settingsCache->value("audit/enable_audit", true).toBool(); diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index 67053590..8456bf7d 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -231,6 +231,7 @@ public: return dbPrefix; } QString getEmailBlackList() const; + QString getEmailWhiteList() const; AuthenticationMethod getAuthenticationMethod() const { return authenticationMethod; diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index a861bb9c..05808eb6 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -67,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -919,14 +920,15 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com } if (userName.isEmpty() && address.isEmpty() && (!clientID.isEmpty())) { - QSqlQuery *query = sqlInterface->prepareQuery("select name from {prefix}_users where clientid = :client_id"); - query->bindValue(":client_id", QString::fromStdString(cmd.clientid())); - sqlInterface->execSqlQuery(query); - if (!sqlInterface->execSqlQuery(query)) { + QSqlQuery *clientIdQuery = + sqlInterface->prepareQuery("select name from {prefix}_users where clientid = :client_id"); + clientIdQuery->bindValue(":client_id", QString::fromStdString(cmd.clientid())); + sqlInterface->execSqlQuery(clientIdQuery); + if (!sqlInterface->execSqlQuery(clientIdQuery)) { qDebug("ClientID username ban lookup failed: SQL Error"); } else { - while (query->next()) { - userName = query->value(0).toString(); + while (clientIdQuery->next()) { + userName = clientIdQuery->value(0).toString(); AbstractServerSocketInterface *user = static_cast(server->getUsers().value(userName)); if (user && !userList.contains(user)) @@ -971,6 +973,43 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com return Response::RespOk; } +QString AbstractServerSocketInterface::parseEmailAddress(const std::string &stdEmailAddress) +{ + QString emailAddress = QString::fromStdString(stdEmailAddress); + + // https://www.regular-expressions.info/email.html + static const QRegularExpression emailRegex(R"(^([A-Z0-9._%+-]+)@([A-Z0-9.-]+\.[A-Z]{2,})$)", + QRegularExpression::CaseInsensitiveOption); + const auto match = emailRegex.match(emailAddress); + + if (emailAddress.isEmpty() || !match.hasMatch()) { + return QString(); + } + + const QString capturedEmailAddressDomain = match.captured(2); + + // Trim out dots and pluses from Google/Gmail domains + if (capturedEmailAddressDomain.toLower() == "gmail.com" || + capturedEmailAddressDomain.toLower() == "googlemail.com") { + QString capturedEmailUser = match.captured(1); + + // Remove all content after first plus sign (as unnecessary with gmail) + // https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html + const int firstPlusSign = capturedEmailUser.indexOf("+"); + if (firstPlusSign != -1) { + capturedEmailUser = capturedEmailUser.left(firstPlusSign); + } + + // Remove all periods (as unnecessary with gmail) + // https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html + capturedEmailUser.replace(".", ""); + + emailAddress = capturedEmailUser + "@" + capturedEmailAddressDomain; + } + + return emailAddress; +} + Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc) { @@ -988,34 +1027,46 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C return Response::RespRegistrationDisabled; } - QString emailBlackList = servatrice->getEmailBlackList(); - QString emailAddress = QString::fromStdString(cmd.email()); + const QString emailBlackList = servatrice->getEmailBlackList(); + const QString emailWhiteList = servatrice->getEmailWhiteList(); + const QString emailAddress = parseEmailAddress(cmd.email()); #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - QStringList emailBlackListFilters = emailBlackList.split(",", Qt::SkipEmptyParts); + const QStringList emailBlackListFilters = emailBlackList.split(",", Qt::SkipEmptyParts); + const QStringList emailWhiteListFilters = emailWhiteList.split(",", Qt::SkipEmptyParts); #else - QStringList emailBlackListFilters = emailBlackList.split(",", QString::SkipEmptyParts); + const QStringList emailBlackListFilters = emailBlackList.split(",", QString::SkipEmptyParts); + const QStringList emailWhiteListFilters = emailWhiteList.split(",", QString::SkipEmptyParts); #endif - // verify that users email/provider is not blacklisted - if (!emailBlackList.trimmed().isEmpty()) { - foreach (QString blackListEmailAddress, emailBlackListFilters) { - if (emailAddress.contains(blackListEmailAddress, Qt::CaseInsensitive)) { - if (servatrice->getEnableRegistrationAudit()) - sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), - this->getAddress(), - QString::fromStdString(cmd.clientid()).simplified(), - "REGISTER_ACCOUNT", "Email used is blacklisted", false); - - return Response::RespEmailBlackListed; - } - } + bool requireEmailForRegistration = settingsCache->value("registration/requireemail", true).toBool(); + if (requireEmailForRegistration && emailAddress.isEmpty()) { + return Response::RespEmailRequiredToRegister; } - bool requireEmailForRegistration = settingsCache->value("registration/requireemail", true).toBool(); - if (requireEmailForRegistration) { - QRegExp rx("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}\\b"); - if (emailAddress.isEmpty() || !rx.exactMatch(emailAddress)) - return Response::RespEmailRequiredToRegister; + const auto emailAddressDomain = emailAddress.split("@").at(1); + + // If a whitelist exists, ensure the email address domain IS in the whitelist + if (!emailWhiteListFilters.isEmpty() && !emailWhiteListFilters.contains(emailAddressDomain, Qt::CaseInsensitive)) { + if (servatrice->getEnableRegistrationAudit()) { + sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(), + QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT", + "Email used is not whitelisted", false); + } + auto *re = new Response_Register; + re->set_denied_reason_str( + "The email address provider used during registration has not been approved for use on this server."); + rc.setResponseExtension(re); + return Response::RespEmailBlackListed; + } + + // If a blacklist exists, ensure the email address domain is NOT in the blacklist + if (!emailBlackListFilters.isEmpty() && emailBlackListFilters.contains(emailAddressDomain, Qt::CaseInsensitive)) { + if (servatrice->getEnableRegistrationAudit()) + sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(), + QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT", + "Email used is blacklisted", false); + + return Response::RespEmailBlackListed; } // TODO: Move this method outside of the db interface @@ -1128,7 +1179,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C return Response::RespRegistrationAccepted; } } else { - if (servatrice->getEnableRegistrationAudit()) sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(), QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT", diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 722e1e0b..8c4f57b4 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -125,6 +125,7 @@ private: bool removeAdminFlagFromUser(const QString &user, int flag); bool isPasswordLongEnough(const int passwordLength); + static QString parseEmailAddress(const std::string &stdEmailAddress); public: AbstractServerSocketInterface(Servatrice *_server,