Sfoglia il codice sorgente

Merge pull request #9999 from wangkx/h16678

HPCC-16678 Add session based authentication to ESP

Reviewed-By: Anthony Fishbeck <anthony.fishbeck@lexisnexis.com>
Reviewed-By: Jake Smith <jake.smith@lexisnexis.com>
Reviewed-By: Richard Chapman <rchapman@hpccsystems.com>
Richard Chapman 8 anni fa
parent
commit
50f69de818

+ 2 - 0
esp/bindings/SOAP/soaplib/CMakeLists.txt

@@ -59,6 +59,8 @@ include_directories(
     ./../../../../system/xmllib
     ./..
     ./../../../bindings/SOAP/xpp
+    ./../../../../system/mp 
+    ./../../../../dali/base
     )
 
 add_definitions(-DESPHTTP_EXPORTS)

+ 104 - 3
esp/bindings/http/platform/httpbinding.cpp

@@ -36,8 +36,6 @@
 
 #include "bindutil.hpp"
 
-#include "espcontext.hpp"
-
 #include "httpbinding.hpp"
 #include "htmlpage.hpp"
 #include  "seclib.hpp"
@@ -47,6 +45,7 @@
 #include "xsdparser.hpp"
 #include "espsecurecontext.hpp"
 #include "jsonhelpers.hpp"
+#include "dasds.hpp"
 
 #define FILE_UPLOAD     "FileUploadAccess"
 
@@ -271,6 +270,108 @@ EspHttpBinding::EspHttpBinding(IPropertyTree* tree, const char *bindname, const
             memCachedInitString.set("--SERVER=127.0.0.1");//using local memcached server
     }
 #endif
+    if (!m_secmgr.get())
+        return;
+
+    processName.set(procname);
+    const char* authDomain = bnd_cfg->queryProp("@authDomain");
+    if (!isEmptyString(authDomain))
+        domainName.set(authDomain);
+    else
+        domainName.set("default");
+
+    setSDSSession();
+    readAuthDomainCfg(proc_cfg);
+
+    checkSessionTimeoutSeconds = proc_cfg->getPropInt("@checkSessionTimeoutSeconds", ESP_CHECK_SESSION_TIMEOUT);
+}
+
+void EspHttpBinding::setSDSSession()
+{
+    espSessionSDSPath.setf("%s/%s[@name=\"%s\"]", PathSessionRoot, PathSessionProcess, processName.get());
+    Owned<IRemoteConnection> conn = querySDS().connect(espSessionSDSPath.str(), myProcessSession(), RTM_LOCK_WRITE, SESSION_SDS_LOCK_TIMEOUT);
+    if (!conn)
+        throw MakeStringException(-1, "Failed to connect SDS ESP Session.");
+
+    IPropertyTree* espSession = conn->queryRoot();
+    VStringBuffer appStr("%s[@port=\"%d\"]", PathSessionApplication, m_port);
+    IPropertyTree* appSessionTree = espSession->queryBranch(appStr.str());
+    if (!appSessionTree)
+    {
+        IPropertyTree* newAppSessionTree = espSession->addPropTree(PathSessionApplication);
+        newAppSessionTree->setPropInt("@port", m_port);
+    }
+    sessionSDSPath.setf("%s/%s/", espSessionSDSPath.str(), appStr.str());
+    sessionIDCookieName.setf("%s%d", SESSION_ID_COOKIE, m_port);
+}
+
+static int compareLength(char const * const *l, char const * const *r) { return strlen(*l) - strlen(*r); }
+
+void EspHttpBinding::readAuthDomainCfg(IPropertyTree* procCfg)
+{
+    VStringBuffer xpath("AuthDomains/AuthDomain[@domainName=\"%s\"]", domainName.get());
+    IPropertyTree* authDomainTree = procCfg->queryPropTree(xpath);
+    if (authDomainTree)
+    {
+        const char* authType = authDomainTree->queryProp("@authType");
+        if (isEmptyString(authType) || strieq(authType, "AuthTypeMixed"))
+            domainAuthType = AuthTypeMixed;
+        else if (strieq(authType, "AuthPerSessionOnly"))
+            domainAuthType = AuthPerSessionOnly;
+        else
+        {
+            domainAuthType = AuthPerRequestOnly;
+            return;
+        }
+
+        int sessionTimeoutMinutes = authDomainTree->getPropInt("@sessionTimeoutMinutes", 0);
+        if (sessionTimeoutMinutes == 0)
+            sessionTimeoutSeconds = ESP_SESSION_TIMEOUT;
+        else if (sessionTimeoutMinutes < 0)
+            sessionTimeoutSeconds = -1;
+        else
+            sessionTimeoutSeconds = sessionTimeoutMinutes * 60;
+
+        //The @unrestrictedResources contains URLs which may be used before a user is authenticated.
+        //For example, an icon file on the login page.
+        const char* unrestrictedResources = authDomainTree->queryProp("@unrestrictedResources");
+        if (!isEmptyString(unrestrictedResources))
+        {
+            StringArray urlArray;
+            urlArray.appendListUniq(unrestrictedResources, ",");
+            ForEachItemIn(i, urlArray)
+            {
+                const char* url = urlArray.item(i);
+                if (isEmptyString(url))
+                    continue;
+                if (isWildString(url))
+                    domainAuthResourcesWildMatch.append(url);
+                else
+                    domainAuthResources.setValue(url, true);
+            }
+        }
+
+        const char* _loginURL = authDomainTree->queryProp("@logonURL");
+        if (!isEmptyString(_loginURL))
+            loginURL.set(_loginURL);
+        else
+            loginURL.set(DEFAULT_LOGIN_URL);
+
+        const char* _logoutURL = authDomainTree->queryProp("@logoutURL");
+        if (!isEmptyString(_logoutURL))
+        {
+            logoutURL.set(_logoutURL);
+            domainAuthResources.setValue(logoutURL.get(), true);
+        }
+    }
+    else
+    {//old environment.xml
+        domainAuthType = AuthTypeMixed;
+        domainAuthResources.setValue(DEFAULT_UNRESTRICTED_RESOURCE1, true);
+        domainAuthResourcesWildMatch.append(DEFAULT_UNRESTRICTED_RESOURCE2);
+        loginURL.set(DEFAULT_LOGIN_URL);
+    }
+    domainAuthResourcesWildMatch.sortCompare(compareLength);
 }
 
 StringBuffer &EspHttpBinding::generateNamespace(IEspContext &context, CHttpRequest* request, const char *serv, const char *method, StringBuffer &ns)
@@ -1071,7 +1172,7 @@ int EspHttpBinding::onGetSoapBuilder(IEspContext &context, CHttpRequest* request
     bool inhouse = user && (user->getStatus()==SecUserStatus_Inhouse);
     xform->setParameter("inhouseUser", inhouse ? "true()" : "false()");
 
-    VStringBuffer url("%s?%s", methodQName.str(), params.str()); 
+    VStringBuffer url("%s?%s", methodQName.str(), params.str());
     xform->setStringParameter("destination", url.str());
         
     StringBuffer page;

+ 48 - 2
esp/bindings/http/platform/httpbinding.hpp

@@ -150,6 +150,20 @@ private:
     MapStringTo<bool> memCachedGlobalMap;
 #endif
 
+    StringAttr              processName;
+    StringAttr              domainName;
+    StringBuffer            sessionSDSPath;
+    StringBuffer            espSessionSDSPath;
+    StringBuffer            sessionIDCookieName;
+    AuthType                domainAuthType;
+
+    StringAttr              loginURL;
+    StringAttr              logoutURL;
+    int                     sessionTimeoutSeconds = ESP_SESSION_TIMEOUT; //-1: never
+    int                     checkSessionTimeoutSeconds = ESP_CHECK_SESSION_TIMEOUT;
+    BoolHash                domainAuthResources;
+    StringArray             domainAuthResourcesWildMatch;
+
     void getXMLMessageTag(IEspContext& ctx, bool isRequest, const char *method, StringBuffer& tag);
 #ifdef USE_LIBMEMCACHED
     void ensureMemCachedClient();
@@ -159,6 +173,7 @@ private:
     void addToMemCached(CHttpRequest* request, CHttpResponse* response, const char* memCachedID);
     bool sendFromMemCached(CHttpRequest* request, CHttpResponse* response, const char* memCachedID);
 #endif
+
 protected:
     MethodInfoArray m_methods;
     bool                    m_includeSoapTest;
@@ -320,7 +335,39 @@ public:
         }
         return false;
     }
-    ISecManager* querySecManager() {return m_secmgr.get(); }
+    ISecManager* querySecManager() const { return m_secmgr.get(); }
+    IAuthMap* queryAuthMAP() const { return m_authmap.get();}
+    const char* queryAuthMethod() const { return m_authmethod.str(); }
+    void setProcessName(const char* name) { processName.set(name); }
+    const char* queryProcessName() const { return processName.get(); }
+    void setDomainName(const char* name) { domainName.set(name ? name : "default"); }
+    const char* queryDomainName() const { return domainName.get(); }
+    void setSessionSDSPath(const char* path) { sessionSDSPath.set(path); }
+    const char* querySessionSDSPath() const { return sessionSDSPath.str(); }
+    void setESPSessionSDSPath(const char* path) { espSessionSDSPath.set(path); }
+    const char* queryESPSessionSDSPath() const { return espSessionSDSPath.str(); }
+    const char* querySessionIDCookieName() const { return sessionIDCookieName.str(); }
+    AuthType getDomainAuthType() const { return domainAuthType; }
+    const char* queryLoginURL() const { return loginURL.get(); }
+    const char* queryLogoutURL() const { return logoutURL.get(); }
+    int getSessionTimeoutSeconds() const { return sessionTimeoutSeconds; }
+    int getCheckSessionTimeoutSeconds() const { return checkSessionTimeoutSeconds; }
+    bool isDomainAuthResources(const char* resource)
+    {
+        bool* found = domainAuthResources.getValue(resource);
+        if (found && *found)
+            return true;
+
+        ForEachItemIn(i, domainAuthResourcesWildMatch)
+        {
+            const char* wildResourcePath = domainAuthResourcesWildMatch.item(i);
+            if (WildMatch(resource, wildResourcePath, true))
+                return true;
+        }
+        return false;
+    }
+    void readAuthDomainCfg(IPropertyTree* procCfg);
+    void setSDSSession();
 
     static void escapeSingleQuote(StringBuffer& src, StringBuffer& escaped);
 
