Fix for SSPCPP-87.
[shibboleth/cpp-sp.git] / isapi_shib / isapi_shib.cpp
1 /*
2  *  Copyright 2001-2005 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 /* isapi_shib.cpp - Shibboleth ISAPI filter
18
19    Scott Cantor
20    8/23/02
21 */
22
23 #include "config_win32.h"
24
25 // SAML Runtime
26 #include <saml/saml.h>
27 #include <shib/shib.h>
28 #include <shib/shib-threads.h>
29 #include <shib-target/shib-target.h>
30
31 #include <ctime>
32 #include <fstream>
33 #include <sstream>
34 #include <stdexcept>
35 #include <process.h>
36
37 #include <httpfilt.h>
38 #include <httpext.h>
39
40 using namespace std;
41 using namespace saml;
42 using namespace shibboleth;
43 using namespace shibtarget;
44
45 // globals
46 namespace {
47     static const XMLCh name[] = { chLatin_n, chLatin_a, chLatin_m, chLatin_e, chNull };
48     static const XMLCh port[] = { chLatin_p, chLatin_o, chLatin_r, chLatin_t, chNull };
49     static const XMLCh sslport[] = { chLatin_s, chLatin_s, chLatin_l, chLatin_p, chLatin_o, chLatin_r, chLatin_t, chNull };
50     static const XMLCh scheme[] = { chLatin_s, chLatin_c, chLatin_h, chLatin_e, chLatin_m, chLatin_e, chNull };
51     static const XMLCh id[] = { chLatin_i, chLatin_d, chNull };
52     static const XMLCh Implementation[] =
53     { chLatin_I, chLatin_m, chLatin_p, chLatin_l, chLatin_e, chLatin_m, chLatin_e, chLatin_n, chLatin_t, chLatin_a, chLatin_t, chLatin_i, chLatin_o, chLatin_n, chNull };
54     static const XMLCh ISAPI[] = { chLatin_I, chLatin_S, chLatin_A, chLatin_P, chLatin_I, chNull };
55     static const XMLCh Alias[] = { chLatin_A, chLatin_l, chLatin_i, chLatin_a, chLatin_s, chNull };
56     static const XMLCh normalizeRequest[] =
57     { chLatin_n, chLatin_o, chLatin_r, chLatin_m, chLatin_a, chLatin_l, chLatin_i, chLatin_z, chLatin_e,
58       chLatin_R, chLatin_e, chLatin_q, chLatin_u, chLatin_e, chLatin_s, chLatin_t, chNull
59     };
60     static const XMLCh Site[] = { chLatin_S, chLatin_i, chLatin_t, chLatin_e, chNull };
61
62     struct site_t {
63         site_t(const DOMElement* e)
64         {
65             auto_ptr_char n(e->getAttributeNS(NULL,name));
66             auto_ptr_char s(e->getAttributeNS(NULL,scheme));
67             auto_ptr_char p(e->getAttributeNS(NULL,port));
68             auto_ptr_char p2(e->getAttributeNS(NULL,sslport));
69             if (n.get()) m_name=n.get();
70             if (s.get()) m_scheme=s.get();
71             if (p.get()) m_port=p.get();
72             if (p2.get()) m_sslport=p2.get();
73             DOMNodeList* nlist=e->getElementsByTagNameNS(shibtarget::XML::SHIBTARGET_NS,Alias);
74             for (int i=0; nlist && i<nlist->getLength(); i++) {
75                 if (nlist->item(i)->hasChildNodes()) {
76                     auto_ptr_char alias(nlist->item(i)->getFirstChild()->getNodeValue());
77                     m_aliases.insert(alias.get());
78                 }
79             }
80         }
81         string m_scheme,m_port,m_sslport,m_name;
82         set<string> m_aliases;
83     };
84     
85     struct context_t {
86         char* m_user;
87         bool m_checked;
88     };
89     
90     HINSTANCE g_hinstDLL;
91     ShibTargetConfig* g_Config = NULL;
92     map<string,site_t> g_Sites;
93     bool g_bNormalizeRequest = true;
94     string g_unsetHeaderValue;
95     bool g_checkSpoofing = true;
96     bool g_catchAll = true;
97 }
98
99 BOOL LogEvent(
100     LPCSTR  lpUNCServerName,
101     WORD  wType,
102     DWORD  dwEventID,
103     PSID  lpUserSid,
104     LPCSTR  message)
105 {
106     LPCSTR  messages[] = {message, NULL};
107     
108     HANDLE hElog = RegisterEventSource(lpUNCServerName, "Shibboleth ISAPI Filter");
109     BOOL res = ReportEvent(hElog, wType, 0, dwEventID, lpUserSid, 1, 0, messages, NULL);
110     return (DeregisterEventSource(hElog) && res);
111 }
112
113 extern "C" __declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
114 {
115     if (fdwReason==DLL_PROCESS_ATTACH)
116         g_hinstDLL=hinstDLL;
117     return TRUE;
118 }
119
120 extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer)
121 {
122     if (!pVer)
123         return FALSE;
124         
125     if (!g_Config)
126     {
127         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
128                 "Extension mode startup not possible, is the DLL loaded as a filter?");
129         return FALSE;
130     }
131
132     pVer->dwExtensionVersion=HSE_VERSION;
133     strncpy(pVer->lpszExtensionDesc,"Shibboleth ISAPI Extension",HSE_MAX_EXT_DLL_NAME_LEN-1);
134     return TRUE;
135 }
136
137 extern "C" BOOL WINAPI TerminateExtension(DWORD)
138 {
139     return TRUE;    // cleanup should happen when filter unloads
140 }
141
142 extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer)
143 {
144     if (!pVer)
145         return FALSE;
146     else if (g_Config) {
147         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
148                 "Reentrant filter initialization, ignoring...");
149         return TRUE;
150     }
151
152     try {
153         LPCSTR schemadir=getenv("SHIBSCHEMAS");
154         if (!schemadir)
155             schemadir=SHIB_SCHEMAS;
156         LPCSTR config=getenv("SHIBCONFIG");
157         if (!config)
158             config=SHIB_CONFIG;
159         g_Config=&ShibTargetConfig::getConfig();
160         g_Config->setFeatures(
161             ShibTargetConfig::Listener |
162             ShibTargetConfig::Metadata |
163             ShibTargetConfig::AAP |
164             ShibTargetConfig::RequestMapper |
165             ShibTargetConfig::LocalExtensions |
166             ShibTargetConfig::Logging
167             );
168         if (!g_Config->init(schemadir)) {
169             g_Config=NULL;
170             LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
171                     "Filter startup failed during library initialization, check native log for help.");
172             return FALSE;
173         }
174         else if (!g_Config->load(config)) {
175             g_Config=NULL;
176             LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
177                     "Filter startup failed to load configuration, check native log for help.");
178             return FALSE;
179         }
180         
181         // Access the implementation-specifics for site mappings.
182         IConfig* conf=g_Config->getINI();
183         Locker locker(conf);
184         const IPropertySet* props=conf->getPropertySet("Local");
185         if (props) {
186             pair<bool,const char*> unsetValue=props->getString("unsetHeaderValue");
187             if (unsetValue.first)
188                 g_unsetHeaderValue = unsetValue.second;
189             pair<bool,bool> flag=props->getBool("checkSpoofing");
190             g_checkSpoofing = !flag.first || flag.second;
191             flag=props->getBool("catchAll");
192             g_catchAll = !flag.first || flag.second;
193             
194             const DOMElement* impl=saml::XML::getFirstChildElement(
195                 props->getElement(),shibtarget::XML::SHIBTARGET_NS,Implementation
196                 );
197             if (impl && (impl=saml::XML::getFirstChildElement(impl,shibtarget::XML::SHIBTARGET_NS,ISAPI))) {
198                 const XMLCh* ch=impl->getAttributeNS(NULL,normalizeRequest);
199                 g_bNormalizeRequest=(!ch || !*ch || *ch==chDigit_1 || *ch==chLatin_t);
200                 impl=saml::XML::getFirstChildElement(impl,shibtarget::XML::SHIBTARGET_NS,Site);
201                 while (impl) {
202                     auto_ptr_char id(impl->getAttributeNS(NULL,id));
203                     if (id.get())
204                         g_Sites.insert(pair<string,site_t>(id.get(),site_t(impl)));
205                     impl=saml::XML::getNextSiblingElement(impl,shibtarget::XML::SHIBTARGET_NS,Site);
206                 }
207             }
208         }
209     }
210     catch (exception&) {
211         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, "Filter startup failed with an exception.");
212         return FALSE;
213     }
214
215     pVer->dwFilterVersion=HTTP_FILTER_REVISION;
216     strncpy(pVer->lpszFilterDesc,"Shibboleth ISAPI Filter",SF_MAX_FILTER_DESC_LEN);
217     pVer->dwFlags=(SF_NOTIFY_ORDER_HIGH |
218                    SF_NOTIFY_SECURE_PORT |
219                    SF_NOTIFY_NONSECURE_PORT |
220                    SF_NOTIFY_PREPROC_HEADERS |
221                    SF_NOTIFY_LOG);
222     LogEvent(NULL, EVENTLOG_INFORMATION_TYPE, 7701, NULL, "Filter initialized...");
223     return TRUE;
224 }
225
226 extern "C" BOOL WINAPI TerminateFilter(DWORD)
227 {
228     if (g_Config)
229         g_Config->shutdown();
230     g_Config = NULL;
231     LogEvent(NULL, EVENTLOG_INFORMATION_TYPE, 7701, NULL, "Filter shut down...");
232     return TRUE;
233 }
234
235 /* Next up, some suck-free versions of various APIs.
236
237    You DON'T require people to guess the buffer size and THEN tell them the right size.
238    Returning an LPCSTR is apparently way beyond their ken. Not to mention the fact that
239    constant strings aren't typed as such, making it just that much harder. These versions
240    are now updated to use a special growable buffer object, modeled after the standard
241    string class. The standard string won't work because they left out the option to
242    pre-allocate a non-constant buffer.
243 */
244
245 class dynabuf
246 {
247 public:
248     dynabuf() { bufptr=NULL; buflen=0; }
249     dynabuf(size_t s) { bufptr=new char[buflen=s]; *bufptr=0; }
250     ~dynabuf() { delete[] bufptr; }
251     size_t length() const { return bufptr ? strlen(bufptr) : 0; }
252     size_t size() const { return buflen; }
253     bool empty() const { return length()==0; }
254     void reserve(size_t s, bool keep=false);
255     void erase() { if (bufptr) memset(bufptr,0,buflen); }
256     operator char*() { return bufptr; }
257     bool operator ==(const char* s) const;
258     bool operator !=(const char* s) const { return !(*this==s); }
259 private:
260     char* bufptr;
261     size_t buflen;
262 };
263
264 void dynabuf::reserve(size_t s, bool keep)
265 {
266     if (s<=buflen)
267         return;
268     char* p=new char[s];
269     if (keep)
270         while (buflen--)
271             p[buflen]=bufptr[buflen];
272     buflen=s;
273     delete[] bufptr;
274     bufptr=p;
275 }
276
277 bool dynabuf::operator==(const char* s) const
278 {
279     if (buflen==NULL || s==NULL)
280         return (buflen==NULL && s==NULL);
281     else
282         return strcmp(bufptr,s)==0;
283 }
284
285 void GetServerVariable(PHTTP_FILTER_CONTEXT pfc, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
286     throw (bad_alloc, DWORD)
287 {
288     s.reserve(size);
289     s.erase();
290     size=s.size();
291
292     while (!pfc->GetServerVariable(pfc,lpszVariable,s,&size))
293     {
294         // Grumble. Check the error.
295         DWORD e=GetLastError();
296         if (e==ERROR_INSUFFICIENT_BUFFER)
297             s.reserve(size);
298         else
299             break;
300     }
301     if (bRequired && s.empty())
302         throw ERROR_NO_DATA;
303 }
304
305 void GetServerVariable(LPEXTENSION_CONTROL_BLOCK lpECB, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
306     throw (bad_alloc, DWORD)
307 {
308     s.reserve(size);
309     s.erase();
310     size=s.size();
311
312     while (!lpECB->GetServerVariable(lpECB->ConnID,lpszVariable,s,&size))
313     {
314         // Grumble. Check the error.
315         DWORD e=GetLastError();
316         if (e==ERROR_INSUFFICIENT_BUFFER)
317             s.reserve(size);
318         else
319             break;
320     }
321     if (bRequired && s.empty())
322         throw ERROR_NO_DATA;
323 }
324
325 void GetHeader(PHTTP_FILTER_PREPROC_HEADERS pn, PHTTP_FILTER_CONTEXT pfc,
326                LPSTR lpszName, dynabuf& s, DWORD size=80, bool bRequired=true)
327     throw (bad_alloc, DWORD)
328 {
329     s.reserve(size);
330     s.erase();
331     size=s.size();
332
333     while (!pn->GetHeader(pfc,lpszName,s,&size))
334     {
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 ShibTarget
350 {
351   PHTTP_FILTER_CONTEXT m_pfc;
352   PHTTP_FILTER_PREPROC_HEADERS m_pn;
353   string m_cookie;
354   dynabuf m_allhttp;
355
356 public:
357     ShibTargetIsapiF(PHTTP_FILTER_CONTEXT pfc, PHTTP_FILTER_PREPROC_HEADERS pn, const site_t& site)
358         : m_pfc(pfc), m_pn(pn), m_allhttp(4096) {
359
360     // URL path always come from IIS.
361     dynabuf url(256);
362     GetHeader(pn,pfc,"url",url,256,false);
363
364     // Port may come from IIS or from site def.
365     dynabuf port(11);
366     if (!g_bNormalizeRequest || (pfc->fIsSecurePort && site.m_sslport.empty()) || (!pfc->fIsSecurePort && site.m_port.empty()))
367         GetServerVariable(pfc,"SERVER_PORT",port,10);
368     else if (pfc->fIsSecurePort) {
369         strncpy(port,site.m_sslport.c_str(),10);
370         static_cast<char*>(port)[10]=0;
371     }
372     else {
373         strncpy(port,site.m_port.c_str(),10);
374         static_cast<char*>(port)[10]=0;
375     }
376     
377     // Scheme may come from site def or be derived from IIS.
378     const char* scheme=site.m_scheme.c_str();
379     if (!scheme || !*scheme || !g_bNormalizeRequest)
380         scheme=pfc->fIsSecurePort ? "https" : "http";
381
382     // Get the rest of the server variables.
383     dynabuf remote_addr(16),method(5),content_type(32),hostname(32);
384     GetServerVariable(pfc,"SERVER_NAME",hostname,32);
385     GetServerVariable(pfc,"REMOTE_ADDR",remote_addr,16);
386     GetServerVariable(pfc,"REQUEST_METHOD",method,5,false);
387     GetServerVariable(pfc,"CONTENT_TYPE",content_type,32,false);
388
389     // Make sure SERVER_NAME is "authorized" for use on this site. If not, set to canonical name.
390     const char* host=hostname;
391     if (site.m_name!=host && site.m_aliases.find(host)==site.m_aliases.end())
392         host=site.m_name.c_str();
393
394     init(scheme, host, atoi(port), url, content_type, remote_addr, method);
395     if (!pfc->pFilterContext) {
396         pfc->pFilterContext = pfc->AllocMem(pfc, sizeof(context_t), NULL);
397         if (static_cast<context_t*>(pfc->pFilterContext)) {
398             static_cast<context_t*>(pfc->pFilterContext)->m_user = NULL;
399             static_cast<context_t*>(pfc->pFilterContext)->m_checked = false;
400         }
401     }
402   }
403   ~ShibTargetIsapiF() {}
404
405   virtual void log(ShibLogLevel level, const string &msg) {
406     ShibTarget::log(level,msg);
407   }
408   virtual string getCookies() const {
409     dynabuf buf(128);
410     GetHeader(m_pn, m_pfc, "Cookie:", buf, 128, false);
411     return buf.empty() ? "" : buf;
412   }
413   
414   virtual void clearHeader(const string &name) {
415     if (g_checkSpoofing && m_pfc->pFilterContext && !static_cast<context_t*>(m_pfc->pFilterContext)->m_checked) {
416         if (m_allhttp.empty())
417                 GetServerVariable(m_pfc,"ALL_HTTP",m_allhttp,4096);
418
419         // Map to the expected CGI variable name.
420         string transformed("HTTP_");
421         const char* pch = name.c_str();
422         while (*pch) {
423             transformed += (isalnum(*pch) ? toupper(*pch) : '_');
424             pch++;
425         }
426         transformed += ':';
427
428         if (strstr(m_allhttp, transformed.c_str()))
429             throw SAMLException("Attempt to spoof header ($1) was detected.", params(1, name.c_str()));
430     }
431     string hdr = (name=="REMOTE_USER" ? "remote-user" : name) + ":";
432     m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(g_unsetHeaderValue.c_str()));
433   }
434   virtual void setHeader(const string &name, const string &value) {
435     string hdr = name + ":";
436     m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()),
437                     const_cast<char*>(value.c_str()));
438   }
439   virtual string getHeader(const string &name) {
440     string hdr = name + ":";
441     dynabuf buf(1024);
442     GetHeader(m_pn, m_pfc, const_cast<char*>(hdr.c_str()), buf, 1024, false);
443     return string(buf);
444   }
445   virtual void setRemoteUser(const string &user) {
446     setHeader(string("remote-user"), user);
447     if (m_pfc->pFilterContext) {
448         if (user.empty())
449             static_cast<context_t*>(m_pfc->pFilterContext)->m_user = NULL;
450         else if (static_cast<context_t*>(m_pfc->pFilterContext)->m_user = (char*)m_pfc->AllocMem(m_pfc, sizeof(char) * (user.length() + 1), NULL))
451             strcpy(static_cast<context_t*>(m_pfc->pFilterContext)->m_user, user.c_str());
452     }
453   }
454   virtual string getRemoteUser(void) {
455     return getHeader(string("remote-user"));
456   }
457   virtual void* sendPage(
458     const string& msg,
459     int code=200,
460     const string& content_type="text/html",
461     const Iterator<header_t>& headers=EMPTY(header_t)) {
462     string hdr = string ("Connection: close\r\nContent-type: ") + content_type + "\r\n";
463     while (headers.hasNext()) {
464         const header_t& h=headers.next();
465         hdr += h.first + ": " + h.second + "\r\n";
466     }
467     hdr += "\r\n";
468     const char* codestr="200 OK";
469     switch (code) {
470         case 403:   codestr="403 Forbidden"; break;
471         case 404:   codestr="404 Not Found"; break;
472         case 500:   codestr="500 Server Error"; break;
473     }
474     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, (void*)codestr, (DWORD)hdr.c_str(), 0);
475     DWORD resplen = msg.size();
476     m_pfc->WriteClient(m_pfc, (LPVOID)msg.c_str(), &resplen, 0);
477     return (void*)SF_STATUS_REQ_FINISHED;
478   }
479   virtual void* sendRedirect(const string& url) {
480     // XXX: Don't support the httpRedirect option, yet.
481     string hdrs=m_cookie + string("Location: ") + url + "\r\n"
482       "Content-Type: text/html\r\n"
483       "Content-Length: 40\r\n"
484       "Expires: 01-Jan-1997 12:00:00 GMT\r\n"
485       "Cache-Control: private,no-store,no-cache\r\n\r\n";
486     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER,
487                                  "302 Please Wait", (DWORD)hdrs.c_str(), 0);
488     static const char* redmsg="<HTML><BODY>Redirecting...</BODY></HTML>";
489     DWORD resplen=40;
490     m_pfc->WriteClient(m_pfc, (LPVOID)redmsg, &resplen, 0);
491     return reinterpret_cast<void*>(SF_STATUS_REQ_FINISHED);
492   }
493   // XXX: We might not ever hit the 'decline' status in this filter.
494   //virtual void* returnDecline(void) { }
495   virtual void* returnOK(void) { return (void*) SF_STATUS_REQ_NEXT_NOTIFICATION; }
496
497   // The filter never processes the POST, so stub these methods.
498   virtual void setCookie(const string &name, const string &value) {
499     // Set the cookie for later.  Use it during the redirect.
500     m_cookie += "Set-Cookie: " + name + "=" + value + "\r\n";
501   }
502   virtual string getArgs(void) { throw runtime_error("getArgs not implemented"); }
503   virtual string getPostData(void) { throw runtime_error("getPostData not implemented"); }
504 };
505
506 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg)
507 {
508     LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg);
509     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
510     pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",(DWORD)ctype,0);
511     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Filter Error</TITLE></HEAD><BODY>"
512                             "<H1>Shibboleth Filter Error</H1>";
513     DWORD resplen=strlen(xmsg);
514     pfc->WriteClient(pfc,(LPVOID)xmsg,&resplen,0);
515     resplen=strlen(msg);
516     pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
517     static const char* xmsg2="</BODY></HTML>";
518     resplen=strlen(xmsg2);
519     pfc->WriteClient(pfc,(LPVOID)xmsg2,&resplen,0);
520     return SF_STATUS_REQ_FINISHED;
521 }
522
523 extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification)
524 {
525     // Is this a log notification?
526     if (notificationType==SF_NOTIFY_LOG) {
527         if (pfc->pFilterContext && static_cast<context_t*>(pfc->pFilterContext)->m_user)
528             ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=static_cast<context_t*>(pfc->pFilterContext)->m_user;
529         return SF_STATUS_REQ_NEXT_NOTIFICATION;
530     }
531
532     PHTTP_FILTER_PREPROC_HEADERS pn=(PHTTP_FILTER_PREPROC_HEADERS)pvNotification;
533     try {
534         // Determine web site number. This can't really fail, I don't think.
535         dynabuf buf(128);
536         GetServerVariable(pfc,"INSTANCE_ID",buf,10);
537
538         // Match site instance to host name, skip if no match.
539         map<string,site_t>::const_iterator map_i=g_Sites.find(static_cast<char*>(buf));
540         if (map_i==g_Sites.end())
541             return SF_STATUS_REQ_NEXT_NOTIFICATION;
542             
543         ostringstream threadid;
544         threadid << "[" << getpid() << "] isapi_shib" << '\0';
545         saml::NDC ndc(threadid.str().c_str());
546
547         ShibTargetIsapiF stf(pfc, pn, map_i->second);
548
549         // "false" because we don't override the Shib settings
550         pair<bool,void*> res = stf.doCheckAuthN();
551         if (pfc->pFilterContext)
552             static_cast<context_t*>(pfc->pFilterContext)->m_checked = true;
553         if (res.first) return (DWORD)res.second;
554         
555         // "false" because we don't override the Shib settings
556         res = stf.doExportAssertions();
557         if (res.first) return (DWORD)res.second;
558
559         res = stf.doCheckAuthZ();
560         if (res.first) return (DWORD)res.second;
561
562         return SF_STATUS_REQ_NEXT_NOTIFICATION;
563     }
564     catch(bad_alloc) {
565         return WriteClientError(pfc,"Out of Memory");
566     }
567     catch(long e) {
568         if (e==ERROR_NO_DATA)
569             return WriteClientError(pfc,"A required variable or header was empty.");
570         else
571             return WriteClientError(pfc,"Shibboleth Filter detected unexpected IIS error.");
572     }
573     catch (exception& e) {
574         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, e.what());
575         return WriteClientError(pfc,"Shibboleth Filter caught an exception, ask administrator to check Event Log for details.");
576     }
577     catch(...) {
578         if (g_catchAll)
579             return WriteClientError(pfc,"Shibboleth Filter caught an unknown exception.");
580         throw;
581     }
582
583     return WriteClientError(pfc,"Shibboleth Filter reached unreachable code, save my walrus!");
584 }
585         
586
587 /****************************************************************************/
588 // ISAPI Extension
589
590 DWORD WriteClientError(LPEXTENSION_CONTROL_BLOCK lpECB, const char* msg)
591 {
592     LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg);
593     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
594     lpECB->ServerSupportFunction(lpECB->ConnID,HSE_REQ_SEND_RESPONSE_HEADER,"200 OK",0,(LPDWORD)ctype);
595     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Error</TITLE></HEAD><BODY><H1>Shibboleth Error</H1>";
596     DWORD resplen=strlen(xmsg);
597     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg,&resplen,HSE_IO_SYNC);
598     resplen=strlen(msg);
599     lpECB->WriteClient(lpECB->ConnID,(LPVOID)msg,&resplen,HSE_IO_SYNC);
600     static const char* xmsg2="</BODY></HTML>";
601     resplen=strlen(xmsg2);
602     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg2,&resplen,HSE_IO_SYNC);
603     return HSE_STATUS_SUCCESS;
604 }
605
606
607 class ShibTargetIsapiE : public ShibTarget
608 {
609   LPEXTENSION_CONTROL_BLOCK m_lpECB;
610   string m_cookie;
611   
612 public:
613   ShibTargetIsapiE(LPEXTENSION_CONTROL_BLOCK lpECB, const site_t& site) {
614     dynabuf ssl(5);
615     GetServerVariable(lpECB,"HTTPS",ssl,5);
616     bool SSL=(ssl=="on" || ssl=="ON");
617
618     // URL path always come from IIS.
619     dynabuf url(256);
620     GetServerVariable(lpECB,"URL",url,255);
621
622     // Port may come from IIS or from site def.
623     dynabuf port(11);
624     if (!g_bNormalizeRequest || (SSL && site.m_sslport.empty()) || (!SSL && site.m_port.empty()))
625         GetServerVariable(lpECB,"SERVER_PORT",port,10);
626     else if (SSL) {
627         strncpy(port,site.m_sslport.c_str(),10);
628         static_cast<char*>(port)[10]=0;
629     }
630     else {
631         strncpy(port,site.m_port.c_str(),10);
632         static_cast<char*>(port)[10]=0;
633     }
634
635     // Scheme may come from site def or be derived from IIS.
636     const char* scheme=site.m_scheme.c_str();
637     if (!scheme || !*scheme || !g_bNormalizeRequest) {
638         scheme = SSL ? "https" : "http";
639     }
640
641     // Get the other server variables.
642     dynabuf remote_addr(16),hostname(32);
643     GetServerVariable(lpECB, "REMOTE_ADDR", remote_addr, 16);
644     GetServerVariable(lpECB, "SERVER_NAME", hostname, 32);
645
646     // Make sure SERVER_NAME is "authorized" for use on this site. If not, set to canonical name.
647     const char* host=hostname;
648     if (site.m_name!=host && site.m_aliases.find(host)==site.m_aliases.end())
649         host=site.m_name.c_str();
650
651     /*
652      * IIS screws us over on PATH_INFO (the hits keep on coming). We need to figure out if
653      * the server is set up for proper PATH_INFO handling, or "IIS sucks rabid weasels mode",
654      * which is the default. No perfect way to tell, but we can take a good guess by checking
655      * whether the URL is a substring of the PATH_INFO:
656      * 
657      * e.g. for /Shibboleth.sso/SAML/POST
658      * 
659      *  Bad mode (default):
660      *      URL:        /Shibboleth.sso
661      *      PathInfo:   /Shibboleth.sso/SAML/POST
662      * 
663      *  Good mode:
664      *      URL:        /Shibboleth.sso
665      *      PathInfo:   /SAML/POST
666      */
667     
668     string fullurl;
669     
670     // Clearly we're only in bad mode if path info exists at all.
671     if (lpECB->lpszPathInfo && *(lpECB->lpszPathInfo)) {
672         if (strstr(lpECB->lpszPathInfo,url))
673             // Pretty good chance we're in bad mode, unless the PathInfo repeats the path itself.
674             fullurl=lpECB->lpszPathInfo;
675         else {
676             fullurl = url;
677             fullurl+=lpECB->lpszPathInfo;
678         }
679     }
680     else {
681         fullurl = url;
682     }
683     
684     // For consistency with Apache, let's add the query string.
685     if (lpECB->lpszQueryString && *(lpECB->lpszQueryString)) {
686         fullurl+='?';
687         fullurl+=lpECB->lpszQueryString;
688     }
689     init(scheme, host, atoi(port), fullurl.c_str(), lpECB->lpszContentType, remote_addr, lpECB->lpszMethod);
690
691     m_lpECB = lpECB;
692   }
693   ~ShibTargetIsapiE() { }
694
695   virtual void log(ShibLogLevel level, const string &msg) {
696       ShibTarget::log(level,msg);
697       if (level == LogLevelError)
698           LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg.c_str());
699   }
700   virtual string getCookies() const {
701     dynabuf buf(128);
702     GetServerVariable(m_lpECB, "HTTP_COOKIE", buf, 128, false);
703     return buf.empty() ? "" : buf;
704   }
705   virtual void setCookie(const string &name, const string &value) {
706     // Set the cookie for later.  Use it during the redirect.
707     m_cookie += "Set-Cookie: " + name + "=" + value + "\r\n";
708   }
709   virtual string getArgs(void) {
710     return string(m_lpECB->lpszQueryString ? m_lpECB->lpszQueryString : "");
711   }
712   virtual string getPostData(void) {
713     if (m_lpECB->cbTotalBytes > 1024*1024) // 1MB?
714       throw FatalProfileException("Blocked too-large a submission to profile endpoint.");
715     else if (m_lpECB->cbTotalBytes != m_lpECB->cbAvailable) {
716       string cgistr;
717       char buf[8192];
718       DWORD datalen=m_lpECB->cbTotalBytes;
719       while (datalen) {
720         DWORD buflen=8192;
721         BOOL ret = m_lpECB->ReadClient(m_lpECB->ConnID, buf, &buflen);
722         if (!ret || !buflen)
723           throw FatalProfileException("Error reading profile submission from browser.");
724         cgistr.append(buf, buflen);
725         datalen-=buflen;
726       }
727       return cgistr;
728     }
729     else
730       return string(reinterpret_cast<char*>(m_lpECB->lpbData),m_lpECB->cbAvailable);
731   }
732   virtual void* sendPage(
733     const string &msg,
734     int code=200,
735     const string& content_type="text/html",
736     const Iterator<header_t>& headers=EMPTY(header_t)) {
737     string hdr = m_cookie + "Connection: close\r\nContent-type: " + content_type + "\r\n";
738     for (int k = 0; k < headers.size(); k++) {
739       hdr += headers[k].first + ": " + headers[k].second + "\r\n";
740     }
741     hdr += "\r\n";
742     const char* codestr="200 OK";
743     switch (code) {
744         case 403:   codestr="403 Forbidden"; break;
745         case 404:   codestr="404 Not Found"; break;
746         case 500:   codestr="500 Server Error"; break;
747     }
748     m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER, (void*)codestr, 0, (LPDWORD)hdr.c_str());
749     DWORD resplen = msg.size();
750     m_lpECB->WriteClient(m_lpECB->ConnID, (LPVOID)msg.c_str(), &resplen, HSE_IO_SYNC);
751     return (void*)HSE_STATUS_SUCCESS;
752   }
753   virtual void* sendRedirect(const string& url) {
754     // XXX: Don't support the httpRedirect option, yet.
755     string hdrs = m_cookie + "Location: " + url + "\r\n"
756       "Content-Type: text/html\r\n"
757       "Content-Length: 40\r\n"
758       "Expires: 01-Jan-1997 12:00:00 GMT\r\n"
759       "Cache-Control: private,no-store,no-cache\r\n\r\n";
760     m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER,
761                                  "302 Moved", 0, (LPDWORD)hdrs.c_str());
762     static const char* redmsg="<HTML><BODY>Redirecting...</BODY></HTML>";
763     DWORD resplen=40;
764     m_lpECB->WriteClient(m_lpECB->ConnID, (LPVOID)redmsg, &resplen, HSE_IO_SYNC);
765     return (void*)HSE_STATUS_SUCCESS;
766   }
767   // Decline happens in the POST processor if this isn't the shire url
768   // Note that it can also happen with HTAccess, but we don't support that, yet.
769   virtual void* returnDecline(void) {
770     return (void*)
771       WriteClientError(m_lpECB, "ISAPI extension can only be invoked to process Shibboleth protocol requests."
772                        "Make sure the mapped file extension doesn't match actual content.");
773   }
774   virtual void* returnOK(void) { return (void*) HSE_STATUS_SUCCESS; }
775
776   // Not used in the extension.
777   virtual void clearHeader(const string &name) { throw runtime_error("clearHeader not implemented"); }
778   virtual void setHeader(const string &name, const string &value) { throw runtime_error("setHeader not implemented"); }
779   virtual string getHeader(const string &name) { throw runtime_error("getHeader not implemented"); }
780   virtual void setRemoteUser(const string &user) { throw runtime_error("setRemoteUser not implemented"); }
781   virtual string getRemoteUser(void) { throw runtime_error("getRemoteUser not implemented"); }
782 };
783
784 extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB)
785 {
786     try {
787         ostringstream threadid;
788         threadid << "[" << getpid() << "] isapi_shib_extension" << '\0';
789         saml::NDC ndc(threadid.str().c_str());
790
791         // Determine web site number. This can't really fail, I don't think.
792         dynabuf buf(128);
793         GetServerVariable(lpECB,"INSTANCE_ID",buf,10);
794
795         // Match site instance to host name, skip if no match.
796         map<string,site_t>::const_iterator map_i=g_Sites.find(static_cast<char*>(buf));
797         if (map_i==g_Sites.end())
798             return WriteClientError(lpECB, "Shibboleth Extension not configured for this web site.");
799
800         ShibTargetIsapiE ste(lpECB, map_i->second);
801         pair<bool,void*> res = ste.doHandler();
802         if (res.first) return (DWORD)res.second;
803         
804         return WriteClientError(lpECB, "Shibboleth Extension failed to process request");
805
806     }
807     catch(bad_alloc) {
808         return WriteClientError(lpECB,"Out of Memory");
809     }
810     catch(long e) {
811         if (e==ERROR_NO_DATA)
812             return WriteClientError(lpECB,"A required variable or header was empty.");
813         else
814             return WriteClientError(lpECB,"Server detected unexpected IIS error.");
815     }
816     catch (exception& e) {
817         LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, e.what());
818         return WriteClientError(lpECB,"Shibboleth Extension caught an exception, check Event Log for details.");
819     }
820     catch(...) {
821         if (g_catchAll)
822             return WriteClientError(lpECB,"Shibboleth Extension caught an unknown exception.");
823         throw;
824     }
825
826     // If we get here we've got an error.
827     return HSE_STATUS_ERROR;
828 }