Initial version, based on a hybrid of alpha 2.5 and some new work.
[shibboleth/sp.git] / isapi_shib / isapi_shib.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 /* isapi_shib.cpp - Shibboleth ISAPI filter
51
52    Scott Cantor
53    8/23/02
54 */
55
56 #include <windows.h>
57 #include <httpfilt.h>
58
59 // SAML Runtime
60 #include <saml.h>
61 #include <shib.h>
62 #include <eduPerson.h>
63
64 #include <xercesc/util/Base64.hpp>
65
66 #include <strstream>
67 #include <stdexcept>
68
69 using namespace std;
70 using namespace saml;
71 using namespace shibboleth;
72 using namespace eduPerson;
73
74 class CCacheEntry;
75 class CCache
76 {
77 public:
78     CCache();
79     ~CCache();
80
81     SAMLBinding* getBinding(const XMLCh* bindingProt);
82     CCacheEntry* find(const char* key);
83     void insert(const char* key, CCacheEntry* entry);
84     void remove(const char* key);
85     void sweep(time_t lifetime);
86
87     bool lock() { EnterCriticalSection(&m_lock); return true; }
88     void unlock() { LeaveCriticalSection(&m_lock); }
89
90 private:
91     SAMLBinding* m_SAMLBinding;
92     map<string,CCacheEntry*> m_hashtable;
93     CRITICAL_SECTION m_lock;
94 };
95
96 // Per-website global structure
97 struct settings_t
98 {
99     settings_t();
100     string g_CookieName;                    // name of authentication token
101     string g_WAYFLocation;                  // URL of WAYF service
102     string g_GarbageCollector;              // URL of cache garbage collection service
103     string g_SHIRELocation;                 // URL of SHIRE acceptance point
104     string g_SHIRESessionPath;              // path to storage for sessions
105     vector<string> g_MustContain;           // simple URL matching string array
106     bool g_bSSLOnly;                        // only over SSL?
107     time_t g_Lifetime;                      // maximum token lifetime
108     time_t g_Timeout;                       // maximum time between uses
109     bool g_bCheckAddress;                   // validate IP addresses?
110     bool g_bExportAssertion;                // export SAML assertion to header?
111     CCache g_AuthCache;                     // local auth cache
112 };
113
114 settings_t::settings_t()
115 {
116     g_bSSLOnly=true;
117     g_Lifetime=7200;
118     g_Timeout=3600;
119     g_bCheckAddress=true;
120     g_bExportAssertion=false;
121 }
122
123 class CCacheEntry
124 {
125 public:
126     CCacheEntry(const char* sessionFile);
127     ~CCacheEntry();
128
129     SAMLAuthorityBinding* getBinding() { return m_binding; }
130     Iterator<SAMLAttribute*> getAttributes(const char* resource_url, settings_t* pSite);
131     const XMLByte* getSerializedAssertion(const char* resource_url, settings_t* pSite);
132     bool isSessionValid(time_t lifetime, time_t timeout);
133     const XMLCh* getHandle() { return m_handle.c_str(); }
134     const XMLCh* getOriginSite() { return m_originSite.c_str(); }
135     const char* getClientAddress() { return m_clientAddress.c_str(); }
136
137 private:
138     void populate(const char* resource_url, settings_t* pSite);
139
140     xstring m_originSite;
141     xstring m_handle;
142     SAMLAuthorityBinding* m_binding;
143     string m_clientAddress;
144     SAMLResponse* m_response;
145     SAMLAssertion* m_assertion;
146     time_t m_sessionCreated;
147     time_t m_lastAccess;
148     XMLByte* m_serialized;
149
150     static saml::QName g_authorityKind;
151     static saml::QName g_respondWith;
152     friend class CCache;
153 };
154
155 // static members
156 saml::QName CCacheEntry::g_authorityKind(saml::XML::SAMLP_NS,L(AttributeQuery));
157 saml::QName CCacheEntry::g_respondWith(saml::XML::SAML_NS,L(AttributeStatement));
158
159 CCache::CCache()
160 {
161     m_SAMLBinding=SAMLBindingFactory::getInstance();
162     InitializeCriticalSection(&m_lock);
163 }
164
165 CCache::~CCache()
166 {
167     DeleteCriticalSection(&m_lock);
168     delete m_SAMLBinding;
169     for (map<string,CCacheEntry*>::iterator i=m_hashtable.begin(); i!=m_hashtable.end(); i++)
170         delete i->second;
171 }
172
173 SAMLBinding* CCache::getBinding(const XMLCh* bindingProt)
174 {
175     if (!XMLString::compareString(bindingProt,SAMLBinding::SAML_SOAP_HTTPS))
176         return m_SAMLBinding;
177     return NULL;
178 }
179
180 CCacheEntry* CCache::find(const char* key)
181 {
182     map<string,CCacheEntry*>::const_iterator i=m_hashtable.find(key);
183     if (i==m_hashtable.end())
184         return NULL;
185     return i->second;
186 }
187
188 void CCache::insert(const char* key, CCacheEntry* entry)
189 {
190     m_hashtable[key]=entry;
191 }
192
193 void CCache::remove(const char* key)
194 {
195     m_hashtable.erase(key);
196 }
197
198 void CCache::sweep(time_t lifetime)
199 {
200     time_t now=time(NULL);
201     for (map<string,CCacheEntry*>::iterator i=m_hashtable.begin(); i!=m_hashtable.end();)
202     {
203         if (lifetime > 0 && now > i->second->m_sessionCreated+lifetime)
204         {
205             delete i->second;
206             i=m_hashtable.erase(i);
207         }
208         else
209             i++;
210     }
211 }
212
213 CCacheEntry::CCacheEntry(const char* sessionFile)
214   : m_binding(NULL), m_assertion(NULL), m_response(NULL), m_lastAccess(0), m_sessionCreated(0), m_serialized(NULL)
215 {
216     FILE* f;
217     char line[1024];
218     const char* token = NULL;
219     char* w = NULL;
220     auto_ptr<XMLCh> binding,location;
221
222     if (!(f=fopen(sessionFile,"r")))
223     {
224         fprintf(stderr,"CCacheEntry() could not open session file: %s",sessionFile);
225         throw runtime_error("CCacheEntry() could not open session file");
226     }
227
228     while (fgets(line,1024,f))
229     {
230         if ((*line=='#') || (!*line))
231             continue;
232         token = line;
233         w=strchr(token,'=');
234         if (!w)
235             continue;
236         *w++=0;
237         if (w[strlen(w)-1]=='\n')
238             w[strlen(w)-1]=0;
239
240         if (!strcmp("Domain",token))
241         {
242                 auto_ptr<XMLCh> origin(XMLString::transcode(w));
243                 m_originSite=origin.get();
244         }
245         else if (!strcmp("Handle",token))
246         {
247                 auto_ptr<XMLCh> handle(XMLString::transcode(w));
248                 m_handle=handle.get();
249         }
250         else if (!strcmp("PBinding0",token))
251                 binding=auto_ptr<XMLCh>(XMLString::transcode(w));
252         else if (!strcmp("LBinding0",token))
253                 location=auto_ptr<XMLCh>(XMLString::transcode(w));
254         else if (!strcmp("Time",token))
255                 m_sessionCreated=atoi(w);
256         else if (!strcmp("ClientAddress",token))
257                 m_clientAddress=w;
258         else if (!strcmp("EOF",token))
259                 break;
260     }
261     fclose(f);
262     
263     if (binding.get()!=NULL && location.get()!=NULL)
264         m_binding=new SAMLAuthorityBinding(g_authorityKind,binding.get(),location.get());
265
266     m_lastAccess=time(NULL);
267     if (!m_sessionCreated)
268         m_sessionCreated=m_lastAccess;
269 }
270
271 CCacheEntry::~CCacheEntry()
272 {
273     delete m_binding;
274     delete m_response;
275     delete[] m_serialized;
276 }
277
278 bool CCacheEntry::isSessionValid(time_t lifetime, time_t timeout)
279 {
280     time_t now=time(NULL);
281     if (lifetime > 0 && now > m_sessionCreated+lifetime)
282         return false;
283     if (timeout > 0 && now-m_lastAccess >= timeout)
284         return false;
285     m_lastAccess=now;
286     return true;
287 }
288
289 Iterator<SAMLAttribute*> CCacheEntry::getAttributes(const char* resource_url, settings_t* pSite)
290 {
291     populate(resource_url,pSite);
292     if (m_assertion)
293     {
294         Iterator<SAMLStatement*> i=m_assertion->getStatements();
295         if (i.hasNext())
296         {
297             SAMLAttributeStatement* s=dynamic_cast<SAMLAttributeStatement*>(i.next());
298             if (s)
299                 return s->getAttributes();
300         }
301     }
302     return Iterator<SAMLAttribute*>();
303 }
304
305 const XMLByte* CCacheEntry::getSerializedAssertion(const char* resource_url, settings_t* pSite)
306 {
307     populate(resource_url,pSite);
308     if (m_serialized)
309         return m_serialized;
310     if (!m_assertion)
311         return NULL;
312     ostrstream os;
313     os << *m_assertion;
314     unsigned int outlen;
315     return m_serialized=Base64::encode(reinterpret_cast<XMLByte*>(os.str()),os.pcount(),&outlen);
316 }
317
318 void CCacheEntry::populate(const char* resource_url, settings_t* pSite)
319 {
320     // Can we use what we have?
321     if (m_assertion && m_assertion->getNotOnOrAfter())
322     {
323         // This is awful, but the XMLDateTime class is truly horrible.
324         time_t now=time(NULL);
325         struct tm* ptime=gmtime(&now);
326         char timebuf[32];
327         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
328         auto_ptr<XMLCh> timeptr(XMLString::transcode(timebuf));
329         XMLDateTime curDateTime(timeptr.get());
330         int result=XMLDateTime::compareOrder(&curDateTime,m_assertion->getNotOnOrAfter());
331         if (XMLDateTime::LESS_THAN)
332             return;
333
334         delete m_response;
335         delete[] m_serialized;
336         m_assertion=NULL;
337         m_response=NULL;
338         m_serialized=NULL;
339     }
340
341     if (!m_binding)
342         return;
343
344     auto_ptr<XMLCh> resource(XMLString::transcode(resource_url));    
345
346     // Build a SAML Request and send it to the AA.
347     SAMLSubject* subject=new SAMLSubject(m_handle.c_str(),m_originSite.c_str());
348     SAMLAttributeQuery* q=new SAMLAttributeQuery(subject,resource.get());
349     SAMLRequest* req=new SAMLRequest(q,ArrayIterator<saml::QName>(&g_respondWith));
350     SAMLBinding* pBinding=pSite->g_AuthCache.getBinding(m_binding->getBinding());
351     m_response=pBinding->send(*m_binding,*req);
352     delete req;
353
354     // Store off the assertion for quick access. Memory mgmt is based on the response pointer.
355     Iterator<SAMLAssertion*> i=m_response->getAssertions();
356     if (i.hasNext())
357         m_assertion=i.next();
358
359     auto_ptr<char> h(XMLString::transcode(m_handle.c_str()));
360     auto_ptr<char> d(XMLString::transcode(m_originSite.c_str()));
361     fprintf(stderr,"CCacheEntry::populate() fetched and stored SAML response for %s@%s\n",h.get(),d.get());
362 }
363
364 class DummyMapper : public IOriginSiteMapper
365 {
366 public:
367     DummyMapper() {}
368     ~DummyMapper();
369     virtual Iterator<xstring> getHandleServiceNames(const XMLCh* originSite) { return Iterator<xstring>(); }
370     virtual Key* getHandleServiceKey(const XMLCh* handleService) { return NULL; }
371     virtual Iterator<xstring> getSecurityDomains(const XMLCh* originSite);
372     virtual Iterator<X509Certificate*> getTrustedRoots() { return Iterator<X509Certificate*>(); }
373
374 private:
375     typedef map<xstring,vector<xstring>*> domains_t;
376     domains_t m_domains;
377 };
378
379 Iterator<xstring> DummyMapper::getSecurityDomains(const XMLCh* originSite)
380 {
381     SAMLConfig::getConfig()->saml_lock();
382     vector<xstring>* pv=NULL;
383     domains_t::iterator i=m_domains.find(originSite);
384     if (i==m_domains.end())
385     {
386         pv=new vector<xstring>();
387         pv->push_back(originSite);
388         pair<domains_t::iterator,bool> p=m_domains.insert(domains_t::value_type(originSite,pv));
389             i=p.first;
390     }
391     else
392         pv=i->second;
393     SAMLConfig::getConfig()->saml_unlock();
394     return Iterator<xstring>(*pv);
395 }
396
397 DummyMapper::~DummyMapper()
398 {
399     for (domains_t::iterator i=m_domains.begin(); i!=m_domains.end(); i++)
400         delete i->second;
401 }
402
403 // globals
404 HINSTANCE g_hinstDLL;
405 ULONG g_ulMaxSite=1;                        // max IIS site instance to handle
406 settings_t* g_Sites=NULL;                   // array of site settings
407 string g_SchemaPath;                        // location of XML schemas
408 string g_SSLCertFile;                       // PKI components for SHAR
409 string g_SSLKeyFile;
410 string g_SSLKeyPass;
411 string g_SSLCAList;
412 map<string,string> g_mapAttribNameToHeader; // attribute mapping
413 map<xstring,string> g_mapAttribNames;
414
415
416 extern "C" __declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
417 {
418     if (fdwReason==DLL_PROCESS_ATTACH)
419         g_hinstDLL=hinstDLL;
420     return TRUE;
421 }
422
423 extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer)
424 {
425     if (!pVer)
426         return FALSE;
427
428     // Get module pathname and replace file name with ini file name.
429     char inifile[MAX_PATH+1];
430     if (GetModuleFileName(g_hinstDLL,inifile,MAX_PATH+1)==0)
431         return FALSE;
432     char* pch=strrchr(inifile,'\\');
433     if (pch==NULL)
434         return FALSE;
435     pch++;
436     *pch=0;
437     strcat(inifile,"isapi_shib.ini");
438
439     // Read system-wide parameters from isapi_shib.ini.
440     char buf[1024];
441     char buf3[48];
442
443     try
444     {
445         GetPrivateProfileString("shibboleth","ShibSchemaPath","",buf,sizeof(buf),inifile);
446         if (!*buf)
447         {
448             WritePrivateProfileString("startlog","bailed-at","ShibSchemaPath",inifile);
449             return FALSE;
450         }
451         g_SchemaPath=buf;
452         if (*g_SchemaPath.end()!='\\')
453             g_SchemaPath+='\\';
454
455         GetPrivateProfileString("shibboleth","ShibSSLCertFile","",buf,sizeof(buf),inifile);
456         if (!*buf)
457         {
458             WritePrivateProfileString("startlog","bailed-at","ShibSSLCertFile",inifile);
459             return FALSE;
460         }
461         g_SSLCertFile=buf;
462
463         GetPrivateProfileString("shibboleth","ShibSSLKeyFile","",buf,sizeof(buf),inifile);
464         if (!*buf)
465         {
466             WritePrivateProfileString("startlog","bailed-at","ShibSSLKeyFile",inifile);
467             return FALSE;
468         }
469         g_SSLKeyFile=buf;
470
471         GetPrivateProfileString("shibboleth","ShibSSLKeyPass","",buf,sizeof(buf),inifile);
472         g_SSLKeyPass=buf;
473
474         GetPrivateProfileString("shibboleth","ShibSSLCAList","",buf,sizeof(buf),inifile);
475         g_SSLCAList=buf;
476
477         // Read site count and allocate site array.
478         g_ulMaxSite=GetPrivateProfileInt("shibboleth","max-site",0,inifile);
479         if (g_ulMaxSite==0)
480         {
481             WritePrivateProfileString("startlog","bailed-at","max-site check",inifile);
482             return FALSE;
483         }
484         g_Sites=new settings_t[g_ulMaxSite];
485
486         // Read site-specific settings for each site.
487         for (ULONG i=0; i<g_ulMaxSite; i++)
488         {
489             ultoa(i+1,buf3,10);
490             GetPrivateProfileString(buf3,"ShibSiteName","X",buf,sizeof(buf),inifile);
491             if (!strcmp(buf,"X"))
492                 continue;
493
494             GetPrivateProfileString(buf3,"ShibCookieName","",buf,sizeof(buf),inifile);
495             if (!*buf)
496             {
497                 delete[] g_Sites;
498                 WritePrivateProfileString("startlog","bailed-at","ShibCookieName",inifile);
499                 return FALSE;
500             }
501             g_Sites[i].g_CookieName=buf;
502
503             GetPrivateProfileString(buf3,"WAYFLocation","",buf,sizeof(buf),inifile);
504             if (!*buf)
505             {
506                 delete[] g_Sites;
507                 WritePrivateProfileString("startlog","bailed-at","WAYFLocation",inifile);
508                 return FALSE;
509             }
510             g_Sites[i].g_WAYFLocation=buf;
511
512             GetPrivateProfileString(buf3,"GarbageCollector","",buf,sizeof(buf),inifile);
513             if (!*buf)
514             {
515                 delete[] g_Sites;
516                 WritePrivateProfileString("startlog","bailed-at","GarbageCollector",inifile);
517                 return FALSE;
518             }
519             g_Sites[i].g_GarbageCollector=buf;
520
521             GetPrivateProfileString(buf3,"SHIRELocation","",buf,sizeof(buf),inifile);
522             if (!*buf)
523             {
524                 delete[] g_Sites;
525                 WritePrivateProfileString("startlog","bailed-at","SHIRELocation",inifile);
526                 return FALSE;
527             }
528             g_Sites[i].g_SHIRELocation=buf;
529
530             GetPrivateProfileString(buf3,"SHIRESessionPath","",buf,sizeof(buf),inifile);
531             if (!*buf)
532             {
533                 delete[] g_Sites;
534                 WritePrivateProfileString("startlog","bailed-at","SHIRESessionPath",inifile);
535                 return FALSE;
536             }
537             g_Sites[i].g_SHIRESessionPath=buf;
538             if (g_Sites[i].g_SHIRESessionPath[g_Sites[i].g_SHIRESessionPath.length()]!='\\')
539                 g_Sites[i].g_SHIRESessionPath+='\\';
540
541             // Old-style matching string.
542             GetPrivateProfileString(buf3,"ShibMustContain","",buf,sizeof(buf),inifile);
543             _strupr(buf);
544             char* start=buf;
545             while (char* sep=strchr(start,';'))
546             {
547                 *sep='\0';
548                 if (*start)
549                     g_Sites[i].g_MustContain.push_back(start);
550                 start=sep+1;
551             }
552             if (*start)
553                 g_Sites[i].g_MustContain.push_back(start);
554             
555             if (GetPrivateProfileInt(buf3,"ShibSSLOnly",1,inifile)==0)
556                 g_Sites[i].g_bSSLOnly=false;
557             if (GetPrivateProfileInt(buf3,"ShibCheckAddress",1,inifile)==0)
558                 g_Sites[i].g_bCheckAddress=false;
559             if (GetPrivateProfileInt(buf3,"ShibExportAssertion",0,inifile)==1)
560                 g_Sites[i].g_bExportAssertion=true;
561             g_Sites[i].g_Lifetime=GetPrivateProfileInt(buf3,"ShibAuthLifetime",7200,inifile);
562             if (g_Sites[i].g_Lifetime<=0)
563                 g_Sites[i].g_Lifetime=7200;
564             g_Sites[i].g_Timeout=GetPrivateProfileInt(buf3,"ShibAuthTimeout",3600,inifile);
565             if (g_Sites[i].g_Timeout<=0)
566                 g_Sites[i].g_Timeout=3600;
567             WritePrivateProfileString("startlog","site-complete",buf3,inifile);
568         }
569
570         static SAMLConfig SAMLconf;
571         static ShibConfig Shibconf;
572         static DummyMapper mapper;
573
574         SAMLconf.schema_dir=g_SchemaPath;
575         SAMLconf.ssl_certfile=g_SSLCertFile;
576         SAMLconf.ssl_keyfile=g_SSLKeyFile;
577         SAMLconf.ssl_keypass=g_SSLKeyPass;
578         SAMLconf.ssl_calist=g_SSLCAList;
579 #ifdef _DEBUG
580         SAMLconf.bVerbose=true;
581 #else
582         SAMLconf.bVerbose=false;
583 #endif
584         if (!SAMLConfig::init(&SAMLconf))
585         {
586             delete[] g_Sites;
587             WritePrivateProfileString("startlog","bailed-at","SAML init failed",inifile);
588             return FALSE;
589         }
590
591         Shibconf.origin_mapper=&mapper;
592         if (!ShibConfig::init(&Shibconf))
593         {
594             delete[] g_Sites;
595             WritePrivateProfileString("startlog","bailed-at","Shib init failed",inifile);
596             return FALSE;
597         }
598
599         char buf2[32767];
600         DWORD res=GetPrivateProfileSection("ShibMapAttributes",buf2,sizeof(buf2),inifile);
601         if (res==sizeof(buf2)-2)
602         {
603             delete[] g_Sites;
604             WritePrivateProfileString("startlog","bailed-at","ShibMapAttributes too big",inifile);
605             return FALSE;
606         }
607
608         for (char* attr=buf2; *attr; attr++)
609         {
610             char* delim=strchr(attr,'=');
611             if (!delim)
612             {
613                 delete[] g_Sites;
614                 WritePrivateProfileString("startlog","bailed-at","ShibMapAttributes bad key format",inifile);
615                 return FALSE;
616             }
617             *delim++=0;
618             g_mapAttribNameToHeader[attr]=(string(delim) + ':');
619             attr=delim + strlen(delim);
620         }
621
622         WritePrivateProfileString("startlog","attributes","complete",inifile);
623
624         // Transcode the attribute names we know about for quick handling map access.
625         for (map<string,string>::const_iterator j=g_mapAttribNameToHeader.begin();
626              j!=g_mapAttribNameToHeader.end(); j++)
627         {
628             auto_ptr<XMLCh> temp(XMLString::transcode(j->first.c_str()));
629             g_mapAttribNames[temp.get()]=j->first;
630         }
631
632         res=GetPrivateProfileSection("ShibExtensions",buf2,sizeof(buf2),inifile);
633         if (res==sizeof(buf2)-2)
634         {
635             delete[] g_Sites;
636             WritePrivateProfileString("startlog","bailed-at","ShibExtensions too big",inifile);
637             return FALSE;
638         }
639
640         for (char* libpath=buf2; *libpath; libpath+=strlen(libpath)+1)
641             SAMLConfig::getConfig()->saml_register_extension(libpath);
642
643         WritePrivateProfileString("startlog","extensions","complete",inifile);
644     }
645     catch (bad_alloc)
646     {
647         delete[] g_Sites;
648         WritePrivateProfileString("startlog","bailed-at","bad_alloc caught",inifile);
649         return FALSE;
650     }
651     catch (SAMLException& ex)
652     {
653         delete[] g_Sites;
654         WritePrivateProfileString("startlog","bailed-at","SAML Exception caught",inifile);
655         WritePrivateProfileString("startlog","SAMLException",ex.what(),inifile);
656         return FALSE;
657     }
658
659     pVer->dwFilterVersion=HTTP_FILTER_REVISION;
660     strncpy(pVer->lpszFilterDesc,"Shibboleth ISAPI Filter",SF_MAX_FILTER_DESC_LEN);
661     pVer->dwFlags=(SF_NOTIFY_ORDER_HIGH |
662                    SF_NOTIFY_SECURE_PORT |
663                    SF_NOTIFY_NONSECURE_PORT |
664                    SF_NOTIFY_PREPROC_HEADERS |
665                    SF_NOTIFY_LOG);
666     return TRUE;
667 }
668
669 extern "C" BOOL WINAPI TerminateFilter(DWORD dwFlags)
670 {
671     delete[] g_Sites;
672     g_Sites=NULL;
673     ShibConfig::term();
674     SAMLConfig::term();
675     return TRUE;
676 }
677
678 /* Next up, some suck-free versions of various APIs.
679
680    You DON'T require people to guess the buffer size and THEN tell them the right size.
681    Returning an LPCSTR is apparently way beyond their ken. Not to mention the fact that
682    constant strings aren't typed as such, making it just that much harder. These versions
683    are now updated to use a special growable buffer object, modeled after the standard
684    string class. The standard string won't work because they left out the option to
685    pre-allocate a non-constant buffer.
686 */
687
688 class dynabuf
689 {
690 public:
691     dynabuf() { bufptr=NULL; buflen=0; }
692     dynabuf(size_t s) { bufptr=new char[buflen=s]; *bufptr=0; }
693     ~dynabuf() { delete[] bufptr; }
694     size_t length() const { return bufptr ? strlen(bufptr) : 0; }
695     size_t size() const { return buflen; }
696     bool empty() const { return length()==0; }
697     void reserve(size_t s, bool keep=false);
698     void erase() { if (bufptr) *bufptr=0; }
699     operator char*() { return bufptr; }
700     bool operator ==(const char* s) const;
701     bool operator !=(const char* s) const { return !(*this==s); }
702 private:
703     char* bufptr;
704     size_t buflen;
705 };
706
707 void dynabuf::reserve(size_t s, bool keep)
708 {
709     if (s<=buflen)
710         return;
711     char* p=new char[s];
712     if (keep)
713         while (buflen--)
714             p[buflen]=bufptr[buflen];
715     buflen=s;
716     delete[] bufptr;
717     bufptr=p;
718 }
719
720 bool dynabuf::operator==(const char* s) const
721 {
722     if (buflen==NULL || s==NULL)
723         return (buflen==NULL && s==NULL);
724     else
725         return strcmp(bufptr,s)==0;
726 }
727
728 void GetServerVariable(PHTTP_FILTER_CONTEXT pfc,
729                        LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
730     throw (bad_alloc, DWORD)
731 {
732     s.erase();
733     s.reserve(size);
734     size=s.size();
735
736     while (!pfc->GetServerVariable(pfc,lpszVariable,s,&size))
737     {
738         // Grumble. Check the error.
739         DWORD e=GetLastError();
740         if (e==ERROR_INSUFFICIENT_BUFFER)
741             s.reserve(size);
742         else
743             break;
744     }
745     if (bRequired && s.empty())
746         throw ERROR_NO_DATA;
747 }
748
749 void GetHeader(PHTTP_FILTER_PREPROC_HEADERS pn, PHTTP_FILTER_CONTEXT pfc,
750                LPSTR lpszName, dynabuf& s, DWORD size=80, bool bRequired=true)
751     throw (bad_alloc, DWORD)
752 {
753     s.erase();
754     s.reserve(size);
755     size=s.size();
756
757     while (!pn->GetHeader(pfc,lpszName,s,&size))
758     {
759         // Grumble. Check the error.
760         DWORD e=GetLastError();
761         if (e==ERROR_INSUFFICIENT_BUFFER)
762             s.reserve(size);
763         else
764             break;
765     }
766     if (bRequired && s.empty())
767         throw ERROR_NO_DATA;
768 }
769
770 inline char hexchar(unsigned short s)
771 {
772     return (s<=9) ? ('0' + s) : ('A' + s - 10);
773 }
774
775 string url_encode(const char* url) throw (bad_alloc)
776 {
777     static char badchars[]="\"\\+<>#%{}|^~[]`;/?:@=&";
778     string s;
779     for (const char* pch=url; *pch; pch++)
780     {
781         if (strchr(badchars,*pch)!=NULL || *pch<=0x1F || *pch>=0x7F)
782             s=s + '%' + hexchar(*pch >> 4) + hexchar(*pch & 0x0F);
783         else
784             s+=*pch;
785     }
786     return s;
787 }
788
789 string get_target(PHTTP_FILTER_CONTEXT pfc, PHTTP_FILTER_PREPROC_HEADERS pn, settings_t* pSite)
790 {
791     // Reconstructing the requested URL is not fun. Apparently, the PREPROC_HEADERS
792     // event means way pre. As in, none of the usual CGI headers are in place yet.
793     // It's actually almost easier, in a way, because all the path-info and query
794     // stuff is in one place, the requested URL, which we can get. But we have to
795     // reconstruct the protocol/host pair using tweezers.
796     string s;
797     if (pfc->fIsSecurePort)
798         s="https://";
799     else
800         s="http://";
801
802     dynabuf buf(256);
803     GetServerVariable(pfc,"SERVER_NAME",buf);
804     s+=buf;
805
806     GetServerVariable(pfc,"SERVER_PORT",buf,10);
807     if (buf!=(pfc->fIsSecurePort ? "443" : "80"))
808         s=s + ':' + static_cast<char*>(buf);
809
810     GetHeader(pn,pfc,"url",buf,256,false);
811     s+=buf;
812
813     return s;
814 }
815
816 string get_shire_location(PHTTP_FILTER_CONTEXT pfc, settings_t* pSite, const char* target)
817 {
818     if (pSite->g_SHIRELocation[0]!='/')
819         return url_encode(pSite->g_SHIRELocation.c_str());
820     const char* colon=strchr(target,':');
821     const char* slash=strchr(colon+3,'/');
822     string s(target,slash-target);
823     s+=pSite->g_SHIRELocation;
824     return url_encode(s.c_str());
825 }
826
827 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg)
828 {
829     pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",0,0);
830     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Filter Error</TITLE></HEAD><BODY>"
831                             "<H1>Shibboleth Filter Error</H1>";
832     DWORD resplen=strlen(xmsg);
833     pfc->WriteClient(pfc,(LPVOID)xmsg,&resplen,0);
834     resplen=strlen(msg);
835     pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
836     static const char* xmsg2="</BODY></HTML>";
837     resplen=strlen(xmsg2);
838     pfc->WriteClient(pfc,(LPVOID)xmsg2,&resplen,0);
839     return SF_STATUS_REQ_FINISHED;
840 }
841
842 DWORD shib_shar_error(PHTTP_FILTER_CONTEXT pfc, SAMLException& e)
843 {
844     pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",0,0);
845     
846     static const char* msg="<HTML><HEAD><TITLE>Shibboleth Attribute Exchange Failed</TITLE></HEAD>\n"
847                            "<BODY><H3>Shibboleth Attribute Exchange Failed</H3>\n"
848                            "While attempting to securely contact your origin site to obtain "
849                            "information about you, an error occurred:<BR><BLOCKQUOTE>";
850     DWORD resplen=strlen(msg);
851     pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
852
853     const char* msg2=e.what();
854     resplen=strlen(msg2);
855     pfc->WriteClient(pfc,(LPVOID)msg2,&resplen,0);
856
857     bool origin=true;
858     Iterator<saml::QName> i=e.getCodes();
859     if (i.hasNext() && XMLString::compareString(L(Responder),i.next().getLocalName()))
860         origin=false;
861
862     const char* msg4=(origin ? "</BLOCKQUOTE><P>The error appears to be located at your origin site.<BR>" :
863                                "</BLOCKQUOTE><P>The error appears to be located at the resource provider's site.<BR>");
864     resplen=strlen(msg4);
865     pfc->WriteClient(pfc,(LPVOID)msg4,&resplen,0);
866     
867     static const char* msg5="<P>Try restarting your browser and accessing the site again to make "
868                             "sure the problem isn't temporary. Please contact the administrator "
869                             "of that site if this problem recurs. If possible, provide him/her "
870                             "with the error message shown above.</BODY></HTML>";
871     resplen=strlen(msg5);
872     pfc->WriteClient(pfc,(LPVOID)msg5,&resplen,0);
873     return SF_STATUS_REQ_FINISHED;
874 }
875
876 extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification)
877 {
878     // Is this a log notification?
879     if (notificationType==SF_NOTIFY_LOG)
880     {
881         if (pfc->pFilterContext)
882             ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=static_cast<LPCSTR>(pfc->pFilterContext);
883         return SF_STATUS_REQ_NEXT_NOTIFICATION;
884     }
885
886     char* xmsg=NULL;
887     settings_t* pSite=NULL;
888     bool bLocked=false;
889     PHTTP_FILTER_PREPROC_HEADERS pn=(PHTTP_FILTER_PREPROC_HEADERS)pvNotification;
890     try
891     {
892         // Determine web site number.
893         dynabuf buf(128);
894         ULONG site_id=0;
895         GetServerVariable(pfc,"INSTANCE_ID",buf,10);
896         if ((site_id=strtoul(buf,NULL,10))==0)
897             return WriteClientError(pfc,"IIS site instance appears to be invalid.");
898
899         // Match site instance to site settings pointer.
900         if (site_id>g_ulMaxSite || g_Sites[site_id-1].g_CookieName.empty())
901             return SF_STATUS_REQ_NEXT_NOTIFICATION;
902         pSite=&g_Sites[site_id-1];
903
904         string targeturl=get_target(pfc,pn,pSite);
905
906         // If the user is accessing the SHIRE acceptance point, pass on.
907         if (targeturl.find(pSite->g_SHIRELocation)!=string::npos)
908             return SF_STATUS_REQ_NEXT_NOTIFICATION;
909
910         // If this is the garbage collection service, do a cache sweep.
911         if (targeturl==pSite->g_GarbageCollector)
912         {
913             pSite->g_AuthCache.lock();
914             bLocked=true;
915             pSite->g_AuthCache.sweep(pSite->g_Lifetime);
916             pSite->g_AuthCache.unlock();
917             bLocked=false;
918             return WriteClientError(pfc,"The cache was swept for expired sessions.");
919         }
920
921         // Get the url request and scan for the must-contain string.
922         if (!pSite->g_MustContain.empty())
923         {
924             char* upcased=new char[targeturl.length()+1];
925             strcpy(upcased,targeturl.c_str());
926             _strupr(upcased);
927             for (vector<string>::const_iterator index=pSite->g_MustContain.begin(); index!=pSite->g_MustContain.end(); index++)
928                 if (strstr(upcased,index->c_str()))
929                     break;
930             delete[] upcased;
931             if (index==pSite->g_MustContain.end())
932                 return SF_STATUS_REQ_NEXT_NOTIFICATION;
933         }
934
935         // SSL check.
936         if (pSite->g_bSSLOnly && !pfc->fIsSecurePort)
937         {
938             xmsg="<HTML><HEAD><TITLE>Access Denied</TITLE></HEAD><BODY>"
939                  "<H1>Access Denied</H1>"
940                  "This server is configured to deny non-SSL requests for secure resources. "
941                  "Try your request again using https instead of http."
942                  "</BODY></HTML>";
943             DWORD resplen=strlen(xmsg);
944             pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",0,0);
945             pfc->WriteClient(pfc,xmsg,&resplen,0);
946             return SF_STATUS_REQ_FINISHED;
947         }
948
949         // Check for authentication cookie.
950         const char* session_id=NULL;
951         GetHeader(pn,pfc,"Cookie:",buf,128,false);
952         if (buf.empty() || !(session_id=strstr(buf,pSite->g_CookieName.c_str())) ||
953             *(session_id+pSite->g_CookieName.length())!='=')
954         {
955             // Redirect to WAYF.
956             string wayf("Location: ");
957             wayf+=pSite->g_WAYFLocation + "?shire=" + get_shire_location(pfc,pSite,targeturl.c_str()) +
958                                           "&target=" + url_encode(targeturl.c_str()) + "\r\n";
959             // Insert the headers.
960             pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
961             pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
962             return SF_STATUS_REQ_FINISHED;
963         }
964
965         session_id+=pSite->g_CookieName.length() + 1;   /* Skip over the '=' */
966         char* cookieend=strchr(session_id,';');
967         if (cookieend)
968             *cookieend = '\0';  /* Ignore anyting after a ; */
969   
970         pSite->g_AuthCache.lock();    // ---> Get cache lock
971         bLocked=true;
972
973         // The caching logic is the heart of the "SHAR".
974         CCacheEntry* entry=pSite->g_AuthCache.find(session_id);
975         try
976         {
977             if (!entry)
978             {
979                 pSite->g_AuthCache.unlock();    // ---> Release cache lock
980                 bLocked=false;
981
982                 // Construct the path to the session file
983                 string sessionFile=pSite->g_SHIRESessionPath + session_id;
984                 try
985                 {
986                     entry=new CCacheEntry(sessionFile.c_str());
987                 }
988                 catch (runtime_error e)
989                 {
990                     // Redirect to WAYF.
991                     string wayf("Location: ");
992                     wayf+=pSite->g_WAYFLocation + "?shire=" + get_shire_location(pfc,pSite,targeturl.c_str()) +
993                                                   "&target=" + url_encode(targeturl.c_str()) + "\r\n";
994                     // Insert the headers.
995                     pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
996                     pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
997                     return SF_STATUS_REQ_FINISHED;
998                 }
999                 pSite->g_AuthCache.lock();    // ---> Get cache lock
1000                 bLocked=true;
1001                 pSite->g_AuthCache.insert(session_id,entry);
1002             }
1003             
1004             if (!entry->isSessionValid(pSite->g_Lifetime,pSite->g_Timeout))
1005             {
1006                 pSite->g_AuthCache.remove(session_id);
1007                 pSite->g_AuthCache.unlock();    // ---> Release cache lock
1008                 bLocked=false;
1009                 delete entry;
1010
1011                 // Redirect to WAYF.
1012                 string wayf("Location: ");
1013                 wayf+=pSite->g_WAYFLocation + "?shire=" + get_shire_location(pfc,pSite,targeturl.c_str()) +
1014                                               "&target=" + url_encode(targeturl.c_str()) + "\r\n";
1015                 // Insert the headers.
1016                 pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
1017                 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
1018                 return SF_STATUS_REQ_FINISHED;
1019             }
1020
1021             if (pSite->g_bCheckAddress && entry->getClientAddress())
1022             {
1023                 GetServerVariable(pfc,"REMOTE_ADDR",buf,16);
1024                 if (strcmp(entry->getClientAddress(),buf))
1025                 {
1026                     pSite->g_AuthCache.remove(session_id);
1027                     delete entry;
1028                     pSite->g_AuthCache.unlock();  // ---> Release cache lock
1029                     bLocked=false;
1030
1031                     return WriteClientError(pfc,
1032                         "Your session was terminated because the network address associated "
1033                         "with it does not match your current address. This is usually caused "
1034                         "by a firewall or proxy of some sort.");
1035                 }
1036             }
1037
1038             // Clear relevant headers.
1039             pn->SetHeader(pfc,"Shib-Attributes:","");
1040             pn->SetHeader(pfc,"remote-user:","");
1041             for (map<string,string>::const_iterator h_iter=g_mapAttribNameToHeader.begin(); h_iter!=g_mapAttribNameToHeader.end(); h_iter++)
1042                 if (h_iter->second!="REMOTE_USER:")
1043                     pn->SetHeader(pfc,const_cast<char*>(h_iter->second.c_str()),"");
1044
1045             if (pSite->g_bExportAssertion)
1046             {
1047                 string exp((char*)entry->getSerializedAssertion(targeturl.c_str(),pSite));
1048                 string::size_type lfeed;
1049                 while ((lfeed=exp.find('\n'))!=string::npos)
1050                     exp.erase(lfeed,1);
1051                 pn->SetHeader(pfc,"Shib-Attributes:",const_cast<char*>(exp.c_str()));
1052             }
1053             Iterator<SAMLAttribute*> i=entry->getAttributes(targeturl.c_str(),pSite);
1054             
1055             while (i.hasNext())
1056             {
1057                 SAMLAttribute* attr=i.next();
1058
1059                 // Are we supposed to export it?
1060                 map<xstring,string>::const_iterator iname=g_mapAttribNames.find(attr->getName());
1061                 if (iname!=g_mapAttribNames.end())
1062                 {
1063                     string hname=g_mapAttribNameToHeader[iname->second];
1064                     Iterator<string> vals=attr->getSingleByteValues();
1065                     if (hname=="REMOTE_USER:" && vals.hasNext())
1066                     {
1067                         char* principal=const_cast<char*>(vals.next().c_str());
1068                         pn->SetHeader(pfc,"remote-user:",principal);
1069                         pfc->pFilterContext=pfc->AllocMem(pfc,strlen(principal)+1,0);
1070                         if (pfc->pFilterContext)
1071                             strcpy(static_cast<char*>(pfc->pFilterContext),principal);
1072                     }   
1073                     else
1074                     {
1075                         string header(" ");
1076                         while (vals.hasNext())
1077                             header+=vals.next() + " ";
1078                         pn->SetHeader(pfc,const_cast<char*>(hname.c_str()),const_cast<char*>(header.c_str()));
1079                     }
1080                 }
1081             }
1082
1083             pSite->g_AuthCache.unlock();  // ---> Release cache lock
1084             bLocked=false;
1085             return SF_STATUS_REQ_NEXT_NOTIFICATION;
1086         }
1087         catch (SAMLException& e)
1088         {
1089             Iterator<saml::QName> i=e.getCodes();
1090             int c=0;
1091             while (i.hasNext())
1092             {
1093                     c++;
1094                     saml::QName q=i.next();
1095                     if (c==1 && !XMLString::compareString(q.getNamespaceURI(),saml::XML::SAMLP_NS) &&
1096                     !XMLString::compareString(q.getLocalName(),L(Requester)))
1097                     continue;
1098                 else if (c==2 && !XMLString::compareString(q.getNamespaceURI(),shibboleth::XML::SHIB_NS) &&
1099                          !XMLString::compareString(q.getLocalName(),shibboleth::XML::Literals::InvalidHandle))
1100                 {
1101                     if (!bLocked)
1102                         pSite->g_AuthCache.lock();  // ---> Grab cache lock
1103                     pSite->g_AuthCache.remove(session_id);
1104                     pSite->g_AuthCache.unlock();  // ---> Release cache lock
1105                     delete entry;
1106
1107                     // Redirect to WAYF.
1108                     string wayf("Location: ");
1109                     wayf+=pSite->g_WAYFLocation + "?shire=" + get_shire_location(pfc,pSite,targeturl.c_str()) +
1110                                                   "&target=" + url_encode(targeturl.c_str()) + "\r\n";
1111                     // Insert the headers.
1112                     pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
1113                     pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
1114                     return SF_STATUS_REQ_FINISHED;
1115                 }
1116                 break;
1117             }
1118                 return shib_shar_error(pfc,e);
1119         }
1120         catch (XMLException& e)
1121         {
1122             if (bLocked)
1123                 pSite->g_AuthCache.unlock();
1124             auto_ptr<char> msg(XMLString::transcode(e.getMessage()));
1125             SAMLException ex(SAMLException::RESPONDER,msg.get());
1126             return shib_shar_error(pfc,ex);
1127         }
1128     }
1129     catch(bad_alloc)
1130     {
1131         xmsg="Out of memory.";
1132     }
1133     catch(DWORD e)
1134     {
1135         if (e==ERROR_NO_DATA)
1136             xmsg="A required variable or header was empty.";
1137         else
1138             xmsg="Server detected unexpected IIS error.";
1139     }
1140     catch(...)
1141     {
1142         xmsg="Server caught an unknown exception.";
1143     }
1144
1145     // If we drop here, the exception handler set the proper message.
1146     if (bLocked)
1147         pSite->g_AuthCache.unlock();
1148     return WriteClientError(pfc,xmsg);
1149 }