SSPCPP-616 - clean up concatenated string literals
[shibboleth/cpp-sp.git] / shibsp / handler / impl / SAML2LogoutInitiator.cpp
index a4a1115..83a7a02 100644 (file)
@@ -36,6 +36,8 @@
 # include "binding/SOAPClient.h"
 # include "metadata/MetadataProviderCriteria.h"
 # include "security/SecurityPolicy.h"
+# include <boost/algorithm/string.hpp>
+# include <boost/iterator/indirect_iterator.hpp>
 # include <saml/exceptions.h>
 # include <saml/SAMLConfig.h>
 # include <saml/saml2/core/Protocols.h>
@@ -53,6 +55,7 @@ using namespace opensaml;
 
 using namespace shibsp;
 using namespace xmltooling;
+using namespace boost;
 using namespace std;
 
 namespace shibsp {
@@ -66,14 +69,7 @@ namespace shibsp {
     {
     public:
         SAML2LogoutInitiator(const DOMElement* e, const char* appId);
-        virtual ~SAML2LogoutInitiator() {
-#ifndef SHIBSP_LITE
-            if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
-                XMLString::release(&m_outgoing);
-                for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair<const XMLCh*,MessageEncoder>());
-            }
-#endif
-        }
+        virtual ~SAML2LogoutInitiator() {}
 
         void init(const char* location);    // encapsulates actions that need to run either in the c'tor or setParent
 
@@ -91,16 +87,25 @@ namespace shibsp {
             ) const;
 
         string m_appId;
+        auto_ptr_char m_protocol;
 #ifndef SHIBSP_LITE
-        LogoutRequest* buildRequest(
+        auto_ptr<LogoutRequest> buildRequest(
             const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder=nullptr
             ) const;
 
-        XMLCh* m_outgoing;
-        vector<const XMLCh*> m_bindings;
-        map<const XMLCh*,MessageEncoder*> m_encoders;
+        LogoutEvent* newLogoutEvent(
+            const Application& application, const HTTPRequest* request=nullptr, const Session* session=nullptr
+            ) const {
+            LogoutEvent* e = LogoutHandler::newLogoutEvent(application, request, session);
+            if (e)
+                e->m_protocol = m_protocol.get();
+            return e;
+        }
+
+        bool m_async;
+        vector<string> m_bindings;
+        map< string,boost::shared_ptr<MessageEncoder> > m_encoders;
 #endif
-        auto_ptr_char m_protocol;
     };
 
 #if defined (_MSC_VER)
@@ -114,11 +119,10 @@ namespace shibsp {
 };
 
 SAML2LogoutInitiator::SAML2LogoutInitiator(const DOMElement* e, const char* appId)
-    : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".LogoutInitiator.SAML2")), m_appId(appId),
+    : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT ".LogoutInitiator.SAML2")), m_appId(appId), m_protocol(samlconstants::SAML20P_NS)
 #ifndef SHIBSP_LITE
-        m_outgoing(nullptr),
+        ,m_async(true)
 #endif
-        m_protocol(samlconstants::SAML20P_NS)
 {
     // If Location isn't set, defer initialization until the setParent call.
     pair<bool,const char*> loc = getString("Location");
@@ -146,46 +150,37 @@ void SAML2LogoutInitiator::init(const char* location)
 
 #ifndef SHIBSP_LITE
     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
-        // Handle outgoing binding setup.
-        pair<bool,const XMLCh*> outgoing = getXMLString("outgoingBindings");
+        pair<bool,bool> async = getBool("asynchronous");
+        m_async = !async.first || async.second;
+
+        string dupBindings;
+        pair<bool,const char*> outgoing = getString("outgoingBindings");
         if (outgoing.first) {
-            m_outgoing = XMLString::replicate(outgoing.second);
-            XMLString::trim(m_outgoing);
+            dupBindings = outgoing.second;
+            trim(dupBindings);
         }
         else {
             // No override, so we'll install a default binding precedence.
-            string prec = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
+            dupBindings = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
                 samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
-            m_outgoing = XMLString::transcode(prec.c_str());
         }
-
-        int pos;
-        XMLCh* start = m_outgoing;
-        while (start && *start) {
-            pos = XMLString::indexOf(start,chSpace);
-            if (pos != -1)
-                *(start + pos)=chNull;
-            m_bindings.push_back(start);
+        split(m_bindings, dupBindings, is_space(), algorithm::token_compress_on);
+        for (vector<string>::const_iterator b = m_bindings.begin(); b != m_bindings.end(); ++b) {
             try {
-                auto_ptr_char b(start);
-                MessageEncoder * encoder =
-                    SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(), pair<const DOMElement*,const XMLCh*>(getElement(), nullptr));
+                boost::shared_ptr<MessageEncoder> encoder(
+                    SAMLConfig::getConfig().MessageEncoderManager.newPlugin(*b, pair<const DOMElement*,const XMLCh*>(getElement(),nullptr))
+                    );
                 if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
-                    m_encoders[start] = encoder;
-                    m_log.debug("supporting outgoing binding (%s)", b.get());
+                    m_encoders[*b] = encoder;
+                    m_log.debug("supporting outgoing binding (%s)", b->c_str());
                 }
                 else {
-                    delete encoder;
-                    m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b.get());
+                    m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b->c_str());
                 }
             }
