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