Port up URI sanitizer from branch.
[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 Implementation[] =   UNICODE_LITERAL_14(I,m,p,l,e,m,e,n,t,a,t,i,o,n);
64     static const XMLCh ISAPI[] =            UNICODE_LITERAL_5(I,S,A,P,I);
65     static const XMLCh Alias[] =            UNICODE_LITERAL_5(A,l,i,a,s);
66     static const XMLCh normalizeRequest[] = UNICODE_LITERAL_16(n,o,r,m,a,l,i,z,e,R,e,q,u,e,s,t);
67     static const XMLCh Site[] =             UNICODE_LITERAL_4(S,i,t,e);
68
69     struct site_t {
70         site_t(const DOMElement* e)
71         {
72             auto_ptr_char n(e->getAttributeNS(NULL,name));
73             auto_ptr_char s(e->getAttributeNS(NULL,scheme));
74             auto_ptr_char p(e->getAttributeNS(NULL,port));
75             auto_ptr_char p2(e->getAttributeNS(NULL,sslport));
76             if (n.get()) m_name=n.get();
77             if (s.get()) m_scheme=s.get();
78             if (p.get()) m_port=p.get();
79             if (p2.get()) m_sslport=p2.get();
80             e = XMLHelper::getFirstChildElement(e, Alias);
81             while (e) {
82                 if (e->hasChildNodes()) {
83                     auto_ptr_char alias(e->getFirstChild()->getNodeValue());
84                     m_aliases.insert(alias.get());
85                 }
86                 e = XMLHelper::getNextSiblingElement(e, Alias);
87             }
88         }
89         string m_scheme,m_port,m_sslport,m_name;
90         set<string> m_aliases;
91     };
92     
93     HINSTANCE g_hinstDLL;
94     SPConfig* g_Config = NULL;
95     map<string,site_t> g_Sites;
96     bool g_bNormalizeRequest = true;
97     string g_unsetHeaderValue;
98     bool g_checkSpoofing = true;
99     bool g_catchAll = false;
100     vector<string> g_NoCerts;
101 }
102
103 BOOL LogEvent(
104     LPCSTR  lpUNCServerName,
105     WORD  wType,
106     DWORD  dwEventID,
107     PSID  lpUserSid,
108     LPCSTR  message)
109 {
110     LPCSTR  messages[] = {message, NULL};
111     
112     HANDLE hElog = RegisterEventSource(lpUNCServerName, "Shibboleth ISAPI Filter");
113     BOOL res = ReportEvent(hElog, wType, 0, dwEventID, lpUserSid, 1, 0, messages, NULL);
114     return (DeregisterEventSource(hElog) && res);
115 }
116
117 extern "C" __declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
118 {
119     if (fdwReason==DLL_PROCESS_ATTACH)
120         g_hinstDLL=hinstDLL;
121     return TRUE;
122 }
123
124 extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer)
125 {
126     if (!pVer)
127         return FALSE;
128         
129     if (!g_Config) {
130         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
131                 "Extension mode startup not possible, is the DLL loaded as a filter?");
132         return FALSE;
133     }
134
135     pVer->dwExtensionVersion=HSE_VERSION;
136     strncpy(pVer->lpszExtensionDesc,"Shibboleth ISAPI Extension",HSE_MAX_EXT_DLL_NAME_LEN-1);
137     return TRUE;
138 }
139
140 extern "C" BOOL WINAPI TerminateExtension(DWORD)
141 {
142     return TRUE;    // cleanup should happen when filter unloads
143 }
144
145 extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer)
146 {
147     if (!pVer)
148         return FALSE;
149     else if (g_Config) {
150         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
151                 "Reentrant filter initialization, ignoring...");
152         return TRUE;
153     }
154
155     LPCSTR schemadir=getenv("SHIBSP_SCHEMAS");
156     if (!schemadir)
157         schemadir=SHIBSP_SCHEMAS;
158     LPCSTR config=getenv("SHIBSP_CONFIG");
159     if (!config)
160         config=SHIBSP_CONFIG;
161     g_Config=&SPConfig::getConfig();
162     g_Config->setFeatures(
163         SPConfig::Listener |
164         SPConfig::Caching |
165         SPConfig::RequestMapping |
166         SPConfig::InProcess |
167         SPConfig::Logging |
168         SPConfig::Handlers
169         );
170     if (!g_Config->init(schemadir)) {
171         g_Config=NULL;
172         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
173                 "Filter startup failed during library initialization, check native log for help.");
174         return FALSE;
175     }
176
177     try {
178         DOMDocument* dummydoc=XMLToolingConfig::getConfig().getParser().newDocument();
179         XercesJanitor<DOMDocument> docjanitor(dummydoc);
180         DOMElement* dummy = dummydoc->createElementNS(NULL,path);
181         auto_ptr_XMLCh src(config);
182         dummy->setAttributeNS(NULL,path,src.get());
183         dummy->setAttributeNS(NULL,validate,xmlconstants::XML_ONE);
184
185         g_Config->setServiceProvider(g_Config->ServiceProviderManager.newPlugin(XML_SERVICE_PROVIDER,dummy));
186         g_Config->getServiceProvider()->init();
187     }
188     catch (exception& ex) {
189         g_Config->term();
190         g_Config=NULL;
191         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, ex.what());
192         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
193                 "Filter startup failed to load configuration, check native log for details.");
194         return FALSE;
195     }
196     
197     // Access the implementation-specifics for site mappings.
198     ServiceProvider* sp=g_Config->getServiceProvider();
199     Locker locker(sp);
200     const PropertySet* props=sp->getPropertySet("InProcess");
201     if (props) {
202         pair<bool,const char*> unsetValue=props->getString("unsetHeaderValue");
203         if (unsetValue.first)
204             g_unsetHeaderValue = unsetValue.second;
205         pair<bool,bool> flag=props->getBool("checkSpoofing");
206         g_checkSpoofing = !flag.first || flag.second;
207         flag=props->getBool("catchAll");
208         g_catchAll = flag.first && flag.second;
209
210         const DOMElement* impl=XMLHelper::getFirstChildElement(props->getElement(),Implementation);
211         if (impl && (impl=XMLHelper::getFirstChildElement(impl,ISAPI))) {
212             const XMLCh* flag=impl->getAttributeNS(NULL,normalizeRequest);
213             g_bNormalizeRequest=(!flag || !*flag || *flag==chDigit_1 || *flag==chLatin_t);
214             impl=XMLHelper::getFirstChildElement(impl,Site);
215             while (impl) {
216                 auto_ptr_char id(impl->getAttributeNS(NULL,id));
217                 if (id.get())
218                     g_Sites.insert(pair<string,site_t>(id.get(),site_t(impl)));
219                 impl=XMLHelper::getNextSiblingElement(impl,Site);
220             }
221         }
222     }
223
224     pVer->dwFilterVersion=HTTP_FILTER_REVISION;
225     strncpy(pVer->lpszFilterDesc,"Shibboleth ISAPI Filter",SF_MAX_FILTER_DESC_LEN);
226     pVer->dwFlags=(SF_NOTIFY_ORDER_HIGH |
227                    SF_NOTIFY_SECURE_PORT |
228                    SF_NOTIFY_NONSECURE_PORT |
229                    SF_NOTIFY_PREPROC_HEADERS |
230                    SF_NOTIFY_LOG);
231     LogEvent(NULL, EVENTLOG_INFORMATION_TYPE, 7701, NULL, "Filter initialized...");
232     return TRUE;
233 }
234
235 extern "C" BOOL WINAPI TerminateFilter(DWORD)
236 {
237     if (g_Config)
238         g_Config->term();
239     g_Config = NULL;
240     LogEvent(NULL, EVENTLOG_INFORMATION_TYPE, 7701, NULL, "Filter shut down...");
241     return TRUE;
242 }
243
244 /* Next up, some suck-free versions of various APIs.
245
246    You DON'T require people to guess the buffer size and THEN tell them the right size.
247    Returning an LPCSTR is apparently way beyond their ken. Not to mention the fact that
248    constant strings aren't typed as such, making it just that much harder. These versions
249    are now updated to use a special growable buffer object, modeled after the standard
250    string class. The standard string won't work because they left out the option to
251    pre-allocate a non-constant buffer.
252 */
253
254 class dynabuf
255 {
256 public:
257     dynabuf() { bufptr=NULL; buflen=0; }
258     dynabuf(size_t s) { bufptr=new char[buflen=s]; *bufptr=0; }
259     ~dynabuf() { delete[] bufptr; }
260     size_t length() const { return bufptr ? strlen(bufptr) : 0; }
261     size_t size() const { return buflen; }
262     bool empty() const { return length()==0; }
263     void reserve(size_t s, bool keep=false);
264     void erase() { if (bufptr) memset(bufptr,0,buflen); }
265     operator char*() { return bufptr; }
266     bool operator ==(const char* s) const;
267     bool operator !=(const char* s) const { return !(*this==s); }
268 private:
269     char* bufptr;
270     size_t buflen;
271 };
272
273 void dynabuf::reserve(size_t s, bool keep)
274 {
275     if (s<=buflen)
276         return;
277     char* p=new char[s];
278     if (keep)
279         while (buflen--)
280             p[buflen]=bufptr[buflen];
281     buflen=s;
282     delete[] bufptr;
283     bufptr=p;
284 }
285
286 bool dynabuf::operator==(const char* s) const
287 {
288     if (buflen==NULL || s==NULL)
289         return (buflen==NULL && s==NULL);
290     else
291         return strcmp(bufptr,s)==0;
292 }
293
294 void GetServerVariable(PHTTP_FILTER_CONTEXT pfc, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
295 {
296     s.reserve(size);
297     s.erase();
298     size=s.size();
299
300     while (!pfc->GetServerVariable(pfc,lpszVariable,s,&size)) {
301         // Grumble. Check the error.
302         DWORD e=GetLastError();
303         if (e==ERROR_INSUFFICIENT_BUFFER)
304             s.reserve(size);
305         else
306             break;
307     }
308     if (bRequired && s.empty())
309         throw ERROR_NO_DATA;
310 }
311
312 void GetServerVariable(LPEXTENSION_CONTROL_BLOCK lpECB, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
313 {
314     s.reserve(size);
315     s.erase();
316     size=s.size();
317
318     while (!lpECB->GetServerVariable(lpECB->ConnID,lpszVariable,s,&size)) {
319         // Grumble. Check the error.
320         DWORD e=GetLastError();
321         if (e==ERROR_INSUFFICIENT_BUFFER)
322             s.reserve(size);
323         else
324             break;
325     }
326     if (bRequired && s.empty())
327         throw ERROR_NO_DATA;
328 }
329
330 void GetHeader(PHTTP_FILTER_PREPROC_HEADERS pn, PHTTP_FILTER_CONTEXT pfc,
331                LPSTR lpszName, dynabuf& s, DWORD size=80, bool bRequired=true)
332 {
333     s.reserve(size);
334     s.erase();
335     size=s.size();
336
337     while (!pn->GetHeader(pfc,lpszName,s,&size)) {
338         // Grumble. Check the error.
339         DWORD e=GetLastError();
340         if (e==ERROR_INSUFFICIENT_BUFFER)
341             s.reserve(size);
342         else
343             break;
344     }
345     if (bRequired && s.empty())
346         throw ERROR_NO_DATA;
347 }
348
349 /****************************************************************************/
350 // ISAPI Filter
351
352 class ShibTargetIsapiF : public AbstractSPRequest
353 {
354   PHTTP_FILTER_CONTEXT m_pfc;
355   PHTTP_FILTER_PREPROC_HEADERS m_pn;
356   multimap<string,string> m_headers;
357   int m_port;
358   string m_scheme,m_hostname;
359   mutable string m_remote_addr,m_content_type,m_method;
360   dynabuf m_allhttp;
361
362 public:
363   ShibTargetIsapiF(PHTTP_FILTER_CONTEXT pfc, PHTTP_FILTER_PREPROC_HEADERS pn, const site_t& site)
364       : m_pfc(pfc), m_pn(pn), m_allhttp(4096) {
365
366     // URL path always come from IIS.
367     dynabuf var(256);
368     GetHeader(pn,pfc,"url",var,256,false);
369     setRequestURI(var);
370
371     // Port may come from IIS or from site def.
372     if (!g_bNormalizeRequest || (pfc->fIsSecurePort && site.m_sslport.empty()) || (!pfc->fIsSecurePort && site.m_port.empty())) {
373         GetServerVariable(pfc,"SERVER_PORT",var,10);
374         m_port = atoi(var);
375     }
376     else if (pfc->fIsSecurePort) {
377         m_port = atoi(site.m_sslport.c_str());
378     }
379     else {
380         m_port = atoi(site.m_port.c_str());
381     }
382     
383     // Scheme may come from site def or be derived from IIS.
384     m_scheme=site.m_scheme;
385     if (m_scheme.empty() || !g_bNormalizeRequest)
386         m_scheme=pfc->fIsSecurePort ? "https" : "http";
387
388     GetServerVariable(pfc,"SERVER_NAME",var,32);
389
390     // Make sure SERVER_NAME is "authorized" for use on this site. If not, set to canonical name.
391     m_hostname = var;
392     if (site.m_name!=m_hostname && site.m_aliases.find(m_hostname)==site.m_aliases.end())
393         m_hostname=site.m_name;
394   }
395   ~ShibTargetIsapiF() { }
396
397   const char* getScheme() const {
398     return m_scheme.c_str();
399   }
400   const char* getHostname() const {
401     return m_hostname.c_str();
402   }
403   int getPort() const {
404     return m_port;
405   }
406   const char* getMethod() const {
407     if (m_method.empty()) {
408         dynabuf var(5);
409         GetServerVariable(m_pfc,"REQUEST_METHOD",var,5,false);
410         if (!var.empty())
411             m_method = var;
412     }
413     return m_method.c_str();
414   }
415   string getContentType() const {
416     if (m_content_type.empty()) {
417         dynabuf var(32);
418         GetServerVariable(m_pfc,"CONTENT_TYPE",var,32,false);
419         if (!var.empty())
420             m_content_type = var;
421     }
422     return m_content_type;
423   }
424   long getContentLength() const {
425       return 0;
426   }
427   string getRemoteAddr() const {
428     if (m_remote_addr.empty()) {
429         dynabuf var(16);
430         GetServerVariable(m_pfc,"REMOTE_ADDR",var,16,false);
431         if (!var.empty())
432             m_remote_addr = var;
433     }
434     return m_remote_addr;
435   }
436   void log(SPLogLevel level, const string& msg) {
437     AbstractSPRequest::log(level,msg);
438     if (level >= SPError)
439         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg.c_str());
440   }
441   void clearHeader(const char* rawname, const char* cginame) {
442     if (g_checkSpoofing) {
443         if (m_allhttp.empty())
444                 GetServerVariable(m_pfc,"ALL_HTTP",m_allhttp,4096);
445         if (strstr(m_allhttp, cginame))
446             throw opensaml::SecurityPolicyException("Attempt to spoof header ($1) was detected.", params(1, rawname));
447     }
448     string hdr(!strcmp(rawname,"REMOTE_USER") ? "remote-user" : rawname);
449     hdr += ':';
450     m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(g_unsetHeaderValue.c_str()));
451   }
452   void setHeader(const char* name, const char* value) {
453     string hdr(name);
454     hdr += ':';
455     m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(value));
456   }
457   string getHeader(const char* name) const {
458     string hdr(name);
459     hdr += ':';
460     dynabuf buf(256);
461     GetHeader(m_pn, m_pfc, const_cast<char*>(hdr.c_str()), buf, 256, false);
462     return string(buf);
463   }
464   void setRemoteUser(const char* user) {
465     setHeader("remote-user", user);
466   }
467   string getRemoteUser() const {
468     return getHeader("remote-user");
469   }
470   void setResponseHeader(const char* name, const char* value) {
471     // Set for later.
472     if (value)
473         m_headers.insert(make_pair(name,value));
474     else
475         m_headers.erase(name);
476   }
477   long sendResponse(istream& in, long status) {
478     string hdr = string("Connection: close\r\n");
479     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
480         hdr += i->first + ": " + i->second + "\r\n";
481     hdr += "\r\n";
482     const char* codestr="200 OK";
483     switch (status) {
484         case XMLTOOLING_HTTP_STATUS_UNAUTHORIZED:   codestr="401 Authorization Required"; break;
485         case XMLTOOLING_HTTP_STATUS_FORBIDDEN:      codestr="403 Forbidden"; break;
486         case XMLTOOLING_HTTP_STATUS_NOTFOUND:       codestr="404 Not Found"; break;
487         case XMLTOOLING_HTTP_STATUS_ERROR:          codestr="500 Server Error"; break;
488     }
489     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, (void*)codestr, (DWORD)hdr.c_str(), 0);
490     char buf[1024];
491     while (in) {
492         in.read(buf,1024);
493         DWORD resplen = in.gcount();
494         m_pfc->WriteClient(m_pfc, buf, &resplen, 0);
495     }
496     return SF_STATUS_REQ_FINISHED;
497   }
498   long sendRedirect(const char* url) {
499     // XXX: Don't support the httpRedirect option, yet.
500     string hdr=string("Location: ") + url + "\r\n"
501       "Content-Type: text/html\r\n"
502       "Content-Length: 40\r\n"
503       "Expires: 01-Jan-1997 12:00:00 GMT\r\n"
504       "Cache-Control: private,no-store,no-cache\r\n";
505     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
506         hdr += i->first + ": " + i->second + "\r\n";
507     hdr += "\r\n";
508     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, "302 Please Wait", (DWORD)hdr.c_str(), 0);
509     static const char* redmsg="<HTML><BODY>Redirecting...</BODY></HTML>";
510     DWORD resplen=40;
511     m_pfc->WriteClient(m_pfc, (LPVOID)redmsg, &resplen, 0);
512     return SF_STATUS_REQ_FINISHED;
513   }
514   long returnDecline() {
515       return SF_STATUS_REQ_NEXT_NOTIFICATION;
516   }
517   long returnOK() {
518     return SF_STATUS_REQ_NEXT_NOTIFICATION;
519   }
520
521   const vector<string>& getClientCertificates() const {
522       return g_NoCerts;
523   }
524   
525   // The filter never processes the POST, so stub these methods.
526   const char* getQueryString() const { throw IOException("getQueryString not implemented"); }
527   const char* getRequestBody() const { throw IOException("getRequestBody not implemented"); }
528 };
529
530 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg)
531 {
532     LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg);
533     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
534     pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",(DWORD)ctype,0);
535     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Filter Error</TITLE></HEAD><BODY>"
536                             "<H1>Shibboleth Filter Error</H1>";
537     DWORD resplen=strlen(xmsg);
538     pfc->WriteClient(pfc,(LPVOID)xmsg,&resplen,0);
539     resplen=strlen(msg);
540     pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
541     static const char* xmsg2="</BODY></HTML>";
542     resplen=strlen(xmsg2);
543     pfc->WriteClient(pfc,(LPVOID)xmsg2,&resplen,0);
544     return SF_STATUS_REQ_FINISHED;
545 }
546
547 extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification)
548 {
549     // Is this a log notification?
550     if (notificationType==SF_NOTIFY_LOG)
551     {
552         if (pfc->pFilterContext)
553             ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=static_cast<LPCSTR>(pfc->pFilterContext);
554         return SF_STATUS_REQ_NEXT_NOTIFICATION;
555     }
556
557     PHTTP_FILTER_PREPROC_HEADERS pn=(PHTTP_FILTER_PREPROC_HEADERS)pvNotification;
558     try
559     {
560         // Determine web site number. This can't really fail, I don't think.
561         dynabuf buf(128);
562         GetServerVariable(pfc,"INSTANCE_ID",buf,10);
563
564         // Match site instance to host name, skip if no match.
565         map<string,site_t>::const_iterator map_i=g_Sites.find(static_cast<char*>(buf));
566         if (map_i==g_Sites.end())
567             return SF_STATUS_REQ_NEXT_NOTIFICATION;
568             
569         ostringstream threadid;
570         threadid << "[" << getpid() << "] isapi_shib" << '\0';
571         xmltooling::NDC ndc(threadid.str().c_str());
572
573         ShibTargetIsapiF stf(pfc, pn, map_i->second);
574
575         // "false" because we don't override the Shib settings
576         pair<bool,long> res = stf.getServiceProvider().doAuthentication(stf);
577         if (res.first) return res.second;
578
579         // "false" because we don't override the Shib settings
580         res = stf.getServiceProvider().doExport(stf);
581         if (res.first) return res.second;
582
583         res = stf.getServiceProvider().doAuthorization(stf);
584         if (res.first) return res.second;
585
586         return SF_STATUS_REQ_NEXT_NOTIFICATION;
587     }
588     catch(bad_alloc) {
589         return WriteClientError(pfc,"Out of Memory");
590     }
591     catch(long e) {
592         if (e==ERROR_NO_DATA)
593             return WriteClientError(pfc,"A required variable or header was empty.");
594         else
595             return WriteClientError(pfc,"Shibboleth Filter detected unexpected IIS error.");
596     }
597     catch (exception& e) {
598         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, e.what());
599         return WriteClientError(pfc,"Shibboleth Filter caught an exception, check Event Log for details.");
600     }
601     catch(...) {
602         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, "Shibboleth Filter threw an unknown exception.");
603         if (g_catchAll)
604             return WriteClientError(pfc,"Shibboleth Filter threw an unknown exception.");
605         throw;
606     }
607
608     return WriteClientError(pfc,"Shibboleth Filter reached unreachable code, save my walrus!");
609 }
610         
611
612 /****************************************************************************/
613 // ISAPI Extension
614
615 DWORD WriteClientError(LPEXTENSION_CONTROL_BLOCK lpECB, const char* msg)
616 {
617     LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg);
618     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
619     lpECB->ServerSupportFunction(lpECB->ConnID,HSE_REQ_SEND_RESPONSE_HEADER,"200 OK",0,(LPDWORD)ctype);
620     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Error</TITLE></HEAD><BODY><H1>Shibboleth Error</H1>";
621     DWORD resplen=strlen(xmsg);
622     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg,&resplen,HSE_IO_SYNC);
623     resplen=strlen(msg);
624     lpECB->WriteClient(lpECB->ConnID,(LPVOID)msg,&resplen,HSE_IO_SYNC);
625     static const char* xmsg2="</BODY></HTML>";
626     resplen=strlen(xmsg2);
627     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg2,&resplen,HSE_IO_SYNC);
628     return HSE_STATUS_SUCCESS;
629 }
630
631
632 class ShibTargetIsapiE : public AbstractSPRequest
633 {
634   LPEXTENSION_CONTROL_BLOCK m_lpECB;
635   multimap<string,string> m_headers;
636   mutable vector<string> m_certs;
637   mutable string m_body;
638   mutable bool m_gotBody;
639   int m_port;
640   string m_scheme,m_hostname,m_uri;
641   mutable string m_remote_addr,m_remote_user;
642   
643 public:
644   ShibTargetIsapiE(LPEXTENSION_CONTROL_BLOCK lpECB, const site_t& site) : 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 }