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