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