@@ -341,7 +388,6 @@ protected:
                             const char *serviceName, const char* methodName);
     void sortResponse(IEspContext& context, CHttpRequest* request,MemoryBuffer& contentconst,
                             const char *serviceName, const char* methodName);
-    const char* queryAuthMethod() {return m_authmethod.str(); }
 };
 
 inline bool isEclIdeRequest(CHttpRequest *request)

+ 542 - 108
esp/bindings/http/platform/httpservice.cpp

@@ -34,6 +34,7 @@
 #include "http/platform/httptransport.hpp"
 
 #include "htmlpage.hpp"
+#include "dasds.hpp"
 
 /***************************************************************************
  *              CEspHttpServer Implementation
@@ -94,17 +95,6 @@ CEspHttpServer::~CEspHttpServer()
     }
 }
 
-typedef enum espAuthState_
-{
-    authUnknown,
-    authRequired,
-    authProvided,
-    authSucceeded,
-    authPending,
-    authFailed
-} EspAuthState;
-
-
 bool CEspHttpServer::rootAuth(IEspContext* ctx)
 {
     if (!m_apport->rootAuthRequired())
@@ -265,42 +255,18 @@ int CEspHttpServer::processRequest()
         ctx->setHTTPMethod(method.str());
         ctx->setServiceMethod(methodName.str());
 
-        bool isSoapPost=(stricmp(method.str(), POST_METHOD) == 0 && m_request->isSoapMessage());
-        if (!isSoapPost)
-        {
-            StringBuffer peerStr, pathStr;
-            const char *userid=ctx->queryUserId();
-            DBGLOG("%s %s, from %s@%s", method.str(), m_request->getPath(pathStr).str(), (userid) ? userid : "unknown", m_request->getPeer(peerStr).str());
-
-            if (m_apport->rootAuthRequired() && (!ctx->queryUserId() || !*ctx->queryUserId()))
-            {
-                thebinding = dynamic_cast<EspHttpBinding*>(m_defaultBinding.get());
-                StringBuffer realmbuf;
-                if(thebinding)
-                {   
-                    realmbuf.append(thebinding->getChallengeRealm());
-                }
+        StringBuffer peerStr, pathStr;
+        const char *userid=ctx->queryUserId();
+        ESPLOG(LogMin, "%s %s, from %s@%s", method.str(), m_request->getPath(pathStr).str(), (userid) ? userid : "unknown", m_request->getPeer(peerStr).str());
 
-                if(realmbuf.length() == 0)
-                    realmbuf.append("ESP");
-                DBGLOG("User authentication required");
-                m_response->sendBasicChallenge(realmbuf.str(), true);
-                return 0;
-            }
-        }
+        authState = checkUserAuth();
+        if ((authState == authUpdatePassword) || (authState == authFailed))
+            return 0;
 
         if (!stricmp(method.str(), GET_METHOD))
         {
             if (stype==sub_serv_root)
             {
-                if (!rootAuth(ctx))
-                    return 0;
-                if (ctx->queryUser() && (ctx->queryUser()->getAuthenticateStatus() == AS_PASSWORD_VALID_BUT_EXPIRED))
-                    return 0;//allow user to change password
-                // authenticate optional groups
-                if (authenticateOptionalFailed(*ctx,NULL))
-                    throw createEspHttpException(401,"Unauthorized Access","Unauthorized Access");
-
                 return onGetApplicationFrame(m_request.get(), m_response.get(), ctx);
             }
 
@@ -308,14 +274,7 @@ int CEspHttpServer::processRequest()
             {
                 if (!methodName.length())
                     return 0;
-#ifdef _USE_OPENLDAP
-                if (strieq(methodName.str(), "updatepasswordinput"))//process before authentication check
-                    return onUpdatePasswordInput(m_request.get(), m_response.get());
-#endif
-                if (!rootAuth(ctx) )
-                    return 0;
 
-                checkSetCORSAllowOrigin(m_request, m_response);
                 if (methodName.charAt(methodName.length()-1)=='_')
                     methodName.setCharAt(methodName.length()-1, 0);
                 if (!stricmp(methodName.str(), "files"))
@@ -347,15 +306,6 @@ int CEspHttpServer::processRequest()
                     return onGetBuildSoapRequest(m_request.get(), m_response.get());
             }
         }
-#ifdef _USE_OPENLDAP
-        else if (strieq(method.str(), POST_METHOD) && strieq(serviceName.str(), "esp") && (methodName.length() > 0) && strieq(methodName.str(), "updatepassword"))
-        {
-            EspHttpBinding* thebinding = getBinding();
-            if (thebinding)
-                thebinding->populateRequest(m_request.get());
-            return onUpdatePassword(m_request.get(), m_response.get());
-        }
-#endif
 
         if(m_apport != NULL)
         {
@@ -368,13 +318,14 @@ int CEspHttpServer::processRequest()
                     CEspBindingEntry *entry = m_apport->queryBindingItem(0);
                     thebinding = (entry) ? dynamic_cast<EspHttpBinding*>(entry->queryBinding()) : NULL;
 
+                    bool isSoapPost=(strieq(method.str(), POST_METHOD) && m_request->isSoapMessage());
                     if (thebinding && !isSoapPost && !thebinding->isValidServiceName(*ctx, serviceName.str()))
                         thebinding=NULL;
                 }
                 else
                 {
                     EspHttpBinding* lbind=NULL;
-                    for(int index=0; !thebinding && index<ordinality; index++)
+                    for (int index=0; !thebinding && index<ordinality; index++)
                     {
                         CEspBindingEntry *entry = m_apport->queryBindingItem(index);
                         lbind = (entry) ? dynamic_cast<EspHttpBinding*>(entry->queryBinding()) : NULL;
@@ -392,58 +343,8 @@ int CEspHttpServer::processRequest()
                 }
                 if (!thebinding && m_defaultBinding)
                     thebinding=dynamic_cast<EspHttpBinding*>(m_defaultBinding.get());
-                if (thebinding)
-                {
-                    StringBuffer servName(ctx->queryServiceName(NULL));
-                    if (!servName.length())
-                    {
-                        thebinding->getServiceName(servName);
-                        ctx->setServiceName(servName.str());
-                    }
-                    
-                    thebinding->populateRequest(m_request.get());
-                    if(thebinding->authRequired(m_request.get()) && !thebinding->doAuth(ctx))
-                    {
-                        authState=authRequired;
-                        if(isSoapPost)
-                        {
-                            authState = authPending;
-                            ctx->setToBeAuthenticated(true);
-                        }
-                    }
-                    else
-                        authState = authSucceeded;
-                }
-            }
-
-            if (authState==authRequired)
-            {
-                ISecUser *user = ctx->queryUser();
-                if (user && (user->getAuthenticateStatus() == AS_PASSWORD_EXPIRED || user->getAuthenticateStatus() == AS_PASSWORD_VALID_BUT_EXPIRED))
-                {
-                    DBGLOG("ESP password expired for %s", user->getName());
-                    m_response->setContentType(HTTP_TYPE_TEXT_PLAIN);
-                    m_response->setContent("Your ESP password has expired");
-                    m_response->send();
-                }
-                else
-                {
-                    DBGLOG("User authentication required");
-                    StringBuffer realmbuf;
-                    if(thebinding)
-                        realmbuf.append(thebinding->getChallengeRealm());
-                    if(realmbuf.length() == 0)
-                        realmbuf.append("ESP");
-                    m_response->sendBasicChallenge(realmbuf.str(), !isSoapPost);
-                }
-                return 0;
             }
 
-            // authenticate optional groups
-            if (authenticateOptionalFailed(*ctx,thebinding))
-                throw createEspHttpException(401,"Unauthorized Access","Unauthorized Access");
-
-
             if(strieq(method.str(), OPTIONS_METHOD))
                 return onOptions();
 
@@ -974,3 +875,536 @@ int CEspHttpServer::onGet()
     return 0;
 }
 
+EspAuthState CEspHttpServer::checkUserAuth()
+{
+    EspAuthRequest authReq;
+    readAuthRequest(authReq);
+    if (authReq.httpPath.isEmpty())
+        throw MakeStringException(-1, "URL query string cannot be empty.");
+
+    if (!authReq.authBinding)
+        throw MakeStringException(-1, "Cannot find ESP HTTP Binding");
+
+    ESPLOG(LogMax, "checkUserAuth: %s %s", m_request->isSoapMessage() ? "SOAP" : "HTTP", authReq.httpMethod.isEmpty() ? "??" : authReq.httpMethod.str());
+
+    //The preCheckAuth() does not return authUnknown when:
+    //No authentication is required for the ESP binding;
+    //Or no authentication is required for certain situations of not rootAuthRequired();
+    //Or a user is trying to access some resources for displaying login/logout pages;
+    //Or this is a user request for updating password.
+    EspAuthState authState = preCheckAuth(authReq);
+    if (authState != authUnknown)
+        return authState;
+
+    StringBuffer servName(authReq.ctx->queryServiceName(nullptr));
+    if (servName.isEmpty())
+    {
+        authReq.authBinding->getServiceName(servName);
+        authReq.ctx->setServiceName(servName.str());
+    }
+
+    AuthType domainAuthType = authReq.authBinding->getDomainAuthType();
+    authReq.ctx->setDomainAuthType(domainAuthType);
+    if (domainAuthType != AuthPerRequestOnly)
+    {//Try session based authentication now.
+        EspAuthState authState = checkUserAuthPerSession(authReq);
+        if (authState != authUnknown)
+            return authState;
+    }
+    if (domainAuthType != AuthPerSessionOnly)
+    {// BasicAuthentication
+        EspAuthState authState = checkUserAuthPerRequest(authReq);
+        if (authState != authUnknown)
+            return authState;
+    }
+
+    //authentication failed. Send out a login page or 401.
+    StringBuffer userName;
+    bool authSession =  false;
+    if ((domainAuthType == AuthPerSessionOnly) || ((domainAuthType == AuthTypeMixed)
+        && !authReq.ctx->getUserID(userName).length() && strieq(authReq.httpMethod.str(), GET_METHOD)))
+    { //This is in session based authentication and the first request from a browser using GET with no userID.
+        authSession = true;
+    }
+    handleAuthFailed(authSession, authReq);
+    return authFailed;
+}
+
+//Read authentication related information into EspAuthRequest.
+void CEspHttpServer::readAuthRequest(EspAuthRequest& req)
+{
+    StringBuffer pathEx;
+    m_request->getEspPathInfo(req.stype, &pathEx, &req.serviceName, &req.methodName, false);
+    m_request->getMethod(req.httpMethod);
+    m_request->getPath(req.httpPath);//m_httpPath
+
+    req.isSoapPost = (strieq(req.httpMethod.str(), POST_METHOD) && m_request->isSoapMessage());
+    req.ctx = m_request->queryContext();
+    req.authBinding = getEspHttpBinding(req);
+    req.requestParams = m_request->queryParameters();
+}
+
+EspHttpBinding* CEspHttpServer::getEspHttpBinding(EspAuthRequest& authReq)
+{
+    if (strieq(authReq.httpMethod.str(), GET_METHOD) && ((authReq.stype == sub_serv_root)
+            || (!authReq.serviceName.isEmpty() && strieq(authReq.serviceName.str(), "esp"))))
+        return getBinding();
+
+    if(!m_apport)
+        return nullptr;
+
+    int ordinality=m_apport->getBindingCount();
+    if (ordinality < 1)
+        return nullptr;
+
+    EspHttpBinding* espHttpBinding = nullptr;
+    if (ordinality==1)
+    {
+        CEspBindingEntry *entry = m_apport->queryBindingItem(0);
+        espHttpBinding = (entry) ? dynamic_cast<EspHttpBinding*>(entry->queryBinding()) : NULL;
+        //If there is only one binding on the port, we allow SOAP calls to work if they go
+        //to http://IP:Port without any service name on the path. Even without specifying
+        //the service, if the request matches a method, the method will run. So, the espHttpBinding
+        //is set to nullptr only if !authReq.isSoapPost.
+        if (!authReq.isSoapPost && espHttpBinding && !espHttpBinding->isValidServiceName(*authReq.ctx, authReq.serviceName.str()))
+            espHttpBinding=nullptr;
+        return espHttpBinding;
+    }
+
+    for (unsigned index=0; index<ordinality; index++)
+    {
+        CEspBindingEntry *entry = m_apport->queryBindingItem(index);
+        EspHttpBinding* lbind = (entry) ? dynamic_cast<EspHttpBinding*>(entry->queryBinding()) : nullptr;
+        if (lbind && lbind->isValidServiceName(*authReq.ctx, authReq.serviceName.str()))
+        {
+            espHttpBinding=lbind;
+            break;
+        }
+    }
+
+    if (!espHttpBinding && m_defaultBinding)
+        espHttpBinding=dynamic_cast<EspHttpBinding*>(m_defaultBinding.get());
+
+    return espHttpBinding;
+}
+
+EspAuthState CEspHttpServer::preCheckAuth(EspAuthRequest& authReq)
+{
+    if (!isAuthRequiredForBinding(authReq))
+        return authSucceeded;
+
+    if (!m_apport->rootAuthRequired() && strieq(authReq.httpMethod.str(), GET_METHOD) &&
+        ((authReq.stype == sub_serv_root) || (!authReq.serviceName.isEmpty() && strieq(authReq.serviceName.str(), "esp"))))
+        return authSucceeded;
+
+#ifdef _USE_OPENLDAP
+    if (!authReq.httpMethod.isEmpty() && !authReq.serviceName.isEmpty() && !authReq.methodName.isEmpty() && strieq(authReq.serviceName.str(), "esp"))
+    {
+        if (strieq(authReq.httpMethod.str(), POST_METHOD) && strieq(authReq.methodName.str(), "updatepassword"))
+        {
+            EspHttpBinding* thebinding = getBinding();
+            if (thebinding)
+                thebinding->populateRequest(m_request.get());
+            onUpdatePassword(m_request.get(), m_response.get());
+            return authUpdatePassword;
+        }
+        if (strieq(authReq.httpMethod.str(), GET_METHOD) && strieq(authReq.methodName.str(), "updatepasswordinput"))//process before authentication check
+        {
+            onUpdatePasswordInput(m_request.get(), m_response.get());
+            return authUpdatePassword;
+        }
+    }
+#endif
+
+    if ((authReq.authBinding->getDomainAuthType() != AuthPerRequestOnly) && authReq.authBinding->isDomainAuthResources(authReq.httpPath.str()))
+        return authSucceeded;//Give the permission to send out some pages used for login or logout.
+
+    return authUnknown;
+}
+
+bool CEspHttpServer::isAuthRequiredForBinding(EspAuthRequest& authReq)
+{
+    IAuthMap* authmap = authReq.authBinding->queryAuthMAP();
+    if (!authmap) //No auth requirement
+        return false;
+
+    const char* authMethod = authReq.authBinding->queryAuthMethod();
+    if (isEmptyString(authMethod) || strieq(authMethod, "none"))
+        return false;
+
+    ISecResourceList* rlist = authmap->getResourceList(authReq.httpPath.str());
+    if(!rlist) //No auth requirement for the httpPath.
+        return false;
+
+    authReq.ctx->setAuthenticationMethod(authMethod);
+    authReq.ctx->setResources(rlist);
+
+    return true;
+}
+
+EspAuthState CEspHttpServer::checkUserAuthPerSession(EspAuthRequest& authReq)
+{
+    ESPLOG(LogMax, "checkUserAuthPerSession");
+
+    unsigned sessionID = readCookie(authReq.authBinding->querySessionIDCookieName());
+    if (sessionID > 0)
+        return authExistingSession(authReq, sessionID);//Check session based authentication using this session ID.
+
+    StringBuffer urlCookie;
+    readCookie(SESSION_START_URL_COOKIE, urlCookie);
+    if (strieq(authReq.httpPath.str(), authReq.authBinding->queryLoginURL()))
+    {//This is a request to ask for a login page.
+        if (urlCookie.isEmpty())
+            addCookie(SESSION_START_URL_COOKIE, "/", 0); //Will be redirected to / after authenticated.
+        return authSucceeded;
+    }
+
+    if (urlCookie.isEmpty())
+        return authUnknown;
+
+    const char* userName = (authReq.requestParams) ? authReq.requestParams->queryProp("username") : NULL;
+    const char* password = (authReq.requestParams) ? authReq.requestParams->queryProp("password") : NULL;
+    if (!isEmptyString(userName) && !isEmptyString(password))
+        return authNewSession(authReq, userName, password, urlCookie.str());
+
+    if (authReq.isSoapPost) //from SOAP Test page
+        sendMessage("Authentication failed: empty user name or password.", "text/html; charset=UTF-8");
+    else //from other page
+        askUserLogin(authReq);
+    return authFailed;
+}
+
+EspAuthState CEspHttpServer::checkUserAuthPerRequest(EspAuthRequest& authReq)
+{
+    ESPLOG(LogMax, "checkUserAuthPerRequest");
+
+    authReq.authBinding->populateRequest(m_request.get());
+    if (authReq.authBinding->doAuth(authReq.ctx))
+    {//We do pass the authentication per the request
+        // authenticate optional groups. Do we still need?
+        authOptionalGroups(authReq);
+
+        StringBuffer userName, peer;
+        ESPLOG(LogNormal, "Authenticated for %s@%s", authReq.ctx->getUserID(userName).str(), m_request->getPeer(peer).str());
+        return authSucceeded;
+    }
+    if (!authReq.isSoapPost)
+        return authUnknown;
+
+    //If SoapPost, username/password may be in soap:Header which is not in HTTP header.
+    //The doAuth() may check them inside CSoapService::processHeader() later.
+    authReq.ctx->setToBeAuthenticated(true);
+    return authPending;
+}
+
+void CEspHttpServer::sendMessage(const char* msg, const char* msgType)
+{
+    if (!isEmptyString(msg))
+        m_response->setContent(msg);
+    m_response->setContentType(msgType);
+    m_response->setStatus(HTTP_STATUS_OK);
+    m_response->send();
+}
+
+EspAuthState CEspHttpServer::authNewSession(EspAuthRequest& authReq, const char* _userName, const char* _password, const char* sessionStartURL)
+{
+    StringBuffer peer;
+    m_request->getPeer(peer);
+
+    ESPLOG(LogMax, "authNewSession for %s@%s", _userName, peer.str());
+
+    authReq.ctx->setUserID(_userName);
+    authReq.ctx->setPassword(_password);
+    authReq.authBinding->populateRequest(m_request.get());
+    if (!authReq.authBinding->doAuth(authReq.ctx))
+    {
+        ESPLOG(LogMin, "Authentication failed for %s@%s", _userName, peer.str());
+        handleAuthFailed(true, authReq);
+        return authFailed;
+    }
+
+    // authenticate optional groups
+    authOptionalGroups(authReq);
+
+    unsigned sessionID = createHTTPSession(authReq, sessionStartURL);
+    authReq.ctx->addUserToken(sessionID);
+
+    ESPLOG(LogMax, "Authenticated for %s@%s", _userName, peer.str());
+
+    VStringBuffer sessionIDStr("%u", sessionID);
+    addCookie(authReq.authBinding->querySessionIDCookieName(), sessionIDStr.str(), authReq.authBinding->getSessionTimeoutSeconds());
+    clearCookie(SESSION_START_URL_COOKIE);
+    m_response->redirect(*m_request, sessionStartURL);
+
+    return authSucceeded;
+}
+
+EspAuthState CEspHttpServer::authExistingSession(EspAuthRequest& authReq, unsigned sessionID)
+{
+    ESPLOG(LogMax, "authExistingSession: %s<%u>", PropSessionID, sessionID);
+
+    Owned<IRemoteConnection> conn = getSDSConnection(authReq.authBinding->queryESPSessionSDSPath(), RTM_LOCK_WRITE, SESSION_SDS_LOCK_TIMEOUT);
+    IPropertyTree* espSessions = conn->queryRoot();
+    if (authReq.authBinding->getSessionTimeoutSeconds() >= 0)
+    {
+        CDateTime now;
+        now.setNow();
+        time_t timeNow = now.getSimple();
+        if (timeNow - lastSessionCleanUpTime >= authReq.authBinding->getCheckSessionTimeoutSeconds())
+        {
+            lastSessionCleanUpTime = timeNow;
+            timeoutESPSessions(authReq.authBinding, espSessions);
+        }
+    }
+
+    VStringBuffer xpath("%s[@port=\"%d\"]/%s[%s='%u']", PathSessionApplication, authReq.authBinding->getPort(), PathSessionSession, PropSessionID, sessionID);
+    IPropertyTree* sessionTree = espSessions->queryBranch(xpath.str());
+    if (!sessionTree)
+    {
+        ESPLOG(LogMin, "Authentication failed: session:<%u> not found", sessionID);
+        if (authReq.isSoapPost) //from SOAP Test page
+            sendMessage("Session expired. Please close this page and login again.", "text/html; charset=UTF-8");
+        else
+            askUserLogin(authReq);
+        return authFailed;
+    }
+
+    authOptionalGroups(authReq);
+
+    //The UserID has to be set before the populateRequest() because the UserID is used to create the user object.
+    //After the user object is created, we may call addUserToken().
+    StringAttr userName = sessionTree->queryProp(PropSessionUserID);
+    authReq.ctx->setUserID(userName.str());
+    authReq.authBinding->populateRequest(m_request.get());
+    authReq.ctx->addUserToken(sessionID);
+
+    ESPLOG(LogMax, "Authenticated for %s<%u> %s@%s", PropSessionID, sessionID, userName.str(), sessionTree->queryProp(PropSessionNetworkAddress));
+    if (authReq.methodName && strieq(authReq.methodName, "logout"))
+        logoutSession(authReq, sessionID, espSessions);
+    else
+    {
+        CDateTime now;
+        now.setNow();
+        time_t createTime = now.getSimple();
+        sessionTree->setPropInt64(PropSessionLastAccessed, createTime);
+        if (!sessionTree->getPropBool(PropSessionTimeoutByAdmin, false))
+            sessionTree->setPropInt64(PropSessionTimeoutAt, createTime + authReq.authBinding->getSessionTimeoutSeconds());
+
+        ///authReq.ctx->setAuthorized(true);
+        VStringBuffer sessionIDStr("%u", sessionID);
+        addCookie(authReq.authBinding->querySessionIDCookieName(), sessionIDStr.str(), authReq.authBinding->getSessionTimeoutSeconds());
+    }
+
+    return authSucceeded;
+}
+
+void CEspHttpServer::logoutSession(EspAuthRequest& authReq, unsigned sessionID, IPropertyTree* espSessions)
+{
+    //delete this session before logout
+    VStringBuffer path("%s[@port=\"%d\"]", PathSessionApplication, authReq.authBinding->getPort());
+    IPropertyTree* sessionTree = espSessions->queryBranch(path.str());
+    if (sessionTree)
+    {
+        ICopyArrayOf<IPropertyTree> toRemove;
+        path.setf("%s[%s='%d']", PathSessionSession, PropSessionID, sessionID);
+        Owned<IPropertyTreeIterator> it = sessionTree->getElements(path.str());
+        ForEach(*it)
+            toRemove.append(it->query());
+        ForEachItemIn(i, toRemove)
+            sessionTree->removeTree(&toRemove.item(i));
+    }
+    else
+        ESPLOG(LogMin, "Cann't find session tree: %s[@port=\"%d\"]", PathSessionApplication, authReq.authBinding->getPort());
+
+    ///authReq.ctx->setAuthorized(true);
+
+    clearCookie(authReq.authBinding->querySessionIDCookieName());
+    const char* logoutURL = authReq.authBinding->queryLogoutURL();
+    if (!isEmptyString(logoutURL))
+        m_response->redirect(*m_request, authReq.authBinding->queryLogoutURL());
+    else
+        sendMessage(nullptr, "text/html; charset=UTF-8");
+}
+
+void CEspHttpServer::handleAuthFailed(bool sessionAuth, EspAuthRequest& authReq)
+{
+    ISecUser *user = authReq.ctx->queryUser();
+    if (user && (user->getAuthenticateStatus() == AS_PASSWORD_EXPIRED || user->getAuthenticateStatus() == AS_PASSWORD_VALID_BUT_EXPIRED))
+    {
+        ESPLOG(LogMin, "ESP password expired for %s", authReq.ctx->queryUserId());
+        handlePasswordExpired(sessionAuth);
+        return;
+    }
+
+    if (!sessionAuth)
+    {
+        ESPLOG(LogMin, "Authentication failed: send BasicAuthentication.");
+        m_response->sendBasicChallenge(authReq.authBinding->getChallengeRealm(), true);
+        return;
+    }
+
+    ESPLOG(LogMin, "Authentication failed: call askUserLogin.");
+    askUserLogin(authReq);
+}
+
+void CEspHttpServer::askUserLogin(EspAuthRequest& authReq)
+{
+    StringBuffer urlCookie;
+    readCookie(SESSION_START_URL_COOKIE, urlCookie);
+    if (urlCookie.isEmpty())
+    {
+        StringBuffer sessionStartURL = authReq.httpPath;
+        if (authReq.requestParams && authReq.requestParams->hasProp("__querystring"))
+            sessionStartURL.append("?").append(authReq.requestParams->queryProp("__querystring"));
+        if (!sessionStartURL.isEmpty() && streq(sessionStartURL.str(), "/WsSMC/"))
+            sessionStartURL.set("/");
+
+        const char* loginURL = authReq.authBinding->queryLoginURL();
+        if (strieq(loginURL, sessionStartURL))
+            sessionStartURL.set("/");
+
+        addCookie(SESSION_START_URL_COOKIE, sessionStartURL.str(), 0); //time out when browser is closed
+    }
+    m_response->redirect(*m_request, authReq.authBinding->queryLoginURL());
+}
+
+unsigned CEspHttpServer::createHTTPSession(EspAuthRequest& authReq, const char* sessionStartURL)
+{
+    CDateTime now;
+    now.setNow();
+    time_t createTime = now.getSimple();
+
+    StringBuffer peer, sessionIDStr;
+    VStringBuffer idStr("%s_%ld", m_request->getPeer(peer).str(), createTime);
+    unsigned sessionID = hashc((unsigned char *)idStr.str(), idStr.length(), 0);
+    sessionIDStr.append(sessionID);
+
+    VStringBuffer xpath("%s[%s='%u']", PathSessionSession, PropSessionID, sessionID);
+    Owned<IRemoteConnection> conn = getSDSConnection(authReq.authBinding->querySessionSDSPath(), RTM_LOCK_WRITE, SESSION_SDS_LOCK_TIMEOUT);
+    IPropertyTree* domainSessions = conn->queryRoot();
+    IPropertyTree* sessionTree = domainSessions->queryBranch(xpath.str());
+    if (sessionTree)
+    {
+        sessionTree->setPropInt64(PropSessionLastAccessed, createTime);
+        if (!sessionTree->getPropBool(PropSessionTimeoutByAdmin, false))
+            sessionTree->setPropInt64(PropSessionTimeoutAt, createTime + authReq.authBinding->getSessionTimeoutSeconds());
+        return sessionID;
+    }
+    ESPLOG(LogMax, "New sessionID <%d> at <%ld> in createHTTPSession()", sessionID, createTime);
+
+    IPropertyTree* ptree = domainSessions->addPropTree(PathSessionSession);
+    ptree->setProp(PropSessionNetworkAddress, peer.str());
+    ptree->setPropInt64(PropSessionID, sessionID);
+    ptree->setPropInt64(PropSessionExternalID, hashc((unsigned char *)sessionIDStr.str(), sessionIDStr.length(), 0));
+    ptree->setProp(PropSessionUserID, authReq.requestParams->queryProp("username"));
+    ptree->setPropInt64(PropSessionCreateTime, createTime);
+    ptree->setPropInt64(PropSessionLastAccessed, createTime);
+    ptree->setPropInt64(PropSessionTimeoutAt, createTime + authReq.authBinding->getSessionTimeoutSeconds());
+    ptree->setProp(PropSessionLoginURL, sessionStartURL);
+    return sessionID;
+}
+
+void CEspHttpServer::timeoutESPSessions(EspHttpBinding* authBinding, IPropertyTree* espSessions)
+{
+    //Removing HTTPSessions if timed out
+    CDateTime now;
+    now.setNow();
+    time_t timeNow = now.getSimple();
+
+    Owned<IPropertyTreeIterator> iter1 = espSessions->getElements(PathSessionApplication);
+    ForEach(*iter1)
+    {
+        ICopyArrayOf<IPropertyTree> toRemove;
+        Owned<IPropertyTreeIterator> iter2 = iter1->query().getElements(PathSessionSession);
+        ForEach(*iter2)
+        {
+            IPropertyTree& item = iter2->query();
+            if (timeNow >= item.getPropInt64(PropSessionTimeoutAt, 0))
+                toRemove.append(item);
+        }
+        ForEachItemIn(i, toRemove)
+            iter1->query().removeTree(&toRemove.item(i));
+    }
+}
+
+void CEspHttpServer::handlePasswordExpired(bool sessionAuth)
+{
+    if (sessionAuth)
+        m_response->redirect(*m_request.get(), "/esp/updatepasswordinput");
+    else
+    {
+        Owned<IMultiException> me = MakeMultiException();
+        me->append(*MakeStringException(-1, "Your ESP password has expired."));
+        m_response->handleExceptions(nullptr, me, "ESP Authentication", "PasswordExpired", nullptr);
+    }
+}
+
+void CEspHttpServer::authOptionalGroups(EspAuthRequest& authReq)
+{
+    if (strieq(authReq.httpMethod.str(), GET_METHOD) && (authReq.stype==sub_serv_root) && authenticateOptionalFailed(*authReq.ctx, nullptr))
+        throw MakeStringException(-1, "Unauthorized Access to service root");
+    if ((!strieq(authReq.httpMethod.str(), GET_METHOD) || !strieq(authReq.serviceName.str(), "esp")) && authenticateOptionalFailed(*authReq.ctx, authReq.authBinding))
+        throw MakeStringException(-1, "Unauthorized Access: %s %s", authReq.httpMethod.str(), authReq.serviceName.str());
+}
+
+IRemoteConnection* CEspHttpServer::getSDSConnection(const char* xpath, unsigned mode, unsigned timeout)
+{
+    Owned<IRemoteConnection> globalLock = querySDS().connect(xpath, myProcessSession(), RTM_LOCK_READ, SESSION_SDS_LOCK_TIMEOUT);
+    if (!globalLock)
+        throw MakeStringException(-1, "Unable to connect to ESP Session information in dali %s", xpath);
+    return globalLock.getClear();
+}
+
+void CEspHttpServer::addCookie(const char* cookieName, const char *cookieValue, int maxAgeSec)
+{
+    CEspCookie* cookie = new CEspCookie(cookieName, cookieValue);
+    if (maxAgeSec > 0)
+    {
+        char expiresTime[64];
+        time_t tExpires;
+        time(&tExpires);
+        tExpires += maxAgeSec;
+#ifdef _WIN32
+        struct tm *gmtExpires;
+        gmtExpires = gmtime(&tExpires);
+        strftime(expiresTime, 64, "%a, %d %b %Y %H:%M:%S GMT", gmtExpires);
+#else
+        struct tm gmtExpires;
+        gmtime_r(&tExpires, &gmtExpires);
+        strftime(expiresTime, 64, "%a, %d %b %Y %H:%M:%S GMT", &gmtExpires);
+#endif //_WIN32
+
+        cookie->setExpires(expiresTime);
+    }
+    cookie->setHTTPOnly(true);
+    cookie->setSameSite("Lax");
+    m_response->addCookie(cookie);
+}
+
+void CEspHttpServer::clearCookie(const char* cookieName)
+{
+    CEspCookie* cookie = new CEspCookie(cookieName, "");
+    cookie->setExpires("Thu, 01 Jan 1970 00:00:01 GMT");
+    m_response->addCookie(cookie);
+    m_response->addHeader(cookieName,  "max-age=0");
+}
+
+unsigned CEspHttpServer::readCookie(const char* cookieName)
+{
+    CEspCookie* sessionIDCookie = m_request->queryCookie(cookieName);
+    if (sessionIDCookie)
+    {
+        StringBuffer sessionIDStr = sessionIDCookie->getValue();
+        if (sessionIDStr.length())
+            return atoi(sessionIDStr.str());
+    }
+    return 0;
+}
+
+const char* CEspHttpServer::readCookie(const char* cookieName, StringBuffer& cookieValue)
+{
+    CEspCookie* sessionIDCookie = m_request->queryCookie(cookieName);
+    if (sessionIDCookie)
+        cookieValue.append(sessionIDCookie->getValue());
+    return cookieValue.str();
+}

+ 47 - 0
esp/bindings/http/platform/httpservice.hpp

@@ -33,10 +33,33 @@
 #include "SOAP/Platform/soapmessage.hpp"
 
 #include "espsession.ipp"
+#include "jhash.hpp"
 
+typedef enum espAuthState_
+{
+    authUnknown,
+    authRequired,
+    authProvided,
+    authSucceeded,
+    authPending,
+    authUpdatePassword,
+    authFailed
+} EspAuthState;
+
+struct EspAuthRequest
+{
+    IEspContext* ctx;
+    EspHttpBinding* authBinding;
+    IProperties* requestParams;
+    StringBuffer httpPath, httpMethod, serviceName, methodName;
+    sub_service stype = sub_serv_unknown;
+    bool isSoapPost;
+};
 
+interface IRemoteConnection;
 class CEspHttpServer : implements IHttpServerService, public CInterface
 {
+    CriticalSection critDaliSession;
 protected:
     ISocket&                m_socket;
     Owned<CHttpRequest>     m_request;
@@ -46,9 +69,33 @@ protected:
 
     bool m_viewConfig;
     int m_MaxRequestEntityLength;
+    int lastSessionCleanUpTime = 0;
 
     int unsupported();
     EspHttpBinding* getBinding();
+    EspAuthState checkUserAuth();
+    void readAuthRequest(EspAuthRequest& req);
+    EspAuthState preCheckAuth(EspAuthRequest& authReq);
+    EspAuthState checkUserAuthPerRequest(EspAuthRequest& authReq);
+    EspAuthState checkUserAuthPerSession(EspAuthRequest& authReq);
+    EspAuthState authNewSession(EspAuthRequest& authReq, const char* _userName, const char* _password, const char* sessionStartURL);
+    EspAuthState authExistingSession(EspAuthRequest& req, unsigned sessionID);
+    void logoutSession(EspAuthRequest& authReq, unsigned sessionID, IPropertyTree* domainSessions);
+    void askUserLogin(EspAuthRequest& authReq);
+    void handleAuthFailed(bool sessionAuth, EspAuthRequest& authReq);
+    void handlePasswordExpired(bool sessionAuth);
+    EspHttpBinding* getEspHttpBinding(EspAuthRequest& req);
+    bool isAuthRequiredForBinding(EspAuthRequest& req);
+    void authOptionalGroups(EspAuthRequest& req);
+    unsigned createHTTPSession(EspAuthRequest& authReq, const char* loginURL);
+    void timeoutESPSessions(EspHttpBinding* authBinding, IPropertyTree* espSessions);
+    void addCookie(const char* cookieName, const char *cookieValue, int maxAgeSec);
+    void clearCookie(const char* cookieName);
+    unsigned readCookie(const char* cookieName);
+    const char* readCookie(const char* cookieName, StringBuffer& cookieValue);
+    void sendMessage(const char* msg, const char* msgType);
+    IRemoteConnection* getSDSConnection(const char* xpath, unsigned mode, unsigned timeout);
+
 public:
     IMPLEMENT_IINTERFACE;
 

+ 18 - 0
esp/files/esp_app_tree.html

@@ -360,6 +360,20 @@ function writeESPappname(appname)
         YAHOO.util.Connect.asyncRequest('GET', sUrl, callback);
       }
 
+      var logout = function()
+      {
+        var logoutRequest = new XMLHttpRequest();
+        logoutRequest.onreadystatechange = function()
+        { 
+          if (logoutRequest.readyState == 4 && logoutRequest.status == 200)
+            parent.location = '/esp/files/eclwatch/templates/Login.html';
+          else
+            console.log("Logout failed: " + logoutRequest.status);
+        }
+        logoutRequest.open( "GET", 'esp/logout', true );            
+        logoutRequest.send( null );
+      }
+
             var copy_url = function()
                 {
                     var inner = parent.frames['main'].location;
@@ -444,6 +458,10 @@ function writeESPappname(appname)
                                     { text: "Refresh", onclick: { fn: refresh_main }}
                                 ]
                         ] }
+                    },
+                    {
+                        text: "<b>Log Out</b>",
+                        onclick: { fn: logout }
                     }
                 ];
 

+ 50 - 0
esp/files/userlogon.html

@@ -0,0 +1,50 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+        <title>User Log On</title>
+	<script>
+	    function keyupFunction()
+            {
+    		var f = document.getElementById("user_input_form");
+                if(f.username.value != '' && f.password.value != '')
+                    f.LogOn.disabled=false;
+                else
+                    f.LogOn.disabled=true;
+            }
+            function onLoad()
+            {
+                document.getElementById("username").focus();
+            }
+         </script>
+    </head>
+    <body class="yui-skin-sam" onload="onLoad()">
+        <p align="left" />
+        <h3>Please log on.</h3>
+        <form id="user_input_form" name="user_input_form" method="POST" action="/">
+            <table>
+                <tr>
+                    <td>
+                        <b>UserName: </b>
+                    </td>
+                    <td>
+                        <input type="text" id="username" name="username" size="20" onKeyUp="keyupFunction()"/>
+                    </td>
+                </tr>
+                <tr>
+                    <td>
+                        <b>Password: </b>
+                    </td>
+                    <td>
+                        <input type="password" name="password" size="20" value="" onKeyUp="keyupFunction()"/>
+                    </td>
+                </tr>
+                <tr>
+                    <td/>
+                    <td>
+                        <input type="submit" id="LogOn" name="LogOn" value="Log on" disabled="true"/>
+                    </td>
+                </tr>
+            </table>
+        </form>
+    </body>
+</html>

+ 17 - 0
esp/files/userlogout.html

@@ -0,0 +1,17 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+        <title>User Log Out</title>
+	<script>
+            function onLoad()
+            {
+                //document.getElementById("username").focus();
+            }
+         </script>
+    </head>
+    <body class="yui-skin-sam" onload="onLoad()">
+        <p align="left" />
+        <h3>ESP log out.</h3>
+        <h4><a href="userlogon.html" target="_top">Click here to login.</a> </h4>
+    </body>
+</html>

+ 41 - 0
esp/platform/espcfg.cpp

@@ -111,6 +111,47 @@ StringBuffer &CVSBuildToEspVersion(char const * tag, StringBuffer & out)
     return out;
 }
 
+void CEspConfig::ensureSDSSessionDomains()
+{
+    bool hasDefaultSessionDomain = false;
+    Owned<IPropertyTree> proc_cfg = getProcessConfig(m_envpt, m_process.str());
+    Owned<IPropertyTreeIterator> it = proc_cfg->getElements("AuthDomains/AuthDomain");
+    ForEach(*it)
+    {
+        IPropertyTree& authDomain = it->query();
+        const char* authType = authDomain.queryProp("@authType");
+        if (isEmptyString(authType) || (!strieq(authType, "AuthPerSessionOnly") && !strieq(authType, "AuthTypeMixed")))
+            continue;
+
+        const char* authDomainName = authDomain.queryProp("@name");
+        if (isEmptyString(authDomainName))
+        {
+            if (hasDefaultSessionDomain)
+                throw MakeStringException(-1, ">1 AuthDomains are not named.");
+
+            hasDefaultSessionDomain = true;
+            authDomainName = "default";
+        }
+
+        Owned<IRemoteConnection> conn = querySDS().connect(PathSessionRoot, myProcessSession(), RTM_LOCK_WRITE|RTM_CREATE_QUERY, SESSION_SDS_LOCK_TIMEOUT);
+        if (!conn)
+            throw MakeStringException(-1, "Failed to connect to %s.", PathSessionRoot);
+
+        ensureESPSessionInTree(conn->queryRoot(), m_process.str());
+    }
+}
+
+void CEspConfig::ensureESPSessionInTree(IPropertyTree* sessionRoot, const char* procName)
+{
+    VStringBuffer xpath("%s[@name=\"%s\"]", PathSessionProcess, procName);
+    IPropertyTree* procSessionTree = sessionRoot->queryBranch(xpath.str());
+    if (!procSessionTree)
+    {
+        IPropertyTree* processSessionTree = sessionRoot->addPropTree(PathSessionProcess);
+        processSessionTree->setProp("@name", procName);
+    }
+}
+
 
 
 CEspConfig::CEspConfig(IProperties* inputs, IPropertyTree* envpt, IPropertyTree* procpt, bool isDali)

+ 7 - 0
esp/platform/espcfg.ipp

@@ -30,6 +30,8 @@
 #include "environment.hpp"
 #include <dalienv.hpp>
 
+#include "bindutil.hpp"
+
 //STL
 #include <list>
 #include <map>
@@ -166,12 +168,17 @@ public:
 
     const SocketEndpoint &getLocalEndpoint(){return m_address;}
 
+    void ensureESPSessionInTree(IPropertyTree* sessionRoot, const char* procName);
+    void ensureSDSSessionDomains();
+
     void loadProtocols();
     void loadServices();
     void loadBindings();
 
     void loadAll()
     {
+        ensureSDSSessionDomains();
+
         DBGLOG("loadServices");
         loadServices();
         loadProtocols();

+ 25 - 0
esp/platform/espcontext.cpp

@@ -78,6 +78,8 @@ private:
     unsigned    m_exceptionTime;
     bool        m_hasException;
     int         m_exceptionCode;
+    StringAttr  authenticationMethod;
+    AuthType    domainAuthType;
 
     ESPSerializationFormat respSerializationFormat;
 
@@ -163,6 +165,16 @@ public:
     {
         return m_password.get();
     }
+    virtual bool addUserToken(unsigned token)
+    {
+        if (!m_user)
+            return false;
+
+        MemoryBuffer buf;
+        buf.append(token);
+        m_user->credentials().addToken(&buf);
+        return true;
+    }
 
     virtual void setRealm(const char* realm)
     {
@@ -486,6 +498,19 @@ public:
         m_txSummary.clear();
     }
 
+    virtual void setAuthenticationMethod(const char* method)
+    {
+        authenticationMethod.set(method);
+    }
+
+    virtual const char * getAuthenticationMethod()
+    {
+        return authenticationMethod.get();
+    }
+
+    virtual void setDomainAuthType(AuthType type) { domainAuthType = type; }
+    virtual AuthType getDomainAuthType(){ return domainAuthType; }
+
     virtual ESPSerializationFormat getResponseFormat(){return respSerializationFormat;}
     virtual void setResponseFormat(ESPSerializationFormat fmt){respSerializationFormat = fmt;}
 

+ 56 - 0
esp/platform/espcontext.hpp

@@ -31,6 +31,62 @@
 #include "esp.hpp"
 #include "esphttp.hpp"
 
+#define SESSION_SDS_LOCK_TIMEOUT (30*1000) // 30 seconds
+#define ESP_SESSION_TIMEOUT (600) // 10 Mins
+#define ESP_CHECK_SESSION_TIMEOUT (30) // 30 seconds
+
+static const char* const SESSION_ID_COOKIE = "ESPSessionID";
+static const char* const SESSION_START_URL_COOKIE = "ESPAuthURL";
+static const char* const DEFAULT_LOGIN_URL = "/esp/files/eclwatch/templates/Login.html";
+static const char* const DEFAULT_UNRESTRICTED_RESOURCE1 = "/favicon.ico";
+static const char* const DEFAULT_UNRESTRICTED_RESOURCE2 = "/esp/files/*,/esp/xslt/*";
+
+//xpath in dali
+static const char* const PathSessionRoot="Sessions";
+static const char* const PathSessionProcess="Process";
+static const char* const PathSessionApplication="Application";
+static const char* const PathSessionSession="Session";
+static const char* const PropSessionID = "@id";
+static const char* const PropSessionExternalID = "@externalid";
+static const char* const PropSessionUserID = "@userid";
+static const char* const PropSessionNetworkAddress = "@netaddr";
+static const char* const PropSessionState = "@state";
+static const char* const PropSessionCreateTime = "@createtime";
+static const char* const PropSessionLastAccessed = "@lastaccessed";
+static const char* const PropSessionTimeoutAt = "@timeoutAt";
+static const char* const PropSessionTimeoutByAdmin = "@timeoutByAdmin";
+static const char* const PropSessionLoginURL = "@loginurl";
+/* The following is an example of session data stored in Dali.
+<Sessions>
+ <Process name="myesp">
+   <Application port="8010">
+    <Session createtime="1497376914"
+             id="3831947145"
+             lastaccessed="1497377015"
+             loginurl="/"
+             netaddr="10.176.152.200"
+             state="1"
+             userid="TheAdmin"/>
+    <Session createtime="1497377427"
+             id="4106750941"
+             lastaccessed="1497377427"
+             loginurl="/"
+             netaddr="10.176.152.200"
+             state="0"/>
+   </Application>
+   <Application port="8002">
+    <Session createtime="1497376989"
+             id="3680948651"
+             lastaccessed="1497377003"
+             loginurl="/"
+             netaddr="10.176.152.200"
+             state="1"
+             userid="TheAdmin"/>
+   </Application>
+ </Process>
+</Sessions>
+ */
+
 interface IEspSecureContext;
 
 ESPHTTP_API IEspContext* createEspContext(IEspSecureContext* secureContext = NULL);

