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