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