Add first draft of protocol extension for registration

Stub for registration command handling in server

First draft of handling registration requests

WIP (will be rebased)

clean up bad imports (rebase this later)

Finish checkUserIsBanned method

Add username validity check

Check servatrice registration settings

WIP

Finish(?) server side of registration

Needs testing

Fix switch case compile failure

I have no idea why I have to do this

WIP for registration testing python script

Stub register script initial attempt

Rearrange register script

First try at sending reg

register.py sends commands correctly now

Add more debug to register.py

Pack bytes the right way - servatrice can parse py script sends now

register.py should be working now

Parse xml hack correctly

Log registration enabled settings on server start

Insert gender correctly on register

Show tcpserver error message on failed gameserver listen

Fail startup if db configured and can't be opened.

TIL qt5 comes without mysql by default in homebrew...
This commit is contained in:
Gavin Bises 2015-02-21 21:29:59 -05:00 committed by Fabio Bas
parent d1b243481b
commit 735fcbf311
16 changed files with 394 additions and 48 deletions

1
servatrice/scripts/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
pypb/

10
servatrice/scripts/mk_pypb.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/bash
SRC_DIR=../../common/pb/
DST_DIR=./pypb
rm -rf "$DST_DIR"
mkdir -p "$DST_DIR"
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/*.proto
touch "$DST_DIR/__init__.py"

73
servatrice/scripts/register.py Executable file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env python
import socket, sys, struct, time
from pypb.server_message_pb2 import ServerMessage
from pypb.session_commands_pb2 import Command_Register as Reg
from pypb.commands_pb2 import CommandContainer as Cmd
from pypb.event_server_identification_pb2 import Event_ServerIdentification as ServerId
from pypb.response_pb2 import Response
HOST = "localhost"
PORT = 4747
CMD_ID = 1
def build_reg():
global CMD_ID
cmd = Cmd()
sc = cmd.session_command.add()
reg = sc.Extensions[Reg.ext]
reg.user_name = "testUser"
reg.email = "test@example.com"
reg.password = "password"
cmd.cmd_id = CMD_ID
CMD_ID += 1
return cmd
def send(msg):
packed = struct.pack('>I', len(msg))
sock.sendall(packed)
sock.sendall(msg)
def print_resp(resp):
print "<<<"
print repr(resp)
m = ServerMessage()
m.ParseFromString(bytes(resp))
print m
def recv(sock):
print "< header"
header = sock.recv(4)
msg_size = struct.unpack('>I', header)[0]
print "< ", msg_size
raw_msg = sock.recv(msg_size)
print_resp(raw_msg)
if __name__ == "__main__":
address = (HOST, PORT)
sock = socket.socket()
print "Connecting to server ", address
sock.connect(address)
# hack for old xml clients - server expects this and discards first message
print ">>> xml hack"
xmlClientHack = Cmd().SerializeToString()
send(xmlClientHack)
print sock.recv(60)
recv(sock)
print ">>> register"
r = build_reg()
print r
msg = r.SerializeToString()
send(msg)
recv(sock)
print "Done"

View file

@ -54,6 +54,13 @@ password=123456
; Accept only registered users? default is 0 (accept unregistered users)
regonly=0
[registration]
; Servatrice can process registration requests to add new users on the fly.
; Enable this feature? Default false.
;enabled=false
; Require users to provide an email address in order to register. Default true.
;requireemail=true
[database]

View file

@ -160,6 +160,13 @@ bool Servatrice::initServer()
authenticationMethod = AuthenticationNone;
}
registrationEnabled = settingsCache->value("registration/enabled", false).toBool();
requireEmailForRegistration = settingsCache->value("registration/requireemail", true).toBool();
qDebug() << "Registration enabled: " << registrationEnabled;
if (registrationEnabled)
qDebug() << "Require email address to register: " << requireEmailForRegistration;
QString dbTypeStr = settingsCache->value("database/type").toString();
if (dbTypeStr == "mysql")
databaseType = DatabaseMySql;
@ -172,12 +179,17 @@ bool Servatrice::initServer()
if (databaseType != DatabaseNone) {
settingsCache->beginGroup("database");
dbPrefix = settingsCache->value("prefix").toString();
servatriceDatabaseInterface->initDatabase("QMYSQL",
settingsCache->value("hostname").toString(),
settingsCache->value("database").toString(),
settingsCache->value("user").toString(),
settingsCache->value("password").toString());
bool dbOpened =
servatriceDatabaseInterface->initDatabase("QMYSQL",
settingsCache->value("hostname").toString(),
settingsCache->value("database").toString(),
settingsCache->value("user").toString(),
settingsCache->value("password").toString());
settingsCache->endGroup();
if (!dbOpened) {
qDebug() << "Failed to open database";
return false;
}
updateServerList();
@ -342,7 +354,7 @@ bool Servatrice::initServer()
if (gameServer->listen(QHostAddress::Any, gamePort))
qDebug() << "Server listening.";
else {
qDebug() << "gameServer->listen(): Error.";
qDebug() << "gameServer->listen(): Error:" << gameServer->errorString();
return false;
}
return true;

View file

@ -9,6 +9,7 @@
#include <QSqlError>
#include <QSqlQuery>
#include <QDateTime>
#include <QChar>
Servatrice_DatabaseInterface::Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server)
: instanceId(_instanceId),
@ -34,7 +35,9 @@ void Servatrice_DatabaseInterface::initDatabase(const QSqlDatabase &_sqlDatabase
}
}
void Servatrice_DatabaseInterface::initDatabase(const QString &type, const QString &hostName, const QString &databaseName, const QString &userName, const QString &password)
bool Servatrice_DatabaseInterface::initDatabase(const QString &type, const QString &hostName,
const QString &databaseName, const QString &userName,
const QString &password)
{
sqlDatabase = QSqlDatabase::addDatabase(type, "main");
sqlDatabase.setHostName(hostName);
@ -42,7 +45,7 @@ void Servatrice_DatabaseInterface::initDatabase(const QString &type, const QStri
sqlDatabase.setUserName(userName);
sqlDatabase.setPassword(password);
openDatabase();
return openDatabase();
}
bool Servatrice_DatabaseInterface::openDatabase()
@ -102,11 +105,52 @@ bool Servatrice_DatabaseInterface::usernameIsValid(const QString &user)
return re.exactMatch(user);
}
// TODO move this to Server
bool Servatrice_DatabaseInterface::getRequireRegistration()
{
return settingsCache->value("authentication/regonly", 0).toBool();
}
bool Servatrice_DatabaseInterface::registerUser(const QString &userName, const QString &realName, ServerInfo_User_Gender const &gender, const QString &passwordSha512, const QString &emailAddress, const QString &country, bool active)
{
if (!checkSql())
return false;
QSqlQuery *query = prepareQuery("insert into {prefix}_users "
"(name, realname, gender, password_sha512, email, country, registrationDate, active) "
"values "
"(:userName, :realName, :gender, :password_sha512, :email, :country, UTC_TIMESTAMP(), :active)");
query->bindValue(":userName", userName);
query->bindValue(":realName", realName);
query->bindValue(":gender", getGenderChar(gender));
query->bindValue(":password_sha512", passwordSha512);
query->bindValue(":email", emailAddress);
query->bindValue(":country", country);
query->bindValue(":active", active ? 1 : 0);
if (!execSqlQuery(query)) {
qDebug() << "Failed to insert user: " << query->lastError() << " sql: " << query->lastQuery();
// TODO handle duplicate insert error
return false;
}
return true;
}
QChar Servatrice_DatabaseInterface::getGenderChar(ServerInfo_User_Gender const &gender)
{
switch (gender) {
case ServerInfo_User_Gender_GenderUnknown:
return QChar('u');
case ServerInfo_User_Gender_Male:
return QChar('m');
case ServerInfo_User_Gender_Female:
return QChar('f');
default:
return QChar('u');
}
}
AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, QString &reasonStr, int &banSecondsLeft)
{
switch (server->getAuthenticationMethod()) {
@ -125,43 +169,8 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot
if (!usernameIsValid(user))
return UsernameInvalid;
QSqlQuery *ipBanQuery = prepareQuery("select time_to_sec(timediff(now(), date_add(b.time_from, interval b.minutes minute))), b.minutes <=> 0, b.visible_reason from {prefix}_bans b where b.time_from = (select max(c.time_from) from {prefix}_bans c where c.ip_address = :address) and b.ip_address = :address2");
ipBanQuery->bindValue(":address", static_cast<ServerSocketInterface *>(handler)->getPeerAddress().toString());
ipBanQuery->bindValue(":address2", static_cast<ServerSocketInterface *>(handler)->getPeerAddress().toString());
if (!execSqlQuery(ipBanQuery)) {
qDebug("Login denied: SQL error");
return NotLoggedIn;
}
if (ipBanQuery->next()) {
const int secondsLeft = -ipBanQuery->value(0).toInt();
const bool permanentBan = ipBanQuery->value(1).toInt();
if ((secondsLeft > 0) || permanentBan) {
reasonStr = ipBanQuery->value(2).toString();
banSecondsLeft = permanentBan ? 0 : secondsLeft;
qDebug("Login denied: banned by address");
return UserIsBanned;
}
}
QSqlQuery *nameBanQuery = prepareQuery("select time_to_sec(timediff(now(), date_add(b.time_from, interval b.minutes minute))), b.minutes <=> 0, b.visible_reason from {prefix}_bans b where b.time_from = (select max(c.time_from) from {prefix}_bans c where c.user_name = :name2) and b.user_name = :name1");
nameBanQuery->bindValue(":name1", user);
nameBanQuery->bindValue(":name2", user);
if (!execSqlQuery(nameBanQuery)) {
qDebug("Login denied: SQL error");
return NotLoggedIn;
}
if (nameBanQuery->next()) {
const int secondsLeft = -nameBanQuery->value(0).toInt();
const bool permanentBan = nameBanQuery->value(1).toInt();
if ((secondsLeft > 0) || permanentBan) {
reasonStr = nameBanQuery->value(2).toString();
banSecondsLeft = permanentBan ? 0 : secondsLeft;
qDebug("Login denied: banned by name");
return UserIsBanned;
}
}
if (checkUserIsBanned(handler->getAddress(), user, reasonStr, banSecondsLeft))
return UserIsBanned;
QSqlQuery *passwordQuery = prepareQuery("select password_sha512 from {prefix}_users where name = :name and active = 1");
passwordQuery->bindValue(":name", user);
@ -188,6 +197,79 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot
return UnknownUser;
}
bool Servatrice_DatabaseInterface::checkUserIsBanned(const QString &ipAddress, const QString &userName, QString &banReason, int &banSecondsRemaining)
{
if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql)
return false;
if (!checkSql()) {
qDebug("Failed to check if user is banned. Database invalid.");
return false;
}
return
checkUserIsIpBanned(ipAddress, banReason, banSecondsRemaining)
|| checkUserIsNameBanned(userName, banReason, banSecondsRemaining);
}
bool Servatrice_DatabaseInterface::checkUserIsNameBanned(const QString &userName, QString &banReason, int &banSecondsRemaining)
{
QSqlQuery *nameBanQuery = prepareQuery("select time_to_sec(timediff(now(), date_add(b.time_from, interval b.minutes minute))), b.minutes <=> 0, b.visible_reason from {prefix}_bans b where b.time_from = (select max(c.time_from) from {prefix}_bans c where c.user_name = :name2) and b.user_name = :name1");
nameBanQuery->bindValue(":name1", userName);
nameBanQuery->bindValue(":name2", userName);
if (!execSqlQuery(nameBanQuery)) {
qDebug() << "Name ban check failed: SQL error" << nameBanQuery->lastError();
return false;
}
if (nameBanQuery->next()) {
const int secondsLeft = -nameBanQuery->value(0).toInt();
const bool permanentBan = nameBanQuery->value(1).toInt();
if ((secondsLeft > 0) || permanentBan) {
banReason = nameBanQuery->value(2).toString();
banSecondsRemaining = permanentBan ? 0 : secondsLeft;
qDebug() << "Username" << userName << "is banned by name";
return true;
}
}
return false;
}
bool Servatrice_DatabaseInterface::checkUserIsIpBanned(const QString &ipAddress, QString &banReason, int &banSecondsRemaining)
{
QSqlQuery *ipBanQuery = prepareQuery(
"select"
" time_to_sec(timediff(now(), date_add(b.time_from, interval b.minutes minute))),"
" b.minutes <=> 0,"
" b.visible_reason"
" from {prefix}_bans b"
" where"
" b.time_from = (select max(c.time_from)"
" from {prefix}_bans c"
" where c.ip_address = :address)"
" and b.ip_address = :address2");
ipBanQuery->bindValue(":address", ipAddress);
ipBanQuery->bindValue(":address2", ipAddress);
if (!execSqlQuery(ipBanQuery)) {
qDebug() << "IP ban check failed: SQL error." << ipBanQuery->lastError();
return false;
}
if (ipBanQuery->next()) {
const int secondsLeft = -ipBanQuery->value(0).toInt();
const bool permanentBan = ipBanQuery->value(1).toInt();
if ((secondsLeft > 0) || permanentBan) {
banReason = ipBanQuery->value(2).toString();
banSecondsRemaining = permanentBan ? 0 : secondsLeft;
qDebug() << "User is banned by address" << ipAddress;
return true;
}
}
return false;
}
bool Servatrice_DatabaseInterface::userExists(const QString &user)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
@ -579,4 +661,4 @@ void Servatrice_DatabaseInterface::logMessage(const int senderId, const QString
query->bindValue(":target_id", (targetType == MessageTargetChat && targetId < 1) ? QVariant() : targetId);
query->bindValue(":target_name", targetName);
execSqlQuery(query);
}
}

View file

@ -4,6 +4,7 @@
#include <QObject>
#include <QSqlDatabase>
#include <QHash>
#include <qchar.h>
#include "server.h"
#include "server_database_interface.h"
@ -18,15 +19,22 @@ private:
QHash<QString, QSqlQuery *> preparedStatements;
Servatrice *server;
ServerInfo_User evalUserQueryResult(const QSqlQuery *query, bool complete, bool withId = false);
bool usernameIsValid(const QString &user);
/** Must be called after checkSql and server is known to be in auth mode. */
bool checkUserIsIpBanned(const QString &ipAddress, QString &banReason, int &banSecondsRemaining);
/** Must be called after checkSql and server is known to be in auth mode. */
bool checkUserIsNameBanned(QString const &userName, QString &banReason, int &banSecondsRemaining);
QChar getGenderChar(ServerInfo_User_Gender const &gender);
protected:
bool usernameIsValid(const QString &user);
AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, QString &reasonStr, int &secondsLeft);
bool checkUserIsBanned(const QString &ipAddress, const QString &userName, QString &banReason, int &banSecondsRemaining);
public slots:
void initDatabase(const QSqlDatabase &_sqlDatabase);
public:
Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server);
~Servatrice_DatabaseInterface();
void initDatabase(const QString &type, const QString &hostName, const QString &databaseName, const QString &userName, const QString &password);
bool initDatabase(const QString &type, const QString &hostName, const QString &databaseName,
const QString &userName, const QString &password);
bool openDatabase();
bool checkSql();
QSqlQuery * prepareQuery(const QString &queryText);
@ -55,6 +63,7 @@ public:
bool userSessionExists(const QString &userName);
bool getRequireRegistration();
bool registerUser(const QString &userName, const QString &realName, ServerInfo_User_Gender const &gender, const QString &passwordSha512, const QString &emailAddress, const QString &country, bool active = false);
void logMessage(const int senderId, const QString &senderName, const QString &senderIp, const QString &logMessage, LogMessage_TargetType targetType, const int targetId, const QString &targetName);
};