-            catch (exception& ex) {
+            catch (std::exception& ex) {
                 m_log.error("error building MessageEncoder: %s", ex.what());
             }
-            if (pos != -1)
-                start = start + pos + 1;
-            else
-                break;
         }
     }
 #endif
@@ -206,17 +201,17 @@ pair<bool,long> SAML2LogoutInitiator::run(SPRequest& request, bool isHandler) co
     try {
         session = request.getSession(false, true, false);  // don't cache it and ignore all checks
         if (!session)
-            return make_pair(false,0L);
+            return make_pair(false, 0L);
 
         // We only handle SAML 2.0 sessions.
         if (!XMLString::equals(session->getProtocol(), m_protocol.get())) {
             session->unlock();
-            return make_pair(false,0L);
+            return make_pair(false, 0L);
         }
     }
-    catch (exception& ex) {
+    catch (std::exception& ex) {
         m_log.error("error accessing current session: %s", ex.what());
-        return make_pair(false,0L);
+        return make_pair(false, 0L);
     }
 
     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
@@ -251,18 +246,18 @@ void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
     }
 
     // Unpack the request.
-    auto_ptr<HTTPRequest> req(getRequest(in));
+    scoped_ptr<HTTPRequest> req(getRequest(in));
 
     // Set up a response shim.
     DDF ret(nullptr);
     DDFJanitor jout(ret);
-    auto_ptr<HTTPResponse> resp(getResponse(ret));
+    scoped_ptr<HTTPResponse> resp(getResponse(ret));
 
     Session* session = nullptr;
     try {
-         session = app->getServiceProvider().getSessionCache()->find(*app, *req.get(), nullptr, nullptr);
+         session = app->getServiceProvider().getSessionCache()->find(*app, *req, nullptr, nullptr);
     }
-    catch (exception& ex) {
+    catch (std::exception& ex) {
         m_log.error("error accessing current session: %s", ex.what());
     }
 
@@ -272,12 +267,12 @@ void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
             // Since we're remoted, the result should either be a throw, which we pass on,
             // a false/0 return, which we just return as an empty structure, or a response/redirect,
             // which we capture in the facade and send back.
-            doRequest(*app, *req.get(), *resp.get(), session);
+            doRequest(*app, *req, *resp, session);
         }
         else {
-             m_log.log(getParent() ? Priority::WARN : Priority::ERROR, "bypassing SAML 2.0 logout, no NameID or issuing entityID found in session");
-             session->unlock();
-             app->getServiceProvider().getSessionCache()->remove(*app, *req.get(), resp.get());
+            session->unlock();
+            m_log.log(getParent() ? Priority::WARN : Priority::ERROR, "bypassing SAML 2.0 logout, no NameID or issuing entityID found in session");
+            app->getServiceProvider().getSessionCache()->remove(*app, *req, resp.get());
         }
     }
     out << ret;
