Reworked profile handlers outside of core library.
[shibboleth/cpp-sp.git] / shib-target / shib-handlers.cpp
1 /*
2  * The Shibboleth License, Version 1.
3  * Copyright (c) 2002
4  * University Corporation for Advanced Internet Development, Inc.
5  * All rights reserved
6  *
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions are met:
10  *
11  * Redistributions of source code must retain the above copyright notice, this
12  * list of conditions and the following disclaimer.
13  *
14  * Redistributions in binary form must reproduce the above copyright notice,
15  * this list of conditions and the following disclaimer in the documentation
16  * and/or other materials provided with the distribution, if any, must include
17  * the following acknowledgment: "This product includes software developed by
18  * the University Corporation for Advanced Internet Development
19  * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement
20  * may appear in the software itself, if and wherever such third-party
21  * acknowledgments normally appear.
22  *
23  * Neither the name of Shibboleth nor the names of its contributors, nor
24  * Internet2, nor the University Corporation for Advanced Internet Development,
25  * Inc., nor UCAID may be used to endorse or promote products derived from this
26  * software without specific prior written permission. For written permission,
27  * please contact shibboleth@shibboleth.org
28  *
29  * Products derived from this software may not be called Shibboleth, Internet2,
30  * UCAID, or the University Corporation for Advanced Internet Development, nor
31  * may Shibboleth appear in their name, without prior written permission of the
32  * University Corporation for Advanced Internet Development.
33  *
34  *
35  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
38  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
39  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
40  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
41  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT,
42  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
43  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
44  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
46  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
47  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48  */
49
50 /*
51  * shib-handlers.cpp -- profile handlers that plug into SP
52  *
53  * Scott Cantor
54  * 5/17/2005
55  */
56
57 #include "internal.h"
58
59 #ifdef HAVE_UNISTD_H
60 # include <unistd.h>
61 #endif
62
63 #include <shib/shib-threads.h>
64 #include <log4cpp/Category.hh>
65 #include <xercesc/util/Base64.hpp>
66 #include <xercesc/util/regx/RegularExpression.hpp>
67
68 #ifndef HAVE_STRCASECMP
69 # define strcasecmp stricmp
70 #endif
71
72 using namespace std;
73 using namespace saml;
74 using namespace shibboleth;
75 using namespace shibtarget;
76 using namespace log4cpp;
77
78 namespace {
79   class CgiParse
80   {
81   public:
82     CgiParse(const char* data, unsigned int len);
83     ~CgiParse();
84     const char* get_value(const char* name) const;
85     
86     static char x2c(char *what);
87     static void url_decode(char *url);
88     static string url_encode(const char* s);
89   private:
90     char * fmakeword(char stop, unsigned int *cl, const char** ppch);
91     char * makeword(char *line, char stop);
92     void plustospace(char *str);
93
94     map<string,char*> kvp_map;
95   };
96
97   class SessionInitiator : virtual public IHandler
98   {
99   public:
100     SessionInitiator(const DOMElement* e) {}
101     ~SessionInitiator() {}
102     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
103     pair<bool,void*> ShibAuthnRequest(
104         ShibTarget* st,
105         const IPropertySet* shire,
106         const char* dest,
107         const char* target,
108         const char* providerId
109         );
110   };
111
112   class SAML1Consumer : virtual public IHandler
113   {
114   public:
115     SAML1Consumer(const DOMElement* e) {}
116     ~SAML1Consumer() {}
117     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
118   };
119
120   class ShibLogout : virtual public IHandler
121   {
122   public:
123     ShibLogout(const DOMElement* e) {}
124     ~ShibLogout() {}
125     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
126   };
127 }
128
129
130 IPlugIn* ShibSessionInitiatorFactory(const DOMElement* e)
131 {
132     return new SessionInitiator(e);
133 }
134
135 IPlugIn* SAML1POSTFactory(const DOMElement* e)
136 {
137     return new SAML1Consumer(e);
138 }
139
140 IPlugIn* SAML1ArtifactFactory(const DOMElement* e)
141 {
142     return new SAML1Consumer(e);
143 }
144
145 IPlugIn* ShibLogoutFactory(const DOMElement* e)
146 {
147     return new ShibLogout(e);
148 }
149
150 pair<bool,void*> SessionInitiator::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
151 {
152     string dupresource;
153     const char* resource=NULL;
154     const IPropertySet* ACS=NULL;
155     const IApplication* app=st->getApplication();
156     
157     if (isHandler) {
158         /* 
159          * Binding is CGI query string with:
160          *  target      the resource to direct back to later
161          *  acsIndex    optional index of an ACS to use on the way back in
162          *  providerId  optional direct invocation of a specific IdP
163          */
164         string query=st->getArgs();
165         CgiParse parser(query.c_str(),query.length());
166
167         const char* option=parser.get_value("acsIndex");
168         if (option)
169             ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
170         option=parser.get_value("providerId");
171         
172         resource=parser.get_value("target");
173         if (!resource || !*resource) {
174             pair<bool,const char*> home=app->getString("homeURL");
175             if (home.first)
176                 resource=home.second;
177             else
178                 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
179         }
180         else if (!option) {
181             dupresource=resource;
182             resource=dupresource.c_str();
183         }
184         
185         if (option) {
186             // Here we actually use metadata to invoke the SSO service directly.
187             // The only currently understood binding is the Shibboleth profile.
188             Metadata m(app->getMetadataProviders());
189             const IEntityDescriptor* entity=m.lookup(option);
190             if (!entity)
191                 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
192             const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(saml::XML::SAML11_PROTOCOL_ENUM);
193             if (!role)
194                 throw MetadataException(
195                     "Session initiator unable to locate SAML identity provider role for provider ($1).", params(1,option)
196                     );
197             const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
198             const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI);
199             if (!ep)
200                 throw MetadataException(
201                     "Session initiator unable to locate compatible SSO service for provider ($1).", params(1,option)
202                     );
203             auto_ptr_char dest(ep->getLocation());
204             return ShibAuthnRequest(
205                 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
206                 );
207         }
208     }
209     else {
210         // We're running as a "virtual handler" from within the filter.
211         // The target resource is the current one and everything else is defaulted.
212         resource=st->getRequestURL();
213     }
214     
215     if (!ACS) ACS=app->getDefaultAssertionConsumerService();
216     
217     // For now, we only support external session initiation via a wayfURL
218     pair<bool,const char*> wayfURL=handler->getString("wayfURL");
219     if (!wayfURL.first)
220         throw ConfigurationException("Session initiator is missing wayfURL property.");
221
222     pair<bool,const XMLCh*> wayfBinding=handler->getXMLString("wayfBinding");
223     if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI))
224         // Standard Shib 1.x
225         return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
226     else if (!XMLString::compareString(wayfBinding.second,Constants::SHIB_LEGACY_AUTHNREQUEST_PROFILE_URI))
227         // Shib pre-1.2
228         return ShibAuthnRequest(st,ACS,wayfURL.second,resource,NULL);
229     else if (!strcmp(handler->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
230         // TODO: Finalize E-Auth profile URI
231         pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
232         if (!localRelayState.first || !localRelayState.second)
233             throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
234
235         // Here we store the state in a cookie.
236         pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
237         st->setCookie(shib_cookie.first,CgiParse::url_encode(resource) + shib_cookie.second);
238         return make_pair(true, st->sendRedirect(wayfURL.second));
239     }
240    
241     throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,handler->getString("wayfBinding").second));
242 }
243
244 // Handles Shib 1.x AuthnRequest profile.
245 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
246     ShibTarget* st,
247     const IPropertySet* shire,
248     const char* dest,
249     const char* target,
250     const char* providerId
251     )
252 {
253     // Compute the ACS URL. We add the ACS location to the handler baseURL.
254     // Legacy configs will not have an ACS specified, so no suffix will be added.
255     string ACSloc=st->getHandlerURL(target);
256     if (shire) ACSloc+=shire->getString("Location").second;
257     
258     char timebuf[16];
259     sprintf(timebuf,"%u",time(NULL));
260     string req=string(dest) + "?shire=" + CgiParse::url_encode(ACSloc.c_str()) + "&time=" + timebuf;
261
262     // How should the resource value be preserved?
263     pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
264     if (!localRelayState.first || !localRelayState.second) {
265         // The old way, just send it along.
266         req+="&target=" + CgiParse::url_encode(target);
267     }
268     else {
269         // Here we store the state in a cookie and send a fixed
270         // value to the IdP so we can recognize it on the way back.
271         pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
272         st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second);
273         req+="&target=cookie";
274     }
275     
276     // Only omitted for 1.1 style requests.
277     if (providerId)
278         req+="&providerId=" + CgiParse::url_encode(providerId);
279
280     return make_pair(true, st->sendRedirect(req));
281 }
282
283 pair<bool,void*> SAML1Consumer::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
284 {
285     int profile=0;
286     string input,cookie,target,providerId;
287     const IApplication* app=st->getApplication();
288
289     // Right now, this only handles SAML 1.1.
290     pair<bool,const XMLCh*> binding=handler->getXMLString("Binding");
291     if (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
292         if (strcasecmp(st->getRequestMethod(), "POST"))
293             throw FatalProfileException(
294                 "SAML 1.1 Browser/POST handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
295                 );
296         
297         if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded"))
298             throw FatalProfileException(
299                 "Blocked invalid content-type ($1) submitted to SAML 1.1 Browser/POST handler.", params(1,st->getContentType())
300                 );
301         input=st->getPostData();
302         profile|=SAML11_POST;
303     }
304     else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
305         if (strcasecmp(st->getRequestMethod(), "GET"))
306             throw FatalProfileException(
307                 "SAML 1.1 Browser/Artifact handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
308                 );
309         input=st->getArgs();
310         profile|=SAML11_ARTIFACT;
311     }
312     
313     if (input.empty())
314         throw FatalProfileException("SAML 1.1 Browser Profile handler received no data from browser.");
315     
316     string hURL=st->getHandlerURL(st->getRequestURL());
317     pair<bool,const char*> loc=handler->getString("Location");
318     string recipient=loc.first ? hURL + loc.second : hURL;
319     st->getConfig()->getListener()->sessionNew(
320         app,
321         profile,
322         recipient.c_str(),
323         input.c_str(),
324         st->getRemoteAddr(),
325         target,
326         cookie,
327         providerId
328         );
329
330     st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + cookie + ")");
331
332     if (target=="default") {
333         pair<bool,const char*> homeURL=app->getString("homeURL");
334         target=homeURL.first ? homeURL.second : "/";
335     }
336     else if (target=="cookie") {
337         // Pull the target value from the "relay state" cookie.
338         pair<string,const char*> relay_cookie = st->getCookieNameProps("_shibstate_");
339         const char* relay_state = st->getCookie(relay_cookie.first);
340         if (!relay_state || !*relay_state) {
341             // No apparent relay state value to use, so fall back on the default.
342             pair<bool,const char*> homeURL=app->getString("homeURL");
343             target=homeURL.first ? homeURL.second : "/";
344         }
345         else {
346             char* rscopy=strdup(relay_state);
347             CgiParse::url_decode(rscopy);
348             target=rscopy;
349             free(rscopy);
350         }
351     }
352
353     // We've got a good session, set the session cookie.
354     pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibsession_");
355     st->setCookie(shib_cookie.first, cookie + shib_cookie.second);
356
357     const IPropertySet* sessionProps=app->getPropertySet("Sessions");
358     pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
359     if (!idpHistory.first || idpHistory.second) {
360         // Set an IdP history cookie locally (essentially just a CDC).
361         CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
362
363         // Either leave in memory or set an expiration.
364         pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
365             if (!days.first || days.second==0)
366                 st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId.c_str())) + shib_cookie.second);
367             else {
368                 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
369 #ifdef HAVE_GMTIME_R
370                 struct tm res;
371                 struct tm* ptime=gmtime_r(&now,&res);
372 #else
373                 struct tm* ptime=gmtime(&now);
374 #endif
375                 char timebuf[64];
376                 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
377                 st->setCookie(
378                     CommonDomainCookie::CDCName,
379                     string(cdc.set(providerId.c_str())) + shib_cookie.second + "; expires=" + timebuf
380                     );
381         }
382     }
383
384     // Now redirect to the target.
385     return make_pair(true, st->sendRedirect(target));
386 }
387
388 pair<bool,void*> ShibLogout::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
389 {
390     // Recover the session key.
391     pair<string,const char*> shib_cookie = st->getCookieNameProps("_shibsession_");
392     const char* session_id = st->getCookie(shib_cookie.first);
393     
394     // Logout is best effort.
395     if (session_id && *session_id) {
396         try {
397             st->getConfig()->getListener()->sessionEnd(st->getApplication(),session_id);
398         }
399         catch (SAMLException& e) {
400             st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
401         }
402 #ifndef _DEBUG
403         catch (...) {
404             st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
405         }
406 #endif
407         st->setCookie(shib_cookie.first,"");
408     }
409     
410     string query=st->getArgs();
411     CgiParse parser(query.c_str(),query.length());
412
413     const char* ret=parser.get_value("return");
414     if (!ret)
415         ret=handler->getString("ResponseLocation").second;
416     if (!ret)
417         ret=st->getApplication()->getString("homeURL").second;
418     if (!ret)
419         ret="/";
420     return make_pair(true, st->sendRedirect(ret));
421 }
422
423 /*************************************************************************
424  * CGI Parser implementation
425  */
426
427 CgiParse::CgiParse(const char* data, unsigned int len)
428 {
429     const char* pch = data;
430     unsigned int cl = len;
431         
432     while (cl && pch) {
433         char *name;
434         char *value;
435         value=fmakeword('&',&cl,&pch);
436         plustospace(value);
437         url_decode(value);
438         name=makeword(value,'=');
439         kvp_map[name]=value;
440         free(name);
441     }
442 }
443
444 CgiParse::~CgiParse()
445 {
446     for (map<string,char*>::iterator i=kvp_map.begin(); i!=kvp_map.end(); i++)
447         free(i->second);
448 }
449
450 const char*
451 CgiParse::get_value(const char* name) const
452 {
453     map<string,char*>::const_iterator i=kvp_map.find(name);
454     if (i==kvp_map.end())
455         return NULL;
456     return i->second;
457 }
458
459 /* Parsing routines modified from NCSA source. */
460 char *
461 CgiParse::makeword(char *line, char stop)
462 {
463     int x = 0,y;
464     char *word = (char *) malloc(sizeof(char) * (strlen(line) + 1));
465
466     for(x=0;((line[x]) && (line[x] != stop));x++)
467         word[x] = line[x];
468
469     word[x] = '\0';
470     if(line[x])
471         ++x;
472     y=0;
473
474     while(line[x])
475       line[y++] = line[x++];
476     line[y] = '\0';
477     return word;
478 }
479
480 char *
481 CgiParse::fmakeword(char stop, unsigned int *cl, const char** ppch)
482 {
483     int wsize;
484     char *word;
485     int ll;
486
487     wsize = 1024;
488     ll=0;
489     word = (char *) malloc(sizeof(char) * (wsize + 1));
490
491     while(1)
492     {
493         word[ll] = *((*ppch)++);
494         if(ll==wsize-1)
495         {
496             word[ll+1] = '\0';
497             wsize+=1024;
498             word = (char *)realloc(word,sizeof(char)*(wsize+1));
499         }
500         --(*cl);
501         if((word[ll] == stop) || word[ll] == EOF || (!(*cl)))
502         {
503             if(word[ll] != stop)
504                 ll++;
505             word[ll] = '\0';
506             return word;
507         }
508         ++ll;
509     }
510 }
511
512 void
513 CgiParse::plustospace(char *str)
514 {
515     register int x;
516
517     for(x=0;str[x];x++)
518         if(str[x] == '+') str[x] = ' ';
519 }
520
521 char
522 CgiParse::x2c(char *what)
523 {
524     register char digit;
525
526     digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
527     digit *= 16;
528     digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));
529     return(digit);
530 }
531
532 void
533 CgiParse::url_decode(char *url)
534 {
535     register int x,y;
536
537     for(x=0,y=0;url[y];++x,++y)
538     {
539         if((url[x] = url[y]) == '%')
540         {
541             url[x] = x2c(&url[y+1]);
542             y+=2;
543         }
544     }
545     url[x] = '\0';
546 }
547
548 static inline char hexchar(unsigned short s)
549 {
550     return (s<=9) ? ('0' + s) : ('A' + s - 10);
551 }
552
553 string CgiParse::url_encode(const char* s)
554 {
555     static char badchars[]="\"\\+<>#%{}|^~[]`;/?:@=&";
556
557     string ret;
558     for (; *s; s++) {
559         if (strchr(badchars,*s) || *s<=0x1F || *s>=0x7F) {
560             ret+='%';
561         ret+=hexchar(*s >> 4);
562         ret+=hexchar(*s & 0x0F);
563         }
564         else
565             ret+=*s;
566     }
567     return ret;
568 }
569
570 // CDC implementation
571
572 const char CommonDomainCookie::CDCName[] = "_saml_idp";
573
574 CommonDomainCookie::CommonDomainCookie(const char* cookie)
575 {
576     if (!cookie)
577         return;
578
579     Category& log=Category::getInstance(SHIBT_LOGCAT".CommonDomainCookie");
580
581     // Copy it so we can URL-decode it.
582     char* b64=strdup(cookie);
583     CgiParse::url_decode(b64);
584
585     // Chop it up and save off elements.
586     vector<string> templist;
587     char* ptr=b64;
588     while (*ptr) {
589         while (*ptr && isspace(*ptr)) ptr++;
590         char* end=ptr;
591         while (*end && !isspace(*end)) end++;
592         templist.push_back(string(ptr,end-ptr));
593         ptr=end;
594     }
595     free(b64);
596
597     // Now Base64 decode the list.
598     for (vector<string>::iterator i=templist.begin(); i!=templist.end(); i++) {
599         unsigned int len;
600         XMLByte* decoded=Base64::decode(reinterpret_cast<const XMLByte*>(i->c_str()),&len);
601         if (decoded && *decoded) {
602             m_list.push_back(reinterpret_cast<char*>(decoded));
603             XMLString::release(&decoded);
604         }
605         else
606             log.warn("cookie element does not appear to be base64-encoded");
607     }
608 }
609
610 const char* CommonDomainCookie::set(const char* providerId)
611 {
612     // First scan the list for this IdP.
613     for (vector<string>::iterator i=m_list.begin(); i!=m_list.end(); i++) {
614         if (*i == providerId) {
615             m_list.erase(i);
616             break;
617         }
618     }
619     
620     // Append it to the end.
621     m_list.push_back(providerId);
622     
623     // Now rebuild the delimited list.
624     string delimited;
625     for (vector<string>::const_iterator j=m_list.begin(); j!=m_list.end(); j++) {
626         if (!delimited.empty()) delimited += ' ';
627         
628         unsigned int len;
629         XMLByte* b64=Base64::encode(reinterpret_cast<const XMLByte*>(j->c_str()),j->length(),&len);
630         XMLByte *pos, *pos2;
631         for (pos=b64, pos2=b64; *pos2; pos2++)
632             if (isgraph(*pos2))
633                 *pos++=*pos2;
634         *pos=0;
635         
636         delimited += reinterpret_cast<char*>(b64);
637         XMLString::release(&b64);
638     }
639     
640     m_encoded=CgiParse::url_encode(delimited.c_str());
641     return m_encoded.c_str();
642 }