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