@@ -290,16 +285,28 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
     const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session
     ) const
 {
+    Locker sessionLocker(session, false);
+#ifndef SHIBSP_LITE
+    scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(application, &httpRequest, session));
+#endif
+
     // Do back channel notification.
     vector<string> sessions(1, session->getID());
     if (!notifyBackChannel(application, httpRequest.getRequestURL(), sessions, false)) {
-        session->unlock();
+#ifndef SHIBSP_LITE
+        if (logout_event) {
+            logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
+            application.getServiceProvider().getTransactionLog()->write(*logout_event);
+        }
+#endif
+        sessionLocker.assign();
+        session = nullptr;
         application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
         return sendLogoutPage(application, httpRequest, httpResponse, "partial");
     }
 
 #ifndef SHIBSP_LITE
-    pair<bool,long> ret = make_pair(false,0L);
+    pair<bool,long> ret = make_pair(false, 0L);
     try {
         // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
         MetadataProvider* m = application.getMetadataProvider();
@@ -324,14 +331,14 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
                 );
         }
 
-        const EndpointType* ep=nullptr;
-        const MessageEncoder* encoder=nullptr;
-        vector<const XMLCh*>::const_iterator b;
-        for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
-            if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
-                map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
-                if (enc!=m_encoders.end())
-                    encoder = enc->second;
+        const EndpointType* ep = nullptr;
+        const MessageEncoder* encoder = nullptr;
+        for (vector<string>::const_iterator b = m_bindings.begin(); b != m_bindings.end(); ++b) {
+            auto_ptr_XMLCh wideb(b->c_str());
+            if (ep = EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(wideb.get())) {
+                map< string,boost::shared_ptr<MessageEncoder> >::const_iterator enc = m_encoders.find(*b);
+                if (enc != m_encoders.end())
+                    encoder = enc->second.get();
                 break;
             }
         }
@@ -341,25 +348,34 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
             shibsp::SOAPClient soaper(policy);
             MetadataCredentialCriteria mcc(*role);
 
-            LogoutResponse* logoutResponse=nullptr;
+            LogoutResponse* logoutResponse = nullptr;
+            scoped_ptr<StatusResponseType> srt;
             auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
-            const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
-            for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
+            const vector<SingleLogoutService*>& endpoints = role->getSingleLogoutServices();
+            for (indirect_iterator<vector<SingleLogoutService*>::const_iterator> epit = make_indirect_iterator(endpoints.begin());
+                    !logoutResponse && epit != make_indirect_iterator(endpoints.end()); ++epit) {
                 try {
-                    if (!XMLString::equals((*epit)->getBinding(),binding.get()))
+                    if (!XMLString::equals(epit->getBinding(), binding.get()))
                         continue;
-                    LogoutRequest* msg = buildRequest(application, *session, *role);
-                    auto_ptr_char dest((*epit)->getLocation());
+                    auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role));
+
+                    // Log the request.
+                    if (logout_event) {
+                        logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_UNKNOWN;
+                        logout_event->m_saml2Request = msg.get();
+                        application.getServiceProvider().getTransactionLog()->write(*logout_event);
+                        logout_event->m_saml2Request = nullptr;
+                    }
 
+                    auto_ptr_char dest(epit->getLocation());
                     SAML2SOAPClient client(soaper, false);
-                    client.sendSAML(msg, application.getId(), mcc, dest.get());
-                    StatusResponseType* srt = client.receiveSAML();
-                    if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
-                        delete srt;
+                    client.sendSAML(msg.release(), application.getId(), mcc, dest.get());
+                    srt.reset(client.receiveSAML());
+                    if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt.get()))) {
                         break;
                     }
                 }
-                catch (exception& ex) {
+                catch (std::exception& ex) {
                     m_log.error("error sending LogoutRequest message: %s", ex.what());
                     soaper.reset();
                 }
@@ -371,6 +387,13 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
                     m_log.info("IdP doesn't support single logout protocol over a compatible binding");
                 else
                     m_log.warn("IdP didn't respond to logout request");
+
+                // Log the end result.
+                if (logout_event) {
+                    logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
+                    application.getServiceProvider().getTransactionLog()->write(*logout_event);
+                }
+
                 ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
             }
             else {
@@ -381,22 +404,39 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
                     // Success, but still need to check for partial.
                     partial = XMLString::equals(sc->getStatusCode()->getValue(), StatusCode::PARTIAL_LOGOUT);
                 }
