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