Change license header.
[shibboleth/cpp-sp.git] / shibsp / handler / impl / LogoutHandler.cpp
1 /**
2  * Licensed to the University Corporation for Advanced Internet
3  * Development, Inc. (UCAID) under one or more contributor license
4  * agreements. See the NOTICE file distributed with this work for
5  * additional information regarding copyright ownership.
6  *
7  * UCAID licenses this file to you under the Apache License,
8  * Version 2.0 (the "License"); you may not use this file except
9  * in compliance with the License. You may obtain a copy of the
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17  * either express or implied. See the License for the specific
18  * language governing permissions and limitations under the License.
19  */
20
21 /**
22  * LogoutHandler.cpp
23  *
24  * Base class for logout-related handlers.
25  */
26
27 #include "internal.h"
28 #include "exceptions.h"
29 #include "Application.h"
30 #include "ServiceProvider.h"
31 #include "SessionCache.h"
32 #include "SPRequest.h"
33 #include "handler/LogoutHandler.h"
34 #include "util/TemplateParameters.h"
35
36 #include <fstream>
37 #include <xmltooling/XMLToolingConfig.h>
38 #include <xmltooling/util/PathResolver.h>
39 #include <xmltooling/util/URLEncoder.h>
40
41 using namespace shibsp;
42 using namespace xmltooling;
43 using namespace std;
44
45 LogoutHandler::LogoutHandler() : m_initiator(true)
46 {
47 }
48
49 LogoutHandler::~LogoutHandler()
50 {
51 }
52
53 pair<bool,long> LogoutHandler::sendLogoutPage(
54     const Application& application, const HTTPRequest& request, HTTPResponse& response, bool local, const char* status
55     ) const
56 {
57     return sendLogoutPage(application, request, response, local ? "local" : "global");
58 }
59
60 pair<bool,long> LogoutHandler::sendLogoutPage(
61     const Application& application, const HTTPRequest& request, HTTPResponse& response, const char* type
62     ) const
63 {
64     string tname = string(type) + "Logout";
65     const PropertySet* props = application.getPropertySet("Errors");
66     pair<bool,const char*> prop = props ? props->getString(tname.c_str()) : pair<bool,const char*>(false,nullptr);
67     if (!prop.first) {
68         tname += ".html";
69         prop.second = tname.c_str();
70     }
71     response.setContentType("text/html");
72     response.setResponseHeader("Expires","01-Jan-1997 12:00:00 GMT");
73     response.setResponseHeader("Cache-Control","private,no-store,no-cache");
74     string fname(prop.second);
75     ifstream infile(XMLToolingConfig::getConfig().getPathResolver()->resolve(fname, PathResolver::XMLTOOLING_CFG_FILE).c_str());
76     if (!infile)
77         throw ConfigurationException("Unable to access $1 HTML template.", params(1,prop.second));
78     TemplateParameters tp;
79     tp.m_request = &request;
80     tp.setPropertySet(props);
81     tp.m_map["logoutStatus"] = "Logout completed successfully.";  // Backward compatibility.
82     stringstream str;
83     XMLToolingConfig::getConfig().getTemplateEngine()->run(infile, str, tp);
84     return make_pair(true,response.sendResponse(str));
85 }
86
87 pair<bool,long> LogoutHandler::run(SPRequest& request, bool isHandler) const
88 {
89     // If we're inside a chain, do nothing.
90     if (getParent())
91         return make_pair(false,0L);
92
93     // If this isn't a LogoutInitiator, we only "continue" a notification loop, rather than starting one.
94     if (!m_initiator && !request.getParameter("notifying"))
95         return make_pair(false,0L);
96
97     // Try another front-channel notification. No extra parameters and the session is implicit.
98     return notifyFrontChannel(request.getApplication(), request, request);
99 }
100
101 void LogoutHandler::receive(DDF& in, ostream& out)
102 {
103     DDF ret(nullptr);
104     DDFJanitor jout(ret);
105     if (in["notify"].integer() != 1)
106         throw ListenerException("Unsupported operation.");
107
108     // Find application.
109     const char* aid=in["application_id"].string();
110     const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
111     if (!app) {
112         // Something's horribly wrong.
113         Category::getInstance(SHIBSP_LOGCAT".Logout").error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
114         throw ConfigurationException("Unable to locate application for logout, deleted?");
115     }
116
117     vector<string> sessions;
118     DDF s = in["sessions"];
119     DDF temp = s.first();
120     while (temp.isstring()) {
121         sessions.push_back(temp.string());
122         temp = s.next();
123         if (notifyBackChannel(*app, in["url"].string(), sessions, in["local"].integer()==1))
124             ret.integer(1);
125     }
126
127     out << ret;
128 }
129
130 pair<bool,long> LogoutHandler::notifyFrontChannel(
131     const Application& application,
132     const HTTPRequest& request,
133     HTTPResponse& response,
134     const map<string,string>* params
135     ) const
136 {
137     // Index of notification point starts at 0.
138     unsigned int index = 0;
139     const char* param = request.getParameter("index");
140     if (param)
141         index = atoi(param);
142
143     // "return" is a backwards-compatible "eventual destination" to go back to after logout completes.
144     param = request.getParameter("return");
145
146     // Fetch the next front notification URL and bump the index for the next round trip.
147     string loc = application.getNotificationURL(request.getRequestURL(), true, index++);
148     if (loc.empty())
149         return make_pair(false,0L);
150
151     const URLEncoder* encoder = XMLToolingConfig::getConfig().getURLEncoder();
152
153     // Start with an "action" telling the application what this is about.
154     loc = loc + (strchr(loc.c_str(),'?') ? '&' : '?') + "action=logout";
155
156     // Now we create a second URL representing the return location back to us.
157     ostringstream locstr;
158     const char* start = request.getRequestURL();
159     const char* end = strchr(start,'?');
160     string tempstr(start, end ? end-start : strlen(start));
161
162     // Add a signal that we're coming back from notification and the next index.
163     locstr << tempstr << "?notifying=1&index=" << index;
164
165     // Add return if set.
166     if (param)
167         locstr << "&return=" << encoder->encode(param);
168
169     // We preserve anything we're instructed to directly.
170     if (params) {
171         for (map<string,string>::const_iterator p = params->begin(); p!=params->end(); ++p)
172             locstr << '&' << p->first << '=' << encoder->encode(p->second.c_str());
173     }
174     else {
175         for (vector<string>::const_iterator q = m_preserve.begin(); q!=m_preserve.end(); ++q) {
176             param = request.getParameter(q->c_str());
177             if (param)
178                 locstr << '&' << *q << '=' << encoder->encode(param);
179         }
180     }
181
182     // Add the notifier's return parameter to the destination location and redirect.
183     // This is NOT the same as the return parameter that might be embedded inside it ;-)
184     loc = loc + "&return=" + encoder->encode(locstr.str().c_str());
185     return make_pair(true,response.sendRedirect(loc.c_str()));
186 }
187
188 #ifndef SHIBSP_LITE
189 #include "util/SPConstants.h"
190 #include <xmltooling/impl/AnyElement.h>
191 #include <xmltooling/soap/SOAP.h>
192 #include <xmltooling/soap/SOAPClient.h>
193 #include <xmltooling/soap/HTTPSOAPTransport.h>
194 using namespace soap11;
195 namespace {
196     static const XMLCh LogoutNotification[] =   UNICODE_LITERAL_18(L,o,g,o,u,t,N,o,t,i,f,i,c,a,t,i,o,n);
197     static const XMLCh SessionID[] =            UNICODE_LITERAL_9(S,e,s,s,i,o,n,I,D);
198     static const XMLCh _type[] =                UNICODE_LITERAL_4(t,y,p,e);
199     static const XMLCh _local[] =               UNICODE_LITERAL_5(l,o,c,a,l);
200     static const XMLCh _global[] =              UNICODE_LITERAL_6(g,l,o,b,a,l);
201
202     class SHIBSP_DLLLOCAL SOAPNotifier : public soap11::SOAPClient
203     {
204     public:
205         SOAPNotifier() {}
206         virtual ~SOAPNotifier() {}
207     private:
208         void prepareTransport(SOAPTransport& transport) {
209             transport.setVerifyHost(false);
210             HTTPSOAPTransport* http = dynamic_cast<HTTPSOAPTransport*>(&transport);
211             if (http) {
212                 http->useChunkedEncoding(false);
213                 http->setRequestHeader("User-Agent", PACKAGE_NAME);
214                 http->setRequestHeader(PACKAGE_NAME, PACKAGE_VERSION);
215             }
216         }
217     };
218 };
219 #endif
220
221 bool LogoutHandler::notifyBackChannel(
222     const Application& application, const char* requestURL, const vector<string>& sessions, bool local
223     ) const
224 {
225     if (sessions.empty()) {
226         Category::getInstance(SHIBSP_LOGCAT".Logout").error("no sessions supplied to back channel notification method");
227         return false;
228     }
229
230     unsigned int index = 0;
231     string endpoint = application.getNotificationURL(requestURL, false, index++);
232     if (endpoint.empty())
233         return true;
234
235     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
236 #ifndef SHIBSP_LITE
237         auto_ptr<Envelope> env(EnvelopeBuilder::buildEnvelope());
238         Body* body = BodyBuilder::buildBody();
239         env->setBody(body);
240         ElementProxy* msg = new AnyElementImpl(shibspconstants::SHIB2SPNOTIFY_NS, LogoutNotification);
241         body->getUnknownXMLObjects().push_back(msg);
242         msg->setAttribute(xmltooling::QName(nullptr, _type), local ? _local : _global);
243         for (vector<string>::const_iterator s = sessions.begin(); s!=sessions.end(); ++s) {
244             auto_ptr_XMLCh temp(s->c_str());
245             ElementProxy* child = new AnyElementImpl(shibspconstants::SHIB2SPNOTIFY_NS, SessionID);
246             child->setTextContent(temp.get());
247             msg->getUnknownXMLObjects().push_back(child);
248         }
249
250         bool result = true;
251         SOAPNotifier soaper;
252         while (!endpoint.empty()) {
253             try {
254                 soaper.send(*env.get(), SOAPTransport::Address(application.getId(), application.getId(), endpoint.c_str()));
255                 delete soaper.receive();
256             }
257             catch (exception& ex) {
258                 Category::getInstance(SHIBSP_LOGCAT".Logout").error("error notifying application of logout event: %s", ex.what());
259                 result = false;
260             }
261             soaper.reset();
262             endpoint = application.getNotificationURL(requestURL, false, index++);
263         }
264         return result;
265 #else
266         return false;
267 #endif
268     }
269
270     // When not out of process, we remote the back channel work.
271     DDF out,in(m_address.c_str());
272     DDFJanitor jin(in), jout(out);
273     in.addmember("notify").integer(1);
274     in.addmember("application_id").string(application.getId());
275     in.addmember("url").string(requestURL);
276     if (local)
277         in.addmember("local").integer(1);
278     DDF s = in.addmember("sessions").list();
279     for (vector<string>::const_iterator i = sessions.begin(); i!=sessions.end(); ++i) {
280         DDF temp = DDF(nullptr).string(i->c_str());
281         s.add(temp);
282     }
283     out=application.getServiceProvider().getListenerService()->send(in);
284     return (out.integer() == 1);
285 }