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