+ 9 - 7
esp/platform/espprotocol.cpp

@@ -153,14 +153,16 @@ const StringBuffer &CEspApplicationPort::getTitleBarHtml(IEspContext& ctx, bool
 {
     if (xslp)
     {
-        StringBuffer titleBarXml;
+        VStringBuffer titleBarXml("<EspHeader><BuildVersion>%s</BuildVersion><ConfigAccess>%d</ConfigAccess>", build_ver, viewConfig);
+
+        const char* authMethod = ctx.getAuthenticationMethod();
+        if (authMethod && !strieq(authMethod, "none") && (ctx.getDomainAuthType() != AuthPerRequestOnly))
+            titleBarXml.append("<LogOut>1</LogOut>");
+
         const char* user = ctx.queryUserId();
-                if (!user || !*user)
-            titleBarXml.appendf("<EspHeader><BuildVersion>%s</BuildVersion><ConfigAccess>%d</ConfigAccess>"
-                "<LoginId>&lt;nobody&gt;</LoginId><NoUser>1</NoUser></EspHeader>", build_ver, viewConfig);
-                else
-            titleBarXml.appendf("<EspHeader><BuildVersion>%s</BuildVersion><ConfigAccess>%d</ConfigAccess>"
-                "<LoginId>%s</LoginId></EspHeader>", build_ver, viewConfig, user);
+        if (user && *user)
+            titleBarXml.appendf("<LoginId>%s</LoginId>", user);
+        titleBarXml.append("</EspHeader>");
 
         if (rawXml)
         {

+ 39 - 0
esp/platform/espsession.ipp

@@ -36,7 +36,10 @@ private:
     int        m_maxage;
     StringAttr m_path;
     bool       m_secure;
+    bool       m_httponly;
     bool       m_discard;
+    StringAttr expires;
+    StringAttr sameSite;
 
 public:
     IMPLEMENT_IINTERFACE;
@@ -49,6 +52,7 @@ public:
         m_version = version;
 
         m_secure = false;
+        m_httponly = false; //For backward compatible
         m_discard = false;
         m_path.set("/");
     }
@@ -156,6 +160,15 @@ public:
         m_secure = flag;
     }
 
+    bool getHTTPOnly()
+    {
+        return m_httponly;
+    }
+    void setHTTPOnly(bool flag)
+    {
+        m_httponly = flag;
+    }
+
     bool getDiscard()
     {
         return m_discard;
@@ -174,6 +187,26 @@ public:
         m_version = version;
     }
 
+    const char* getExpires()
+    {
+        return expires.get();
+    }
+
+    void setExpires(const char* _expires)
+    {
+        expires.set(_expires);
+    }
+
+    const char* getSameSite()
+    {
+        return sameSite.get();
+    }
+
+    void setSameSite(const char* _sameSite)
+    {
+        sameSite.set(_sameSite);
+    }
+
     void appendToRequestHeader(StringBuffer& buf)
     {
         buf.append(m_name.get()).append("=").append(m_value.get());
@@ -197,6 +230,12 @@ public:
             buf.append("; Domain=").append(m_domain.get());
         if(m_secure)
             buf.append("; Secure");
+        if(m_httponly)
+            buf.append("; HttpOnly");
+        if (expires.length() > 0)
+            buf.append("; Expires=").append(expires.get());
+        if (sameSite.length() > 0)
+            buf.append("; SameSite=").append(sameSite.get());
         if(m_version >= 1)
         {
             buf.append("; Version=").append(m_version);     

+ 2 - 0
esp/protocols/http/CMakeLists.txt

@@ -61,6 +61,8 @@ include_directories(
     ./../../services/common
     ./../../../system/security/shared
     ./../../../system/security/LdapSecurity
+    ./../../../system/mp 
+    ./../../../dali/base
     )
 
 add_definitions(-DESPHTTP_EXPORTS -DESP_TIMING -D_USRDLL -DESP_PLUGIN)

+ 13 - 0
esp/scm/esp.ecm

@@ -55,6 +55,13 @@ typedef enum ESPSerializationFormat_
     ESPSerializationCSV
 } ESPSerializationFormat;
 
+typedef enum AuthType_
+{
+    AuthTypeMixed,
+    AuthPerSessionOnly,
+    AuthPerRequestOnly
+} AuthType;
+
 #define ESPCTX_NO_NAMESPACES    0x00000001
 #define ESPCTX_WSDL             0x00000010
 #define ESPCTX_WSDL_EXT         0x00000100
@@ -169,6 +176,12 @@ interface IEspContext : extends IInterface
 
     virtual void setTransactionID(const char * trxid) = 0;
     virtual const char * queryTransactionID() = 0;
+
+    virtual const char * getAuthenticationMethod()=0;
+    virtual void setAuthenticationMethod(const char * method)=0;
+    virtual void setDomainAuthType(AuthType type)=0;
+    virtual AuthType getDomainAuthType()=0;
+    virtual bool addUserToken(unsigned id)=0;
 };
 
 

+ 66 - 1
esp/scm/ws_espcontrol.ecm

@@ -30,9 +30,74 @@ ESPresponse [exceptions_inline, nil_remove, http_encode(0)] SetLoggingResponse
     string Message;
 };
 
-ESPservice [auth_feature("NONE"), version("1.00"), default_client_version("1.00"), exceptions_inline("./smc_xslt/exceptions.xslt")] WSESPControl
+ESPStruct [nil_remove] Session
+{
+    string ID;
+    string UserID;
+    string NetworkAddress;
+    string CreateTime;
+    string LastAccessed;
+    string TimeoutAt;
+    int    Port;
+    bool   TimeoutByAdmin;
+};
+
+ESPrequest [nil_remove] SessionQueryRequest
+{
+    int    Port;
+    string FromIP;
+};
+
+ESPresponse [exceptions_inline, nil_remove, http_encode(0)] SessionQueryResponse
+{
+    ESParray<ESPstruct Session> Sessions;
+};
+
+ESPrequest [nil_remove] SessionInfoRequest
+{
+    int    Port;
+    string ID;
+};
+
+ESPresponse [exceptions_inline, nil_remove, http_encode(0)] SessionInfoResponse
+{
+    ESPstruct Session Session;
+};
+
+ESPrequest [nil_remove] CleanSessionRequest
+{
+    string ID;
+    string FromIP;
+    int    Port;
+};
+
+ESPresponse [exceptions_inline, nil_remove, http_encode(0)] CleanSessionResponse
+{
+    int Status;
+    string Message;
+};
+
+ESPrequest [nil_remove] SetSessionTimeoutRequest
+{
+    string ID;
+    string FromIP;
+    int    Port;
+    int    TimeoutMinutes;
+};
+
+ESPresponse [exceptions_inline, nil_remove, http_encode(0)] SetSessionTimeoutResponse
+{
+    int Status;
+    string Message;
+};
+
+ESPservice [auth_feature("NONE"), version("1.01"), default_client_version("1.01"), exceptions_inline("./smc_xslt/exceptions.xslt")] WSESPControl
 {
     ESPmethod SetLogging(SetLoggingRequest, SetLoggingResponse);
+    ESPmethod [min_ver("1.01")] SessionQuery(SessionQueryRequest, SessionQueryResponse);
+    ESPmethod [min_ver("1.01")] SessionInfo(SessionInfoRequest, SessionInfoResponse);
+    ESPmethod [min_ver("1.01")] CleanSession(CleanSessionRequest, CleanSessionResponse);
+    ESPmethod [min_ver("1.01")] SetSessionTimeout(SetSessionTimeoutRequest, SetSessionTimeoutResponse);
 };
 
 SCMexportdef(WSESPControl);

+ 2 - 0
esp/services/espcontrol/CMakeLists.txt

@@ -41,6 +41,8 @@ include_directories (
          ./../../../system/security/securesocket
          ./../../../system/security/LdapSecurity
          ./../../../system/security/shared
+         ./../../../system/mp 
+         ./../../../dali/base
          ./../../clients
          ./../../bindings
          ./../../bindings/SOAP/xpp

+ 233 - 0
esp/services/espcontrol/ws_espcontrolservice.cpp

@@ -22,6 +22,59 @@
 #include "ws_espcontrolservice.hpp"
 #include "jlib.hpp"
 #include "exception_util.hpp"
+#include "dasds.hpp"
+
+#define SDS_LOCK_TIMEOUT (5*60*1000) // 5 mins
+
+const char* CWSESPControlEx::readSessionTimeStamp(int t, StringBuffer& str)
+{
+    CDateTime time;
+    time.set(t);
+    return time.getString(str).str();
+}
+
+IEspSession* CWSESPControlEx::setSessionInfo(const char* id, IPropertyTree* espSessionTree, unsigned port, IEspSession* session)
+{
+    if (espSessionTree == nullptr)
+        return nullptr;
+
+    StringBuffer createTimeStr, lastAccessedStr, TimeoutAtStr;
+    int lastAccessed = espSessionTree->getPropInt(PropSessionLastAccessed, 0);
+
+    session->setPort(port);
+    session->setID(id);
+    session->setUserID(espSessionTree->queryProp(PropSessionUserID));
+    session->setNetworkAddress(espSessionTree->queryProp(PropSessionNetworkAddress));
+    session->setCreateTime(readSessionTimeStamp(espSessionTree->getPropInt(PropSessionCreateTime, 0), createTimeStr));
+    session->setLastAccessed(readSessionTimeStamp(espSessionTree->getPropInt(PropSessionLastAccessed, 0), lastAccessedStr));
+    session->setTimeoutAt(readSessionTimeStamp(espSessionTree->getPropInt(PropSessionTimeoutAt, 0), TimeoutAtStr));
+    session->setTimeoutByAdmin(espSessionTree->getPropBool(PropSessionTimeoutByAdmin, false));
+    return session;
+}
+
+void CWSESPControlEx::init(IPropertyTree *cfg, const char *process, const char *service)
+{
+    if(cfg == NULL)
+        throw MakeStringException(-1, "Can't initialize CWSESPControlEx, cfg is NULL");
+
+    espProcess.set(process);
+
+    VStringBuffer xpath("Software/EspProcess[@name=\"%s\"]", process);
+    IPropertyTree* espCFG = cfg->queryPropTree(xpath.str());
+    if (!espCFG)
+        throw MakeStringException(-1, "Can't find EspBinding for %s", process);
+
+    Owned<IPropertyTreeIterator> it = espCFG->getElements("AuthDomains/AuthDomain");
+    ForEach(*it)
+    {
+        IPropertyTree& authDomain = it->query();
+        StringBuffer name = authDomain.queryProp("@domainName");
+        if (name.isEmpty())
+            name.set("default");
+        sessionTimeoutMinutesMap.setValue(name.str(), authDomain.getPropInt("@sessionTimeoutMinutes", 0));
+    }
+}
+
 
 bool CWSESPControlEx::onSetLogging(IEspContext& context, IEspSetLoggingRequest& req, IEspSetLoggingResponse& resp)
 {
@@ -51,3 +104,183 @@ bool CWSESPControlEx::onSetLogging(IEspContext& context, IEspSetLoggingRequest&
     }
     return true;
 }
+
+IRemoteConnection* CWSESPControlEx::querySDSConnection(const char* xpath, unsigned mode, unsigned timeout)
+{
+    Owned<IRemoteConnection> globalLock = querySDS().connect(xpath, myProcessSession(), RTM_LOCK_READ, SESSION_SDS_LOCK_TIMEOUT);
+    if (!globalLock)
+        throw MakeStringException(ECLWATCH_INTERNAL_ERROR, "Unable to connect to ESP Session information in dali %s", xpath);
+    return globalLock.getClear();
+}
+
+bool CWSESPControlEx::onSessionQuery(IEspContext& context, IEspSessionQueryRequest& req, IEspSessionQueryResponse& resp)
+{
+    try
+    {
+#ifdef _USE_OPENLDAP
+        CLdapSecManager* secmgr = dynamic_cast<CLdapSecManager*>(context.querySecManager());
+        if(secmgr && !secmgr->isSuperUser(context.queryUser()))
+            throw MakeStringException(ECLWATCH_SUPER_USER_ACCESS_DENIED, "Failed to query session. Permission denied.");
+#endif
+
+        StringBuffer fromIP = req.getFromIP();
+        unsigned port = 8010;
+        if (!req.getPort_isNull())
+            port = req.getPort();
+
+        VStringBuffer xpath("/%s/%s[@name='%s']/%s[@port='%d']", PathSessionRoot, PathSessionProcess,
+            espProcess.get(), PathSessionApplication, port);
+        Owned<IRemoteConnection> globalLock = querySDSConnection(xpath.str(), RTM_LOCK_READ, SESSION_SDS_LOCK_TIMEOUT);
+
+        IArrayOf<IEspSession> sessions;
+        if (!fromIP.trim().isEmpty())
+            xpath.setf("%s[%s='%s']", PathSessionSession, PropSessionNetworkAddress, fromIP.str());
+        else
+            xpath.set("*");
+        Owned<IPropertyTreeIterator> iter = globalLock->queryRoot()->getElements(xpath.str());
+        ForEach(*iter)
+        {
+            IPropertyTree& sessionTree = iter->query();
+            Owned<IEspSession> s = createSession();
+            setSessionInfo(sessionTree.queryProp(PropSessionExternalID), &sessionTree, port, s);
+            sessions.append(*s.getLink());
+        }
+        resp.setSessions(sessions);
+    }
+    catch(IException* e)
+    {
+        FORWARDEXCEPTION(context, e, ECLWATCH_INTERNAL_ERROR);
+    }
+    return true;
+}
+
+bool CWSESPControlEx::onSessionInfo(IEspContext& context, IEspSessionInfoRequest& req, IEspSessionInfoResponse& resp)
+{
+    try
+    {
+#ifdef _USE_OPENLDAP
+        CLdapSecManager* secmgr = dynamic_cast<CLdapSecManager*>(context.querySecManager());
+        if(secmgr && !secmgr->isSuperUser(context.queryUser()))
+            throw MakeStringException(ECLWATCH_SUPER_USER_ACCESS_DENIED, "Failed to get session information. Permission denied.");
+#endif
+
+        StringBuffer id = req.getID();
+        if (id.trim().isEmpty())
+            throw MakeStringException(ECLWATCH_INVALID_INPUT, "ID not specified.");
+
+        unsigned port = 8010;
+        if (!req.getPort_isNull())
+            port = req.getPort();
+
+        VStringBuffer xpath("/%s/%s[@name='%s']/%s[@port='%d']/%s[%s='%s']", PathSessionRoot, PathSessionProcess, espProcess.get(),
+            PathSessionApplication, port, PathSessionSession, PropSessionExternalID, id.str());
+        Owned<IRemoteConnection> globalLock = querySDSConnection(xpath.str(), RTM_LOCK_READ, SESSION_SDS_LOCK_TIMEOUT);
+        setSessionInfo(id.str(), globalLock->queryRoot(), port, &resp.updateSession());
+    }
+    catch(IException* e)
+    {
+        FORWARDEXCEPTION(context, e, ECLWATCH_INTERNAL_ERROR);
+    }
+    return true;
+}
+
+bool CWSESPControlEx::onCleanSession(IEspContext& context, IEspCleanSessionRequest& req, IEspCleanSessionResponse& resp)
+{
+    try
+    {
+#ifdef _USE_OPENLDAP
+        CLdapSecManager* secmgr = dynamic_cast<CLdapSecManager*>(context.querySecManager());
+        if(secmgr && !secmgr->isSuperUser(context.queryUser()))
+            throw MakeStringException(ECLWATCH_SUPER_USER_ACCESS_DENIED, "Failed to clean session. Permission denied.");
+#endif
+
+        StringBuffer id = req.getID();
+        StringBuffer fromIP = req.getFromIP();
+        if ((id.trim().isEmpty()) && (fromIP.trim().isEmpty()))
+            throw MakeStringException(ECLWATCH_INVALID_INPUT, "ID or FromIP has to be specified.");
+
+        unsigned port = 8010;
+        if (!req.getPort_isNull())
+            port = req.getPort();
+
+        VStringBuffer xpath("/%s/%s[@name='%s']/%s[@port='%d']", PathSessionRoot, PathSessionProcess,
+            espProcess.get(), PathSessionApplication, port);
+        Owned<IRemoteConnection> globalLock = querySDSConnection(xpath.str(), RTM_LOCK_WRITE, SESSION_SDS_LOCK_TIMEOUT);
+
+        IArrayOf<IPropertyTree> toRemove;
+        if (!id.isEmpty())
+            xpath.setf("%s[%s='%s']", PathSessionSession, PropSessionExternalID, id.str());
+        else
+            xpath.setf("%s[%s='%s']", PathSessionSession, PropSessionNetworkAddress, fromIP.str());
+        Owned<IPropertyTreeIterator> iter = globalLock->queryRoot()->getElements(xpath.str());
+        ForEach(*iter)
+            toRemove.append(*LINK(&iter->query()));
+        ForEachItemIn(i, toRemove)
+            globalLock->queryRoot()->removeTree(&toRemove.item(i));
+
+        resp.setStatus(0);
+        resp.setMessage("Session is cleaned.");
+    }
+    catch(IException* e)
+    {
+        FORWARDEXCEPTION(context, e, ECLWATCH_INTERNAL_ERROR);
+    }
+    return true;
+}
+
+bool CWSESPControlEx::onSetSessionTimeout(IEspContext& context, IEspSetSessionTimeoutRequest& req, IEspSetSessionTimeoutResponse& resp)
+{
+    try
+    {
+#ifdef _USE_OPENLDAP
+        CLdapSecManager* secmgr = dynamic_cast<CLdapSecManager*>(context.querySecManager());
+        if(secmgr && !secmgr->isSuperUser(context.queryUser()))
+            throw MakeStringException(ECLWATCH_SUPER_USER_ACCESS_DENIED, "Failed to set session timeout. Permission denied.");
+#endif
+
+        StringBuffer id = req.getID();
+        StringBuffer fromIP = req.getFromIP();
+        if (id.trim().isEmpty() && fromIP.trim().isEmpty())
+            throw MakeStringException(ECLWATCH_INVALID_INPUT, "ID or FromIP has to be specified.");
+
+        unsigned port = 8010;
+        if (!req.getPort_isNull())
+            port = req.getPort();
+
+        VStringBuffer xpath("/%s/%s[@name='%s']/%s[@port='%d']", PathSessionRoot, PathSessionProcess,
+            espProcess.get(), PathSessionApplication, port);
+        Owned<IRemoteConnection> globalLock = querySDSConnection(xpath.str(), RTM_LOCK_WRITE, SESSION_SDS_LOCK_TIMEOUT);
+
+        IArrayOf<IPropertyTree> toRemove;
+        int timeoutMinutes = req.getTimeoutMinutes_isNull() ? 0 : req.getTimeoutMinutes();
+        if (!id.isEmpty())
+            xpath.setf("%s[%s='%s']", PathSessionSession, PropSessionExternalID, id.str());
+        else
+            xpath.setf("%s[%s='%s']", PathSessionSession, PropSessionNetworkAddress, fromIP.str());
+        Owned<IPropertyTreeIterator> iter = globalLock->queryRoot()->getElements(xpath.str());
+        ForEach(*iter)
+        {
+            IPropertyTree& item = iter->query();
+            if (timeoutMinutes <= 0)
+                toRemove.append(*LINK(&item));
+            else
+            {
+                CDateTime timeNow;
+                timeNow.setNow();
+                time_t simple = timeNow.getSimple() + timeoutMinutes*60;
+                item.setPropInt64(PropSessionTimeoutAt, simple);
+                item.setPropBool(PropSessionTimeoutByAdmin, true);
+            }
+        }
+        ForEachItemIn(i, toRemove)
+            globalLock->queryRoot()->removeTree(&toRemove.item(i));
+
+        resp.setStatus(0);
+        resp.setMessage("Session timeout is updated.");
+    }
+    catch(IException* e)
+    {
+        FORWARDEXCEPTION(context, e, ECLWATCH_INTERNAL_ERROR);
+    }
+    return true;
+}

+ 13 - 0
esp/services/espcontrol/ws_espcontrolservice.hpp

@@ -22,7 +22,15 @@
 
 class CWSESPControlEx : public CWSESPControl
 {
+    StringAttr espProcess;
+    MapStringTo<int> sessionTimeoutMinutesMap;
     IEspContainer* m_container;
+
+    const char* readSessionTimeStamp(int t, StringBuffer& str);
+    float readSessionTimeoutMin(int sessionTimeoutMinutes, int lastAccessed);
+    IRemoteConnection* querySDSConnection(const char* xpath, unsigned mode, unsigned timeout);
+    IEspSession* setSessionInfo(const char* sessionID, IPropertyTree* espSessionTree, unsigned port, IEspSession* session);
+
 public:
     IMPLEMENT_IINTERFACE;
 
@@ -31,7 +39,12 @@ public:
         m_container = container;
     }
 
+    virtual void init(IPropertyTree *cfg, const char *process, const char *service);
     virtual bool onSetLogging(IEspContext &context, IEspSetLoggingRequest &req, IEspSetLoggingResponse &resp);
+    virtual bool onSessionQuery(IEspContext& context, IEspSessionQueryRequest& req, IEspSessionQueryResponse& resp);
+    virtual bool onSessionInfo(IEspContext& context, IEspSessionInfoRequest& req, IEspSessionInfoResponse& resp);
+    virtual bool onCleanSession(IEspContext& context, IEspCleanSessionRequest& req, IEspCleanSessionResponse& resp);
+    virtual bool onSetSessionTimeout(IEspContext& context, IEspSetSessionTimeoutRequest& req, IEspSetSessionTimeoutResponse& resp);
 };
 
 #endif //_ESPWIZ_ws_espcontrol_HPP__

+ 1 - 0
esp/src/eclwatch/templates/HPCCPlatformWidget.html

@@ -36,6 +36,7 @@
                         <span data-dojo-type="dijit.MenuSeparator"></span>
                         <div id="${id}Configuration" data-dojo-attach-event="onClick:_onOpenConfiguration" data-dojo-type="dijit.MenuItem">${i18n.Configuration}</div>
                         <div id="${id}About" data-dojo-attach-event="onClick:_onAbout" data-dojo-type="dijit.MenuItem">${i18n.About}</div>
+                        <div id="${id}Logout" data-dojo-attach-event="onClick:_onLogout" data-dojo-type="dijit.MenuItem">Logout</div>
                         <div data-dojo-props="hidden:true" data-dojo-type="dijit.PopupMenuItem">
                             <span>${i18n.Debug}</span>
                             <div data-dojo-type="dijit.Menu">

+ 1 - 1
esp/xslt/appframe.xsl

@@ -62,7 +62,7 @@
             ]]></xsl:text>
         </script>
       </head>
