diff --git a/src/server/AuthorizationManager.cpp b/src/server/AuthorizationManager.cpp index d1abcf3..94d8a92 100644 --- a/src/server/AuthorizationManager.cpp +++ b/src/server/AuthorizationManager.cpp @@ -4,9 +4,6 @@ // Written by: Ken Moore July 2015 // ================================= #include "AuthorizationManager.h" -#include -#include -#include // Stuff for PAM to work #include @@ -19,15 +16,21 @@ #include //Internal defines +// -- token management #define TIMEOUTSECS 900 // (15 minutes) time before a token becomes invalid #define AUTHCHARS QString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") #define TOKENLENGTH 20 +// -- Connection failure limitations +#define AUTHFAILLIMIT 5 //number of sequential failures before IP is blocked for a time +#define FAILOVERMINS 10 //after this many minutes without a new login attempt the failure count will reset -AuthorizationManager::AuthorizationManager(){ +AuthorizationManager::AuthorizationManager() : QObject(){ HASH.clear(); + IPFAIL.clear(); //initialize the random number generator (need to generate auth tokens) qsrand(QDateTime::currentMSecsSinceEpoch()); } + AuthorizationManager::~AuthorizationManager(){ } @@ -71,8 +74,9 @@ int AuthorizationManager::checkAuthTimeoutSecs(QString token){ // == Token Generation functions -QString AuthorizationManager::LoginUP(bool localhost, QString user, QString pass){ +QString AuthorizationManager::LoginUP(QHostAddress host, QString user, QString pass){ //Login w/ username & password + bool localhost = ( (host== QHostAddress::LocalHost) || (host== QHostAddress::LocalHostIPv6) ); bool ok = false; //First check that the user is valid on the system and part of the operator group bool isOperator = false; @@ -92,19 +96,33 @@ QString AuthorizationManager::LoginUP(bool localhost, QString user, QString pass } qDebug() << "User Login Attempt:" << user << " Success:" << ok << " Local Login:" << localhost; - if(!ok){ return ""; } //invalid login - else{ return generateNewToken(isOperator); } //valid login - generate a new token for it + if(!ok){ + //invalid login + //Bump the fail count for this host + bool overlimit = BumpFailCount(host.toString()); + if(overlimit){ emit BlockHost(host); } + return (overlimit ? "REFUSED" : ""); + }else{ + //valid login - generate a new token for it + ClearHostFail(host.toString()); + return generateNewToken(isOperator); + } } -QString AuthorizationManager::LoginService(bool localhost, QString service){ +QString AuthorizationManager::LoginService(QHostAddress host, QString service){ + bool localhost = ( (host== QHostAddress::LocalHost) || (host== QHostAddress::LocalHostIPv6) ); //Login a particular automated service qDebug() << "Service Login Attempt:" << service << " Success:" << localhost; if(!localhost){ return ""; } //invalid - services must be local for access //Check that the service is valid on the system - // -- TO-DO - + bool isok = false; + if(service!="root" && service!="toor"){ + QStringList groups = getUserGroups(service); + isok = (groups.contains(service) && !groups.contains("wheel") && !groups.contains("operator")); + } //Now generate a new token and send it back - return generateNewToken(false); //services are never given operator privileges + if(!isok){ return ""; } + else{ return generateNewToken(false); }//services are never given operator privileges } // ========================= @@ -145,6 +163,28 @@ QStringList AuthorizationManager::getUserGroups(QString user){ return out; } +bool AuthorizationManager::BumpFailCount(QString host){ + //Returns: true if the failure count is over the limit + //key: "::::" + QStringList keys = QStringList(IPFAIL.keys()).filter(host+"::::"); + int fails = 0; + if(!keys.isEmpty()){ + //Take the existing key/value and put a new one in (this limits the filter to 1 value maximum) + QDateTime last = IPFAIL.take(keys[0]); + if(last.addSecs(FAILOVERMINS*60) > QDateTime::currentDateTime() ){ + fails = keys[0].section("::::",1,1).toInt(); + } + } + fails++; + IPFAIL.insert(host+"::::"+QString::number(fails), QDateTime::currentDateTime() ); + return (fails>=AUTHFAILLIMIT); +} + +void AuthorizationManager::ClearHostFail(QString host){ + QStringList keys = QStringList(IPFAIL.keys()).filter(host+"::::"); + for(int i=0; i -#include -#include -#include +#include "globals-qt.h" -class AuthorizationManager{ +class AuthorizationManager : public QObject{ + Q_OBJECT public: AuthorizationManager(); ~AuthorizationManager(); @@ -24,14 +22,20 @@ public: int checkAuthTimeoutSecs(QString token); //Return the number of seconds that a token is valid for // == Token Generation functions - QString LoginUP(bool localhost, QString user, QString pass); //Login w/ username & password - QString LoginService(bool localhost, QString service); //Login a particular automated service + QString LoginUP(QHostAddress host, QString user, QString pass); //Login w/ username & password + QString LoginService(QHostAddress host, QString service); //Login a particular automated service private: QHash HASH; + QHash IPFAIL; + QString generateNewToken(bool isOperator); QStringList getUserGroups(QString user); + //Failure count management + bool BumpFailCount(QString host); + void ClearHostFail(QString host); + //token->hashID filter simplification QString hashID(QString token){ QStringList tmp = QStringList(HASH.keys()).filter(token+"::::"); @@ -42,6 +46,10 @@ private: //PAM login/check files bool pam_checkPW(QString user, QString pass); void pam_logFailure(int ret); + +signals: + void BlockHost(QHostAddress); //block a host address temporarily + }; #endif diff --git a/src/server/WebServer.cpp b/src/server/WebServer.cpp index 7a09dcf..be67bc3 100644 --- a/src/server/WebServer.cpp +++ b/src/server/WebServer.cpp @@ -15,6 +15,7 @@ WebServer::WebServer(){ WSServer = 0; TCPServer = 0; AUTH = new AuthorizationManager(); + connect(AUTH, SIGNAL(BlockHost(QHostAddress)), this, SLOT(BlackListConnection(QHostAddress)) ); } WebServer::~WebServer(){ @@ -97,6 +98,24 @@ bool WebServer::setupTcp(quint16 port){ return TCPServer->listen(QHostAddress::Any, port); } +//Server Blacklist / DDOS mitigator +bool WebServer::allowConnection(QHostAddress addr){ + //Check if this addr is on the blacklist + QString key = "blacklist/"+addr.toString(); + if(!CONFIG->contains(key) ){ return true; } //not in the list + //Address on the list - see if the timeout has expired + QDateTime dt = CONFIG->value(key,QDateTime()).toDateTime(); + int minblock = CONFIG->value("blacklist/RefuseMinutes",60).toInt(); + if(dt.addSecs(minblock*60) < QDateTime::currentDateTime()){ + //This entry has timed out - go ahead and allow it + CONFIG->remove(key); //make the next connection check for this IP faster again + return true; + }else{ + return false; //blacklist block is still in effect + } + +} + QString WebServer::generateID(){ int id = 0; for(int i=0; ihasPendingConnections()){ sock = new WebSocket( WSServer->nextPendingConnection(), generateID(), AUTH); } + if(WSServer->hasPendingConnections()){ + QWebSocket *ws = WSServer->nextPendingConnection(); + if( !allowConnection(ws->peerAddress()) ){ ws->close(); } + else{ sock = new WebSocket( ws, generateID(), AUTH); } + } }else if(TCPServer!=0){ - if(TCPServer->hasPendingConnections()){ sock = new WebSocket( TCPServer->nextPendingConnection(), generateID(), AUTH); } + if(TCPServer->hasPendingConnections()){ + QSslSocket *ss = TCPServer->nextPendingConnection(); + if( !allowConnection(ss->peerAddress()) ){ ss->close(); } + else{ sock = new WebSocket( ss, generateID(), AUTH); } + } } if(sock==0){ return; } //no new connection qDebug() << "New Socket Connection"; connect(sock, SIGNAL(SocketClosed(QString)), this, SLOT(SocketClosed(QString)) ); connect(EVENTS, SIGNAL(NewEvent(EventWatcher::EVENT_TYPE, QJsonValue)), sock, SLOT(EventUpdate(EventWatcher::EVENT_TYPE, QJsonValue)) ); + connect(sock, SIGNAL(BlackListAddress(QHostAddress)), this, SLOT(BlackListConnection(QHostAddress)) ); OpenSockets << sock; } @@ -129,6 +157,15 @@ void WebServer::NewConnectError(QAbstractSocket::SocketError err){ QTimer::singleShot(0,this, SLOT(NewSocketConnection()) ); //check for a new connection } +//Socket Blacklist function +void WebServer::BlackListConnection(QHostAddress addr){ + //Make sure this is not the localhost (never block that) + if(addr!= QHostAddress(QHostAddress::LocalHost) && addr != QHostAddress(QHostAddress::LocalHostIPv6) ){ + //Block this remote host + CONFIG->setValue("blacklist/"+addr.toString(), QDateTime::currentDateTime()); + } +} + //WEBSOCKET SERVER SIGNALS // Overall Server signals void WebServer::ServerClosed(){ diff --git a/src/server/WebServer.h b/src/server/WebServer.h index 7f8748d..a3fa0c5 100644 --- a/src/server/WebServer.h +++ b/src/server/WebServer.h @@ -11,6 +11,7 @@ #include "WebSocket.h" #include "AuthorizationManager.h" #include "SslServer.h" + class WebServer : public QObject{ Q_OBJECT public: @@ -31,6 +32,9 @@ private: //Server Setup functions bool setupWebSocket(quint16 port); bool setupTcp(quint16 port); + + //Server Blacklist / DDOS mitigator + bool allowConnection(QHostAddress addr); //Generic functions for either type of server QString generateID(); //generate a new ID for a socket @@ -39,6 +43,8 @@ private slots: // Generic Server Slots void NewSocketConnection(); //newConnection() signal void NewConnectError(QAbstractSocket::SocketError); //acceptError() signal + //Socket Blacklist function + void BlackListConnection(QHostAddress addr); // (WebSocket-only) Server signals/slots void ServerClosed(); //closed() signal diff --git a/src/server/WebSocket.cpp b/src/server/WebSocket.cpp index 48f4b5c..8b4a227 100644 --- a/src/server/WebSocket.cpp +++ b/src/server/WebSocket.cpp @@ -123,6 +123,9 @@ void WebSocket::EvaluateREST(QString msg){ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){ RestOutputStruct out; out.in_struct = REQ; + QHostAddress host; + if(SOCKET!=0){ host = SOCKET->peerAddress(); } + else if(TSOCKET!=0){ host = TSOCKET->peerAddress(); } if(!REQ.VERB.isEmpty() && REQ.VERB != "GET" && REQ.VERB!="POST" && REQ.VERB!="PUT"){ //Non-supported request (at the moment) - return an error message out.CODE = RestOutputStruct::BADREQUEST; @@ -134,7 +137,7 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){ //First check for a REST authorization (not stand-alone request) if(!out.in_struct.auth.isEmpty()){ AUTHSYSTEM->clearAuth(SockAuthToken); //new auth requested - clear any old token - SockAuthToken = AUTHSYSTEM->LoginUP(false, out.in_struct.auth.section(":",0,0), out.in_struct.auth.section(":",1,1)); + SockAuthToken = AUTHSYSTEM->LoginUP(host, out.in_struct.auth.section(":",0,0), out.in_struct.auth.section(":",1,1)); } //Now check the body of the message and do what it needs @@ -144,16 +147,13 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){ //Note: This sets/changes the current SockAuthToken AUTHSYSTEM->clearAuth(SockAuthToken); //new auth requested - clear any old token if(DEBUG){ qDebug() << "Authenticate Peer:" << SOCKET->peerAddress().toString(); } - bool localhost = false; - if(SOCKET!=0){ localhost = (SOCKET->peerAddress() == QHostAddress::LocalHost) || (SOCKET->peerAddress() == QHostAddress::LocalHostIPv6); } - else if(TSOCKET!=0){ localhost = (TSOCKET->peerAddress() == QHostAddress::LocalHost) || (TSOCKET->peerAddress() == QHostAddress::LocalHostIPv6); } //Now do the auth if(out.in_struct.name=="auth" && out.in_struct.args.isObject() ){ //username/password authentication QString user, pass; if(out.in_struct.args.toObject().contains("username")){ user = JsonValueToString(out.in_struct.args.toObject().value("username")); } if(out.in_struct.args.toObject().contains("password")){ pass = JsonValueToString(out.in_struct.args.toObject().value("password")); } - SockAuthToken = AUTHSYSTEM->LoginUP(localhost, user, pass); + SockAuthToken = AUTHSYSTEM->LoginUP(host, user, pass); }else if(out.in_struct.name == "auth_token" && out.in_struct.args.isObject()){ SockAuthToken = JsonValueToString(out.in_struct.args.toObject().value("token")); }else if(out.in_struct.name == "auth_clear"){ @@ -169,9 +169,12 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){ out.out_args = array; out.CODE = RestOutputStruct::OK; }else{ + if(SockAuthToken=="REFUSED"){ + out.CODE = RestOutputStruct::FORBIDDEN; + } SockAuthToken.clear(); //invalid token //Bad Authentication - return error - out.CODE = RestOutputStruct::UNAUTHORIZED; + out.CODE = RestOutputStruct::UNAUTHORIZED; } }else if( AUTHSYSTEM->checkAuth(SockAuthToken) ){ //validate current Authentication token @@ -242,6 +245,9 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){ } //Return any information this->sendReply(out.assembleMessage()); + if(out.CODE == RestOutputStruct::FORBIDDEN && SOCKET!=0){ + SOCKET->close(QWebSocketProtocol::CloseCodeNormal, "Too Many Authorization Failures - Try again later"); + } } // === GENERAL PURPOSE UTILITY FUNCTIONS === diff --git a/src/server/WebSocket.h b/src/server/WebSocket.h index 7ab88e8..db1725a 100644 --- a/src/server/WebSocket.h +++ b/src/server/WebSocket.h @@ -74,7 +74,6 @@ public slots: signals: void SocketClosed(QString); //ID - }; #endif diff --git a/src/server/dispatcher-client.cpp b/src/server/dispatcher-client.cpp index bb358e2..c5e63d3 100644 --- a/src/server/dispatcher-client.cpp +++ b/src/server/dispatcher-client.cpp @@ -21,7 +21,7 @@ bool DispatcherClient::setupProcAuth(){ QString key = ReadKey(); if(!AUTH->checkAuth(key) ){ //Key now invalid - generate a new one (this ensures that the secure key rotates on a regular basis) - key = AUTH->LoginService(true, "dispatcher"); + key = AUTH->LoginService(QHostAddress::LocalHost, "dispatcher"); //Save the auth key to the file and lock it down if(!WriteKey(key)){ qWarning() << "Could not save dispatcher authorization key: **No dispatcher availability**. ";