d286c4b108974db13a8b5adcf3cf651d9f7ccf92
[shibboleth/sp.git] / isapi_shib / isapi_shib.cpp
1 /*
2  *  Copyright 2001-2007 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  * isapi_shib.cpp
19  * 
20  * Shibboleth ISAPI filter
21  */
22
23 #define SHIBSP_LITE
24 #include "config_win32.h"
25
26 #define _CRT_NONSTDC_NO_DEPRECATE 1
27 #define _CRT_SECURE_NO_DEPRECATE 1
28
29 #include <shibsp/AbstractSPRequest.h>
30 #include <shibsp/SPConfig.h>
31 #include <shibsp/ServiceProvider.h>
32 #include <xmltooling/unicode.h>
33 #include <xmltooling/XMLToolingConfig.h>
34 #include <xmltooling/util/NDC.h>
35 #include <xmltooling/util/XMLConstants.h>
36 #include <xmltooling/util/XMLHelper.h>
37 #include <xercesc/util/Base64.hpp>
38 #include <xercesc/util/XMLUniDefs.hpp>
39
40 #include <set>
41 #include <sstream>
42 #include <fstream>
43 #include <process.h>
44
45 #include <windows.h>
46 #include <httpfilt.h>
47 #include <httpext.h>
48
49 using namespace shibsp;
50 using namespace xmltooling;
51 using namespace xercesc;
52 using namespace std;
53
54 // globals
55 namespace {
56     static const XMLCh path[] =             UNICODE_LITERAL_4(p,a,t,h);
57     static const XMLCh validate[] =         UNICODE_LITERAL_8(v,a,l,i,d,a,t,e);
58     static const XMLCh name[] =             UNICODE_LITERAL_4(n,a,m,e);
59     static const XMLCh port[] =             UNICODE_LITERAL_4(p,o,r,t);
60     static const XMLCh sslport[] =          UNICODE_LITERAL_7(s,s,l,p,o,r,t);
61     static const XMLCh scheme[] =           UNICODE_LITERAL_6(s,c,h,e,m,e);
62     static const XMLCh id[] =               UNICODE_LITERAL_2(i,d);
63     static const XMLCh ISAPI[] =            UNICODE_LITERAL_5(I,S,A,P,I);
64     static const XMLCh Alias[] =            UNICODE_LITERAL_5(A,l,i,a,s);
65     static const XMLCh normalizeRequest[] = UNICODE_LITERAL_16(n,o,r,m,a,l,i,z,e,R,e,q,u,e,s,t);
66     static const XMLCh Site[] =             UNICODE_LITERAL_4(S,i,t,e);
67
68     struct site_t {
69         site_t(const DOMElement* e)
70         {
71             auto_ptr_char n(e->getAttributeNS(NULL,name));
72             auto_ptr_char s(e->getAttributeNS(NULL,scheme));
73             auto_ptr_char p(e->getAttributeNS(NULL,port));
74             auto_ptr_char p2(e->getAttributeNS(NULL,sslport));
75             if (n.get()) m_name=n.get();
76             if (s.get()) m_scheme=s.get();
77             if (p.get()) m_port=p.get();
78             if (p2.get()) m_sslport=p2.get();
79             e = XMLHelper::getFirstChildElement(e, Alias);
80             while (e) {
81                 if (e->hasChildNodes()) {
82                     auto_ptr_char alias(e->getFirstChild()->getNodeValue());
83                     m_aliases.insert(alias.get());
84                 }
85                 e = XMLHelper::getNextSiblingElement(e, Alias);
86             }
87         }
88         string m_scheme,m_port,m_sslport,m_name;
89         set<string> m_aliases;
90     };
91     
92     HINSTANCE g_hinstDLL;
93     SPConfig* g_Config = NULL;
94     map<string,site_t> g_Sites;
95     bool g_bNormalizeRequest = true;
96     string g_unsetHeaderValue;
97     bool g_checkSpoofing = true;
98     bool g_catchAll = false;
99     vector<string> g_NoCerts;
100 }
101
102 BOOL LogEvent(
103     LPCSTR  lpUNCServerName,
104     WORD  wType,
105     DWORD  dwEventID,
106     PSID  lpUserSid,
107     LPCSTR  message)
108 {
109     LPCSTR  messages[] = {message, NULL};
110     
111     HANDLE hElog = RegisterEventSource(lpUNCServerName, "Shibboleth ISAPI Filter");
112     BOOL res = ReportEvent(hElog, wType, 0, dwEventID, lpUserSid, 1, 0, messages, NULL);
113     return (DeregisterEventSource(hElog) && res);
114 }
115
116 extern "C" __declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
117 {
118     if (fdwReason==DLL_PROCESS_ATTACH)
119         g_hinstDLL=hinstDLL;
120     return TRUE;
121 }
122
123 extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer)
124 {
125     if (!pVer)
126         return FALSE;
127         
128     if (!g_Config) {
129         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
130                 "Extension mode startup not possible, is the DLL loaded as a filter?");
131         return FALSE;
132     }
133
134     pVer->dwExtensionVersion=HSE_VERSION;
135     strncpy(pVer->lpszExtensionDesc,"Shibboleth ISAPI Extension",HSE_MAX_EXT_DLL_NAME_LEN-1);
136     return TRUE;
137 }
138
139 extern "C" BOOL WINAPI TerminateExtension(DWORD)
140 {
141     return TRUE;    // cleanup should happen when filter unloads
142 }
143
144 extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer)
145 {
146     if (!pVer)
147         return FALSE;
148     else if (g_Config) {
149         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
150                 "Reentrant filter initialization, ignoring...");
151         return TRUE;
152     }
153
154     LPCSTR schemadir=getenv("SHIBSP_SCHEMAS");
155     if (!schemadir)
156         schemadir=SHIBSP_SCHEMAS;
157     LPCSTR config=getenv("SHIBSP_CONFIG");
158     if (!config)
159         config=SHIBSP_CONFIG;
160     g_Config=&SPConfig::getConfig();
161     g_Config->setFeatures(
162         SPConfig::Listener |
163         SPConfig::Caching |
164         SPConfig::RequestMapping |
165         SPConfig::InProcess |
166         SPConfig::Logging |
167         SPConfig::Handlers
168         );
169     if (!g_Config->init(schemadir)) {
170         g_Config=NULL;
171         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
172                 "Filter startup failed during library initialization, check native log for help.");
173         return FALSE;
174     }
175
176     try {
177         DOMDocument* dummydoc=XMLToolingConfig::getConfig().getParser().newDocument();
178         XercesJanitor<DOMDocument> docjanitor(dummydoc);
179         DOMElement* dummy = dummydoc->createElementNS(NULL,path);
180         auto_ptr_XMLCh src(config);
181         dummy->setAttributeNS(NULL,path,src.get());
182         dummy->setAttributeNS(NULL,validate,xmlconstants::XML_ONE);
183
184         g_Config->setServiceProvider(g_Config->ServiceProviderManager.newPlugin(XML_SERVICE_PROVIDER,dummy));
185         g_Config->getServiceProvider()->init();
186     }
187     catch (exception& ex) {
188         g_Config->term();
189         g_Config=NULL;
190         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, ex.what());
191         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
192                 "Filter startup failed to load configuration, check native log for details.");
193         return FALSE;
194     }
195     
196     // Access implementation-specifics and site mappings.
197     ServiceProvider* sp=g_Config->getServiceProvider();
198     Locker locker(sp);
199     const PropertySet* props=sp->getPropertySet("InProcess");
200     if (props) {
201         pair<bool,const char*> unsetValue=props->getString("unsetHeaderValue");
202         if (unsetValue.first)
203             g_unsetHeaderValue = unsetValue.second;
204         pair<bool,bool> flag=props->getBool("checkSpoofing");
205         g_checkSpoofing = !flag.first || flag.second;
206         flag=props->getBool("catchAll");
207         g_catchAll = flag.first && flag.second;
208
209         props = props->getPropertySet("ISAPI");
210         if (props) {
211             flag = props->getBool("normalizeRequest");
212             g_bNormalizeRequest = !flag.first || flag.second;
213             const DOMElement* child = XMLHelper::getFirstChildElement(props->getElement(),Site);
214             while (child) {
215                 auto_ptr_char id(child->getAttributeNS(NULL,id));
216                 if (id.get())
217                     g_Sites.insert(pair<string,site_t>(id.get(),site_t(child)));
218                 child=XMLHelper::getNextSiblingElement(child,Site);
219             }
220         }
221     }
222
223     pVer->dwFilterVersion=HTTP_FILTER_REVISION;
224     strncpy(pVer->lpszFilterDesc,"Shibboleth ISAPI Filter",SF_MAX_FILTER_DESC_LEN);
225     pVer->dwFlags=(SF_NOTIFY_ORDER_HIGH |
226                    SF_NOTIFY_SECURE_PORT |
227                    SF_NOTIFY_NONSECURE_PORT |
228                    SF_NOTIFY_PREPROC_HEADERS |
229                    SF_NOTIFY_LOG);
230     LogEvent(NULL, EVENTLOG_INFORMATION_TYPE, 7701, NULL, "Filter initialized...");
231     return TRUE;
232 }
233
234 extern "C" BOOL WINAPI TerminateFilter(DWORD)
235 {
236     if (g_Config)
237         g_Config->term();
238     g_Config = NULL;
239     LogEvent(NULL, EVENTLOG_INFORMATION_TYPE, 7701, NULL, "Filter shut down...");
240     return TRUE;
241 }
242
243 /* Next up, some suck-free versions of various APIs.
244
245    You DON'T require people to guess the buffer size and THEN tell them the right size.
246    Returning an LPCSTR is apparently way beyond their ken. Not to mention the fact that
247    constant strings aren't typed as such, making it just that much harder. These versions
248    are now updated to use a special growable buffer object, modeled after the standard
249    string class. The standard string won't work because they left out the option to
250    pre-allocate a non-constant buffer.
251 */
252
253 class dynabuf
254 {
255 public:
256     dynabuf() { bufptr=NULL; buflen=0; }
257     dynabuf(size_t s) { bufptr=new char[buflen=s]; *bufptr=0; }
258     ~dynabuf() { delete[] bufptr; }
259     size_t length() const { return bufptr ? strlen(bufptr) : 0; }
260     size_t size() const { return buflen; }
261     bool empty() const { return length()==0; }
262     void reserve(size_t s, bool keep=false);
263     void erase() { if (bufptr) memset(bufptr,0,buflen); }
264     operator char*() { return bufptr; }
265     bool operator ==(const char* s) const;
266     bool operator !=(const char* s) const { return !(*this==s); }
267 private:
268     char* bufptr;
269     size_t buflen;
270 };
271
272 void dynabuf::reserve(size_t s, bool keep)
273 {
274     if (s<=buflen)
275         return;
276     char* p=new char[s];
277     if (keep)
278         while (buflen--)
279             p[buflen]=bufptr[buflen];
280     buflen=s;
281     delete[] bufptr;
282     bufptr=p;
283 }
284
285 bool dynabuf::operator==(const char* s) const
286 {
287     if (buflen==NULL || s==NULL)
288         return (buflen==NULL && s==NULL);
289     else
290         return strcmp(bufptr,s)==0;
291 }
292
293 void GetServerVariable(PHTTP_FILTER_CONTEXT pfc, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
294 {
295     s.reserve(size);
296     s.erase();
297     size=s.size();
298
299     while (!pfc->GetServerVariable(pfc,lpszVariable,s,&size)) {
300         // Grumble. Check the error.
301         DWORD e=GetLastError();
302         if (e==ERROR_INSUFFICIENT_BUFFER)
303             s.reserve(size);
304         else
305             break;
306     }
307     if (bRequired && s.empty())
308         throw ERROR_NO_DATA;
309 }
310
311 void GetServerVariable(LPEXTENSION_CONTROL_BLOCK lpECB, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
312 {
313     s.reserve(size);
314     s.erase();
315     size=s.size();
316
317     while (!lpECB->GetServerVariable(lpECB->ConnID,lpszVariable,s,&size)) {
318         // Grumble. Check the error.
319         DWORD e=GetLastError();
320         if (e==ERROR_INSUFFICIENT_BUFFER)
321             s.reserve(size);
322         else
323             break;
324     }
325     if (bRequired && s.empty())
326         throw ERROR_NO_DATA;
327 }
328
329 void GetHeader(PHTTP_FILTER_PREPROC_HEADERS pn, PHTTP_FILTER_CONTEXT pfc,
330                LPSTR lpszName, dynabuf& s, DWORD size=80, bool bRequired=true)
331 {
332     s.reserve(size);
333     s.erase();
334     size=s.size();
335
336     while (!pn->GetHeader(pfc,lpszName,s,&size)) {
337         // Grumble. Check the error.
338         DWORD e=GetLastError();
339         if (e==ERROR_INSUFFICIENT_BUFFER)
340             s.reserve(size);
341         else
342             break;
343     }
344     if (bRequired && s.empty())
345         throw ERROR_NO_DATA;
346 }
347
348 /****************************************************************************/
349 // ISAPI Filter
350
351 class ShibTargetIsapiF : public AbstractSPRequest
352 {
353   PHTTP_FILTER_CONTEXT m_pfc;
354   PHTTP_FILTER_PREPROC_HEADERS m_pn;
355   multimap<string,string> m_headers;
356   int m_port;
357   string m_scheme,m_hostname;
358   mutable string m_remote_addr,m_content_type,m_method;
359   dynabuf m_allhttp;
360
361 public:
362   ShibTargetIsapiF(PHTTP_FILTER_CONTEXT pfc, PHTTP_FILTER_PREPROC_HEADERS pn, const site_t& site)
363       : AbstractSPRequest(SHIBSP_LOGCAT".ISAPI"), m_pfc(pfc), m_pn(pn), m_allhttp(4096) {
364
365     // URL path always come from IIS.
366     dynabuf var(256);
367     GetHeader(pn,pfc,"url",var,256,false);
368     setRequestURI(var);
369
370     // Port may come from IIS or from site def.
371     if (!g_bNormalizeRequest || (pfc->fIsSecurePort && site.m_sslport.empty()) || (!pfc->fIsSecurePort && site.m_port.empty())) {
372         GetServerVariable(pfc,"SERVER_PORT",var,10);
373         m_port = atoi(var);
374     }
375     else if (pfc->fIsSecurePort) {
376         m_port = atoi(site.m_sslport.c_str());
377     }
378     else {
379         m_port = atoi(site.m_port.c_str());
380     }
381     
382     // Scheme may come from site def or be derived from IIS.
383     m_scheme=site.m_scheme;
384     if (m_scheme.empty() || !g_bNormalizeRequest)
385         m_scheme=pfc->fIsSecurePort ? "https" : "http";
386
387     GetServerVariable(pfc,"SERVER_NAME",var,32);
388
389     // Make sure SERVER_NAME is "authorized" for use on this site. If not, set to canonical name.
390     m_hostname = var;
391     if (site.m_name!=m_hostname && site.m_aliases.find(m_hostname)==site.m_aliases.end())
392         m_hostname=site.m_name;
393   }
394   ~ShibTargetIsapiF() { }
395
396   const char* getScheme() const {
397     return m_scheme.c_str();
398   }
399   const char* getHostname() const {
400     return m_hostname.c_str();
401   }
402   int getPort() const {
403     return m_port;
404   }
405   const char* getMethod() const {
406     if (m_method.empty()) {
407         dynabuf var(5);
408         GetServerVariable(m_pfc,"REQUEST_METHOD",var,5,false);
409         if (!var.empty())
410             m_method = var;
411     }
412     return m_method.c_str();
413   }
414   string getContentType() const {
415     if (m_content_type.empty()) {
416         dynabuf var(32);
417         GetServerVariable(m_pfc,"CONTENT_TYPE",var,32,false);
418         if (!var.empty())
419             m_content_type = var;
420     }
421     return m_content_type;
422   }
423   long getContentLength() const {
424       return 0;
425   }
426   string getRemoteAddr() const {
427     if (m_remote_addr.empty()) {
428         dynabuf var(16);
429         GetServerVariable(m_pfc,"REMOTE_ADDR",var,16,false);
430         if (!var.empty())
431             m_remote_addr = var;
432     }
433     return m_remote_addr;
434   }
435   void log(SPLogLevel level, const string& msg) {
436     AbstractSPRequest::log(level,msg);
437     if (level >= SPError)
438         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg.c_str());
439   }
440   void clearHeader(const char* rawname, const char* cginame) {
441     if (g_checkSpoofing) {
442         if (m_allhttp.empty())
443                 GetServerVariable(m_pfc,"ALL_HTTP",m_allhttp,4096);
444         if (strstr(m_allhttp, cginame))
445             throw opensaml::SecurityPolicyException("Attempt to spoof header ($1) was detected.", params(1, rawname));
446     }
447     string hdr(!strcmp(rawname,"REMOTE_USER") ? "remote-user" : rawname);
448     hdr += ':';
449     m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(g_unsetHeaderValue.c_str()));
450   }
451   void setHeader(const char* name, const char* value) {
452     string hdr(name);
453     hdr += ':';
454     m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(value));
455   }
456   string getHeader(const char* name) const {
457     string hdr(name);
458     hdr += ':';
459     dynabuf buf(256);
460     GetHeader(m_pn, m_pfc, const_cast<char*>(hdr.c_str()), buf, 256, false);
461     return string(buf);
462   }
463   void setRemoteUser(const char* user) {
464     setHeader("remote-user", user);
465   }
466   string getRemoteUser() const {
467     return getHeader("remote-user");
468   }
469   void setResponseHeader(const char* name, const char* value) {
470     // Set for later.
471     if (value)
472         m_headers.insert(make_pair(name,value));
473     else
474         m_headers.erase(name);
475   }
476   long sendResponse(istream& in, long status) {
477     string hdr = string("Connection: close\r\n");
478     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
479         hdr += i->first + ": " + i->second + "\r\n";
480     hdr += "\r\n";
481     const char* codestr="200 OK";
482     switch (status) {
483         case XMLTOOLING_HTTP_STATUS_UNAUTHORIZED:   codestr="401 Authorization Required"; break;
484         case XMLTOOLING_HTTP_STATUS_FORBIDDEN:      codestr="403 Forbidden"; break;
485         case XMLTOOLING_HTTP_STATUS_NOTFOUND:       codestr="404 Not Found"; break;
486         case XMLTOOLING_HTTP_STATUS_ERROR:          codestr="500 Server Error"; break;
487     }
488     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, (void*)codestr, (DWORD)hdr.c_str(), 0);
489     char buf[1024];
490     while (in) {
491         in.read(buf,1024);
492         DWORD resplen = in.gcount();
493         m_pfc->WriteClient(m_pfc, buf, &resplen, 0);
494     }
495     return SF_STATUS_REQ_FINISHED;
496   }
497   long sendRedirect(const char* url) {
498     // XXX: Don't support the httpRedirect option, yet.
499     string hdr=string("Location: ") + url + "\r\n"
500       "Content-Type: text/html\r\n"
501       "Content-Length: 40\r\n"
502       "Expires: 01-Jan-1997 12:00:00 GMT\r\n"
503       "Cache-Control: private,no-store,no-cache\r\n";
504     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
505         hdr += i->first + ": " + i->second + "\r\n";
506     hdr += "\r\n";
507     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, "302 Please Wait", (DWORD)hdr.c_str(), 0);
508     static const char* redmsg="<HTML><BODY>Redirecting...</BODY></HTML>";
509     DWORD resplen=40;
510     m_pfc->WriteClient(m_pfc, (LPVOID)redmsg, &resplen, 0);
511     return SF_STATUS_REQ_FINISHED;
512   }
513   long returnDecline() {
514       return SF_STATUS_REQ_NEXT_NOTIFICATION;
515   }
516   long returnOK() {
517     return SF_STATUS_REQ_NEXT_NOTIFICATION;
518   }
519
520   const vector<string>& getClientCertificates() const {
521       return g_NoCerts;
522   }
523   
524   // The filter never processes the POST, so stub these methods.
525   const char* getQueryString() const { throw IOException("getQueryString not implemented"); }
526   const char* getRequestBody() const { throw IOException("getRequestBody not implemented"); }
527 };
528
529 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg)
530 {
531     LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg);
532     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
533     pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",(DWORD)ctype,0);
534     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Filter Error</TITLE></HEAD><BODY>"
535                             "<H1>Shibboleth Filter Error</H1>";
536     DWORD resplen=strlen(xmsg);
537     pfc->WriteClient(pfc,(LPVOID)xmsg,&resplen,0);
538     resplen=strlen(msg);
539     pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
540     static const char* xmsg2="</BODY></HTML>";
541     resplen=strlen(xmsg2);
542     pfc->WriteClient(pfc,(LPVOID)xmsg2,&resplen,0);
543     return SF_STATUS_REQ_FINISHED;
544 }
545
546 extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification)
547 {
548     // Is this a log notification?
549     if (notificationType==SF_NOTIFY_LOG)
550     {
551         if (pfc->pFilterContext)
552             ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=static_cast<LPCSTR>(pfc->pFilterContext);
553         return SF_STATUS_REQ_NEXT_NOTIFICATION;
554     }
555
556     PHTTP_FILTER_PREPROC_HEADERS pn=(PHTTP_FILTER_PREPROC_HEADERS)pvNotification;
557     try
558     {
559         // Determine web site number. This can't really fail, I don't think.
560         dynabuf buf(128);
561         GetServerVariable(pfc,"INSTANCE_ID",buf,10);
562
563         // Match site instance to host name, skip if no match.
564         map<string,site_t>::const_iterator map_i=g_Sites.find(static_cast<char*>(buf));
565         if (map_i==g_Sites.end())
566             return SF_STATUS_REQ_NEXT_NOTIFICATION;
567             
568         ostringstream threadid;
569         threadid << "[" << getpid() << "] isapi_shib" << '\0';
570         xmltooling::NDC ndc(threadid.str().c_str());
571
572         ShibTargetIsapiF stf(pfc, pn, map_i->second);
573
574         // "false" because we don't override the Shib settings
575         pair<bool,long> res = stf.getServiceProvider().doAuthentication(stf);
576         if (res.first) return res.second;
577
578         // "false" because we don't override the Shib settings
579         res = stf.getServiceProvider().doExport(stf);
580         if (res.first) return res.second;
581
582         res = stf.getServiceProvider().doAuthorization(stf);
583         if (res.first) return res.second;
584
585         return SF_STATUS_REQ_NEXT_NOTIFICATION;
586     }
587     catch(bad_alloc) {
588         return WriteClientError(pfc,"Out of Memory");
589     }
590     catch(long e) {
591         if (e==ERROR_NO_DATA)
592             return WriteClientError(pfc,"A required variable or header was empty.");
593         else
594             return WriteClientError(pfc,"Shibboleth Filter detected unexpected IIS error.");
595     }
596     catch (exception& e) {
597         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, e.what());
598         return WriteClientError(pfc,"Shibboleth Filter caught an exception, check Event Log for details.");
599     }
600     catch(...) {
601         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, "Shibboleth Filter threw an unknown exception.");
602         if (g_catchAll)
603             return WriteClientError(pfc,"Shibboleth Filter threw an unknown exception.");
604         throw;
605     }
606
607     return WriteClientError(pfc,"Shibboleth Filter reached unreachable code, save my walrus!");
608 }
609         
610
611 /****************************************************************************/
612 // ISAPI Extension
613
614 DWORD WriteClientError(LPEXTENSION_CONTROL_BLOCK lpECB, const char* msg)
615 {
616     LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg);
617     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
618     lpECB->ServerSupportFunction(lpECB->ConnID,HSE_REQ_SEND_RESPONSE_HEADER,"200 OK",0,(LPDWORD)ctype);
619     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Error</TITLE></HEAD><BODY><H1>Shibboleth Error</H1>";
620     DWORD resplen=strlen(xmsg);
621     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg,&resplen,HSE_IO_SYNC);
622     resplen=strlen(msg);
623     lpECB->WriteClient(lpECB->ConnID,(LPVOID)msg,&resplen,HSE_IO_SYNC);
624     static const char* xmsg2="</BODY></HTML>";
625     resplen=strlen(xmsg2);
626     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg2,&resplen,HSE_IO_SYNC);
627     return HSE_STATUS_SUCCESS;
628 }
629
630
631 class ShibTargetIsapiE : public AbstractSPRequest
632 {
633   LPEXTENSION_CONTROL_BLOCK m_lpECB;
634   multimap<string,string> m_headers;
635   mutable vector<string> m_certs;
636   mutable string m_body;
637   mutable bool m_gotBody;
638   int m_port;
639   string m_scheme,m_hostname,m_uri;
640   mutable string m_remote_addr,m_remote_user;
641   
642 public:
643   ShibTargetIsapiE(LPEXTENSION_CONTROL_BLOCK lpECB, const site_t& site)
644       : AbstractSPRequest(SHIBSP_LOGCAT".ISAPI"), m_lpECB(lpECB), m_gotBody(false) {
645     dynabuf ssl(5);
646     GetServerVariable(lpECB,"HTTPS",ssl,5);
647     bool SSL=(ssl=="on" || ssl=="ON");
648
649     // Scheme may come from site def or be derived from IIS.
650     m_scheme=site.m_scheme;
651     if (m_scheme.empty() || !g_bNormalizeRequest)
652         m_scheme = SSL ? "https" : "http";
653
654     // URL path always come from IIS.
655     dynabuf url(256);
656     GetServerVariable(lpECB,"URL",url,255);
657
658     // Port may come from IIS or from site def.
659     dynabuf port(11);
660     if (!g_bNormalizeRequest || (SSL && site.m_sslport.empty()) || (!SSL && site.m_port.empty()))
661         GetServerVariable(lpECB,"SERVER_PORT",port,10);
662     else if (SSL) {
663         strncpy(port,site.m_sslport.c_str(),10);
664         static_cast<char*>(port)[10]=0;
665     }
666     else {
667         strncpy(port,site.m_port.c_str(),10);
668         static_cast<char*>(port)[10]=0;
669     }
670     m_port = atoi(port);
671
672     dynabuf var(32);
673     GetServerVariable(lpECB, "SERVER_NAME", var, 32);
674
675     // Make sure SERVER_NAME is "authorized" for use on this site. If not, set to canonical name.
676     m_hostname=var;
677     if (site.m_name!=m_hostname && site.m_aliases.find(m_hostname)==site.m_aliases.end())
678         m_hostname=site.m_name;
679
680     /*
681      * IIS screws us over on PATH_INFO (the hits keep on coming). We need to figure out if
682      * the server is set up for proper PATH_INFO handling, or "IIS sucks rabid weasels mode",
683      * which is the default. No perfect way to tell, but we can take a good guess by checking
684      * whether the URL is a substring of the PATH_INFO:
685      * 
686      * e.g. for /Shibboleth.sso/SAML/POST
687      * 
688      *  Bad mode (default):
689      *      URL:        /Shibboleth.sso
690      *      PathInfo:   /Shibboleth.sso/SAML/POST
691      * 
692      *  Good mode:
693      *      URL:        /Shibboleth.sso
694      *      PathInfo:   /SAML/POST
695      */
696     
697     string uri;
698
699     // Clearly we're only in bad mode if path info exists at all.
700     if (lpECB->lpszPathInfo && *(lpECB->lpszPathInfo)) {
701         if (strstr(lpECB->lpszPathInfo,url))
702             // Pretty good chance we're in bad mode, unless the PathInfo repeats the path itself.
703             uri = lpECB->lpszPathInfo;
704         else {
705             uri = url;
706             uri += lpECB->lpszPathInfo;
707         }
708     }
709     else {
710         uri = url;
711     }
712     
713     // For consistency with Apache, let's add the query string.
714     if (lpECB->lpszQueryString && *(lpECB->lpszQueryString)) {
715         uri += '?';
716         uri += lpECB->lpszQueryString;
717     }
718
719     setRequestURI(uri.c_str());
720   }
721   ~ShibTargetIsapiE() { }
722
723   const char* getScheme() const {
724     return m_scheme.c_str();
725   }
726   const char* getHostname() const {
727     return m_hostname.c_str();
728   }
729   int getPort() const {
730     return m_port;
731   }
732   const char* getMethod() const {
733     return m_lpECB->lpszMethod;
734   }
735   string getContentType() const {
736     return m_lpECB->lpszContentType ? m_lpECB->lpszContentType : "";
737   }
738   long getContentLength() const {
739       return m_lpECB->cbTotalBytes;
740   }
741   string getRemoteUser() const {
742     if (m_remote_user.empty()) {
743         dynabuf var(16);
744         GetServerVariable(m_lpECB, "REMOTE_USER", var, 32, false);
745         if (!var.empty())
746             m_remote_user = var;
747     }
748     return m_remote_user;
749   }
750   string getRemoteAddr() const {
751     if (m_remote_addr.empty()) {
752         dynabuf var(16);
753         GetServerVariable(m_lpECB, "REMOTE_ADDR", var, 16, false);
754         if (!var.empty())
755             m_remote_addr = var;
756     }
757     return m_remote_addr;
758   }
759   void log(SPLogLevel level, const string& msg) const {
760       AbstractSPRequest::log(level,msg);
761       if (level >= SPError)
762           LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg.c_str());
763   }
764   string getHeader(const char* name) const {
765     string hdr("HTTP_");
766     for (; *name; ++name) {
767         if (*name=='-')
768             hdr += '_';
769         else
770             hdr += toupper(*name);
771     }
772     dynabuf buf(128);
773     GetServerVariable(m_lpECB, const_cast<char*>(hdr.c_str()), buf, 128, false);
774     return buf.empty() ? "" : buf;
775   }
776   void setResponseHeader(const char* name, const char* value) {
777     // Set for later.
778     if (value)
779         m_headers.insert(make_pair(name,value));
780     else
781         m_headers.erase(name);
782   }
783   const char* getQueryString() const {
784     return m_lpECB->lpszQueryString;
785   }
786   const char* getRequestBody() const {
787     if (m_gotBody)
788         return m_body.c_str();
789     if (m_lpECB->cbTotalBytes > 1024*1024) // 1MB?
790         throw opensaml::SecurityPolicyException("Size of request body exceeded 1M size limit.");
791     else if (m_lpECB->cbTotalBytes > m_lpECB->cbAvailable) {
792       m_gotBody=true;
793       char buf[8192];
794       DWORD datalen=m_lpECB->cbTotalBytes;
795       while (datalen) {
796         DWORD buflen=8192;
797         BOOL ret = m_lpECB->ReadClient(m_lpECB->ConnID, buf, &buflen);
798         if (!ret || !buflen)
799             throw IOException("Error reading request body from browser.");
800         m_body.append(buf, buflen);
801         datalen-=buflen;
802       }
803     }
804     else if (m_lpECB->cbAvailable) {
805         m_gotBody=true;
806         m_body.assign(reinterpret_cast<char*>(m_lpECB->lpbData),m_lpECB->cbAvailable);
807     }
808     return m_body.c_str();
809   }
810   long sendResponse(istream& in, long status) {
811     string hdr = string("Connection: close\r\n");
812     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
813         hdr += i->first + ": " + i->second + "\r\n";
814     hdr += "\r\n";
815     const char* codestr="200 OK";
816     switch (status) {
817         case XMLTOOLING_HTTP_STATUS_UNAUTHORIZED:   codestr="401 Authorization Required"; break;
818         case XMLTOOLING_HTTP_STATUS_FORBIDDEN:      codestr="403 Forbidden"; break;
819         case XMLTOOLING_HTTP_STATUS_NOTFOUND:       codestr="404 Not Found"; break;
820         case XMLTOOLING_HTTP_STATUS_ERROR:          codestr="500 Server Error"; break;
821     }
822     m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER, (void*)codestr, 0, (LPDWORD)hdr.c_str());
823     char buf[1024];
824     while (in) {
825         in.read(buf,1024);
826         DWORD resplen = in.gcount();
827         m_lpECB->WriteClient(m_lpECB->ConnID, buf, &resplen, HSE_IO_SYNC);
828     }
829     return HSE_STATUS_SUCCESS;
830   }
831   long sendRedirect(const char* url) {
832     string hdr=string("Location: ") + url + "\r\n"
833       "Content-Type: text/html\r\n"
834       "Content-Length: 40\r\n"
835       "Expires: 01-Jan-1997 12:00:00 GMT\r\n"
836       "Cache-Control: private,no-store,no-cache\r\n";
837     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
838         hdr += i->first + ": " + i->second + "\r\n";
839     hdr += "\r\n";
840     m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER, "302 Moved", 0, (LPDWORD)hdr.c_str());
841     static const char* redmsg="<HTML><BODY>Redirecting...</BODY></HTML>";
842     DWORD resplen=40;
843     m_lpECB->WriteClient(m_lpECB->ConnID, (LPVOID)redmsg, &resplen, HSE_IO_SYNC);
844     return HSE_STATUS_SUCCESS;
845   }
846   // Decline happens in the POST processor if this isn't the shire url
847   // Note that it can also happen with HTAccess, but we don't support that, yet.
848   long returnDecline() {
849     return WriteClientError(
850         m_lpECB,
851         "ISAPI extension can only be invoked to process Shibboleth protocol requests."
852                 "Make sure the mapped file extension doesn't match actual content."
853         );
854   }
855   long returnOK() {
856       return HSE_STATUS_SUCCESS;
857   }
858
859   const vector<string>& getClientCertificates() const {
860       if (m_certs.empty()) {
861         char CertificateBuf[8192];
862         CERT_CONTEXT_EX ccex;
863         ccex.cbAllocated = sizeof(CertificateBuf);
864         ccex.CertContext.pbCertEncoded = (BYTE*)CertificateBuf;
865         DWORD dwSize = sizeof(ccex);
866
867         if (m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_GET_CERT_INFO_EX, (LPVOID)&ccex, (LPDWORD)dwSize, NULL)) {
868             if (ccex.CertContext.cbCertEncoded) {
869                 unsigned int outlen;
870                 XMLByte* serialized = Base64::encode(reinterpret_cast<XMLByte*>(CertificateBuf), ccex.CertContext.cbCertEncoded, &outlen);
871                 m_certs.push_back(reinterpret_cast<char*>(serialized));
872                 XMLString::release(&serialized);
873             }
874         }
875       }
876       return m_certs;
877   }
878
879   // Not used in the extension.
880   void clearHeader(const char* rawname, const char* cginame) { throw runtime_error("clearHeader not implemented"); }
881   void setHeader(const char* name, const char* value) { throw runtime_error("setHeader not implemented"); }
882   void setRemoteUser(const char* user) { throw runtime_error("setRemoteUser not implemented"); }
883 };
884
885 extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB)
886 {
887     try {
888         ostringstream threadid;
889         threadid << "[" << getpid() << "] isapi_shib_extension" << '\0';
890         xmltooling::NDC ndc(threadid.str().c_str());
891
892         // Determine web site number. This can't really fail, I don't think.
893         dynabuf buf(128);
894         GetServerVariable(lpECB,"INSTANCE_ID",buf,10);
895
896         // Match site instance to host name, skip if no match.
897         map<string,site_t>::const_iterator map_i=g_Sites.find(static_cast<char*>(buf));
898         if (map_i==g_Sites.end())
899             return WriteClientError(lpECB, "Shibboleth Extension not configured for this web site.");
900
901         ShibTargetIsapiE ste(lpECB, map_i->second);
902         pair<bool,long> res = ste.getServiceProvider().doHandler(ste);
903         if (res.first) return res.second;
904         
905         return WriteClientError(lpECB, "Shibboleth Extension failed to process request");
906
907     }
908     catch(bad_alloc) {
909         return WriteClientError(lpECB,"Out of Memory");
910     }
911     catch(long e) {
912         if (e==ERROR_NO_DATA)
913             return WriteClientError(lpECB,"A required variable or header was empty.");
914         else
915             return WriteClientError(lpECB,"Server detected unexpected IIS error.");
916     }
917     catch (exception& e) {
918         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, e.what());
919         return WriteClientError(lpECB,"Shibboleth Extension caught an exception, check Event Log for details.");
920     }
921     catch(...) {
922         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, "Shibboleth Extension threw an unknown exception.");
923         if (g_catchAll)
924             return WriteClientError(lpECB,"Shibboleth Extension threw an unknown exception.");
925         throw;
926     }
927
928     // If we get here we've got an error.
929     return HSE_STATUS_ERROR;
930 }