61fa65198820bf2d1561f67f63182b529f316459
[shibboleth/sp.git] / shib-target / shib-handlers.cpp
1 /*
2  *  Copyright 2001-2005 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  * shib-handlers.cpp -- profile handlers that plug into SP
19  *
20  * Scott Cantor
21  * 5/17/2005
22  */
23
24 #include "internal.h"
25
26 #include <ctime>
27 #include <saml/SAMLConfig.h>
28 #include <saml/binding/URLEncoder.h>
29 #include <saml/saml2/metadata/Metadata.h>
30 #include <saml/saml2/metadata/EndpointManager.h>
31 #include <saml/util/CommonDomainCookie.h>
32 #include <shibsp/AbstractHandler.h>
33 #include <shibsp/SPConfig.h>
34 #include <shibsp/SPRequest.h>
35
36 #ifdef HAVE_UNISTD_H
37 # include <unistd.h>
38 #endif
39
40 using namespace shibsp;
41 using namespace shibtarget;
42 using namespace shibboleth;
43 using namespace saml;
44 using namespace opensaml::saml2md;
45 using namespace log4cpp;
46 using namespace std;
47
48 using opensaml::CommonDomainCookie;
49 using opensaml::URLEncoder;
50
51 #if defined (_MSC_VER)
52     #pragma warning( push )
53     #pragma warning( disable : 4250 )
54 #endif
55
56 namespace {
57   class SessionInitiator : public AbstractHandler
58   {
59   public:
60     SessionInitiator(const DOMElement* e) : AbstractHandler(e) {}
61     ~SessionInitiator() {}
62     pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
63     pair<bool,long> ShibAuthnRequest(
64         SPRequest& request,
65         const Handler* shire,
66         const char* dest,
67         const char* target,
68         const char* providerId
69         ) const;
70   };
71
72   class SAML1Consumer : public AbstractHandler, public virtual Remoted
73   {
74   public:
75     SAML1Consumer(const DOMElement* e);
76     ~SAML1Consumer();
77     pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
78     void receive(DDF& in, ostream& out);
79   private:
80     string m_address;
81     static int counter;
82   };
83
84   int SAML1Consumer::counter = 0;
85
86   class ShibLogout : public AbstractHandler
87   {
88   public:
89     ShibLogout(const DOMElement* e) : AbstractHandler(e) {}
90     ~ShibLogout() {}
91     pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
92   };
93 }
94
95 #if defined (_MSC_VER)
96     #pragma warning( pop )
97 #endif
98
99 Handler* ShibSessionInitiatorFactory(const DOMElement* const & e)
100 {
101     return new SessionInitiator(e);
102 }
103
104 Handler* SAML1POSTFactory(const DOMElement* const & e)
105 {
106     return new SAML1Consumer(e);
107 }
108
109 Handler* SAML1ArtifactFactory(const DOMElement* const & e)
110 {
111     return new SAML1Consumer(e);
112 }
113
114 Handler* ShibLogoutFactory(const DOMElement* const & e)
115 {
116     return new ShibLogout(e);
117 }
118
119 pair<bool,long> SessionInitiator::run(SPRequest& request, bool isHandler) const
120 {
121     string dupresource;
122     const char* resource=NULL;
123     const Handler* ACS=NULL;
124     const IApplication& app=dynamic_cast<const IApplication&>(request.getApplication());
125     
126     if (isHandler) {
127         /* 
128          * Binding is CGI query string with:
129          *  target      the resource to direct back to later
130          *  acsIndex    optional index of an ACS to use on the way back in
131          *  providerId  optional direct invocation of a specific IdP
132          */
133         const char* option=request.getParameter("acsIndex");
134         if (option)
135             ACS=app.getAssertionConsumerServiceByIndex(atoi(option));
136         option=request.getParameter("providerId");
137         
138         resource=request.getParameter("target");
139         if (!resource || !*resource) {
140             pair<bool,const char*> home=app.getString("homeURL");
141             if (home.first)
142                 resource=home.second;
143             else
144                 throw opensaml::FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
145         }
146         else if (!option) {
147             dupresource=resource;
148             resource=dupresource.c_str();
149         }
150         
151         if (option) {
152             // Here we actually use metadata to invoke the SSO service directly.
153             // The only currently understood binding is the Shibboleth profile.
154             
155             MetadataProvider* m=app.getMetadataProvider();
156             xmltooling::Locker locker(m);
157             const EntityDescriptor* entity=m->getEntityDescriptor(option);
158             if (!entity)
159                 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", xmltooling::params(1,option));
160             const IDPSSODescriptor* role=entity->getIDPSSODescriptor(shibspconstants::SHIB1_PROTOCOL_ENUM);
161             if (!role)
162                 throw MetadataException(
163                     "Session initiator unable to locate a Shibboleth-aware identity provider role for provider ($1).",
164                     xmltooling::params(1,option)
165                     );
166             const EndpointType* ep=EndpointManager<SingleSignOnService>(role->getSingleSignOnServices()).getByBinding(
167                 shibspconstants::SHIB1_AUTHNREQUEST_PROFILE_URI
168                 );
169             if (!ep)
170                 throw MetadataException(
171                     "Session initiator unable to locate compatible SSO service for provider ($1).", xmltooling::params(1,option)
172                     );
173             auto_ptr_char dest(ep->getLocation());
174             return ShibAuthnRequest(
175                 request,ACS ? ACS : app.getDefaultAssertionConsumerService(),dest.get(),resource,app.getString("providerId").second
176                 );
177         }
178     }
179     else {
180         // We're running as a "virtual handler" from within the filter.
181         // The target resource is the current one and everything else is defaulted.
182         resource=request.getRequestURL();
183     }
184     
185     if (!ACS) ACS=app.getDefaultAssertionConsumerService();
186     
187     // For now, we only support external session initiation via a wayfURL
188     pair<bool,const char*> wayfURL=getString("wayfURL");
189     if (!wayfURL.first)
190         throw ConfigurationException("Session initiator is missing wayfURL property.");
191
192     pair<bool,const XMLCh*> wayfBinding=getXMLString("wayfBinding");
193     if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,shibspconstants::SHIB1_AUTHNREQUEST_PROFILE_URI))
194         // Standard Shib 1.x
195         return ShibAuthnRequest(request,ACS,wayfURL.second,resource,app.getString("providerId").second);
196     else if (!strcmp(getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
197         // TODO: Finalize E-Auth profile URI
198         pair<bool,bool> localRelayState=request.getServiceProvider().getPropertySet("InProcess")->getBool("localRelayState");
199         if (!localRelayState.first || !localRelayState.second)
200             throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
201
202         // Here we store the state in a cookie.
203         pair<string,const char*> shib_cookie=app.getCookieNameProps("_shibstate_");
204         string stateval = opensaml::SAMLConfig::getConfig().getURLEncoder()->encode(resource) + shib_cookie.second;
205             request.setCookie(shib_cookie.first.c_str(),stateval.c_str());
206         return make_pair(true, request.sendRedirect(wayfURL.second));
207     }
208    
209     throw opensaml::BindingException("Unsupported WAYF binding ($1).", xmltooling::params(1,getString("wayfBinding").second));
210 }
211
212 // Handles Shib 1.x AuthnRequest profile.
213 pair<bool,long> SessionInitiator::ShibAuthnRequest(
214     SPRequest& request,
215     const Handler* shire,
216     const char* dest,
217     const char* target,
218     const char* providerId
219     ) const
220 {
221     // Compute the ACS URL. We add the ACS location to the base handlerURL.
222     // Legacy configs will not have the Location property specified, so no suffix will be added.
223     string ACSloc=request.getHandlerURL(target);
224     pair<bool,const char*> loc=shire ? shire->getString("Location") : pair<bool,const char*>(false,NULL);
225     if (loc.first) ACSloc+=loc.second;
226     
227     URLEncoder* urlenc = opensaml::SAMLConfig::getConfig().getURLEncoder();
228
229     char timebuf[16];
230     sprintf(timebuf,"%u",time(NULL));
231     string req=string(dest) + "?shire=" + urlenc->encode(ACSloc.c_str()) + "&time=" + timebuf;
232
233     // How should the resource value be preserved?
234     pair<bool,bool> localRelayState=request.getServiceProvider().getPropertySet("InProcess")->getBool("localRelayState");
235     if (!localRelayState.first || !localRelayState.second) {
236         // The old way, just send it along.
237         req+="&target=" + urlenc->encode(target);
238     }
239     else {
240         // Here we store the state in a cookie and send a fixed
241         // value to the IdP so we can recognize it on the way back.
242         pair<string,const char*> shib_cookie=request.getApplication().getCookieNameProps("_shibstate_");
243         string stateval = urlenc->encode(target) + shib_cookie.second;
244         request.setCookie(shib_cookie.first.c_str(),stateval.c_str());
245         req+="&target=cookie";
246     }
247     
248     // Only omitted for 1.1 style requests.
249     if (providerId)
250         req+="&providerId=" + urlenc->encode(providerId);
251
252     return make_pair(true, request.sendRedirect(req.c_str()));
253 }
254
255 SAML1Consumer::SAML1Consumer(const DOMElement* e) : AbstractHandler(e)
256 {
257     m_address += ('A' + (counter++));
258     m_address += "::SAML1Consumer::run";
259
260     // Register for remoted messages.
261     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess))
262         SPConfig::getConfig().getServiceProvider()->getListenerService()->regListener(m_address.c_str(),this);
263 }
264
265 SAML1Consumer::~SAML1Consumer()
266 {
267     ListenerService* listener=SPConfig::getConfig().getServiceProvider()->getListenerService(false);
268     if (listener && SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess))
269         listener->unregListener(m_address.c_str(),this);
270     counter--;
271 }
272
273 /*
274  * IPC message definitions:
275  * 
276  *  [A-Z]::SAML1Consumer::run
277  * 
278  *      IN
279  *      application_id
280  *      client_address
281  *      recipient
282  *      SAMLResponse or SAMLart list
283  * 
284  *      OUT
285  *      key
286  *      provider_id
287  */
288 void SAML1Consumer::receive(DDF& in, ostream& sink)
289 {
290 #ifdef _DEBUG
291     xmltooling::NDC ndc("receive");
292 #endif
293     Category& log=Category::getInstance(SHIBT_LOGCAT".SAML1Consumer");
294
295     // Find application.
296     const char* aid=in["application_id"].string();
297     const IApplication* app=aid ? dynamic_cast<const IApplication*>(SPConfig::getConfig().getServiceProvider()->getApplication(aid)) : NULL;
298     if (!app) {
299         // Something's horribly wrong.
300         log.error("couldn't find application (%s) for new session", aid ? aid : "(missing)");
301         throw SAMLException("Unable to locate application for new session, deleted?");
302     }
303
304     // Check required parameters.
305     const char* client_address=in["client_address"].string();
306     const char* recipient=in["recipient"].string();
307     if (!client_address || !recipient)
308         throw SAMLException("Required parameters missing in call to SAML1Consumer::run");
309     
310     log.debug("processing new assertion for %s", client_address);
311     log.debug("recipient: %s", recipient);
312     log.debug("application: %s", app->getId());
313
314     // Access the application config.
315     ServiceProvider* conf=SPConfig::getConfig().getServiceProvider();
316     xmltooling::Locker confLocker(conf);
317
318     auto_ptr_XMLCh wrecipient(recipient);
319
320     pair<bool,bool> checkAddress=pair<bool,bool>(false,true);
321     pair<bool,bool> checkReplay=pair<bool,bool>(false,true);
322     const PropertySet* props=app->getPropertySet("Sessions");
323     if (props) {
324         checkAddress=props->getBool("checkAddress");
325         if (!checkAddress.first)
326             checkAddress.second=true;
327         checkReplay=props->getBool("checkReplay");
328         if (!checkReplay.first)
329             checkReplay.second=true;
330     }
331
332     // Supports either version...
333     pair<bool,unsigned int> version=getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
334     if (!version.first)
335         version.second=1;
336
337     const EntityDescriptor* provider=NULL;
338     const RoleDescriptor* role=NULL;
339     MetadataProvider* m=app->getMetadataProvider();
340     xmltooling::Locker locker(m);
341     SAMLBrowserProfile::BrowserProfileResponse bpr;
342
343     try {
344         const char* samlResponse=in["SAMLResponse"].string();
345         if (samlResponse) {
346             // POST profile
347             log.debug("executing Browser/POST profile...");
348             bpr=app->getBrowserProfile()->receive(
349                 samlResponse,
350                 wrecipient.get(),
351                 NULL,
352                 version.second
353                 );
354         }
355         else {
356             // Artifact profile
357             vector<const char*> SAMLart;
358             DDF arts=in["SAMLart"];
359             DDF art=arts.first();
360             while (art.isstring()) {
361                 SAMLart.push_back(art.string());
362                 art=arts.next();
363             }
364             auto_ptr<SAMLBrowserProfile::ArtifactMapper> artifactMapper(app->getArtifactMapper());
365             log.debug("executing Browser/Artifact profile...");
366             bpr=app->getBrowserProfile()->receive(
367                 SAMLart,
368                 wrecipient.get(),
369                 artifactMapper.get(),
370                 NULL,
371                 version.second
372                 );
373
374             // Blow it away to clear any locks that might be held.
375             delete artifactMapper.release();
376         }
377
378         // Try and map to metadata (again).
379         // Once the metadata layer is in the SAML core, the repetition will be fixed.
380         provider=m->getEntityDescriptor(bpr.assertion->getIssuer());
381         if (!provider && bpr.authnStatement->getSubject()->getNameIdentifier() &&
382                 bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier())
383             provider=m->getEntityDescriptor(bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier());
384         if (provider) {
385             role=provider->getIDPSSODescriptor(
386                 version.second==1 ? samlconstants::SAML11_PROTOCOL_ENUM : samlconstants::SAML10_PROTOCOL_ENUM
387                 );
388         }
389         
390         // This isn't likely, since the profile must have found a role.
391         if (!role) {
392             MetadataException ex("Unable to locate role-specific metadata for identity provider.");
393             annotateException(&ex,provider); // throws it
394         }
395     
396         // Maybe verify the client address....
397         if (checkAddress.second) {
398             log.debug("verifying client address");
399             // Verify the client address exists
400             const XMLCh* wip = bpr.authnStatement->getSubjectIP();
401             if (wip && *wip) {
402                 // Verify the client address matches authentication
403                 auto_ptr_char this_ip(wip);
404                 if (strcmp(client_address, this_ip.get())) {
405                     opensaml::FatalProfileException ex(
406                        "Your client's current address ($1) differs from the one used when you authenticated "
407                         "to your identity provider. To correct this problem, you may need to bypass a proxy server. "
408                         "Please contact your local support staff or help desk for assistance.",
409                         xmltooling::params(1,client_address)
410                         );
411                     annotateException(&ex,role); // throws it
412                 }
413             }
414         }
415     }
416     catch (exception&) {
417         bpr.clear();
418         throw;
419     }
420     catch (...) {
421         log.error("caught unknown exception");
422         bpr.clear();
423 #ifdef _DEBUG
424         throw;
425 #else
426         SAMLException e("An unexpected error occurred while creating your session.");
427         shibboleth::annotateException(&e,role);
428 #endif
429     }
430
431     // It passes all our tests -- create a new session.
432     log.info("creating new session");
433
434     // Insert into cache.
435     auto_ptr_char authContext(bpr.authnStatement->getAuthMethod());
436     string key=dynamic_cast<ISessionCache*>(conf->getSessionCache())->insert(
437         app,
438         role,
439         client_address,
440         bpr.authnStatement->getSubject(),
441         authContext.get(),
442         bpr.response
443         );
444     // objects owned by cache now
445     log.debug("new session id: %s", key.c_str());
446     auto_ptr_char oname(provider->getEntityID());
447     DDF out=DDF(NULL).structure();
448     out.addmember("key").string(key.c_str());
449     out.addmember("provider_id").string(oname.get());
450
451     sink << out;
452 }
453
454 pair<bool,long> SAML1Consumer::run(SPRequest& request, bool isHandler) const
455 {
456     DDF in,out;
457     DDFJanitor jin(in),jout(out);
458
459     pair<bool,const XMLCh*> binding=getXMLString("Binding");
460     if (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
461 #ifdef HAVE_STRCASECMP
462         if (strcasecmp(request.getMethod(), "POST")) {
463 #else
464         if (_stricmp(request.getMethod(), "POST")) {
465 #endif
466             request.log(SPRequest::SPInfo, "SAML 1.x Browser/POST handler ignoring non-POST request");
467             return pair<bool,long>(false,NULL);
468         }
469 #ifdef HAVE_STRCASECMP
470         if (strcasecmp(request.getContentType().c_str(),"application/x-www-form-urlencoded")) {
471 #else
472         if (_stricmp(request.getContentType().c_str(),"application/x-www-form-urlencoded")) {
473 #endif
474             request.log(SPRequest::SPInfo, "SAML 1.x Browser/POST handler ignoring submission with unknown content-type.");
475             return pair<bool,long>(false,0);
476         }
477
478         const char* samlResponse = request.getParameter("SAMLResponse");
479         if (!samlResponse) {
480             request.log(SPRequest::SPInfo, "SAML 1.x Browser/POST handler ignoring request with no SAMLResponse parameter.");
481             return pair<bool,long>(false,0);
482         }
483
484         in=DDF(m_address.c_str()).structure();
485         in.addmember("SAMLResponse").string(samlResponse);
486     }
487     else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
488 #ifdef HAVE_STRCASECMP
489         if (strcasecmp(request.getMethod(), "GET")) {
490 #else
491         if (_stricmp(request.getMethod(), "GET")) {
492 #endif
493             request.log(SPRequest::SPInfo, "SAML 1.x Browser/Artifact handler ignoring non-GET request");
494             return pair<bool,long>(false,0);
495         }
496
497         vector<const char*> arts;
498         if (request.getParameters("SAMLart",arts)==0) {
499             request.log(SPRequest::SPInfo, "SAML 1.x Browser/Artifact handler ignoring request with no SAMLart parameter.");
500             return pair<bool,long>(false,0);
501         }
502
503         in=DDF(m_address.c_str()).structure();
504         DDF artlist=in.addmember("SAMLart").list();
505
506         for (vector<const char*>::const_iterator a=arts.begin(); a!=arts.end(); ++a)
507             artlist.add(DDF(NULL).string(*a));
508     }
509     
510     // Compute the endpoint location.
511     string hURL=request.getHandlerURL(request.getRequestURL());
512     pair<bool,const char*> loc=getString("Location");
513     string recipient=loc.first ? hURL + loc.second : hURL;
514     in.addmember("recipient").string(recipient.c_str());
515
516     // Add remaining parameters.
517     in.addmember("application_id").string(request.getApplication().getId());
518     in.addmember("client_address").string(request.getRemoteAddr().c_str());
519
520     out=request.getServiceProvider().getListenerService()->send(in);
521     if (!out["key"].isstring())
522         throw opensaml::FatalProfileException("Remote processing of SAML 1.x Browser profile did not return a usable session key.");
523     string key=out["key"].string();
524
525     request.log(SPRequest::SPDebug, string("profile processing succeeded, new session created (") + key + ")");
526
527     const char* target=request.getParameter("TARGET");
528     if (target && !strcmp(target,"default")) {
529         pair<bool,const char*> homeURL=request.getApplication().getString("homeURL");
530         target=homeURL.first ? homeURL.second : "/";
531     }
532     else if (!target || !strcmp(target,"cookie")) {
533         // Pull the target value from the "relay state" cookie.
534         pair<string,const char*> relay_cookie = request.getApplication().getCookieNameProps("_shibstate_");
535         const char* relay_state = request.getCookie(relay_cookie.first.c_str());
536         if (!relay_state || !*relay_state) {
537             // No apparent relay state value to use, so fall back on the default.
538             pair<bool,const char*> homeURL=request.getApplication().getString("homeURL");
539             target=homeURL.first ? homeURL.second : "/";
540         }
541         else {
542             char* rscopy=strdup(relay_state);
543             opensaml::SAMLConfig::getConfig().getURLEncoder()->decode(rscopy);
544             hURL=rscopy;
545             free(rscopy);
546             target=hURL.c_str();
547         }
548         request.setCookie(relay_cookie.first.c_str(),relay_cookie.second);
549     }
550
551     // We've got a good session, set the session cookie.
552     pair<string,const char*> shib_cookie=request.getApplication().getCookieNameProps("_shibsession_");
553     key += shib_cookie.second;
554     request.setCookie(shib_cookie.first.c_str(), key.c_str());
555
556     const char* providerId=out["provider_id"].string();
557     if (providerId) {
558         const PropertySet* sessionProps=request.getApplication().getPropertySet("Sessions");
559         pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
560         if (!idpHistory.first || idpHistory.second) {
561             // Set an IdP history cookie locally (essentially just a CDC).
562             CommonDomainCookie cdc(request.getCookie(CommonDomainCookie::CDCName));
563
564             // Either leave in memory or set an expiration.
565             pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
566             if (!days.first || days.second==0) {
567                 key = string(cdc.set(providerId)) + shib_cookie.second;
568                 request.setCookie(CommonDomainCookie::CDCName, key.c_str());
569             }
570             else {
571                 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
572 #ifdef HAVE_GMTIME_R
573                 struct tm res;
574                 struct tm* ptime=gmtime_r(&now,&res);
575 #else
576                 struct tm* ptime=gmtime(&now);
577 #endif
578                 char timebuf[64];
579                 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
580                 key = string(cdc.set(providerId)) + shib_cookie.second + "; expires=" + timebuf;
581                 request.setCookie(CommonDomainCookie::CDCName, key.c_str());
582             }
583         }
584     }
585
586     // Now redirect to the target.
587     return make_pair(true, request.sendRedirect(target));
588 }
589
590 pair<bool,long> ShibLogout::run(SPRequest& request, bool isHandler) const
591 {
592     // Recover the session key.
593     pair<string,const char*> shib_cookie = request.getApplication().getCookieNameProps("_shibsession_");
594     const char* session_id = request.getCookie(shib_cookie.first.c_str());
595     
596     // Logout is best effort.
597     if (session_id && *session_id) {
598         try {
599             // TODO: port to new cache API
600             //request.getServiceProvider().getSessionCache()->remove(session_id,request.getApplication(),request.getRemoteAddr().c_str());
601         }
602         catch (exception& e) {
603             request.log(SPRequest::SPError, string("logout processing failed with exception: ") + e.what());
604         }
605 #ifndef _DEBUG
606         catch (...) {
607             request.log(SPRequest::SPError, "logout processing failed with unknown exception");
608         }
609 #endif
610         // We send the cookie property alone, which acts as an empty value.
611         request.setCookie(shib_cookie.first.c_str(),shib_cookie.second);
612     }
613     
614     const char* ret=request.getParameter("return");
615     if (!ret)
616         ret=getString("ResponseLocation").second;
617     if (!ret)
618         ret=request.getApplication().getString("homeURL").second;
619     if (!ret)
620         ret="/";
621     return make_pair(true, request.sendRedirect(ret));
622 }