2 * The Shibboleth License, Version 1.
4 * University Corporation for Advanced Internet Development, Inc.
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions are met:
11 * Redistributions of source code must retain the above copyright notice, this
12 * list of conditions and the following disclaimer.
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.
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
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.
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.
50 /* isapi_shib.cpp - Shibboleth ISAPI filter
62 #include <eduPerson.h>
64 #include <log4cpp/Category.hh>
65 #include <log4cpp/PropertyConfigurator.hh>
66 #include <xercesc/util/Base64.hpp>
73 using namespace log4cpp;
75 using namespace shibboleth;
76 using namespace eduPerson;
85 SAMLBinding* getBinding(const XMLCh* bindingProt);
86 CCacheEntry* find(const char* key);
87 void insert(const char* key, CCacheEntry* entry);
88 void remove(const char* key);
89 void sweep(time_t lifetime);
91 bool lock() { EnterCriticalSection(&m_lock); return true; }
92 void unlock() { LeaveCriticalSection(&m_lock); }
95 SAMLBinding* m_SAMLBinding;
96 map<string,CCacheEntry*> m_hashtable;
97 CRITICAL_SECTION m_lock;
100 // Per-website global structure
104 string g_CookieName; // name of authentication token
105 string g_WAYFLocation; // URL of WAYF service
106 string g_GarbageCollector; // URL of cache garbage collection service
107 string g_SHIRELocation; // URL of SHIRE acceptance point
108 string g_SHIRESessionPath; // path to storage for sessions
109 vector<string> g_MustContain; // simple URL matching string array
110 bool g_bSSLOnly; // only over SSL?
111 time_t g_Lifetime; // maximum token lifetime
112 time_t g_Timeout; // maximum time between uses
113 bool g_bCheckAddress; // validate IP addresses?
114 bool g_bExportAssertion; // export SAML assertion to header?
115 CCache g_AuthCache; // local auth cache
118 settings_t::settings_t()
123 g_bCheckAddress=true;
124 g_bExportAssertion=false;
130 CCacheEntry(const char* sessionFile);
133 SAMLAuthorityBinding* getBinding() { return m_binding; }
134 Iterator<SAMLAttribute*> getAttributes(const char* resource_url, settings_t* pSite);
135 const XMLByte* getSerializedAssertion(const char* resource_url, settings_t* pSite);
136 bool isSessionValid(time_t lifetime, time_t timeout);
137 const XMLCh* getHandle() { return m_handle.c_str(); }
138 const XMLCh* getOriginSite() { return m_originSite.c_str(); }
139 const char* getClientAddress() { return m_clientAddress.c_str(); }
142 void populate(const char* resource_url, settings_t* pSite);
144 xstring m_originSite;
146 SAMLAuthorityBinding* m_binding;
147 string m_clientAddress;
148 SAMLResponse* m_response;
149 SAMLAssertion* m_assertion;
150 time_t m_sessionCreated;
152 XMLByte* m_serialized;
154 static saml::QName g_authorityKind;
155 static saml::QName g_respondWith;
160 saml::QName CCacheEntry::g_authorityKind(saml::XML::SAMLP_NS,L(AttributeQuery));
161 saml::QName CCacheEntry::g_respondWith(saml::XML::SAML_NS,L(AttributeStatement));
165 m_SAMLBinding=SAMLBindingFactory::getInstance();
166 InitializeCriticalSection(&m_lock);
171 DeleteCriticalSection(&m_lock);
172 delete m_SAMLBinding;
173 for (map<string,CCacheEntry*>::iterator i=m_hashtable.begin(); i!=m_hashtable.end(); i++)
177 SAMLBinding* CCache::getBinding(const XMLCh* bindingProt)
179 if (!XMLString::compareString(bindingProt,SAMLBinding::SAML_SOAP_HTTPS))
180 return m_SAMLBinding;
184 CCacheEntry* CCache::find(const char* key)
186 map<string,CCacheEntry*>::const_iterator i=m_hashtable.find(key);
187 if (i==m_hashtable.end())
192 void CCache::insert(const char* key, CCacheEntry* entry)
194 m_hashtable[key]=entry;
197 void CCache::remove(const char* key)
199 m_hashtable.erase(key);
202 void CCache::sweep(time_t lifetime)
204 time_t now=time(NULL);
205 for (map<string,CCacheEntry*>::iterator i=m_hashtable.begin(); i!=m_hashtable.end();)
207 if (lifetime > 0 && now > i->second->m_sessionCreated+lifetime)
210 i=m_hashtable.erase(i);
217 CCacheEntry::CCacheEntry(const char* sessionFile)
218 : m_binding(NULL), m_assertion(NULL), m_response(NULL), m_lastAccess(0), m_sessionCreated(0), m_serialized(NULL)
222 const char* token = NULL;
224 auto_ptr<XMLCh> binding,location;
226 if (!(f=fopen(sessionFile,"r")))
228 fprintf(stderr,"CCacheEntry() could not open session file: %s",sessionFile);
229 throw runtime_error("CCacheEntry() could not open session file");
232 while (fgets(line,1024,f))
234 if ((*line=='#') || (!*line))
241 if (w[strlen(w)-1]=='\n')
244 if (!strcmp("Domain",token))
246 auto_ptr<XMLCh> origin(XMLString::transcode(w));
247 m_originSite=origin.get();
249 else if (!strcmp("Handle",token))
251 auto_ptr<XMLCh> handle(XMLString::transcode(w));
252 m_handle=handle.get();
254 else if (!strcmp("PBinding0",token))
255 binding=auto_ptr<XMLCh>(XMLString::transcode(w));
256 else if (!strcmp("LBinding0",token))
257 location=auto_ptr<XMLCh>(XMLString::transcode(w));
258 else if (!strcmp("Time",token))
259 m_sessionCreated=atoi(w);
260 else if (!strcmp("ClientAddress",token))
262 else if (!strcmp("EOF",token))
267 if (binding.get()!=NULL && location.get()!=NULL)
268 m_binding=new SAMLAuthorityBinding(g_authorityKind,binding.get(),location.get());
270 m_lastAccess=time(NULL);
271 if (!m_sessionCreated)
272 m_sessionCreated=m_lastAccess;
275 CCacheEntry::~CCacheEntry()
279 delete[] m_serialized;
282 bool CCacheEntry::isSessionValid(time_t lifetime, time_t timeout)
284 time_t now=time(NULL);
285 if (lifetime > 0 && now > m_sessionCreated+lifetime)
287 if (timeout > 0 && now-m_lastAccess >= timeout)
293 Iterator<SAMLAttribute*> CCacheEntry::getAttributes(const char* resource_url, settings_t* pSite)
295 populate(resource_url,pSite);
298 Iterator<SAMLStatement*> i=m_assertion->getStatements();
301 SAMLAttributeStatement* s=dynamic_cast<SAMLAttributeStatement*>(i.next());
303 return s->getAttributes();
306 return Iterator<SAMLAttribute*>();
309 const XMLByte* CCacheEntry::getSerializedAssertion(const char* resource_url, settings_t* pSite)
311 populate(resource_url,pSite);
319 return m_serialized=Base64::encode(reinterpret_cast<XMLByte*>(os.str()),os.pcount(),&outlen);
322 void CCacheEntry::populate(const char* resource_url, settings_t* pSite)
325 #define FUNC populate
326 Category& log=Category::getInstance("isapi_shib.CCacheEntry");
328 // Can we use what we have?
329 if (m_assertion && m_assertion->getNotOnOrAfter())
331 // This is awful, but the XMLDateTime class is truly horrible.
332 time_t now=time(NULL);
333 struct tm* ptime=gmtime(&now);
335 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
336 auto_ptr<XMLCh> timeptr(XMLString::transcode(timebuf));
337 XMLDateTime curDateTime(timeptr.get());
338 int result=XMLDateTime::compareOrder(&curDateTime,m_assertion->getNotOnOrAfter());
339 if (XMLDateTime::LESS_THAN)
343 delete[] m_serialized;
348 log.info("%s: cached attributes have expired",FUNC);
354 auto_ptr<XMLCh> resource(XMLString::transcode(resource_url));
356 // Build a SAML Request and send it to the AA.
357 SAMLSubject* subject=new SAMLSubject(m_handle.c_str(),m_originSite.c_str());
358 SAMLAttributeQuery* q=new SAMLAttributeQuery(subject,resource.get());
359 SAMLRequest* req=new SAMLRequest(q,ArrayIterator<saml::QName>(&g_respondWith));
360 SAMLBinding* pBinding=pSite->g_AuthCache.getBinding(m_binding->getBinding());
361 m_response=pBinding->send(*m_binding,*req);
364 // Store off the assertion for quick access. Memory mgmt is based on the response pointer.
365 Iterator<SAMLAssertion*> i=m_response->getAssertions();
367 m_assertion=i.next();
369 auto_ptr<char> h(XMLString::transcode(m_handle.c_str()));
370 auto_ptr<char> d(XMLString::transcode(m_originSite.c_str()));
371 log.info("%s: fetched and stored SAML response for %s@%s",FUNC,h.get(),d.get());
374 class DummyMapper : public IOriginSiteMapper
377 DummyMapper() { InitializeCriticalSection(&m_lock); }
379 virtual Iterator<xstring> getHandleServiceNames(const XMLCh* originSite) { return Iterator<xstring>(); }
380 virtual Key* getHandleServiceKey(const XMLCh* handleService) { return NULL; }
381 virtual Iterator<xstring> getSecurityDomains(const XMLCh* originSite);
382 virtual Iterator<X509Certificate*> getTrustedRoots() { return Iterator<X509Certificate*>(); }
385 typedef map<xstring,vector<xstring>*> domains_t;
387 CRITICAL_SECTION m_lock;
390 Iterator<xstring> DummyMapper::getSecurityDomains(const XMLCh* originSite)
392 EnterCriticalSection(&m_lock);
393 vector<xstring>* pv=NULL;
394 domains_t::iterator i=m_domains.find(originSite);
395 if (i==m_domains.end())
397 pv=new vector<xstring>();
398 pv->push_back(originSite);
399 pair<domains_t::iterator,bool> p=m_domains.insert(domains_t::value_type(originSite,pv));
404 LeaveCriticalSection(&m_lock);
405 return Iterator<xstring>(*pv);
408 DummyMapper::~DummyMapper()
410 for (domains_t::iterator i=m_domains.begin(); i!=m_domains.end(); i++)
412 DeleteCriticalSection(&m_lock);
416 HINSTANCE g_hinstDLL;
417 ULONG g_ulMaxSite=1; // max IIS site instance to handle
418 settings_t* g_Sites=NULL; // array of site settings
419 map<string,string> g_mapAttribNameToHeader; // attribute mapping
420 map<xstring,string> g_mapAttribNames;
423 extern "C" __declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
425 if (fdwReason==DLL_PROCESS_ATTACH)
430 extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer)
435 // Get module pathname and replace file name with ini file name.
436 char inifile[MAX_PATH+1];
437 if (GetModuleFileName(g_hinstDLL,inifile,MAX_PATH+1)==0)
439 char* pch=strrchr(inifile,'\\');
444 strcat(inifile,"isapi_shib.ini");
446 // Read system-wide parameters from isapi_shib.ini.
452 SAMLConfig& SAMLconf=SAMLConfig::getConfig();
454 GetPrivateProfileString("shibboleth","ShibLogConfig","",buf,sizeof(buf),inifile);
456 PropertyConfigurator::configure(buf);
457 Category& log=Category::getInstance("isapi_shib.GetFilterVersion");
458 log.info("using INI file: %s",inifile);
460 GetPrivateProfileString("shibboleth","ShibSchemaPath","",buf,sizeof(buf),inifile);
463 log.fatal("ShibSchemaPath missing");
466 SAMLconf.schema_dir=buf;
467 if (*SAMLconf.schema_dir.end()!='\\')
468 SAMLconf.schema_dir+='\\';
470 GetPrivateProfileString("shibboleth","ShibSSLCertFile","",buf,sizeof(buf),inifile);
473 log.fatal("ShibSSLCertFile missing");
476 SAMLconf.ssl_certfile=buf;
478 GetPrivateProfileString("shibboleth","ShibSSLKeyFile","",buf,sizeof(buf),inifile);
481 log.fatal("ShibSSLKeyFile missing");
484 SAMLconf.ssl_keyfile=buf;
486 GetPrivateProfileString("shibboleth","ShibSSLKeyPass","",buf,sizeof(buf),inifile);
487 SAMLconf.ssl_keypass=buf;
489 GetPrivateProfileString("shibboleth","ShibSSLCAList","",buf,sizeof(buf),inifile);
490 SAMLconf.ssl_calist=buf;
492 // Read site count and allocate site array.
493 g_ulMaxSite=GetPrivateProfileInt("shibboleth","max-site",0,inifile);
496 log.fatal("max-site was 0 or invalid");
499 log.debug("max-site is %d",g_ulMaxSite);
500 g_Sites=new settings_t[g_ulMaxSite];
502 // Read site-specific settings for each site.
503 for (ULONG i=0; i<g_ulMaxSite; i++)
506 GetPrivateProfileString(buf3,"ShibSiteName","X",buf,sizeof(buf),inifile);
507 if (!strcmp(buf,"X"))
509 log.info("skipping site %d (no ShibSiteName)",i);
513 GetPrivateProfileString(buf3,"ShibCookieName","",buf,sizeof(buf),inifile);
517 log.fatal("ShibCookieName missing in site %d",i);
520 g_Sites[i].g_CookieName=buf;
522 GetPrivateProfileString(buf3,"WAYFLocation","",buf,sizeof(buf),inifile);
526 log.fatal("WAYFLocation missing in site %d",i);
529 g_Sites[i].g_WAYFLocation=buf;
531 GetPrivateProfileString(buf3,"GarbageCollector","",buf,sizeof(buf),inifile);
535 log.fatal("GarbageCollector missing in site %d",i);
538 g_Sites[i].g_GarbageCollector=buf;
540 GetPrivateProfileString(buf3,"SHIRELocation","",buf,sizeof(buf),inifile);
544 log.fatal("SHIRELocation missing in site %d",i);
547 g_Sites[i].g_SHIRELocation=buf;
549 GetPrivateProfileString(buf3,"SHIRESessionPath","",buf,sizeof(buf),inifile);
553 log.fatal("SHIRESessionPath missing in site %d",i);
556 g_Sites[i].g_SHIRESessionPath=buf;
557 if (g_Sites[i].g_SHIRESessionPath[g_Sites[i].g_SHIRESessionPath.length()]!='\\')
558 g_Sites[i].g_SHIRESessionPath+='\\';
560 // Old-style matching string.
561 GetPrivateProfileString(buf3,"ShibMustContain","",buf,sizeof(buf),inifile);
564 while (char* sep=strchr(start,';'))
569 g_Sites[i].g_MustContain.push_back(start);
570 log.info("site %d told to match against %s",i,start);
576 g_Sites[i].g_MustContain.push_back(start);
577 log.info("site %d told to match against %s",i,start);
580 if (GetPrivateProfileInt(buf3,"ShibSSLOnly",1,inifile)==0)
581 g_Sites[i].g_bSSLOnly=false;
582 if (GetPrivateProfileInt(buf3,"ShibCheckAddress",1,inifile)==0)
583 g_Sites[i].g_bCheckAddress=false;
584 if (GetPrivateProfileInt(buf3,"ShibExportAssertion",0,inifile)==1)
585 g_Sites[i].g_bExportAssertion=true;
586 g_Sites[i].g_Lifetime=GetPrivateProfileInt(buf3,"ShibAuthLifetime",7200,inifile);
587 if (g_Sites[i].g_Lifetime<=0)
588 g_Sites[i].g_Lifetime=7200;
589 g_Sites[i].g_Timeout=GetPrivateProfileInt(buf3,"ShibAuthTimeout",3600,inifile);
590 if (g_Sites[i].g_Timeout<=0)
591 g_Sites[i].g_Timeout=3600;
592 log.info("configuration of site %d complete",i);
595 ShibConfig& Shibconf=ShibConfig::getConfig();
596 static DummyMapper mapper;
598 if (!SAMLconf.init())
601 log.fatal("SAML initialization failed");
605 Shibconf.origin_mapper=&mapper;
606 if (!Shibconf.init())
609 log.fatal("Shibboleth initialization failed");
614 DWORD res=GetPrivateProfileSection("ShibMapAttributes",buf2,sizeof(buf2),inifile);
615 if (res==sizeof(buf2)-2)
618 log.fatal("ShibMapAttributes INI section was larger than 32k");
622 for (char* attr=buf2; *attr; attr++)
624 char* delim=strchr(attr,'=');
628 log.fatal("unrecognizable ShibMapAttributes directive: %s",attr);
632 g_mapAttribNameToHeader[attr]=(string(delim) + ':');
633 log.info("mapping attribute %s to request header %s",attr,delim);
634 attr=delim + strlen(delim);
637 log.info("configuration of attributes complete");
639 // Transcode the attribute names we know about for quick handling map access.
640 for (map<string,string>::const_iterator j=g_mapAttribNameToHeader.begin();
641 j!=g_mapAttribNameToHeader.end(); j++)
643 auto_ptr<XMLCh> temp(XMLString::transcode(j->first.c_str()));
644 g_mapAttribNames[temp.get()]=j->first;
647 res=GetPrivateProfileSection("ShibExtensions",buf2,sizeof(buf2),inifile);
648 if (res==sizeof(buf2)-2)
651 log.fatal("ShibExtensions INI section was larger than 32k");
655 for (char* libpath=buf2; *libpath; libpath+=strlen(libpath)+1)
656 SAMLconf.saml_register_extension(libpath);
658 log.info("completed loading of extension libraries");
663 Category::getInstance("isapi_shib.GetFilterVersion").fatal("out of memory");
666 catch (log4cpp::ConfigureFailure& ex)
669 WritePrivateProfileString("startlog","bailed-at","log4cpp exception caught",inifile);
670 WritePrivateProfileString("startlog","log4cpp",ex.what(),inifile);
673 catch (SAMLException& ex)
676 Category::getInstance("isapi_shib.GetFilterVersion").fatal("caught SAML exception: %s",ex.what());
680 pVer->dwFilterVersion=HTTP_FILTER_REVISION;
681 strncpy(pVer->lpszFilterDesc,"Shibboleth ISAPI Filter",SF_MAX_FILTER_DESC_LEN);
682 pVer->dwFlags=(SF_NOTIFY_ORDER_HIGH |
683 SF_NOTIFY_SECURE_PORT |
684 SF_NOTIFY_NONSECURE_PORT |
685 SF_NOTIFY_PREPROC_HEADERS |
690 extern "C" BOOL WINAPI TerminateFilter(DWORD dwFlags)
692 Category::getInstance("isapi_shib.TerminateFilter").info("shutting down...");
695 ShibConfig::getConfig().term();
696 SAMLConfig::getConfig().term();
697 Category::getInstance("isapi_shib.TerminateFilter").info("shut down complete");
701 /* Next up, some suck-free versions of various APIs.
703 You DON'T require people to guess the buffer size and THEN tell them the right size.
704 Returning an LPCSTR is apparently way beyond their ken. Not to mention the fact that
705 constant strings aren't typed as such, making it just that much harder. These versions
706 are now updated to use a special growable buffer object, modeled after the standard
707 string class. The standard string won't work because they left out the option to
708 pre-allocate a non-constant buffer.
714 dynabuf() { bufptr=NULL; buflen=0; }
715 dynabuf(size_t s) { bufptr=new char[buflen=s]; *bufptr=0; }
716 ~dynabuf() { delete[] bufptr; }
717 size_t length() const { return bufptr ? strlen(bufptr) : 0; }
718 size_t size() const { return buflen; }
719 bool empty() const { return length()==0; }
720 void reserve(size_t s, bool keep=false);
721 void erase() { if (bufptr) *bufptr=0; }
722 operator char*() { return bufptr; }
723 bool operator ==(const char* s) const;
724 bool operator !=(const char* s) const { return !(*this==s); }
730 void dynabuf::reserve(size_t s, bool keep)
737 p[buflen]=bufptr[buflen];
743 bool dynabuf::operator==(const char* s) const
745 if (buflen==NULL || s==NULL)
746 return (buflen==NULL && s==NULL);
748 return strcmp(bufptr,s)==0;
751 void GetServerVariable(PHTTP_FILTER_CONTEXT pfc,
752 LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
753 throw (bad_alloc, DWORD)
759 while (!pfc->GetServerVariable(pfc,lpszVariable,s,&size))
761 // Grumble. Check the error.
762 DWORD e=GetLastError();
763 if (e==ERROR_INSUFFICIENT_BUFFER)
768 if (bRequired && s.empty())
772 void GetHeader(PHTTP_FILTER_PREPROC_HEADERS pn, PHTTP_FILTER_CONTEXT pfc,
773 LPSTR lpszName, dynabuf& s, DWORD size=80, bool bRequired=true)
774 throw (bad_alloc, DWORD)
780 while (!pn->GetHeader(pfc,lpszName,s,&size))
782 // Grumble. Check the error.
783 DWORD e=GetLastError();
784 if (e==ERROR_INSUFFICIENT_BUFFER)
789 if (bRequired && s.empty())
793 inline char hexchar(unsigned short s)
795 return (s<=9) ? ('0' + s) : ('A' + s - 10);
798 string url_encode(const char* url) throw (bad_alloc)
800 static char badchars[]="\"\\+<>#%{}|^~[]`;/?:@=&";
802 for (const char* pch=url; *pch; pch++)
804 if (strchr(badchars,*pch)!=NULL || *pch<=0x1F || *pch>=0x7F)
805 s=s + '%' + hexchar(*pch >> 4) + hexchar(*pch & 0x0F);
812 string get_target(PHTTP_FILTER_CONTEXT pfc, PHTTP_FILTER_PREPROC_HEADERS pn, settings_t* pSite)
814 // Reconstructing the requested URL is not fun. Apparently, the PREPROC_HEADERS
815 // event means way pre. As in, none of the usual CGI headers are in place yet.
816 // It's actually almost easier, in a way, because all the path-info and query
817 // stuff is in one place, the requested URL, which we can get. But we have to
818 // reconstruct the protocol/host pair using tweezers.
820 if (pfc->fIsSecurePort)
826 GetServerVariable(pfc,"SERVER_NAME",buf);
829 GetServerVariable(pfc,"SERVER_PORT",buf,10);
830 if (buf!=(pfc->fIsSecurePort ? "443" : "80"))
831 s=s + ':' + static_cast<char*>(buf);
833 GetHeader(pn,pfc,"url",buf,256,false);
839 string get_shire_location(PHTTP_FILTER_CONTEXT pfc, settings_t* pSite, const char* target)
841 if (pSite->g_SHIRELocation[0]!='/')
842 return url_encode(pSite->g_SHIRELocation.c_str());
843 const char* colon=strchr(target,':');
844 const char* slash=strchr(colon+3,'/');
845 string s(target,slash-target);
846 s+=pSite->g_SHIRELocation;
847 return url_encode(s.c_str());
850 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg)
852 Category::getInstance("isapi_shib.WriteClientError").error("sending error page to browser: %s",msg);
854 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",0,0);
855 static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Filter Error</TITLE></HEAD><BODY>"
856 "<H1>Shibboleth Filter Error</H1>";
857 DWORD resplen=strlen(xmsg);
858 pfc->WriteClient(pfc,(LPVOID)xmsg,&resplen,0);
860 pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
861 static const char* xmsg2="</BODY></HTML>";
862 resplen=strlen(xmsg2);
863 pfc->WriteClient(pfc,(LPVOID)xmsg2,&resplen,0);
864 return SF_STATUS_REQ_FINISHED;
867 DWORD shib_shar_error(PHTTP_FILTER_CONTEXT pfc, SAMLException& e)
869 Category::getInstance("isapi_shib.shib_shar_error").errorStream()
870 << "exception during SHAR request: " << e;
872 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",0,0);
874 static const char* msg="<HTML><HEAD><TITLE>Shibboleth Attribute Exchange Failed</TITLE></HEAD>\n"
875 "<BODY><H3>Shibboleth Attribute Exchange Failed</H3>\n"
876 "While attempting to securely contact your origin site to obtain "
877 "information about you, an error occurred:<BR><BLOCKQUOTE>";
878 DWORD resplen=strlen(msg);
879 pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
881 const char* msg2=e.what();
882 resplen=strlen(msg2);
883 pfc->WriteClient(pfc,(LPVOID)msg2,&resplen,0);
886 Iterator<saml::QName> i=e.getCodes();
887 if (i.hasNext() && XMLString::compareString(L(Responder),i.next().getLocalName()))
890 const char* msg4=(origin ? "</BLOCKQUOTE><P>The error appears to be located at your origin site.<BR>" :
891 "</BLOCKQUOTE><P>The error appears to be located at the resource provider's site.<BR>");
892 resplen=strlen(msg4);
893 pfc->WriteClient(pfc,(LPVOID)msg4,&resplen,0);
895 static const char* msg5="<P>Try restarting your browser and accessing the site again to make "
896 "sure the problem isn't temporary. Please contact the administrator "
897 "of that site if this problem recurs. If possible, provide him/her "
898 "with the error message shown above.</BODY></HTML>";
899 resplen=strlen(msg5);
900 pfc->WriteClient(pfc,(LPVOID)msg5,&resplen,0);
901 return SF_STATUS_REQ_FINISHED;
904 extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification)
906 // Is this a log notification?
907 if (notificationType==SF_NOTIFY_LOG)
909 if (pfc->pFilterContext)
910 ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=static_cast<LPCSTR>(pfc->pFilterContext);
911 return SF_STATUS_REQ_NEXT_NOTIFICATION;
915 settings_t* pSite=NULL;
917 PHTTP_FILTER_PREPROC_HEADERS pn=(PHTTP_FILTER_PREPROC_HEADERS)pvNotification;
918 Category& log=Category::getInstance("isapi_shib.HttpFilterProc");
921 // Determine web site number.
924 GetServerVariable(pfc,"INSTANCE_ID",buf,10);
925 if ((site_id=strtoul(buf,NULL,10))==0)
926 return WriteClientError(pfc,"IIS site instance appears to be invalid.");
928 // Match site instance to site settings pointer.
929 if (site_id>g_ulMaxSite || g_Sites[site_id-1].g_CookieName.empty())
930 return SF_STATUS_REQ_NEXT_NOTIFICATION;
931 pSite=&g_Sites[site_id-1];
933 string targeturl=get_target(pfc,pn,pSite);
935 // If the user is accessing the SHIRE acceptance point, pass on.
936 if (targeturl.find(pSite->g_SHIRELocation)!=string::npos)
938 log.debug("passing on SHIRE acceptance request");
939 return SF_STATUS_REQ_NEXT_NOTIFICATION;
942 // If this is the garbage collection service, do a cache sweep.
943 if (targeturl==pSite->g_GarbageCollector)
945 log.notice("garbage collector triggered");
946 pSite->g_AuthCache.lock();
948 pSite->g_AuthCache.sweep(pSite->g_Lifetime);
949 pSite->g_AuthCache.unlock();
951 return WriteClientError(pfc,"The cache was swept for expired sessions.");
954 // Get the url request and scan for the must-contain string.
955 if (!pSite->g_MustContain.empty())
957 char* upcased=new char[targeturl.length()+1];
958 strcpy(upcased,targeturl.c_str());
960 for (vector<string>::const_iterator index=pSite->g_MustContain.begin(); index!=pSite->g_MustContain.end(); index++)
961 if (strstr(upcased,index->c_str()))
964 if (index==pSite->g_MustContain.end())
965 return SF_STATUS_REQ_NEXT_NOTIFICATION;
969 if (pSite->g_bSSLOnly && !pfc->fIsSecurePort)
971 log.warn("blocking non-SSL request");
972 xmsg="<HTML><HEAD><TITLE>Access Denied</TITLE></HEAD><BODY>"
973 "<H1>Access Denied</H1>"
974 "This server is configured to deny non-SSL requests for secure resources. "
975 "Try your request again using https instead of http."
977 DWORD resplen=strlen(xmsg);
978 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",0,0);
979 pfc->WriteClient(pfc,xmsg,&resplen,0);
980 return SF_STATUS_REQ_FINISHED;
983 // Check for authentication cookie.
984 const char* session_id=NULL;
985 GetHeader(pn,pfc,"Cookie:",buf,128,false);
986 if (buf.empty() || !(session_id=strstr(buf,pSite->g_CookieName.c_str())) ||
987 *(session_id+pSite->g_CookieName.length())!='=')
989 log.info("session cookie not found, redirecting to WAYF");
992 string wayf("Location: ");
993 wayf+=pSite->g_WAYFLocation + "?shire=" + get_shire_location(pfc,pSite,targeturl.c_str()) +
994 "&target=" + url_encode(targeturl.c_str()) + "\r\n";
995 // Insert the headers.
996 pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
997 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
998 return SF_STATUS_REQ_FINISHED;
1001 session_id+=pSite->g_CookieName.length() + 1; /* Skip over the '=' */
1002 char* cookieend=strchr(session_id,';');
1004 *cookieend = '\0'; /* Ignore anyting after a ; */
1006 pSite->g_AuthCache.lock(); // ---> Get cache lock
1009 // The caching logic is the heart of the "SHAR".
1010 CCacheEntry* entry=pSite->g_AuthCache.find(session_id);
1015 pSite->g_AuthCache.unlock(); // ---> Release cache lock
1018 // Construct the path to the session file
1019 string sessionFile=pSite->g_SHIRESessionPath + session_id;
1022 entry=new CCacheEntry(sessionFile.c_str());
1024 catch (runtime_error e)
1026 log.info("unable to load session from file '%s', redirecting to WAYF",sessionFile.c_str());
1028 // Redirect to WAYF.
1029 string wayf("Location: ");
1030 wayf+=pSite->g_WAYFLocation + "?shire=" + get_shire_location(pfc,pSite,targeturl.c_str()) +
1031 "&target=" + url_encode(targeturl.c_str()) + "\r\n";
1032 wayf+="Set-Cookie: " + pSite->g_CookieName + "=; path=/; expires=19-Mar-1971 08:23:00 GMT\r\n";
1034 // Insert the headers.
1035 pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
1036 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
1037 return SF_STATUS_REQ_FINISHED;
1039 pSite->g_AuthCache.lock(); // ---> Get cache lock
1041 pSite->g_AuthCache.insert(session_id,entry);
1042 log.info("new session established: %s",session_id);
1045 if (!entry->isSessionValid(pSite->g_Lifetime,pSite->g_Timeout))
1047 pSite->g_AuthCache.remove(session_id);
1048 pSite->g_AuthCache.unlock(); // ---> Release cache lock
1052 log.warn("invalidating session because of timeout, redirecting to WAYF");
1054 // Redirect to WAYF.
1055 string wayf("Location: ");
1056 wayf+=pSite->g_WAYFLocation + "?shire=" + get_shire_location(pfc,pSite,targeturl.c_str()) +
1057 "&target=" + url_encode(targeturl.c_str()) + "\r\n";
1058 wayf+="Set-Cookie: " + pSite->g_CookieName + "=; path=/; expires=19-Mar-1971 08:23:00 GMT\r\n";
1060 // Insert the headers.
1061 pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
1062 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
1063 return SF_STATUS_REQ_FINISHED;
1066 if (pSite->g_bCheckAddress && entry->getClientAddress())
1068 GetServerVariable(pfc,"REMOTE_ADDR",buf,16);
1069 if (strcmp(entry->getClientAddress(),buf))
1071 pSite->g_AuthCache.remove(session_id);
1073 pSite->g_AuthCache.unlock(); // ---> Release cache lock
1076 log.warn("IP address mismatch detected, clearing session");
1078 string clearcookie("Set-Cookie: ");
1079 clearcookie+=pSite->g_CookieName + "=; path=/; expires=19-Mar-1971 08:23:00 GMT\r\n";
1080 pfc->AddResponseHeaders(pfc,const_cast<char*>(clearcookie.c_str()),0);
1081 return WriteClientError(pfc,
1082 "Your session was terminated because the network address associated "
1083 "with it does not match your current address. This is usually caused "
1084 "by a firewall or proxy of some sort.");
1088 // Clear relevant headers.
1089 pn->SetHeader(pfc,"Shib-Attributes:","");
1090 pn->SetHeader(pfc,"remote-user:","");
1091 for (map<string,string>::const_iterator h_iter=g_mapAttribNameToHeader.begin(); h_iter!=g_mapAttribNameToHeader.end(); h_iter++)
1092 if (h_iter->second!="REMOTE_USER:")
1093 pn->SetHeader(pfc,const_cast<char*>(h_iter->second.c_str()),"");
1095 if (pSite->g_bExportAssertion)
1097 string exp((char*)entry->getSerializedAssertion(targeturl.c_str(),pSite));
1098 string::size_type lfeed;
1099 while ((lfeed=exp.find('\n'))!=string::npos)
1101 pn->SetHeader(pfc,"Shib-Attributes:",const_cast<char*>(exp.c_str()));
1103 Iterator<SAMLAttribute*> i=entry->getAttributes(targeturl.c_str(),pSite);
1107 SAMLAttribute* attr=i.next();
1109 // Are we supposed to export it?
1110 map<xstring,string>::const_iterator iname=g_mapAttribNames.find(attr->getName());
1111 if (iname!=g_mapAttribNames.end())
1113 string hname=g_mapAttribNameToHeader[iname->second];
1114 Iterator<string> vals=attr->getSingleByteValues();
1115 if (hname=="REMOTE_USER:" && vals.hasNext())
1117 char* principal=const_cast<char*>(vals.next().c_str());
1118 pn->SetHeader(pfc,"remote-user:",principal);
1119 pfc->pFilterContext=pfc->AllocMem(pfc,strlen(principal)+1,0);
1120 if (pfc->pFilterContext)
1121 strcpy(static_cast<char*>(pfc->pFilterContext),principal);
1126 while (vals.hasNext())
1127 header+=vals.next() + " ";
1128 pn->SetHeader(pfc,const_cast<char*>(hname.c_str()),const_cast<char*>(header.c_str()));
1133 pSite->g_AuthCache.unlock(); // ---> Release cache lock
1135 return SF_STATUS_REQ_NEXT_NOTIFICATION;
1137 catch (SAMLException& e)
1139 Iterator<saml::QName> i=e.getCodes();
1144 saml::QName q=i.next();
1145 if (c==1 && !XMLString::compareString(q.getNamespaceURI(),saml::XML::SAMLP_NS) &&
1146 !XMLString::compareString(q.getLocalName(),L(Requester)))
1148 else if (c==2 && !XMLString::compareString(q.getNamespaceURI(),shibboleth::XML::SHIB_NS) &&
1149 !XMLString::compareString(q.getLocalName(),shibboleth::XML::Literals::InvalidHandle))
1152 pSite->g_AuthCache.lock(); // ---> Grab cache lock
1153 pSite->g_AuthCache.remove(session_id);
1154 pSite->g_AuthCache.unlock(); // ---> Release cache lock
1157 log.info("invaliding session due to shib:InvalidHandle code from AA");
1159 // Redirect to WAYF.
1160 string wayf("Location: ");
1161 wayf+=pSite->g_WAYFLocation + "?shire=" + get_shire_location(pfc,pSite,targeturl.c_str()) +
1162 "&target=" + url_encode(targeturl.c_str()) + "\r\n";
1163 wayf+="Set-Cookie: " + pSite->g_CookieName + "=; path=/; expires=19-Mar-1971 08:23:00 GMT\r\n";
1165 // Insert the headers.
1166 pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
1167 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
1168 return SF_STATUS_REQ_FINISHED;
1173 pSite->g_AuthCache.unlock();
1174 return shib_shar_error(pfc,e);
1176 catch (XMLException& e)
1179 pSite->g_AuthCache.unlock();
1180 auto_ptr<char> msg(XMLString::transcode(e.getMessage()));
1181 SAMLException ex(SAMLException::RESPONDER,msg.get());
1182 return shib_shar_error(pfc,ex);
1187 xmsg="Out of memory.";
1188 log.error("out of memory");
1192 if (e==ERROR_NO_DATA)
1193 xmsg="A required variable or header was empty.";
1195 xmsg="Server detected unexpected IIS error.";
1199 xmsg="Server caught an unknown exception.";
1202 // If we drop here, the exception handler set the proper message.
1204 pSite->g_AuthCache.unlock();
1205 return WriteClientError(pfc,xmsg);