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