Imported Upstream version 2.4+dfsg
[shibboleth/sp.git] / shibsp / handler / impl / SAML2Logout.cpp
1 /*
2  *  Copyright 2001-2010 Internet2
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /**
18  * SAML2Logout.cpp
19  *
20  * Handles SAML 2.0 single logout protocol messages.
21  */
22
23 #include "internal.h"
24 #include "exceptions.h"
25 #include "Application.h"
26 #include "ServiceProvider.h"
27 #include "SPRequest.h"
28 #include "handler/AbstractHandler.h"
29 #include "handler/LogoutHandler.h"
30 #include "util/SPConstants.h"
31
32 #ifndef SHIBSP_LITE
33 # include "SessionCacheEx.h"
34 # include "security/SecurityPolicy.h"
35 # include "security/SecurityPolicyProvider.h"
36 # include "metadata/MetadataProviderCriteria.h"
37 # include "util/TemplateParameters.h"
38 # include <fstream>
39 # include <saml/exceptions.h>
40 # include <saml/SAMLConfig.h>
41 # include <saml/saml2/core/Protocols.h>
42 # include <saml/saml2/metadata/EndpointManager.h>
43 # include <saml/saml2/metadata/Metadata.h>
44 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
45 # include <xmltooling/util/URLEncoder.h>
46 using namespace opensaml::saml2;
47 using namespace opensaml::saml2p;
48 using namespace opensaml::saml2md;
49 using namespace opensaml;
50 #else
51 # include "lite/SAMLConstants.h"
52 #endif
53
54 using namespace shibsp;
55 using namespace xmltooling;
56 using namespace std;
57
58 namespace shibsp {
59
60 #if defined (_MSC_VER)
61     #pragma warning( push )
62     #pragma warning( disable : 4250 )
63 #endif
64
65     class SHIBSP_DLLLOCAL SAML2Logout : public AbstractHandler, public LogoutHandler
66     {
67     public:
68         SAML2Logout(const DOMElement* e, const char* appId);
69         virtual ~SAML2Logout() {
70 #ifndef SHIBSP_LITE
71             if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
72                 delete m_decoder;
73                 XMLString::release(&m_outgoing);
74                 for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair<const XMLCh*,MessageEncoder>());
75             }
76 #endif
77         }
78
79         void receive(DDF& in, ostream& out);
80         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
81
82 #ifndef SHIBSP_LITE
83         void generateMetadata(SPSSODescriptor& role, const char* handlerURL) const {
84             const char* loc = getString("Location").second;
85             string hurl(handlerURL);
86             if (*loc != '/')
87                 hurl += '/';
88             hurl += loc;
89             auto_ptr_XMLCh widen(hurl.c_str());
90             SingleLogoutService* ep = SingleLogoutServiceBuilder::buildSingleLogoutService();
91             ep->setLocation(widen.get());
92             ep->setBinding(getXMLString("Binding").second);
93             role.getSingleLogoutServices().push_back(ep);
94             role.addSupport(samlconstants::SAML20P_NS);
95         }
96
97         const char* getType() const {
98             return "SingleLogoutService";
99         }
100 #endif
101         const XMLCh* getProtocolFamily() const {
102             return samlconstants::SAML20P_NS;
103         }
104
105     private:
106         pair<bool,long> doRequest(const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse) const;
107
108 #ifndef SHIBSP_LITE
109         pair<bool,long> sendResponse(
110             const XMLCh* requestID,
111             const XMLCh* code,
112             const XMLCh* subcode,
113             const char* msg,
114             const char* relayState,
115             const RoleDescriptor* role,
116             const Application& application,
117             HTTPResponse& httpResponse,
118             bool front
119             ) const;
120
121         xmltooling::QName m_role;
122         MessageDecoder* m_decoder;
123         XMLCh* m_outgoing;
124         vector<const XMLCh*> m_bindings;
125         map<const XMLCh*,MessageEncoder*> m_encoders;
126 #endif
127     };
128
129 #if defined (_MSC_VER)
130     #pragma warning( pop )
131 #endif
132
133     Handler* SHIBSP_DLLLOCAL SAML2LogoutFactory(const pair<const DOMElement*,const char*>& p)
134     {
135         return new SAML2Logout(p.first, p.second);
136     }
137 };
138
139 SAML2Logout::SAML2Logout(const DOMElement* e, const char* appId)
140     : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".Logout.SAML2"))
141 #ifndef SHIBSP_LITE
142         ,m_role(samlconstants::SAML20MD_NS, IDPSSODescriptor::LOCAL_NAME), m_decoder(nullptr), m_outgoing(nullptr)
143 #endif
144 {
145     m_initiator = false;
146 #ifndef SHIBSP_LITE
147     m_preserve.push_back("ID");
148     m_preserve.push_back("entityID");
149     m_preserve.push_back("RelayState");
150
151     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
152         SAMLConfig& conf = SAMLConfig::getConfig();
153
154         // Handle incoming binding.
155         m_decoder = conf.MessageDecoderManager.newPlugin(
156             getString("Binding").second, pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS)
157             );
158         m_decoder->setArtifactResolver(SPConfig::getConfig().getArtifactResolver());
159
160         if (m_decoder->isUserAgentPresent()) {
161             // Handle front-channel binding setup.
162             pair<bool,const XMLCh*> outgoing = getXMLString("outgoingBindings", m_configNS.get());
163             if (outgoing.first) {
164                 m_outgoing = XMLString::replicate(outgoing.second);
165                 XMLString::trim(m_outgoing);
166             }
167             else {
168                 // No override, so we'll install a default binding precedence.
169                 string prec = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
170                     samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
171                 m_outgoing = XMLString::transcode(prec.c_str());
172             }
173
174             int pos;
175             XMLCh* start = m_outgoing;
176             while (start && *start) {
177                 pos = XMLString::indexOf(start,chSpace);
178                 if (pos != -1)
179                     *(start + pos)=chNull;
180                 m_bindings.push_back(start);
181                 try {
182                     auto_ptr_char b(start);
183                     MessageEncoder * encoder = conf.MessageEncoderManager.newPlugin(
184                         b.get(), pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS)
185                         );
186                     if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
187                         m_encoders[start] = encoder;
188                         m_log.debug("supporting outgoing binding (%s)", b.get());
189                     }
190                     else {
191                         delete encoder;
192                         m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b.get());
193                     }
194                 }
195                 catch (exception& ex) {
196                     m_log.error("error building MessageEncoder: %s", ex.what());
197                 }
198                 if (pos != -1)
199                     start = start + pos + 1;
200                 else
201                     break;
202             }
203         }
204         else {
205             MessageEncoder* encoder = conf.MessageEncoderManager.newPlugin(
206                 getString("Binding").second, pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS)
207                 );
208             m_encoders.insert(pair<const XMLCh*,MessageEncoder*>(nullptr, encoder));
209         }
210     }
211 #endif
212
213     string address(appId);
214     address += getString("Location").second;
215     setAddress(address.c_str());
216 }
217
218 pair<bool,long> SAML2Logout::run(SPRequest& request, bool isHandler) const
219 {
220     // Defer to base class for front-channel loop first.
221     // This won't initiate the loop, only continue/end it.
222     pair<bool,long> ret = LogoutHandler::run(request, isHandler);
223     if (ret.first)
224         return ret;
225
226     SPConfig& conf = SPConfig::getConfig();
227     if (conf.isEnabled(SPConfig::OutOfProcess)) {
228         // When out of process, we run natively and directly process the message.
229         return doRequest(request.getApplication(), request, request);
230     }
231     else {
232         // When not out of process, we remote all the message processing.
233         vector<string> headers(1,"Cookie");
234         DDF out,in = wrap(request, &headers, true);
235         DDFJanitor jin(in), jout(out);
236         out=request.getServiceProvider().getListenerService()->send(in);
237         return unwrap(request, out);
238     }
239 }
240
241 void SAML2Logout::receive(DDF& in, ostream& out)
242 {
243     // Find application.
244     const char* aid=in["application_id"].string();
245     const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
246     if (!app) {
247         // Something's horribly wrong.
248         m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
249         throw ConfigurationException("Unable to locate application for logout, deleted?");
250     }
251
252     // Unpack the request.
253     auto_ptr<HTTPRequest> req(getRequest(in));
254
255     // Wrap a response shim.
256     DDF ret(nullptr);
257     DDFJanitor jout(ret);
258     auto_ptr<HTTPResponse> resp(getResponse(ret));
259
260     // Since we're remoted, the result should either be a throw, which we pass on,
261     // a false/0 return, which we just return as an empty structure, or a response/redirect,
262     // which we capture in the facade and send back.
263     doRequest(*app, *req.get(), *resp.get());
264     out << ret;
265 }
266
267 pair<bool,long> SAML2Logout::doRequest(const Application& application, const HTTPRequest& request, HTTPResponse& response) const
268 {
269 #ifndef SHIBSP_LITE
270     // First capture the active session ID.
271     SessionCache* cache = application.getServiceProvider().getSessionCache();
272     SessionCacheEx* cacheex = dynamic_cast<SessionCacheEx*>(cache);
273     string session_id = cache->active(application, request);
274
275     if (!strcmp(request.getMethod(),"GET") && request.getParameter("notifying")) {
276         // This is returning from a front-channel notification, so we have to do the back-channel and then
277         // respond. To do that, we need state from the original request.
278         if (!request.getParameter("entityID")) {
279             cache->remove(application, request, &response);
280             throw FatalProfileException("Application notification loop did not return entityID for LogoutResponse.");
281         }
282
283         // Best effort on back channel and to remove the user agent's session.
284         bool worked1 = false,worked2 = false;
285         if (!session_id.empty()) {
286             vector<string> sessions(1,session_id);
287             worked1 = notifyBackChannel(application, request.getRequestURL(), sessions, false);
288             try {
289                 cache->remove(application, request, &response);
290                 worked2 = true;
291             }
292             catch (exception& ex) {
293                 m_log.error("error removing session (%s): %s", session_id.c_str(), ex.what());
294             }
295         }
296         else {
297             worked1 = worked2 = true;
298         }
299
300         // We need metadata to issue a response.
301         MetadataProvider* m = application.getMetadataProvider();
302         Locker metadataLocker(m);
303         MetadataProviderCriteria mc(application, request.getParameter("entityID"), &IDPSSODescriptor::ELEMENT_QNAME, samlconstants::SAML20P_NS);
304         pair<const EntityDescriptor*,const RoleDescriptor*> entity = m->getEntityDescriptor(mc);
305         if (!entity.first) {
306             throw MetadataException(
307                 "Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", request.getParameter("entityID"))
308                 );
309         }
310         else if (!entity.second) {
311             throw MetadataException(
312                 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).",
313                 namedparams(1, "entityID", request.getParameter("entityID"))
314                 );
315         }
316
317         auto_ptr_XMLCh reqid(request.getParameter("ID"));
318         if (worked1 && worked2) {
319             // Successful LogoutResponse. Has to be front-channel or we couldn't be here.
320             return sendResponse(
321                 reqid.get(), StatusCode::SUCCESS, nullptr, nullptr, request.getParameter("RelayState"), entity.second, application, response, true
322                 );
323         }
324
325         return sendResponse(
326             reqid.get(),
327             StatusCode::RESPONDER, nullptr, "Unable to fully destroy principal's session.",
328             request.getParameter("RelayState"),
329             entity.second,
330             application,
331             response,
332             true
333             );
334     }
335
336     // If we get here, it's an external protocol message to decode.
337
338     // Locate policy key.
339     pair<bool,const char*> policyId = getString("policyId", m_configNS.get());  // namespace-qualified if inside handler element
340     if (!policyId.first)
341         policyId = application.getString("policyId");   // unqualified in Application(s) element
342
343     // Lock metadata for use by policy.
344     Locker metadataLocker(application.getMetadataProvider());
345
346     // Create the policy.
347     auto_ptr<SecurityPolicy> policy(
348         application.getServiceProvider().getSecurityPolicyProvider()->createSecurityPolicy(application, &m_role, policyId.second)
349         );
350
351     // Decode the message.
352     string relayState;
353     auto_ptr<XMLObject> msg(m_decoder->decode(relayState, request, *policy.get()));
354     const LogoutRequest* logoutRequest = dynamic_cast<LogoutRequest*>(msg.get());
355     if (logoutRequest) {
356         if (!policy->isAuthenticated())
357             throw SecurityPolicyException("Security of LogoutRequest not established.");
358
359         // Message from IdP to logout one or more sessions.
360
361         // If this is front-channel, we have to have a session_id to use already.
362         if (m_decoder->isUserAgentPresent() && session_id.empty()) {
363             m_log.error("no active session");
364             return sendResponse(
365                 logoutRequest->getID(),
366                 StatusCode::REQUESTER, StatusCode::UNKNOWN_PRINCIPAL, "No active session found in request.",
367                 relayState.c_str(),
368                 policy->getIssuerMetadata(),
369                 application,
370                 response,
371                 true
372                 );
373         }
374
375         bool ownedName = false;
376         NameID* nameid = logoutRequest->getNameID();
377         if (!nameid) {
378             // Check for EncryptedID.
379             EncryptedID* encname = logoutRequest->getEncryptedID();
380             if (encname) {
381                 CredentialResolver* cr=application.getCredentialResolver();
382                 if (!cr)
383                     m_log.warn("found encrypted NameID, but no decryption credential was available");
384                 else {
385                     Locker credlocker(cr);
386                     auto_ptr<MetadataCredentialCriteria> mcc(
387                         policy->getIssuerMetadata() ? new MetadataCredentialCriteria(*policy->getIssuerMetadata()) : nullptr
388                         );
389                     try {
390                         auto_ptr<XMLObject> decryptedID(
391                             encname->decrypt(
392                                 *cr,
393                                 application.getRelyingParty(
394                                     policy->getIssuerMetadata() ?
395                                         dynamic_cast<EntityDescriptor*>(policy->getIssuerMetadata()->getParent()) :
396                                             nullptr)->getXMLString("entityID").second,
397                                 mcc.get()
398                                 )
399                             );
400                         nameid = dynamic_cast<NameID*>(decryptedID.get());
401                         if (nameid) {
402                             ownedName = true;
403                             decryptedID.release();
404                         }
405                     }
406                     catch (exception& ex) {
407                         m_log.error(ex.what());
408                     }
409                 }
410             }
411         }
412         if (!nameid) {
413             // No NameID, so must respond with an error.
414             m_log.error("NameID not found in request");
415             return sendResponse(
416                 logoutRequest->getID(),
417                 StatusCode::REQUESTER, StatusCode::UNKNOWN_PRINCIPAL, "NameID not found in request.",
418                 relayState.c_str(),
419                 policy->getIssuerMetadata(),
420                 application,
421                 response,
422                 m_decoder->isUserAgentPresent()
423                 );
424         }
425
426         auto_ptr<NameID> namewrapper(ownedName ? nameid : nullptr);
427
428         // Suck indexes out of the request for next steps.
429         set<string> indexes;
430         EntityDescriptor* entity = policy->getIssuerMetadata() ? dynamic_cast<EntityDescriptor*>(policy->getIssuerMetadata()->getParent()) : nullptr;
431         const vector<SessionIndex*> sindexes = logoutRequest->getSessionIndexs();
432         for (vector<SessionIndex*>::const_iterator i = sindexes.begin(); i != sindexes.end(); ++i) {
433             auto_ptr_char sindex((*i)->getSessionIndex());
434             indexes.insert(sindex.get());
435         }
436
437         // For a front-channel LogoutRequest, we have to match the information in the request
438         // against the current session.
439         if (!session_id.empty()) {
440             if (!cache->matches(application, request, entity, *nameid, &indexes)) {
441                 return sendResponse(
442                     logoutRequest->getID(),
443                     StatusCode::REQUESTER, StatusCode::REQUEST_DENIED, "Active session did not match logout request.",
444                     relayState.c_str(),
445                     policy->getIssuerMetadata(),
446                     application,
447                     response,
448                     true
449                     );
450             }
451
452         }
453
454         // Now we perform "logout" by finding the matching sessions.
455         vector<string> sessions;
456         try {
457             if (cacheex) {
458                 time_t expires = logoutRequest->getNotOnOrAfter() ? logoutRequest->getNotOnOrAfterEpoch() : 0;
459                 cacheex->logout(application, entity, *nameid, &indexes, expires, sessions);
460                 m_log.debug("session cache returned %d sessions bound to NameID in logout request", sessions.size());
461
462                 // Now we actually terminate everything except for the active session,
463                 // if this is front-channel, for notification purposes.
464                 for (vector<string>::const_iterator sit = sessions.begin(); sit != sessions.end(); ++sit)
465                     if (*sit != session_id)
466                         cacheex->remove(application, sit->c_str());   // using the ID-based removal operation
467             }
468             else {
469                 m_log.warn("session cache does not support extended API, can't implement indirect logout of sessions");
470                 if (!session_id.empty())
471                     sessions.push_back(session_id);
472             }
473         }
474         catch (exception& ex) {
475             m_log.error("error while logging out matching sessions: %s", ex.what());
476             return sendResponse(
477                 logoutRequest->getID(),
478                 StatusCode::RESPONDER, nullptr, ex.what(),
479                 relayState.c_str(),
480                 policy->getIssuerMetadata(),
481                 application,
482                 response,
483                 m_decoder->isUserAgentPresent()
484                 );
485         }
486
487         if (m_decoder->isUserAgentPresent()) {
488             // Pass control to the first front channel notification point, if any.
489             map<string,string> parammap;
490             if (!relayState.empty())
491                 parammap["RelayState"] = relayState;
492             auto_ptr_char entityID(entity ? entity->getEntityID() : nullptr);
493             if (entityID.get())
494                 parammap["entityID"] = entityID.get();
495             auto_ptr_char reqID(logoutRequest->getID());
496             if (reqID.get())
497                 parammap["ID"] = reqID.get();
498             pair<bool,long> result = notifyFrontChannel(application, request, response, &parammap);
499             if (result.first)
500                 return result;
501         }
502
503         // For back-channel requests, or if no front-channel notification is needed...
504         bool worked1 = notifyBackChannel(application, request.getRequestURL(), sessions, false);
505         bool worked2 = true;
506         if (!session_id.empty()) {
507             // One last session to yoink...
508             try {
509                 cache->remove(application, request, &response);
510             }
511             catch (exception& ex) {
512                 worked2 = false;
513                 m_log.error("error removing active session (%s): %s", session_id.c_str(), ex.what());
514             }
515         }
516
517         return sendResponse(
518             logoutRequest->getID(),
519             (worked1 && worked2) ? StatusCode::SUCCESS : StatusCode::RESPONDER,
520             (worked1 && worked2) ? nullptr : StatusCode::PARTIAL_LOGOUT,
521             nullptr,
522             relayState.c_str(),
523             policy->getIssuerMetadata(),
524             application,
525             response,
526             m_decoder->isUserAgentPresent()
527             );
528     }
529
530     // A LogoutResponse completes an SP-initiated logout sequence.
531     const LogoutResponse* logoutResponse = dynamic_cast<LogoutResponse*>(msg.get());
532     if (logoutResponse) {
533         if (!policy->isAuthenticated()) {
534             SecurityPolicyException ex("Security of LogoutResponse not established.");
535             if (policy->getIssuerMetadata())
536                 annotateException(&ex, policy->getIssuerMetadata()); // throws it
537             ex.raise();
538         }
539         checkError(logoutResponse, policy->getIssuerMetadata()); // throws if Status doesn't look good...
540
541         // If relay state is set, recover the original return URL.
542         if (!relayState.empty())
543             recoverRelayState(application, request, response, relayState);
544
545         // Check for partial logout.
546         const StatusCode* sc = logoutResponse->getStatus() ? logoutResponse->getStatus()->getStatusCode() : nullptr;
547         sc = sc ? sc->getStatusCode() : nullptr;
548         if (sc && XMLString::equals(sc->getValue(), StatusCode::PARTIAL_LOGOUT))
549             return sendLogoutPage(application, request, response, "partial");
550
551         if (!relayState.empty())
552             return make_pair(true, response.sendRedirect(relayState.c_str()));
553
554         // Return template for completion of logout.
555         return sendLogoutPage(application, request, response, "global");
556     }
557
558     FatalProfileException ex("Incoming message was not a samlp:LogoutRequest or samlp:LogoutResponse.");
559     if (policy->getIssuerMetadata())
560         annotateException(&ex, policy->getIssuerMetadata()); // throws it
561     ex.raise();
562     return make_pair(false,0L);  // never happen, satisfies compiler
563 #else
564     throw ConfigurationException("Cannot process logout message using lite version of shibsp library.");
565 #endif
566 }
567
568 #ifndef SHIBSP_LITE
569
570 pair<bool,long> SAML2Logout::sendResponse(
571     const XMLCh* requestID,
572     const XMLCh* code,
573     const XMLCh* subcode,
574     const char* msg,
575     const char* relayState,
576     const RoleDescriptor* role,
577     const Application& application,
578     HTTPResponse& httpResponse,
579     bool front
580     ) const
581 {
582     // Get endpoint and encoder to use.
583     const EndpointType* ep = nullptr;
584     const MessageEncoder* encoder = nullptr;
585     if (front) {
586         const IDPSSODescriptor* idp = dynamic_cast<const IDPSSODescriptor*>(role);
587         for (vector<const XMLCh*>::const_iterator b = m_bindings.begin(); idp && b!=m_bindings.end(); ++b) {
588             if (ep=EndpointManager<SingleLogoutService>(idp->getSingleLogoutServices()).getByBinding(*b)) {
589                 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
590                 if (enc!=m_encoders.end())
591                     encoder = enc->second;
592                 break;
593             }
594         }
595         if (!ep || !encoder) {
596             auto_ptr_char id(dynamic_cast<EntityDescriptor*>(role->getParent())->getEntityID());
597             m_log.error("unable to locate compatible SLO service for provider (%s)", id.get());
598             MetadataException ex("Unable to locate endpoint at IdP ($entityID) to send LogoutResponse.");
599             annotateException(&ex, role);   // throws it
600         }
601     }
602     else {
603         encoder = m_encoders.begin()->second;
604     }
605
606     // Prepare response.
607     auto_ptr<LogoutResponse> logout(LogoutResponseBuilder::buildLogoutResponse());
608     logout->setInResponseTo(requestID);
609     if (ep) {
610         const XMLCh* loc = ep->getResponseLocation();
611         if (!loc || !*loc)
612             loc = ep->getLocation();
613         logout->setDestination(loc);
614     }
615     Issuer* issuer = IssuerBuilder::buildIssuer();
616     logout->setIssuer(issuer);
617     issuer->setName(application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role->getParent()))->getXMLString("entityID").second);
618     fillStatus(*logout.get(), code, subcode, msg);
619
620     auto_ptr_char dest(logout->getDestination());
621
622     long ret = sendMessage(*encoder, logout.get(), relayState, dest.get(), role, application, httpResponse);
623     logout.release();  // freed by encoder
624     return make_pair(true,ret);
625 }
626
627 #endif