#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, int timeout) : ScgiServer(port) { // setup default cypher cypher = new Snow2; // client timeout defaults to 300 seconds (5 minutes) clientTimeout = timeout; } // 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; clientLinks.Remove(iClient); 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) { int iClient = clientLinks.Find(clientID); if(iClient >= 0) { clientLinks[iClient].time = GetSysTime() + clientTimeout; return true; } return false; } // connects a client -- returns a new clientID // doesn't check anything, that one must be done elsewhere dword ProtectServer::ConnectClient(String const &eMail) { // generates an unique random clientID dword id; do { id = Random(); } while(id == 0 || id == -1 || clientLinks.Find(id) >= 0); // adds to clientLinks and to registered emails count clientLinks.Add(id, TimeMail(GetSysTime() + clientTimeout, eMail)); int i = registeredConnections.Find(eMail); if(i >= 0) registeredConnections[i]++; else registeredConnections.Add(eMail, 1); return id; } // disconnects a client or refresh its expiration time void ProtectServer::DisconnectClient(dword clientID) { int i = clientLinks.Find(clientID); if(i >= 0) { String eMail = clientLinks[i].eMail; clientLinks.Remove(i); i = registeredConnections.Find(eMail); if(i >= 0) { registeredConnections[i]--; if(registeredConnections[i] <= 0) registeredConnections.Remove(i); } } } void ProtectServer::OnAccepted() { Cout() << "Accepted connection from client " << FormatIP(clientIP) << "\n"; } void ProtectServer::OnRequest() { int err = 0; VectorMap data; VectorMap results; String IV; // POST rwquest ? if(HasPostData()) { // all requests besides PROTECT_ACTIVATE go through post if(post.Find("IV") < 0) err = PROTECT_MISSING_IV; else { IV = ScanHexString(post.GetString("IV")); if(post.Find("DATA") < 0) err = PROTECT_MISSING_DATA; else { cypher->SetKey(key, IV); String decoded = (*cypher)(ScanHexString(post.GetString("DATA"))); LoadFromXML(data, decoded); } } } // GET rwquest ? else if(query.GetCount()) { // here should come just PROTECT_ACTIVATE requests if(query.Find("IV") < 0) err = PROTECT_MISSING_IV; else { IV = ScanHexString(query.GetString("IV")); if(query.Find("DATA") < 0) err = PROTECT_MISSING_DATA; else { cypher->SetKey(key, IV); String decoded = (*cypher)(ScanHexString(query.GetString("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 // GETLICENSEINFO gets info about license (name, expiration date, app version....) if(!err && data.Find("REASON") < 0) err = PROTECT_MISSING_REASON; 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"))); // if not ACTIVATION REQUEST encodes results and send back to client if(data.Get("REASON") != PROTECT_ACTIVATE) { clientSock.Write("Content-Type: text/plain\r\n\r\n"); cypher->SetKey(key); clientSock.Write("IV=" + HexString(cypher->GetNonce()) + "\r\n"); String encoded = HexString((*cypher)(StoreAsXML(results, "ProtectServer"))); clientSock.Write("DATA=" + encoded + "\r\n"); } // otherwise send a fancy welcome html -- replacing %NAME% with user name else { String w; clientSock.Write("Content-Type: text/html\r\n\r\n"); if(results.Find("ERROR") >= 0) w = welcome; else w = activationFailed; data = db.Get(data.Get("EMAIL")); w.Replace("%USER%", data.Get("NAME")); clientSock.Write(w); } } void ProtectServer::OnClosed() { Cout() << "Connection with " << FormatIP(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; 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; } // 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("ACTIVATIONSENT"); vs.RemoveKey("ACTIVATED"); // try to get user record from database, if any userRec = db.Get(eMail); // if not found, new registration if(!userRec.GetCount()) { // new registration db.Set(vs); userRec = db.Get(eMail); if(SendActivationMail(userRec)) res.Add("STATUS", "ACTIVATION RESENT"); else res.Add("ERROR", PROTECT_MAIL_SEND_ERROR); } // otherwise, we shall check if activated else if(!userRec.Get("ACTIVATED")) { // already registered but still not activated // resend activation mail if(SendActivationMail(userRec)) res.Add("STATUS", "ACTIVATION RESENT"); else res.Add("ERROR", PROTECT_MAIL_SEND_ERROR); } // otherwise we shall check if license expired else if(userRec.Get("EXPIRATION") <= GetSysTime()) { // signals expiration and leave res.Add("ERROR", PROTECT_MAIL_ALREADY_USED); } // otherwise, just tell user that's already registered else { res.Add("STATUS", "ALREADY REGISTERED"); } } break; case PROTECT_ACTIVATE: // we need email to activate if(v.Find("EMAIL") < 0) { res.Add("ERROR", PROTECT_MISSING_EMAIL); return res; } eMail = v.Get("EMAIL"); // we need ACTIVATIONKEY too if(v.Find("ACTIVATIONKEY") < 0) { res.Add("ERROR", PROTECT_MISSING_ACTIVATIONKEY); return res; } activationKey = v.Get("ACTIVATIONKEY"); // get user record by email userRec = db.Get(eMail); // if no mail data, product is unregistered if(!userRec.GetCount()) { res.Add("ERROR", PROTECT_UNREGISTERED); return res; } // sets registered flag and update record userRec.Get("ACTIVATED") = true; db.Set(userRec); res.Add("STATUS", "ACTIVATION SUCCESSFUL"); 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"); // 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 is registered, now we check if activation mail was sent if(!userRec.Get("ACTIVATED")) { res.Add("ERROR", PROTECT_LICENSE_NOT_ACTIVATED); SendActivationMail(userRec); return res; } // ok, product registered and activated... is license expired ? if(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); res.Add("CLIENTID", (int)clientID); break; case PROTECT_DISCONNECT : // we need the conneciton ID if(v.Find("CLIENTID") < 0) { res.Add("ERROR", PROTECT_MISSING_CLIENTID); return res; } clientID = (int)v.Get("CLIENTID"); DisconnectClient(clientID); break; case PROTECT_REFRESH : // 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)) { res.Add("ERROR", PROTECT_NOT_CONNECTED); return res; } break; case PROTECT_GETKEY: // 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)) { res.Add("ERROR", PROTECT_NOT_CONNECTED); return res; } // all ok, return the key res.Add("KEY", appKey); break; case PROTECT_GETLICENSEINFO : // 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)) { res.Add("ERROR", PROTECT_NOT_CONNECTED); return res; } // get user data from database res = db.Get(clientLinks.Get(clientID).eMail); // remove unnecessary data res.RemoveKey("ACTIVATIONKEY"); res.RemoveKey("ACTIVATIONSENT"); break; case PROTECT_UPDATEUSERDATA : { // 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)) { res.Add("ERROR", PROTECT_NOT_CONNECTED); return res; } // get user data from database userRec = db.Get(clientLinks.Get(clientID).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("ACTIVATIONSENT"); vs.RemoveKey("ACTIVATED"); 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) { // build request map VectorMapreq; req.Add("REASON", PROTECT_ACTIVATE); req.Add("EMAIL", userData.Get("EMAIL")); req.Add("ACTIVATIONKEY", userData.Get("ACTIVATIONKEY")); // builds activation link String link = "http://" + serverVars.Get("SERVER_NAME") + serverVars.Get("REQUEST_URI") + "?"; // encodes results and send back to client cypher->SetKey(key); link += "IV=" + HexString(cypher->GetNonce()) + "&"; String encoded = HexString((*cypher)(StoreAsXML(req, "ProtectServer"))); link += "DATA=" + encoded; smtp.To(userData.Get("EMAIL")); smtp.Subject("APPLICATION ACTIVATION"); smtp.From(serverVars.Get("SERVER_NAME")); smtp.Text(link); return smtp.Send(); } // 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 ScgiServer::Run(); } END_UPP_NAMESPACE