-                delete logoutResponse;
+
+                // Log the end result.
+                if (logout_event) {
+                    logout_event->m_logoutType = partial ? LogoutEvent::LOGOUT_EVENT_PARTIAL : LogoutEvent::LOGOUT_EVENT_GLOBAL;
+                    logout_event->m_saml2Response = logoutResponse;
+                    application.getServiceProvider().getTransactionLog()->write(*logout_event);
+                }
+
                 if (partial)
                     ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
                 else {
                     const char* returnloc = httpRequest.getParameter("return");
                     if (returnloc) {
-                        limitRelayState(m_log, application, httpRequest, returnloc);
-                        ret.second = httpResponse.sendRedirect(returnloc);
+                        // Relative URLs get promoted, absolutes get validated.
+                        if (*returnloc == '/') {
+                            string loc(returnloc);
+                            httpRequest.absolutize(loc);
+                            ret.second = httpResponse.sendRedirect(loc.c_str());
+                        }
+                        else {
+                            application.limitRedirect(httpRequest, returnloc);
+                            ret.second = httpResponse.sendRedirect(returnloc);
+                        }
                         ret.first = true;
                     }
-                    ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
+                    else {
+                        ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
+                    }
                 }
             }
 
             if (session) {
-                session->unlock();
+                sessionLocker.assign();
                 session = nullptr;
                 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
             }
@@ -408,40 +448,51 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
         string relayState;
         const char* returnloc = httpRequest.getParameter("return");
         if (returnloc) {
-            limitRelayState(m_log, application, httpRequest, returnloc);
+            application.limitRedirect(httpRequest, returnloc);
             relayState = returnloc;
+            httpRequest.absolutize(relayState);
+            cleanRelayState(application, httpRequest, httpResponse);
             preserveRelayState(application, httpResponse, relayState);
         }
 
         auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
-
         msg->setDestination(ep->getLocation());
+
+        // Log the request.
+        if (logout_event) {
+            logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_UNKNOWN;
+            logout_event->m_saml2Request = msg.get();
+            application.getServiceProvider().getTransactionLog()->write(*logout_event);
+        }
+
         auto_ptr_char dest(ep->getLocation());
-        ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse);
+        ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse, true);
         ret.first = true;
         msg.release();  // freed by encoder
+
+        if (session) {
+            sessionLocker.assign();
+            session = nullptr;
+            application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
+        }
     }
-    catch (exception& ex) {
-        m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
+    catch (MetadataException& mex) {
+        // Less noise for IdPs that don't support logout (i.e. most)
+        m_log.info("unable to issue SAML 2.0 logout request: %s", mex.what());
     }
-
-    if (session) {
-        session->unlock();
-        session = nullptr;
-        application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
+    catch (std::exception& ex) {
+        m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
     }
 
     return ret;
 #else
-    session->unlock();
-    application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
 #endif
 }
 
 #ifndef SHIBSP_LITE
 
-LogoutRequest* SAML2LogoutInitiator::buildRequest(
+auto_ptr<LogoutRequest> SAML2LogoutInitiator::buildRequest(
     const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder
     ) const
 {
@@ -471,13 +522,24 @@ LogoutRequest* SAML2LogoutInitiator::buildRequest(
             encoder ? encoder->isCompact() : false,
             relyingParty->getXMLString("encryptionAlg").second
             );
-        msg->setEncryptedID(encrypted.release());
+        msg->setEncryptedID(encrypted.get());
+        encrypted.release();
     }
     else {
         msg->setNameID(nameid->cloneNameID());
     }
 
-    return msg.release();
+    XMLCh* msgid = SAMLConfig::getConfig().generateIdentifier();
+    msg->setID(msgid);
+    XMLString::release(&msgid);
+    msg->setIssueInstant(time(nullptr));
+
+    if (m_async && encoder) {
+        msg->setExtensions(saml2p::ExtensionsBuilder::buildExtensions());
+        msg->getExtensions()->getUnknownXMLObjects().push_back(AsynchronousBuilder::buildAsynchronous());
+    }
+
+    return msg;
 }
 
 #endif