First set of logout base classes and non-building draft of SP-initiated logout.
[shibboleth/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 #ifndef SHIBSP_LITE
37 #endif
38
39 using namespace shibsp;
40 using namespace xmltooling;
41 using namespace std;
42
43 pair<bool,long> LogoutHandler::run(SPRequest& request, bool isHandler) const
44 {
45     // If no location for this handler, we're inside a chain, so do nothing.
46     if (!getString("Location").first)
47         return make_pair(false,0);
48
49     string session_id;
50     try {
51         // Get the session, ignoring most checks and don't cache the lock.
52         Session* session = request.getSession(false,true,false);
53         if (session)
54             session_id = session->getID();
55         session->unlock();
56     }
57     catch (exception& ex) {
58         log4cpp::Category::getInstance(SHIBSP_LOGCAT".Logout").error("error accessing current session: %s", ex.what());
59     }
60
61     if (session_id.empty())
62         return sendLogoutPage(request.getApplication(), request, true, "The local logout process was only partially completed.");
63
64     // Try another front-channel notification. No extra parameters and the session is implicit.
65     pair<bool,long> ret = notifyFrontChannel(request.getApplication(), request, request);
66     if (ret.first)
67         return ret;
68
69     // Now we complete with back-channel notification, which has to be out of process.
70
71     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
72         // When out of process, we run natively.
73         vector<string> sessions(1,session_id);
74         notifyBackChannel(request.getApplication(), sessions);
75     }
76     else {
77         // When not out of process, we remote the back channel work.
78         DDF out,in(m_address.c_str());
79         DDFJanitor jin(in), jout(out);
80         in.addmember("notify").integer(1);
81         in.addmember("application_id").string(request.getApplication().getId());
82         DDF s = DDF(NULL).string(session_id.c_str());
83         in.addmember("sessions").list().add(s);
84         out=request.getServiceProvider().getListenerService()->send(in);
85     }
86
87     return make_pair(false,0);
88 }
89
90 void LogoutHandler::receive(DDF& in, ostream& out)
91 {
92     DDF ret(NULL);
93     DDFJanitor jout(ret);
94     if (in["notify"].integer() != 1)
95         throw ListenerException("Unsupported operation.");
96
97     // Find application.
98     const char* aid=in["application_id"].string();
99     const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : NULL;
100     if (!app) {
101         // Something's horribly wrong.
102         log4cpp::Category::getInstance(SHIBSP_LOGCAT".Logout").error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
103         throw ConfigurationException("Unable to locate application for logout, deleted?");
104     }
105
106     vector<string> sessions;
107     DDF s = in["sessions"];
108     DDF temp = s.first();
109     while (temp.isstring()) {
110         sessions.push_back(temp.string());
111         temp = s.next();
112         notifyBackChannel(*app, sessions);
113     }
114
115     out << ret;
116 }
117
118 pair<bool,long> LogoutHandler::notifyFrontChannel(
119     const Application& application,
120     const HTTPRequest& request,
121     HTTPResponse& response,
122     const map<string,string>* params,
123     const vector<string>* sessions
124     ) const
125 {
126     // Index of notification point starts at 0.
127     unsigned int index = 0;
128     const char* param = request.getParameter("index");
129     if (param)
130         index = atoi(param);
131
132     // Fetch the next front notification URL and bump the index for the next round trip.
133     string loc = application.getNotificationURL(request, true, index++);
134     if (loc.empty())
135         return make_pair(false,0);
136
137     const URLEncoder* encoder = XMLToolingConfig::getConfig().getURLEncoder();
138
139     // Start with an "action" telling the application what this is about.
140     loc = loc + (strchr(loc.c_str(),'?') ? '&' : '?') + "action=logout";
141
142     // Attach the "sessions" parameter, if any.
143     if (sessions) {
144         string keys;
145         for (vector<string>::const_iterator k = sessions->begin(); k!=sessions->end(); ++k) {
146             if (!keys.empty())
147                 keys += ',';
148             keys += *k;
149         }
150         loc = loc + "&sessions=" + keys;
151     }
152     else if (param = request.getParameter("sessions")) {
153         loc = loc + "&sessions=" + request.getParameter("sessions");
154     }
155
156     // Now we create a second URL representing the return location back to us.
157     const char* start = request.getRequestURL();
158     const char* end = strchr(start,'?');
159     string tempstr(start, end ? end-start : strlen(start));
160     ostringstream locstr(tempstr);
161
162     // Add a signal that we're coming back from notification and the next index.
163     locstr << "?notifying=1&index=" << index;
164
165     // Now we preserve anything we're instructed to (or populate the initial values from the map).
166     if (params) {
167         for (map<string,string>::const_iterator p = params->begin(); p!=params->end(); ++p) {
168             if (!p->second.empty())
169                 locstr << '&' << p->first << '=' << encoder->encode(p->second.c_str());
170             else if (param = request.getParameter(p->first.c_str()))
171                 locstr << '&' << p->first << '=' << encoder->encode(param);
172         }
173     }
174
175     // Add the return parameter to the destination location and redirect.
176     loc = loc + "&return=" + encoder->encode(locstr.str().c_str());
177     return make_pair(true,response.sendRedirect(loc.c_str()));
178 }
179
180 bool LogoutHandler::notifyBackChannel(const Application& application, const vector<string>& sessions) const
181 {
182 #ifndef SHIBSP_LITE
183     return false;
184 #else
185     throw ConfigurationException("Cannot perform back channel notification using lite version of shibsp library.");
186 #endif
187 }
188
189 pair<bool,long> LogoutHandler::sendLogoutPage(const Application& application, HTTPResponse& response, bool local, const char* status) const
190 {
191     pair<bool,const char*> prop = application.getString(local ? "localLogout" : "globalLogout");
192     if (prop.first) {
193         response.setContentType("text/html");
194         response.setResponseHeader("Expires","01-Jan-1997 12:00:00 GMT");
195         response.setResponseHeader("Cache-Control","private,no-store,no-cache");
196         ifstream infile(prop.second);
197         if (!infile)
198             throw ConfigurationException("Unable to access $1 HTML template.", params(1,local ? "localLogout" : "globalLogout"));
199         TemplateParameters tp;
200         tp.setPropertySet(application.getPropertySet("Errors"));
201         if (status)
202             tp.m_map["logoutStatus"] = status;
203         stringstream str;
204         XMLToolingConfig::getConfig().getTemplateEngine()->run(infile, str, tp);
205         return make_pair(true,response.sendResponse(str));
206     }
207     prop = application.getString("homeURL");
208     if (!prop.first)
209         prop.second = "/";
210     return make_pair(true,response.sendRedirect(prop.second));
211 }