-      <frameset rows="62,*" FRAMEPADDING="0" PADDING="0" SPACING="0" FRAMEBORDER="0" onload="onLoad()">
+      <frameset rows="72,*" FRAMEPADDING="0" PADDING="0" SPACING="0" FRAMEBORDER="0" onload="onLoad()">
                 <frame src="esp/titlebar" name="header" target="main" scrolling="no"/>
                 <frameset FRAMEPADDING="0" PADDING="0" SPACING="0" FRAMEBORDER="{@navResize}" BORDERCOLOR="black" FRAMESPACING="1">
                     <xsl:attribute name="cols"><xsl:value-of select="@navWidth"/>,*</xsl:attribute>

+ 20 - 0
esp/xslt/espheader.xsl

@@ -37,6 +37,20 @@
                 return false;
           }
 
+          function logout()
+          {
+            var logoutRequest = new XMLHttpRequest();
+            logoutRequest.onreadystatechange = function()
+            { 
+              if (logoutRequest.readyState == 4 && logoutRequest.status == 200)
+                parent.location = '/esp/files/eclwatch/templates/Login.html';
+              else
+                console.log("Logout failed: " + logoutRequest.status);
+            }
+            logoutRequest.open( "GET", 'logout', true );            
+            logoutRequest.send( null );
+          }
+
           function copy_url(inner)
           {
             var ploc = top.location;
@@ -129,6 +143,12 @@
                     <xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text>
                     <img border="0" src="files_/img/topurl.png" title="No Frames" width="13" height="15" style="cursor:pointer"
                     onclick="top.location.href=top.frames['main'].location.href"/>
+                    <xsl:if test="LogOut = '1'">
+                        <xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text>
+                        <a onclick="return logout();">
+                            Logout
+                        </a>
+                    </xsl:if>
                     <noscript>
                       <span style="color:red;">
                         <small>JavaScript needs to be enabled for the Enterprise Services Platform to work correctly.</small>

+ 9 - 2
initfiles/componentfiles/configxml/esp.xsd.in

@@ -408,11 +408,11 @@
 	                </xs:appinfo>
 	              </xs:annotation>
 	            </xs:attribute>
-	            <xs:attribute name="resourceURL" type="xs:string" use="optional" default="/favicon.ico,/esp/files/img/favicon.ico,/esp/files/eclwatch/img/Loginlogo.png,/esp/files/dojo/*,/esp/files/eclwatch/nls/*">
+	            <xs:attribute name="unrestrictedResources" type="xs:string" use="optional" default="/favicon.ico,/esp/files/*,/esp/xslt/*">
 	              <xs:annotation>
 	                <xs:appinfo>
 	                  <width>50</width>
-	                  <tooltip>Resource URL</tooltip>
+	                  <tooltip>unrestricted resources for user authentication</tooltip>
 	                  <colIndex>6</colIndex>
 	                </xs:appinfo>
 	              </xs:annotation>
@@ -870,6 +870,13 @@
                     </xs:appinfo>
                 </xs:annotation>
             </xs:attribute>
+            <xs:attribute name="checkSessionTimeoutSeconds" type="xs:nonNegativeInteger" use="optional">
+                <xs:annotation>
+                    <xs:appinfo>
+                        <tooltip>Check Session Timeout in every given seconds.</tooltip>
+                    </xs:appinfo>
+                </xs:annotation>
+            </xs:attribute>
         </xs:complexType>
     </xs:element>
 </xs:schema>