#include "ProtectServer.h" NAMESPACE_UPP // email validation helper bool ValidateEMail(const char *address) { int count = 0; const char *c, *domain; static const char *rfc822_specials = "()<>@,;:\\\"[]"; // first we validate the name portion (name@domain) for (c = address; *c; c++) { if (*c == '\"' && (c == address || *(c - 1) == '.' || *(c - 1) == '\"')) { while (*++c) { if (*c == '\"') break; if (*c == '\\' && (*++c == ' ')) continue; if (*c < ' ' || *c >= 127) return false; } if (!*c++) return false; if (*c == '@') break; if (*c != '.') return false; continue; } if (*c == '@') break; if (*c <= ' ' || *c >= 127) return false; if (strchr(rfc822_specials, *c)) return false; } if (c == address || *(c - 1) == '.') return false; // next we validate the domain portion (name@domain) if (!*(domain = ++c)) return false; do { if (*c == '.') { if (c == domain || *(c - 1) == '.') return false; count++; } if (*c <= ' ' || *c >= 127) return false; if (strchr(rfc822_specials, *c)) return false; } while (*++c); return (count >= 1); } // constructor - defaults to port 8787 and connection timeout of 5 minutes (300 sec.) ProtectServer::ProtectServer(int port) : Scgi(port) { // setup default cypher cypher = new Snow2; // client timeout defaults to 300 seconds (5 minutes) idCleanupTime = 300; // database cleanup time -- default to 30 days (2592000 seconds) // used to clean up registrations with no activation on database dbCleanupTime = 2592000; // initializes last cleanups times to now() lastIDCleanupTime = GetSysTime(); lastDBCleanupTime = GetSysTime(); // no MAC handling by default recordMACs = false; checkExistingMAC = false; alertMACEMail = ""; // enforce MAC data from client by default enforceMAC = true; } // generates an unique activation key composed by 20 random uppercase chars String ProtectServer::GenerateActivationKey(void) { String key; for(int i = 0; i < 20; i++) key << (char)(Random('Z' - 'A') + 'A'); return key; } // executes database cleanup void ProtectServer::DBCleanup(void) { // @@@ TO DO !!! Cerr() << "DB Cleanup :: TO DO\n"; } // polls clients and remove those which connection has expired void ProtectServer::CheckExpiredClients(void) { Time curTime = GetSysTime(); for(int iClient = clientLinks.GetCount() - 1; iClient >= 0; iClient--) if(clientLinks[iClient].time < curTime) { String eMail = clientLinks[iClient].eMail; bool connected = clientLinks[iClient].connected; clientLinks.Remove(iClient); if(connected && registeredConnections.Find(eMail) >= 0) { registeredConnections.Get(eMail)--; if(registeredConnections.Get(eMail) <= 0) registeredConnections.RemoveKey(eMail); } } } // checks client list to see if a client is connected // and refresh its expire time on the way bool ProtectServer::IsClientConnected(dword clientID, String const &eMail) { int iClient = clientLinks.Find(clientID); if(iClient >= 0 && clientLinks[iClient].connected && clientLinks[iClient].eMail == eMail) { clientLinks[iClient].time = GetSysTime() + idCleanupTime; return true; } return false; } // connects a client -- returns previous clientID or a new one if expired // doesn't check anything, that one must be done elsewhere dword ProtectServer::ConnectClient(String const &eMail, dword id) { // try to fetch id from active pool int iClient = clientLinks.Find(id); // if not found in pool or ID allocated to another user, allocate a new one if(iClient < 0 || clientLinks[iClient].eMail != eMail) { // mark id as invalid iClient = -1; // generate a new id do { id = Random(); } while(id == 0 || id == (unsigned)-1 || clientLinks.Find(id) >= 0); } // adds to clientLinks and to pool if needed, otherwise update record if(iClient < 0) { // a new client ID was generated, add it to pool, by now as non-connected one clientLinks.Add(id, TimeMail(GetSysTime() + idCleanupTime, eMail, false)); iClient = clientLinks.GetCount() - 1; } else { // no new ID was generated, client probably exited without disconnecting // just refresh its time and mark it as connected clientLinks[iClient].time = GetSysTime() + idCleanupTime; } // if client marked as non connected, update connections count if(!clientLinks[iClient].connected) { clientLinks[iClient].connected = true; int i = registeredConnections.Find(eMail); if(i >= 0) registeredConnections[i]++; else registeredConnections.Add(eMail, 1); } return id; } // disconnects a client -- leaves id on pool up to expiration // updates count of clients connected by email void ProtectServer::DisconnectClient(dword clientID) { int i = clientLinks.Find(clientID); if(i >= 0) { String eMail = clientLinks[i].eMail; clientLinks[i].connected = false; i = registeredConnections.Find(eMail); if(i >= 0) { registeredConnections[i]--; if(registeredConnections[i] <= 0) registeredConnections.Remove(i); } } } // alert by email if user want to register for demo with an // already used MAC bool ProtectServer::SendMACAlert(String const &eMail, Vector const &mails) { smtp.New(); smtp.TimeSent(GetSysTime()); smtp.To(alertMACEMail); smtp.Subject(t_("PROTECT SERVER : ATTEMPT TO REGISTER WITH EXPIRED MAC")); // smtp.From(serverVars.Get("SERVER_NAME")); String body = "Attempt to register with e-mail : " + eMail + "\n"; body += "E-Mails connected to this one by MAC addresses :\n"; for(int i = 0; i < mails.GetCount(); i++) body += mails[i] + "\n"; smtp.Body(body, "text/plain; charset=utf-8"); return smtp.Send(); } // process MAC list in data field // if 'registering' is true, returns false if MAC already there bool ProtectServer::ProcessMACList(String const &mail, String const &MACList, bool registering) { // gathered emails from all MACs IndexmailIndex; // quick exit if we don't use MAC features... if(alertMACEMail.IsEmpty() && !checkExistingMAC && !recordMACs) return true; // get vector of client's MACs Vector macs = Split(MACList, ",", true); // gather emails for each MAC and combine them bool found = false; for(int iMac = 0; iMac < macs.GetCount(); iMac++) { VectormacEMails = db.GetMACEMails(macs[iMac]); if(macEMails.GetCount()) { // we found at least an entry found = true; // adds to customer registered mails list for(int i = 0; i < macEMails.GetCount(); i++) { if(mailIndex.Find(macEMails[i]) < 0) mailIndex.Add(macEMails[i]); } } } // add current email to MAC email list if needed if(mailIndex.Find(ToLower(mail)) < 0) mailIndex.Add(ToLower(mail)); // if we wanna record all customer's MACs, just do it // each MAC gets associated with full list of emails if(recordMACs) { for(int iMac = 0; iMac < macs.GetCount(); iMac++) db.UpdateMac(macs[iMac], mailIndex.GetKeys()); } // if we wanna be signaled by email, just do it if(!alertMACEMail.IsEmpty() && found && registering) SendMACAlert(mail, mailIndex.GetKeys()); // if we wanna drop all 'smart' people.... if(checkExistingMAC && found && registering) return false; return true; } void ProtectServer::OnAccepted() { Cout() << "Accepted connection from client " << ClientIP() << "\n"; } void ProtectServer::OnRequest() { int err = 0; VectorMap data; VectorMap results; String IV; // we handle just POST requests, GET not allowed if(Post().GetCount()) { // all requests besides PROTECT_ACTIVATE go through post if(Post().Find("IV") < 0) err = PROTECT_MISSING_IV; else { IV = ScanHexString(Post().Get("IV")); if(Post().Find("DATA") < 0) err = PROTECT_MISSING_DATA; else { cypher->SetKey(communicationKey, IV); String decoded = (*cypher)(ScanHexString(Post().Get("DATA"))); LoadFromXML(data, decoded); } } } else err = PROTECT_BAD_REQUEST; // Get request reason and process it // supports following reasons : // CONNECT establish connection to server // DISCONNECT frees server connection // REFRESH refreshes server connection (to restart timeout) // GETKEY gets application key // REGISTER registers app for timed demo OR re-request activation code if already registered // GETLICENSEINFO gets info about license (name, expiration date, app version....) if(!err && data.Find("REASON") < 0) err = PROTECT_MISSING_REASON; if(!err && enforceMAC && data.Find("MAC") < 0) err = PROTECT_MISSING_MAC; if(!err) results = ProcessRequest(data.Get("REASON"), data); // if previous error, sets it into result packet if(err) { results.Add("ERROR", err); results.Add("ERRORMSG", ProtectMessage(err)); } else if(results.Find("ERROR") >= 0) results.Add("ERRORMSG", ProtectMessage(results.Get("ERROR"))); // encodes results and send back to client ClientSock().Put("Content-Type: text/plain\r\n\r\n"); cypher->SetKey(communicationKey); ClientSock().Put("IV=" + HexString(cypher->GetNonce()) + "\r\n"); String encoded = HexString((*cypher)(StoreAsXML(results, "ProtectServer"))); ClientSock().Put("DATA=" + encoded + "\r\n"); } void ProtectServer::OnClosed() { // do client and db cleanup if related times are expired // NOTE : db cleanup is time consuming -- DON'T put a short time !! // it would be maybe better to put it inside a separate thread/process ?? Time now = GetSysTime(); if(now - lastIDCleanupTime > idCleanupTime) CheckExpiredClients(); if(now - lastDBCleanupTime > dbCleanupTime) DBCleanup(); Cout() << "Connection with " << ClientIP() << " closed\n"; } // process client request // takes a VectorMap on input from client // produces a response VectorMap to be returned VectorMap ProtectServer::ProcessRequest(int reason, VectorMap const &v) { VectorMap res; dword clientID; VectorMap userRec; String eMail; String activationKey; int numConnections; String locale; if(v.Find("LOCALE") >= 0) locale = v.Get("LOCALE"); else locale = "EN-US"; switch(reason) { case PROTECT_REGISTER: { // we need an eMail to register if(v.Find("EMAIL") < 0) { res.Add("ERROR", PROTECT_MISSING_EMAIL); return res; } eMail = v.Get("EMAIL"); if(!ValidateEMail(~eMail)) { res.Add("ERROR", PROTECT_INVALID_EMAIL); return res; } // MAC may or may not be present, depending on client // (old ones don't support it) // so, we can decide if accept MACless clients or not // this is already done on OnRequest handler, here we // just check MAC, *if* present bool denyRegistration = false; if(v.Find("MAC") >= 0 && !ProcessMACList(eMail, v.Get("MAC"), true)) denyRegistration = true; // avoid hacking of source packet stripping eventually // added licenseinfo, expiration, etc VectorMapvs(v, 1); vs.RemoveKey("LICENSES"); vs.RemoveKey("EXPIRATION"); vs.RemoveKey("ACTIVATIONKEY"); vs.RemoveKey("REGISTRATIONDATE"); vs.RemoveKey("ACTIVATED"); vs.RemoveKey("ACTIVATIONDATE"); vs.RemoveKey("LASTCONNECTIONDATE"); vs.RemoveKey("TOTALCONNECTIONS"); vs.RemoveKey("TOTALCONNECTIONTIME"); // try to get user record from database, if any userRec = db.Get(eMail); // if not found, new registration if(!userRec.GetCount()) { // drop 'smart' people.... if(denyRegistration) { // signals expiration and leave res.Add("ERROR", PROTECT_MAIL_ALREADY_USED); return res; } // generates activation key from email vs.Add("ACTIVATIONKEY", GenerateActivationKey()); vs.Add("REGISTRATIONDATE", GetSysTime()); vs.Add("ACTIVATED", false); vs.Add("ACTIVATIONDATE", GetSysTime()); // dummy, maybe not needed now vs.Add("LASTCONNECTIONDATE", GetSysTime()); // dummy, maybe not needed now vs.Add("TOTALCONNECTIONS", 0); vs.Add("TOTALCONNECTIONTIME", 0); // add to database db.Set(vs); // send the activation mail to user userRec = db.Get(eMail); if(SendActivationMail(userRec, locale)) res.Add("STATUS", "ACTIVATION RESENT"); else res.Add("ERROR", PROTECT_MAIL_SEND_ERROR); } // otherwise we shall check if license expired else if((Time)userRec.Get("EXPIRATION") <= GetSysTime()) { // signals expiration and leave res.Add("ERROR", PROTECT_MAIL_ALREADY_USED); } // otherwise, we shall send activation mail again // it may be same user on another machine else { // already registered but still not activated // resend activation mail if(SendActivationMail(userRec, locale)) res.Add("STATUS", "ACTIVATION RESENT"); else res.Add("ERROR", PROTECT_MAIL_SEND_ERROR); } } break; case PROTECT_CONNECT: { // we need email to connect if(v.Find("EMAIL") < 0) { res.Add("ERROR", PROTECT_MISSING_EMAIL); return res; } eMail = v.Get("EMAIL"); // we need previous clientID to connect, (or -1 if none) if(v.Find("CLIENTID") < 0) { res.Add("ERROR", PROTECT_MISSING_CLIENTID); return res; } clientID = (int)v.Get("CLIENTID"); // if already connected, just return ok if(IsClientConnected(clientID, eMail)) return res; // get the user data from database userRec = db.Get(eMail); // if no mail data, product is unregistered if(!userRec.GetCount()) { res.Add("ERROR", PROTECT_UNREGISTERED); return res; } // product registered, we check for activation key if(v.Find("ACTIVATIONKEY") < 0) { res.Add("ERROR", PROTECT_MISSING_ACTIVATIONKEY); SendActivationMail(userRec, locale); return res; } // check for activation key correctness if(v.Get("ACTIVATIONKEY") != userRec.Get("ACTIVATIONKEY")) { res.Add("ERROR", PROTECT_BAD_ACTIVATIONKEY); SendActivationMail(userRec, locale); return res; } // ok, product registered and activated... is license expired ? if((Time)userRec.Get("EXPIRATION") < GetSysTime()) { res.Add("ERROR", PROTECT_LICENSE_EXPIRED); return res; } // all ok, we shall finally check if number of licenses has overrun // first, we get number of current connections with same email numConnections = registeredConnections.Find(eMail); if(numConnections < 0) numConnections = 0; else numConnections = registeredConnections[numConnections]; if(numConnections >= (int)userRec.Get("LICENSES")) { res.Add("ERROR", PROTECT_LICENSES_NUMBER_EXCEEDED); return res; } // all OK, connect client and return connection ID clientID = ConnectClient(eMail, clientID); res.Add("CLIENTID", (int)clientID); // mark product as activated in database, if needed if(!userRec.Get("ACTIVATED")) { userRec.Get("ACTIVATED") = true; userRec.Get("ACTIVATIONDATE") = GetSysTime(); } // updates last connection date in database userRec.Get("LASTCONNECTIONDATE") = GetSysTime(); int connections = userRec.Get("TOTALCONNECTIONS"); userRec.Get("TOTALCONNECTIONS") = connections + 1; // updates database record db.Set(userRec); // process MAC addresses of incoming connection // in case we wanna record them if(v.Find("MAC") >= 0) ProcessMACList(eMail, v.Get("MAC")); break; } case PROTECT_DISCONNECT : { // we need email to disconnect if(v.Find("EMAIL") < 0) { res.Add("ERROR", PROTECT_MISSING_EMAIL); return res; } eMail = v.Get("EMAIL"); // we need the conneciton ID if(v.Find("CLIENTID") < 0) { res.Add("ERROR", PROTECT_MISSING_CLIENTID); return res; } clientID = (int)v.Get("CLIENTID"); // check whether client ID is in pool // otherwise it may be not connected or expired if(!IsClientConnected(clientID, eMail)) return res; // updates database connection time for user // warning --- there's a caveat if client side crashes without disconnecting // the total connection time won't be updated - it would make few sense anyways userRec = db.Get(eMail); int64 time = GetSysTime() - (Time)userRec.Get("LASTCONNECTIONDATE"); time += (int)userRec.Get("TOTALCONNECTIONTIME"); userRec.Get("TOTALCONNECTIONTIME") = time; db.Set(userRec); DisconnectClient(clientID); break; } case PROTECT_REFRESH : // we need email to refresh if(v.Find("EMAIL") < 0) { res.Add("ERROR", PROTECT_MISSING_EMAIL); return res; } eMail = v.Get("EMAIL"); // we need the conneciton ID if(v.Find("CLIENTID") < 0) { res.Add("ERROR", PROTECT_MISSING_CLIENTID); return res; } clientID = (int)v.Get("CLIENTID"); if(!IsClientConnected(clientID, eMail)) { res.Add("ERROR", PROTECT_NOT_CONNECTED); return res; } break; case PROTECT_GETLICENSEKEY: // we need email if(v.Find("EMAIL") < 0) { res.Add("ERROR", PROTECT_MISSING_EMAIL); return res; } eMail = v.Get("EMAIL"); // we need the conneciton ID if(v.Find("CLIENTID") < 0) { res.Add("ERROR", PROTECT_MISSING_CLIENTID); return res; } // we shall be connected clientID = (int)v.Get("CLIENTID"); if(!IsClientConnected(clientID, eMail)) { res.Add("ERROR", PROTECT_NOT_CONNECTED); return res; } // all ok, return the key res.Add("KEY", appKey); break; case PROTECT_GETLICENSEINFO : // we need email if(v.Find("EMAIL") < 0) { res.Add("ERROR", PROTECT_MISSING_EMAIL); return res; } eMail = v.Get("EMAIL"); // we need the conneciton ID if(v.Find("CLIENTID") < 0) { res.Add("ERROR", PROTECT_MISSING_CLIENTID); return res; } // we shall be connected clientID = (int)v.Get("CLIENTID"); if(!IsClientConnected(clientID, eMail)) { res.Add("ERROR", PROTECT_NOT_CONNECTED); return res; } // get user data from database res = db.Get(eMail); // remove unnecessary data res.RemoveKey("ACTIVATIONKEY"); break; case PROTECT_UPDATEUSERDATA : { // we need email if(v.Find("EMAIL") < 0) { res.Add("ERROR", PROTECT_MISSING_EMAIL); return res; } eMail = v.Get("EMAIL"); // we need the conneciton ID if(v.Find("CLIENTID") < 0) { res.Add("ERROR", PROTECT_MISSING_CLIENTID); return res; } // we shall be connected clientID = (int)v.Get("CLIENTID"); if(!IsClientConnected(clientID, eMail)) { res.Add("ERROR", PROTECT_NOT_CONNECTED); return res; } // get user data from database userRec = db.Get(eMail); // avoid hacking of source packet stripping // data non-updateable by user VectorMapvs(v, 1); vs.RemoveKey("LICENSES"); vs.RemoveKey("EXPIRATION"); vs.RemoveKey("ACTIVATIONKEY"); vs.RemoveKey("EMAIL"); // update user record for(int i = 0; i < vs.GetCount(); i++) { int j = userRec.Find(vs.GetKey(i)); if(j >= 0) userRec[j] = vs[i]; } db.Set(userRec); } break; default: // disconnect me anyways if(v.Find("CLIENTID") >= 0) { clientID = (int)v.Get("CLIENTID"); DisconnectClient(clientID); } res.Add("ERROR", PROTECT_UNKNOWN_REASON); break; } return res; } // sends activation mail to user bool ProtectServer::SendActivationMail(VectorMap const &userData, String const &locale) { int lang = LNGFromText(locale); String body = GetLngString(lang, welcomeBody); String subject = GetLngString(lang, welcomeSubject); String key = userData.Get("ACTIVATIONKEY"); // if no activation key field inside body, add it at end // trying to guess if it's HTML or simple text... int i; if( (i = body.Find("%ACTIVATIONKEY%")) < 0) { if(body.Find("HTML") >= 0) body << "&&&" << t_("ACTIVATION KEY : ") << key; else body << "\n\n\n" << t_("ACTIVATION KEY : ") << key; } else body.Replace(String("%ACTIVATIONKEY%"), key); String user = userData.Get("NAME"); if(user == "") user = userData.Get("EMAIL"); body.Replace(String("%USER%"), user); subject.Replace(String("%USER%"), user); String mime; if(body.Find("") >= 0 || body.Find("") >= 0) mime = "text/html; charset=utf-8"; else mime = "text/plain; charset=utf-8"; smtp.New(); smtp.TimeSent(GetSysTime()); smtp.To(userData.Get("EMAIL")); smtp.Subject(subject); // smtp.From(serverVars.Get("SERVER_NAME")); smtp.Body(body, mime); return smtp.Send(); } // sets client ids timeout value ProtectServer &ProtectServer::SetIDCleanupTime(int t) { idCleanupTime = t; lastIDCleanupTime = GetSysTime(); return *this; } // sets database cleanup time // used to clean up registrations with no activation on database ProtectServer &ProtectServer::SetDBCleanupTime(int t) { dbCleanupTime = t; lastDBCleanupTime = GetSysTime(); return *this; } // MAC handling ProtectServer &ProtectServer::SetRecordMACs(bool rec) { recordMACs = rec; return *this; } ProtectServer &ProtectServer::SetCheckExistingMAC(double check) { checkExistingMAC = check; return *this; } ProtectServer &ProtectServer::SetMACAlertEMail(String const &m) { alertMACEMail = m; return *this; } ProtectServer &ProtectServer::ClearMACAlertEMail(void) { alertMACEMail = ""; return *this; } ProtectServer &ProtectServer::SetEnforceMAC(bool e) { enforceMAC = e; return *this; } // runs the server void ProtectServer::Run(void) { // try to connect to db if(!db.Connect()) { Cout() << "Error connecting to database\n"; return; } // runs the SCGI server Scgi::Run(); } END_UPP_NAMESPACE