80991106a609e6b38c16eb727cc53ea26750b146
[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,m_uri;
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     m_uri = 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* getRequestURI() const {
407     return m_uri.c_str();
408   }
409   const char* getMethod() const {
410     if (m_method.empty()) {
411         dynabuf var(5);
412         GetServerVariable(m_pfc,"REQUEST_METHOD",var,5,false);
413         if (!var.empty())
414             m_method = var;
415     }
416     return m_method.c_str();
417   }
418   string getContentType() const {
419     if (m_content_type.empty()) {
420         dynabuf var(32);
421         GetServerVariable(m_pfc,"CONTENT_TYPE",var,32,false);
422         if (!var.empty())
423             m_content_type = var;
424     }
425     return m_content_type;
426   }
427   long getContentLength() const {
428       return 0;
429   }
430   string getRemoteAddr() const {
431     if (m_remote_addr.empty()) {
432         dynabuf var(16);
433         GetServerVariable(m_pfc,"REMOTE_ADDR",var,16,false);
434         if (!var.empty())
435             m_remote_addr = var;
436     }
437     return m_remote_addr;
438   }
439   void log(SPLogLevel level, const string& msg) {
440     AbstractSPRequest::log(level,msg);
441     if (level >= SPError)
442         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg.c_str());
443   }
444   void clearHeader(const char* rawname, const char* cginame) {
445     if (g_checkSpoofing) {
446         if (m_allhttp.empty())
447                 GetServerVariable(m_pfc,"ALL_HTTP",m_allhttp,4096);
448         if (strstr(m_allhttp, cginame))
449             throw opensaml::SecurityPolicyException("Attempt to spoof header ($1) was detected.", params(1, rawname));
450     }
451     string hdr(!strcmp(rawname,"REMOTE_USER") ? "remote-user" : rawname);
452     hdr += ':';
453     m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(g_unsetHeaderValue.c_str()));
454   }
455   void setHeader(const char* name, const char* value) {
456     string hdr(name);
457     hdr += ':';
458     m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(value));
459   }
460   string getHeader(const char* name) const {
461     string hdr(name);
462     hdr += ':';
463     dynabuf buf(256);
464     GetHeader(m_pn, m_pfc, const_cast<char*>(hdr.c_str()), buf, 256, false);
465     return string(buf);
466   }
467   void setRemoteUser(const char* user) {
468     setHeader("remote-user", user);
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_FORBIDDEN:codestr="403 Forbidden"; break;
488         case XMLTOOLING_HTTP_STATUS_NOTFOUND: codestr="404 Not Found"; break;
489         case XMLTOOLING_HTTP_STATUS_ERROR:    codestr="500 Server Error"; break;
490     }
491     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, (void*)codestr, (DWORD)hdr.c_str(), 0);
492     char buf[1024];
493     while (in) {
494         in.read(buf,1024);
495         DWORD resplen = in.gcount();
496         m_pfc->WriteClient(m_pfc, buf, &resplen, 0);
497     }
498     return SF_STATUS_REQ_FINISHED;
499   }
500   long sendRedirect(const char* url) {
501     // XXX: Don't support the httpRedirect option, yet.
502     string hdr=string("Location: ") + url + "\r\n"
503       "Content-Type: text/html\r\n"
504       "Content-Length: 40\r\n"
505       "Expires: 01-Jan-1997 12:00:00 GMT\r\n"
506       "Cache-Control: private,no-store,no-cache\r\n";
507     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
508         hdr += i->first + ": " + i->second + "\r\n";
509     hdr += "\r\n";
510     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, "302 Please Wait", (DWORD)hdr.c_str(), 0);
511     static const char* redmsg="<HTML><BODY>Redirecting...</BODY></HTML>";
512     DWORD resplen=40;
513     m_pfc->WriteClient(m_pfc, (LPVOID)redmsg, &resplen, 0);
514     return SF_STATUS_REQ_FINISHED;
515   }
516   long returnDecline() {
517       return SF_STATUS_REQ_NEXT_NOTIFICATION;
518   }
519   long returnOK() {
520     return SF_STATUS_REQ_NEXT_NOTIFICATION;
521   }
522
523   const vector<string>& getClientCertificates() const {
524       return g_NoCerts;
525   }
526   
527   // The filter never processes the POST, so stub these methods.
528   const char* getQueryString() const { throw IOException("getQueryString not implemented"); }
529   const char* getRequestBody() const { throw IOException("getRequestBody not implemented"); }
530 };
531
532 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg)
533 {
534     LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg);
535     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
536     pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",(DWORD)ctype,0);
537     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Filter Error</TITLE></HEAD><BODY>"
538                             "<H1>Shibboleth Filter Error</H1>";
539     DWORD resplen=strlen(xmsg);
540     pfc->WriteClient(pfc,(LPVOID)xmsg,&resplen,0);
541     resplen=strlen(msg);
542     pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
543     static const char* xmsg2="</BODY></HTML>";
544     resplen=strlen(xmsg2);
545     pfc->WriteClient(pfc,(LPVOID)xmsg2,&resplen,0);
546     return SF_STATUS_REQ_FINISHED;
547 }
548
549 extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification)
550 {
551     // Is this a log notification?
552     if (notificationType==SF_NOTIFY_LOG)
553     {
554         if (pfc->pFilterContext)
555             ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=static_cast<LPCSTR>(pfc->pFilterContext);
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 (res.first) return res.second;
580
581         // "false" because we don't override the Shib settings
582         res = stf.getServiceProvider().doExport(stf);
583         if (res.first) return res.second;
584
585         res = stf.getServiceProvider().doAuthorization(stf);
586         if (res.first) return res.second;
587
588         return SF_STATUS_REQ_NEXT_NOTIFICATION;
589     }
590     catch(bad_alloc) {
591         return WriteClientError(pfc,"Out of Memory");
592     }
593     catch(long e) {
594         if (e==ERROR_NO_DATA)
595             return WriteClientError(pfc,"A required variable or header was empty.");
596         else
597             return WriteClientError(pfc,"Shibboleth Filter detected unexpected IIS error.");
598     }
599     catch (exception& e) {
600         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, e.what());
601         return WriteClientError(pfc,"Shibboleth Filter caught an exception, check Event Log for details.");
602     }
603     catch(...) {
604         if (g_catchAll)
605             return WriteClientError(pfc,"Shibboleth Filter caught an unknown exception.");
606         throw;
607     }
608
609     return WriteClientError(pfc,"Shibboleth Filter reached unreachable code, save my walrus!");
610 }
611         
612
613 /****************************************************************************/
614 // ISAPI Extension
615
616 DWORD WriteClientError(LPEXTENSION_CONTROL_BLOCK lpECB, const char* msg)
617 {
618     LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg);
619     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
620     lpECB->ServerSupportFunction(lpECB->ConnID,HSE_REQ_SEND_RESPONSE_HEADER,"200 OK",0,(LPDWORD)ctype);
621     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Error</TITLE></HEAD><BODY><H1>Shibboleth Error</H1>";
622     DWORD resplen=strlen(xmsg);
623     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg,&resplen,HSE_IO_SYNC);
624     resplen=strlen(msg);
625     lpECB->WriteClient(lpECB->ConnID,(LPVOID)msg,&resplen,HSE_IO_SYNC);
626     static const char* xmsg2="</BODY></HTML>";
627     resplen=strlen(xmsg2);
628     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg2,&resplen,HSE_IO_SYNC);
629     return HSE_STATUS_SUCCESS;
630 }
631
632
633 class ShibTargetIsapiE : public AbstractSPRequest
634 {
635   LPEXTENSION_CONTROL_BLOCK m_lpECB;
636   multimap<string,string> m_headers;
637   mutable vector<string> m_certs;
638   mutable string m_body;
639   mutable bool m_gotBody;
640   int m_port;
641   string m_scheme,m_hostname,m_uri;
642   mutable string m_remote_addr,m_remote_user;
643   
644 public:
645   ShibTargetIsapiE(LPEXTENSION_CONTROL_BLOCK lpECB, const site_t& site) : m_lpECB(lpECB), m_gotBody(false) {
646     dynabuf ssl(5);
647     GetServerVariable(lpECB,"HTTPS",ssl,5);
648     bool SSL=(ssl=="on" || ssl=="ON");
649
650     // Scheme may come from site def or be derived from IIS.
651     m_scheme=site.m_scheme;
652     if (m_scheme.empty() || !g_bNormalizeRequest)
653         m_scheme = SSL ? "https" : "http";
654
655     // URL path always come from IIS.
656     dynabuf url(256);
657     GetServerVariable(lpECB,"URL",url,255);
658
659     // Port may come from IIS or from site def.
660     dynabuf port(11);
661     if (!g_bNormalizeRequest || (SSL && site.m_sslport.empty()) || (!SSL && site.m_port.empty()))
662         GetServerVariable(lpECB,"SERVER_PORT",port,10);
663     else if (SSL) {
664         strncpy(port,site.m_sslport.c_str(),10);
665         static_cast<char*>(port)[10]=0;
666     }
667     else {
668         strncpy(port,site.m_port.c_str(),10);
669         static_cast<char*>(port)[10]=0;
670     }
671     m_port = atoi(port);
672
673     dynabuf var(32);
674     GetServerVariable(lpECB, "SERVER_NAME", var, 32);
675
676     // Make sure SERVER_NAME is "authorized" for use on this site. If not, set to canonical name.
677     m_hostname=var;
678     if (site.m_name!=m_hostname && site.m_aliases.find(m_hostname)==site.m_aliases.end())
679         m_hostname=site.m_name;
680
681     /*
682      * IIS screws us over on PATH_INFO (the hits keep on coming). We need to figure out if
683      * the server is set up for proper PATH_INFO handling, or "IIS sucks rabid weasels mode",
684      * which is the default. No perfect way to tell, but we can take a good guess by checking
685      * whether the URL is a substring of the PATH_INFO:
686      * 
687      * e.g. for /Shibboleth.sso/SAML/POST
688      * 
689      *  Bad mode (default):
690      *      URL:        /Shibboleth.sso
691      *      PathInfo:   /Shibboleth.sso/SAML/POST
692      * 
693      *  Good mode:
694      *      URL:        /Shibboleth.sso
695      *      PathInfo:   /SAML/POST
696      */
697     
698     // Clearly we're only in bad mode if path info exists at all.
699     if (lpECB->lpszPathInfo && *(lpECB->lpszPathInfo)) {
700         if (strstr(lpECB->lpszPathInfo,url))
701             // Pretty good chance we're in bad mode, unless the PathInfo repeats the path itself.
702             m_uri = lpECB->lpszPathInfo;
703         else {
704             m_uri = url;
705             m_uri += lpECB->lpszPathInfo;
706         }
707     }
708     else {
709         m_uri = url;
710     }
711     
712     // For consistency with Apache, let's add the query string.
713     if (lpECB->lpszQueryString && *(lpECB->lpszQueryString)) {
714         m_uri += '?';
715         m_uri += lpECB->lpszQueryString;
716     }
717   }
718   ~ShibTargetIsapiE() { }
719
720   const char* getScheme() const {
721     return m_scheme.c_str();
722   }
723   const char* getHostname() const {
724     return m_hostname.c_str();
725   }
726   int getPort() const {
727     return m_port;
728   }
729   const char* getRequestURI() const {
730     return m_uri.c_str();
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_FORBIDDEN:codestr="403 Forbidden"; break;
818         case XMLTOOLING_HTTP_STATUS_NOTFOUND: codestr="404 Not Found"; break;
819         case XMLTOOLING_HTTP_STATUS_ERROR:    codestr="500 Server Error"; break;
820     }
821     m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER, (void*)codestr, 0, (LPDWORD)hdr.c_str());
822     char buf[1024];
823     while (in) {
824         in.read(buf,1024);
825         DWORD resplen = in.gcount();
826         m_lpECB->WriteClient(m_lpECB->ConnID, buf, &resplen, HSE_IO_SYNC);
827     }
828     return HSE_STATUS_SUCCESS;
829   }
830   long sendRedirect(const char* url) {
831     string hdr=string("Location: ") + url + "\r\n"
832       "Content-Type: text/html\r\n"
833       "Content-Length: 40\r\n"
834       "Expires: 01-Jan-1997 12:00:00 GMT\r\n"
835       "Cache-Control: private,no-store,no-cache\r\n";
836     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
837         hdr += i->first + ": " + i->second + "\r\n";
838     hdr += "\r\n";
839     m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER, "302 Moved", 0, (LPDWORD)hdr.c_str());
840     static const char* redmsg="<HTML><BODY>Redirecting...</BODY></HTML>";
841     DWORD resplen=40;
842     m_lpECB->WriteClient(m_lpECB->ConnID, (LPVOID)redmsg, &resplen, HSE_IO_SYNC);
843     return HSE_STATUS_SUCCESS;
844   }
845   // Decline happens in the POST processor if this isn't the shire url
846   // Note that it can also happen with HTAccess, but we don't support that, yet.
847   long returnDecline() {
848     return WriteClientError(
849         m_lpECB,
850         "ISAPI extension can only be invoked to process Shibboleth protocol requests."
851                 "Make sure the mapped file extension doesn't match actual content."
852         );
853   }
854   long returnOK() {
855       return HSE_STATUS_SUCCESS;
856   }
857
858   const vector<string>& getClientCertificates() const {
859       if (m_certs.empty()) {
860         char CertificateBuf[8192];
861         CERT_CONTEXT_EX ccex;
862         ccex.cbAllocated = sizeof(CertificateBuf);
863         ccex.CertContext.pbCertEncoded = (BYTE*)CertificateBuf;
864         DWORD dwSize = sizeof(ccex);
865
866         if (m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_GET_CERT_INFO_EX, (LPVOID)&ccex, (LPDWORD)dwSize, NULL)) {
867             if (ccex.CertContext.cbCertEncoded) {
868                 unsigned int outlen;
869                 XMLByte* serialized = Base64::encode(reinterpret_cast<XMLByte*>(CertificateBuf), ccex.CertContext.cbCertEncoded, &outlen);
870                 m_certs.push_back(reinterpret_cast<char*>(serialized));
871                 XMLString::release(&serialized);
872             }
873         }
874       }
875       return m_certs;
876   }
877
878   // Not used in the extension.
879   void clearHeader(const char* rawname, const char* cginame) { throw runtime_error("clearHeader not implemented"); }
880   void setHeader(const char* name, const char* value) { throw runtime_error("setHeader not implemented"); }
881   void setRemoteUser(const char* user) { throw runtime_error("setRemoteUser not implemented"); }
882 };
883
884 extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB)
885 {
886     try {
887         ostringstream threadid;
888         threadid << "[" << getpid() << "] isapi_shib_extension" << '\0';
889         xmltooling::NDC ndc(threadid.str().c_str());
890
891         // Determine web site number. This can't really fail, I don't think.
892         dynabuf buf(128);
893         GetServerVariable(lpECB,"INSTANCE_ID",buf,10);
894
895         // Match site instance to host name, skip if no match.
896         map<string,site_t>::const_iterator map_i=g_Sites.find(static_cast<char*>(buf));
897         if (map_i==g_Sites.end())
898             return WriteClientError(lpECB, "Shibboleth Extension not configured for this web site.");
899
900         ShibTargetIsapiE ste(lpECB, map_i->second);
901         pair<bool,long> res = ste.getServiceProvider().doHandler(ste);
902         if (res.first) return res.second;
903         
904         return WriteClientError(lpECB, "Shibboleth Extension failed to process request");
905
906     }
907     catch(bad_alloc) {
908         return WriteClientError(lpECB,"Out of Memory");
909     }
910     catch(long e) {
911         if (e==ERROR_NO_DATA)
912             return WriteClientError(lpECB,"A required variable or header was empty.");
913         else
914             return WriteClientError(lpECB,"Server detected unexpected IIS error.");
915     }
916     catch (exception& e) {
917         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, e.what());
918         return WriteClientError(lpECB,"Shibboleth Extension caught an exception, check Event Log for details.");
919     }
920     catch(...) {
921         if (g_catchAll)
922             return WriteClientError(lpECB,"Shibboleth Extension caught an unknown exception.");
923         throw;
924     }
925
926     // If we get here we've got an error.
927     return HSE_STATUS_ERROR;
928 }