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