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