https://issues.shibboleth.net/jira/browse/SSPCPP-401
[shibboleth/cpp-sp.git] / isapi_shib / isapi_shib.cpp
1 /**
2  * Licensed to the University Corporation for Advanced Internet
3  * Development, Inc. (UCAID) under one or more contributor license
4  * agreements. See the NOTICE file distributed with this work for
5  * additional information regarding copyright ownership.
6  *
7  * UCAID licenses this file to you under the Apache License,
8  * Version 2.0 (the "License"); you may not use this file except
9  * in compliance with the License. You may obtain a copy of the
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17  * either express or implied. See the License for the specific
18  * language governing permissions and limitations under the License.
19  */
20
21 /**
22  * isapi_shib.cpp
23  *
24  * Shibboleth ISAPI filter.
25  */
26
27 #define SHIBSP_LITE
28 #include "config_win32.h"
29
30 #define _CRT_NONSTDC_NO_DEPRECATE 1
31 #define _CRT_SECURE_NO_DEPRECATE 1
32 #define _CRT_RAND_S
33
34 #include <shibsp/exceptions.h>
35 #include <shibsp/AbstractSPRequest.h>
36 #include <shibsp/SPConfig.h>
37 #include <shibsp/ServiceProvider.h>
38
39 #include <set>
40 #include <sstream>
41 #include <fstream>
42 #include <stdexcept>
43 #include <process.h>
44 #include <xmltooling/unicode.h>
45 #include <xmltooling/XMLToolingConfig.h>
46 #include <xmltooling/util/NDC.h>
47 #include <xmltooling/util/XMLConstants.h>
48 #include <xmltooling/util/XMLHelper.h>
49 #include <xercesc/util/Base64.hpp>
50 #include <xercesc/util/XMLUniDefs.hpp>
51
52 #include <windows.h>
53 #include <httpfilt.h>
54 #include <httpext.h>
55
56 using namespace shibsp;
57 using namespace xmltooling;
58 using namespace xercesc;
59 using namespace std;
60
61 // globals
62 namespace {
63     static const XMLCh path[] =             UNICODE_LITERAL_4(p,a,t,h);
64     static const XMLCh validate[] =         UNICODE_LITERAL_8(v,a,l,i,d,a,t,e);
65     static const XMLCh name[] =             UNICODE_LITERAL_4(n,a,m,e);
66     static const XMLCh port[] =             UNICODE_LITERAL_4(p,o,r,t);
67     static const XMLCh sslport[] =          UNICODE_LITERAL_7(s,s,l,p,o,r,t);
68     static const XMLCh scheme[] =           UNICODE_LITERAL_6(s,c,h,e,m,e);
69     static const XMLCh id[] =               UNICODE_LITERAL_2(i,d);
70     static const XMLCh Alias[] =            UNICODE_LITERAL_5(A,l,i,a,s);
71     static const XMLCh Site[] =             UNICODE_LITERAL_4(S,i,t,e);
72
73     struct site_t {
74         site_t(const DOMElement* e)
75         {
76             auto_ptr_char n(e->getAttributeNS(nullptr,name));
77             auto_ptr_char s(e->getAttributeNS(nullptr,scheme));
78             auto_ptr_char p(e->getAttributeNS(nullptr,port));
79             auto_ptr_char p2(e->getAttributeNS(nullptr,sslport));
80             if (n.get()) m_name=n.get();
81             if (s.get()) m_scheme=s.get();
82             if (p.get()) m_port=p.get();
83             if (p2.get()) m_sslport=p2.get();
84             e = XMLHelper::getFirstChildElement(e, Alias);
85             while (e) {
86                 if (e->hasChildNodes()) {
87                     auto_ptr_char alias(e->getFirstChild()->getNodeValue());
88                     m_aliases.insert(alias.get());
89                 }
90                 e = XMLHelper::getNextSiblingElement(e, Alias);
91             }
92         }
93         string m_scheme,m_port,m_sslport,m_name;
94         set<string> m_aliases;
95     };
96
97     HINSTANCE g_hinstDLL;
98     SPConfig* g_Config = nullptr;
99     map<string,site_t> g_Sites;
100     bool g_bNormalizeRequest = true;
101     string g_unsetHeaderValue,g_spoofKey;
102     bool g_checkSpoofing = true;
103     bool g_catchAll = false;
104     bool g_bSafeHeaderNames = false;
105     vector<string> g_NoCerts;
106 }
107
108 BOOL LogEvent(
109     LPCSTR  lpUNCServerName,
110     WORD  wType,
111     DWORD  dwEventID,
112     PSID  lpUserSid,
113     LPCSTR  message)
114 {
115     LPCSTR  messages[] = {message, nullptr};
116
117     HANDLE hElog = RegisterEventSource(lpUNCServerName, "Shibboleth ISAPI Filter");
118     BOOL res = ReportEvent(hElog, wType, 0, dwEventID, lpUserSid, 1, 0, messages, nullptr);
119     return (DeregisterEventSource(hElog) && res);
120 }
121
122 void _my_invalid_parameter_handler(
123    const wchar_t * expression,
124    const wchar_t * function,
125    const wchar_t * file,
126    unsigned int line,
127    uintptr_t pReserved
128    )
129 {
130     return;
131 }
132
133 extern "C" __declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
134 {
135     if (fdwReason==DLL_PROCESS_ATTACH)
136         g_hinstDLL=hinstDLL;
137     return TRUE;
138 }
139
140 extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer)
141 {
142     if (!pVer)
143         return FALSE;
144
145     if (!g_Config) {
146         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr,
147                 "Extension mode startup not possible, is the DLL loaded as a filter?");
148         return FALSE;
149     }
150
151     pVer->dwExtensionVersion=HSE_VERSION;
152     strncpy(pVer->lpszExtensionDesc,"Shibboleth ISAPI Extension",HSE_MAX_EXT_DLL_NAME_LEN-1);
153     return TRUE;
154 }
155
156 extern "C" BOOL WINAPI TerminateExtension(DWORD)
157 {
158     return TRUE;    // cleanup should happen when filter unloads
159 }
160
161 extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer)
162 {
163     if (!pVer)
164         return FALSE;
165     else if (g_Config) {
166         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr,
167                 "Reentrant filter initialization, ignoring...");
168         return TRUE;
169     }
170
171     g_Config=&SPConfig::getConfig();
172     g_Config->setFeatures(
173         SPConfig::Listener |
174         SPConfig::Caching |
175         SPConfig::RequestMapping |
176         SPConfig::InProcess |
177         SPConfig::Logging |
178         SPConfig::Handlers
179         );
180     if (!g_Config->init()) {
181         g_Config=nullptr;
182         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr,
183                 "Filter startup failed during library initialization, check native log for help.");
184         return FALSE;
185     }
186
187     try {
188         if (!g_Config->instantiate(nullptr, true))
189             throw runtime_error("unknown error");
190     }
191     catch (exception& ex) {
192         g_Config->term();
193         g_Config=nullptr;
194         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, ex.what());
195         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr,
196                 "Filter startup failed to load configuration, check native log for details.");
197         return FALSE;
198     }
199
200     // Access implementation-specifics and site mappings.
201     ServiceProvider* sp=g_Config->getServiceProvider();
202     Locker locker(sp);
203     const PropertySet* props=sp->getPropertySet("InProcess");
204     if (props) {
205         pair<bool,bool> flag=props->getBool("checkSpoofing");
206         g_checkSpoofing = !flag.first || flag.second;
207         flag=props->getBool("catchAll");
208         g_catchAll = flag.first && flag.second;
209
210         pair<bool,const char*> unsetValue=props->getString("unsetHeaderValue");
211         if (unsetValue.first)
212             g_unsetHeaderValue = unsetValue.second;
213         if (g_checkSpoofing) {
214             unsetValue = props->getString("spoofKey");
215             if (unsetValue.first)
216                 g_spoofKey = unsetValue.second;
217             else {
218                 _invalid_parameter_handler old = _set_invalid_parameter_handler(_my_invalid_parameter_handler);
219                 unsigned int randkey=0,randkey2=0,randkey3=0,randkey4=0;
220                 if (rand_s(&randkey) == 0 && rand_s(&randkey2) == 0 && rand_s(&randkey3) == 0 && rand_s(&randkey4) == 0) {
221                     _set_invalid_parameter_handler(old);
222                     ostringstream keystr;
223                     keystr << randkey << randkey2 << randkey3 << randkey4;
224                     g_spoofKey = keystr.str();
225                 }
226                 else {
227                     _set_invalid_parameter_handler(old);
228                     LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr,
229                             "Filter failed to generate a random anti-spoofing key (if this is Windows 2000 set one manually).");
230                     locker.assign();    // pops lock on SP config
231                     g_Config->term();
232                     g_Config=nullptr;
233                     return FALSE;
234                 }
235             }
236         }
237
238         props = props->getPropertySet("ISAPI");
239         if (props) {
240             flag = props->getBool("normalizeRequest");
241             g_bNormalizeRequest = !flag.first || flag.second;
242             flag = props->getBool("safeHeaderNames");
243             g_bSafeHeaderNames = flag.first && flag.second;
244             const DOMElement* child = XMLHelper::getFirstChildElement(props->getElement(),Site);
245             while (child) {
246                 auto_ptr_char id(child->getAttributeNS(nullptr,id));
247                 if (id.get())
248                     g_Sites.insert(pair<string,site_t>(id.get(),site_t(child)));
249                 child=XMLHelper::getNextSiblingElement(child,Site);
250             }
251         }
252     }
253
254     pVer->dwFilterVersion=HTTP_FILTER_REVISION;
255     strncpy(pVer->lpszFilterDesc,"Shibboleth ISAPI Filter",SF_MAX_FILTER_DESC_LEN);
256     pVer->dwFlags=(SF_NOTIFY_ORDER_HIGH |
257                    SF_NOTIFY_SECURE_PORT |
258                    SF_NOTIFY_NONSECURE_PORT |
259                    SF_NOTIFY_PREPROC_HEADERS |
260                    SF_NOTIFY_LOG);
261     LogEvent(nullptr, EVENTLOG_INFORMATION_TYPE, 7701, nullptr, "Filter initialized...");
262     return TRUE;
263 }
264
265 extern "C" BOOL WINAPI TerminateFilter(DWORD)
266 {
267     if (g_Config)
268         g_Config->term();
269     g_Config = nullptr;
270     LogEvent(nullptr, EVENTLOG_INFORMATION_TYPE, 7701, nullptr, "Filter shut down...");
271     return TRUE;
272 }
273
274 /* Next up, some suck-free versions of various APIs.
275
276    You DON'T require people to guess the buffer size and THEN tell them the right size.
277    Returning an LPCSTR is apparently way beyond their ken. Not to mention the fact that
278    constant strings aren't typed as such, making it just that much harder. These versions
279    are now updated to use a special growable buffer object, modeled after the standard
280    string class. The standard string won't work because they left out the option to
281    pre-allocate a non-constant buffer.
282 */
283
284 class dynabuf
285 {
286 public:
287     dynabuf() { bufptr=nullptr; buflen=0; }
288     dynabuf(size_t s) { bufptr=new char[buflen=s]; *bufptr=0; }
289     ~dynabuf() { delete[] bufptr; }
290     size_t length() const { return bufptr ? strlen(bufptr) : 0; }
291     size_t size() const { return buflen; }
292     bool empty() const { return length()==0; }
293     void reserve(size_t s, bool keep=false);
294     void erase() { if (bufptr) memset(bufptr,0,buflen); }
295     operator char*() { return bufptr; }
296     bool operator ==(const char* s) const;
297     bool operator !=(const char* s) const { return !(*this==s); }
298 private:
299     char* bufptr;
300     size_t buflen;
301 };
302
303 void dynabuf::reserve(size_t s, bool keep)
304 {
305     if (s<=buflen)
306         return;
307     char* p=new char[s];
308     if (keep)
309         while (buflen--)
310             p[buflen]=bufptr[buflen];
311     buflen=s;
312     delete[] bufptr;
313     bufptr=p;
314 }
315
316 bool dynabuf::operator==(const char* s) const
317 {
318     if (buflen==0 || s==nullptr)
319         return (buflen==0 && s==nullptr);
320     else
321         return strcmp(bufptr,s)==0;
322 }
323
324 /****************************************************************************/
325 // ISAPI Filter
326
327 class ShibTargetIsapiF : public AbstractSPRequest
328 {
329   PHTTP_FILTER_CONTEXT m_pfc;
330   PHTTP_FILTER_PREPROC_HEADERS m_pn;
331   multimap<string,string> m_headers;
332   int m_port;
333   string m_scheme,m_hostname;
334   mutable string m_remote_addr,m_content_type,m_method;
335   dynabuf m_allhttp;
336   bool m_firsttime;
337
338 public:
339   ShibTargetIsapiF(PHTTP_FILTER_CONTEXT pfc, PHTTP_FILTER_PREPROC_HEADERS pn, const site_t& site)
340       : AbstractSPRequest(SHIBSP_LOGCAT".ISAPI"), m_pfc(pfc), m_pn(pn), m_allhttp(4096), m_firsttime(true) {
341
342     // URL path always come from IIS.
343     dynabuf var(256);
344     GetHeader("url",var,256,false);
345     setRequestURI(var);
346
347     // Port may come from IIS or from site def.
348     if (!g_bNormalizeRequest || (pfc->fIsSecurePort && site.m_sslport.empty()) || (!pfc->fIsSecurePort && site.m_port.empty())) {
349         GetServerVariable("SERVER_PORT",var,10);
350         if (var.empty()) {
351             m_port = pfc->fIsSecurePort ? 443 : 80;
352         }
353         else {
354             m_port = atoi(var);
355         }
356     }
357     else if (pfc->fIsSecurePort) {
358         m_port = atoi(site.m_sslport.c_str());
359     }
360     else {
361         m_port = atoi(site.m_port.c_str());
362     }
363
364     // Scheme may come from site def or be derived from IIS.
365     m_scheme=site.m_scheme;
366     if (m_scheme.empty() || !g_bNormalizeRequest)
367         m_scheme=pfc->fIsSecurePort ? "https" : "http";
368
369     GetServerVariable("SERVER_NAME",var,32);
370
371     // Make sure SERVER_NAME is "authorized" for use on this site. If not, or empty, set to canonical name.
372     if (var.empty()) {
373         m_hostname = site.m_name;
374     }
375     else {
376         m_hostname = var;
377         if (site.m_name!=m_hostname && site.m_aliases.find(m_hostname)==site.m_aliases.end())
378             m_hostname=site.m_name;
379     }
380
381     if (!g_spoofKey.empty()) {
382         GetHeader("ShibSpoofCheck:", var, 32, false);
383         if (!var.empty() && g_spoofKey == (char*)var)
384             m_firsttime = false;
385     }
386
387     if (!m_firsttime)
388         log(SPDebug, "ISAPI filter running more than once");
389   }
390   ~ShibTargetIsapiF() { }
391
392   const char* getScheme() const {
393     return m_scheme.c_str();
394   }
395   const char* getHostname() const {
396     return m_hostname.c_str();
397   }
398   int getPort() const {
399     return m_port;
400   }
401   const char* getQueryString() const {
402       const char* uri = getRequestURI();
403       uri = (uri ? strchr(uri, '?') : nullptr);
404       return uri ? (uri + 1) : nullptr;
405   }
406   const char* getMethod() const {
407     if (m_method.empty()) {
408         dynabuf var(5);
409         GetServerVariable("HTTP_METHOD",var,5,false);
410         if (!var.empty())
411             m_method = var;
412     }
413     return m_method.c_str();
414   }
415   string getContentType() const {
416     if (m_content_type.empty()) {
417         dynabuf var(32);
418         GetServerVariable("HTTP_CONTENT_TYPE",var,32,false);
419         if (!var.empty())
420             m_content_type = var;
421     }
422     return m_content_type;
423   }
424   string getRemoteAddr() const {
425     m_remote_addr = AbstractSPRequest::getRemoteAddr();
426     if (m_remote_addr.empty()) {
427         dynabuf var(16);
428         GetServerVariable("REMOTE_ADDR",var,16,false);
429         if (!var.empty())
430             m_remote_addr = var;
431     }
432     return m_remote_addr;
433   }
434   void log(SPLogLevel level, const string& msg) const {
435     AbstractSPRequest::log(level,msg);
436     if (level >= SPCrit)
437         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, msg.c_str());
438   }
439   string makeSafeHeader(const char* rawname) const {
440       string hdr;
441       for (; *rawname; ++rawname) {
442           if (isalnum(*rawname))
443               hdr += *rawname;
444       }
445       return (hdr + ':');
446   }
447   void clearHeader(const char* rawname, const char* cginame) {
448     if (g_checkSpoofing && m_firsttime) {
449         if (m_allhttp.empty())
450                 GetServerVariable( "ALL_HTTP", m_allhttp, 4096, false);
451         if (!m_allhttp.empty()) {
452             string hdr = g_bSafeHeaderNames ? ("HTTP_" + makeSafeHeader(cginame + 5)) : (string(cginame) + ':');
453             if (strstr(m_allhttp, hdr.c_str()))
454                 throw opensaml::SecurityPolicyException("Attempt to spoof header ($1) was detected.", params(1, hdr.c_str()));
455         }
456     }
457     if (g_bSafeHeaderNames) {
458         string hdr = makeSafeHeader(rawname);
459         m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(g_unsetHeaderValue.c_str()));
460     }
461     else if (!strcmp(rawname,"REMOTE_USER")) {
462             m_pn->SetHeader(m_pfc, "remote-user:", const_cast<char*>(g_unsetHeaderValue.c_str()));
463         m_pn->SetHeader(m_pfc, "remote_user:", const_cast<char*>(g_unsetHeaderValue.c_str()));
464         }
465         else {
466             string hdr = string(rawname) + ':';
467             m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(g_unsetHeaderValue.c_str()));
468         }
469   }
470   void setHeader(const char* name, const char* value) {
471     string hdr = g_bSafeHeaderNames ? makeSafeHeader(name) : (string(name) + ':');
472     m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(value));
473   }
474   string getSecureHeader(const char* name) const {
475     string hdr = g_bSafeHeaderNames ? makeSafeHeader(name) : (string(name) + ':');
476     dynabuf buf(256);
477     GetHeader(const_cast<char*>(hdr.c_str()), buf, 256, false);
478     return string(buf.empty() ? "" : buf);
479   }
480   string getHeader(const char* name) const {
481     string hdr(name);
482     hdr += ':';
483     dynabuf buf(256);
484     GetHeader(const_cast<char*>(hdr.c_str()), buf, 256, false);
485     return string(buf.empty() ? "" : buf);
486   }
487   void setRemoteUser(const char* user) {
488     setHeader("remote-user", user);
489     if (!user || !*user)
490         m_pfc->pFilterContext = nullptr;
491     else if (m_pfc->pFilterContext = m_pfc->AllocMem(m_pfc, sizeof(char) * (strlen(user) + 1), 0))
492         strcpy(reinterpret_cast<char*>(m_pfc->pFilterContext), user);
493   }
494   string getRemoteUser() const {
495     return getSecureHeader("remote-user");
496   }
497   void setResponseHeader(const char* name, const char* value) {
498     HTTPResponse::setResponseHeader(name, value);
499     // Set for later.
500     if (value)
501         m_headers.insert(make_pair(name,value));
502     else
503         m_headers.erase(name);
504   }
505   long sendResponse(istream& in, long status) {
506     string hdr = string("Connection: close\r\n");
507     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
508         hdr += i->first + ": " + i->second + "\r\n";
509     hdr += "\r\n";
510     const char* codestr="200 OK";
511     switch (status) {
512         case XMLTOOLING_HTTP_STATUS_NOTMODIFIED:    codestr="304 Not Modified"; break;
513         case XMLTOOLING_HTTP_STATUS_UNAUTHORIZED:   codestr="401 Authorization Required"; break;
514         case XMLTOOLING_HTTP_STATUS_FORBIDDEN:      codestr="403 Forbidden"; break;
515         case XMLTOOLING_HTTP_STATUS_NOTFOUND:       codestr="404 Not Found"; break;
516         case XMLTOOLING_HTTP_STATUS_ERROR:          codestr="500 Server Error"; break;
517     }
518     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, (void*)codestr, (ULONG_PTR)hdr.c_str(), 0);
519     char buf[1024];
520     while (in) {
521         in.read(buf,1024);
522         DWORD resplen = in.gcount();
523         m_pfc->WriteClient(m_pfc, buf, &resplen, 0);
524     }
525     return SF_STATUS_REQ_FINISHED;
526   }
527   long sendRedirect(const char* url) {
528     HTTPResponse::sendRedirect(url);
529     string hdr=string("Location: ") + url + "\r\n"
530       "Content-Type: text/html\r\n"
531       "Content-Length: 40\r\n"
532       "Expires: Wed, 01 Jan 1997 12:00:00 GMT\r\n"
533       "Cache-Control: private,no-store,no-cache,max-age=0\r\n";
534     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
535         hdr += i->first + ": " + i->second + "\r\n";
536     hdr += "\r\n";
537     m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, "302 Please Wait", (ULONG_PTR)hdr.c_str(), 0);
538     static const char* redmsg="<HTML><BODY>Redirecting...</BODY></HTML>";
539     DWORD resplen=40;
540     m_pfc->WriteClient(m_pfc, (LPVOID)redmsg, &resplen, 0);
541     return SF_STATUS_REQ_FINISHED;
542   }
543   long returnDecline() {
544       return SF_STATUS_REQ_NEXT_NOTIFICATION;
545   }
546   long returnOK() {
547     return SF_STATUS_REQ_NEXT_NOTIFICATION;
548   }
549
550   const vector<string>& getClientCertificates() const {
551       return g_NoCerts;
552   }
553
554   // The filter never processes the POST, so stub these methods.
555   long getContentLength() const { throw IOException("The request's Content-Length is not available to an ISAPI filter."); }
556   const char* getRequestBody() const { throw IOException("The request body is not available to an ISAPI filter."); }
557
558   void GetServerVariable(LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true) const {
559     s.reserve(size);
560     s.erase();
561     size=s.size();
562
563     while (!m_pfc->GetServerVariable(m_pfc,lpszVariable,s,&size)) {
564         // Grumble. Check the error.
565         DWORD e=GetLastError();
566         if (e==ERROR_INSUFFICIENT_BUFFER)
567             s.reserve(size);
568         else
569             break;
570     }
571     if (bRequired && s.empty())
572         log(SPRequest::SPError, string("missing required server variable: ") + lpszVariable);
573   }
574
575   void GetHeader(LPSTR lpszName, dynabuf& s, DWORD size=80, bool bRequired=true) const {
576     s.reserve(size);
577     s.erase();
578     size=s.size();
579
580     while (!m_pn->GetHeader(m_pfc,lpszName,s,&size)) {
581         // Grumble. Check the error.
582         DWORD e=GetLastError();
583         if (e==ERROR_INSUFFICIENT_BUFFER)
584             s.reserve(size);
585         else
586             break;
587     }
588     if (bRequired && s.empty())
589         log(SPRequest::SPError, string("missing required header: ") + lpszName);
590   }
591 };
592
593 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg)
594 {
595     LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, msg);
596     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
597     pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",(ULONG_PTR)ctype,0);
598     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Filter Error</TITLE></HEAD><BODY>"
599                             "<H1>Shibboleth Filter Error</H1>";
600     DWORD resplen=strlen(xmsg);
601     pfc->WriteClient(pfc,(LPVOID)xmsg,&resplen,0);
602     resplen=strlen(msg);
603     pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
604     static const char* xmsg2="</BODY></HTML>";
605     resplen=strlen(xmsg2);
606     pfc->WriteClient(pfc,(LPVOID)xmsg2,&resplen,0);
607     return SF_STATUS_REQ_FINISHED;
608 }
609
610 void GetServerVariable(PHTTP_FILTER_CONTEXT pfc, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
611 {
612     s.reserve(size);
613     s.erase();
614     size=s.size();
615
616     while (!pfc->GetServerVariable(pfc,lpszVariable,s,&size)) {
617         // Grumble. Check the error.
618         DWORD e=GetLastError();
619         if (e==ERROR_INSUFFICIENT_BUFFER)
620             s.reserve(size);
621         else
622             break;
623     }
624     if (bRequired && s.empty()) {
625         string msg = string("Missing required server variable: ") + lpszVariable;
626         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, msg.c_str());
627     }
628 }
629
630
631 extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification)
632 {
633     // Is this a log notification?
634     if (notificationType==SF_NOTIFY_LOG) {
635         if (pfc->pFilterContext)
636                 ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=reinterpret_cast<char*>(pfc->pFilterContext);
637         return SF_STATUS_REQ_NEXT_NOTIFICATION;
638     }
639
640     PHTTP_FILTER_PREPROC_HEADERS pn=(PHTTP_FILTER_PREPROC_HEADERS)pvNotification;
641     try
642     {
643         // Determine web site number. This can't really fail, I don't think.
644         dynabuf buf(128);
645         GetServerVariable(pfc,"INSTANCE_ID",buf,10);
646         if (buf.empty())
647             return WriteClientError(pfc, "Shibboleth Filter failed to obtain INSTANCE_ID server variable.");
648
649         // Match site instance to host name, skip if no match.
650         map<string,site_t>::const_iterator map_i=g_Sites.find(static_cast<char*>(buf));
651         if (map_i==g_Sites.end())
652             return SF_STATUS_REQ_NEXT_NOTIFICATION;
653
654         ostringstream threadid;
655         threadid << "[" << getpid() << "] isapi_shib" << '\0';
656         xmltooling::NDC ndc(threadid.str().c_str());
657
658         ShibTargetIsapiF stf(pfc, pn, map_i->second);
659
660         pair<bool,long> res = stf.getServiceProvider().doAuthentication(stf);
661         if (!g_spoofKey.empty())
662             pn->SetHeader(pfc, "ShibSpoofCheck:", const_cast<char*>(g_spoofKey.c_str()));
663         if (res.first) return res.second;
664
665         res = stf.getServiceProvider().doExport(stf);
666         if (res.first) return res.second;
667
668         res = stf.getServiceProvider().doAuthorization(stf);
669         if (res.first) return res.second;
670
671         return SF_STATUS_REQ_NEXT_NOTIFICATION;
672     }
673     catch(bad_alloc) {
674         return WriteClientError(pfc,"Out of Memory");
675     }
676     catch(long e) {
677         if (e==ERROR_NO_DATA)
678             return WriteClientError(pfc,"A required variable or header was empty.");
679         else
680             return WriteClientError(pfc,"Shibboleth Filter detected unexpected IIS error.");
681     }
682     catch (exception& e) {
683         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, e.what());
684         return WriteClientError(pfc,"Shibboleth Filter caught an exception, check Event Log for details.");
685     }
686     catch(...) {
687         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, "Shibboleth Filter threw an unknown exception.");
688         if (g_catchAll)
689             return WriteClientError(pfc,"Shibboleth Filter threw an unknown exception.");
690         throw;
691     }
692
693     return WriteClientError(pfc,"Shibboleth Filter reached unreachable code, save my walrus!");
694 }
695
696
697 /****************************************************************************/
698 // ISAPI Extension
699
700 DWORD WriteClientError(LPEXTENSION_CONTROL_BLOCK lpECB, const char* msg)
701 {
702     LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, msg);
703     static const char* ctype="Connection: close\r\nContent-Type: text/html\r\n\r\n";
704     lpECB->ServerSupportFunction(lpECB->ConnID,HSE_REQ_SEND_RESPONSE_HEADER,"200 OK",0,(LPDWORD)ctype);
705     static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Error</TITLE></HEAD><BODY><H1>Shibboleth Error</H1>";
706     DWORD resplen=strlen(xmsg);
707     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg,&resplen,HSE_IO_SYNC);
708     resplen=strlen(msg);
709     lpECB->WriteClient(lpECB->ConnID,(LPVOID)msg,&resplen,HSE_IO_SYNC);
710     static const char* xmsg2="</BODY></HTML>";
711     resplen=strlen(xmsg2);
712     lpECB->WriteClient(lpECB->ConnID,(LPVOID)xmsg2,&resplen,HSE_IO_SYNC);
713     return HSE_STATUS_SUCCESS;
714 }
715
716
717 class ShibTargetIsapiE : public AbstractSPRequest
718 {
719   LPEXTENSION_CONTROL_BLOCK m_lpECB;
720   multimap<string,string> m_headers;
721   mutable vector<string> m_certs;
722   mutable string m_body;
723   mutable bool m_gotBody;
724   int m_port;
725   string m_scheme,m_hostname,m_uri;
726   mutable string m_remote_addr,m_remote_user;
727
728 public:
729   ShibTargetIsapiE(LPEXTENSION_CONTROL_BLOCK lpECB, const site_t& site)
730       : AbstractSPRequest(SHIBSP_LOGCAT".ISAPI"), m_lpECB(lpECB), m_gotBody(false) {
731     dynabuf ssl(5);
732     GetServerVariable("HTTPS",ssl,5);
733     bool SSL=(ssl=="on" || ssl=="ON");
734
735     // Scheme may come from site def or be derived from IIS.
736     m_scheme=site.m_scheme;
737     if (m_scheme.empty() || !g_bNormalizeRequest)
738         m_scheme = SSL ? "https" : "http";
739
740     // URL path always come from IIS.
741     dynabuf url(256);
742     GetServerVariable("URL",url,255);
743
744     // Port may come from IIS or from site def.
745     if (!g_bNormalizeRequest || (SSL && site.m_sslport.empty()) || (!SSL && site.m_port.empty())) {
746         dynabuf port(11);
747         GetServerVariable("SERVER_PORT",port,10);
748         if (port.empty()) {
749             m_port = SSL ? 443 : 80;
750         }
751         else {
752             m_port = atoi(port);
753         }
754     }
755     else if (SSL) {
756         m_port = atoi(site.m_sslport.c_str());
757     }
758     else {
759         m_port = atoi(site.m_port.c_str());
760     }
761
762     dynabuf var(32);
763     GetServerVariable("SERVER_NAME", var, 32);
764     if (var.empty()) {
765         m_hostname = site.m_name;
766     }
767     else {
768         // Make sure SERVER_NAME is "authorized" for use on this site. If not, set to canonical name.
769         m_hostname=var;
770         if (site.m_name!=m_hostname && site.m_aliases.find(m_hostname)==site.m_aliases.end())
771             m_hostname=site.m_name;
772     }
773
774     /*
775      * IIS screws us over on PATH_INFO (the hits keep on coming). We need to figure out if
776      * the server is set up for proper PATH_INFO handling, or "IIS sucks rabid weasels mode",
777      * which is the default. No perfect way to tell, but we can take a good guess by checking
778      * whether the URL is a substring of the PATH_INFO:
779      *
780      * e.g. for /Shibboleth.sso/SAML/POST
781      *
782      *  Bad mode (default):
783      *      URL:        /Shibboleth.sso
784      *      PathInfo:   /Shibboleth.sso/SAML/POST
785      *
786      *  Good mode:
787      *      URL:        /Shibboleth.sso
788      *      PathInfo:   /SAML/POST
789      */
790
791     string uri;
792
793     // Clearly we're only in bad mode if path info exists at all.
794     if (lpECB->lpszPathInfo && *(lpECB->lpszPathInfo)) {
795         if (strstr(lpECB->lpszPathInfo,url))
796             // Pretty good chance we're in bad mode, unless the PathInfo repeats the path itself.
797             uri = lpECB->lpszPathInfo;
798         else {
799             if (!url.empty())
800                 uri = url;
801             uri += lpECB->lpszPathInfo;
802         }
803     }
804     else if (!url.empty()) {
805         uri = url;
806     }
807
808     // For consistency with Apache, let's add the query string.
809     if (lpECB->lpszQueryString && *(lpECB->lpszQueryString)) {
810         uri += '?';
811         uri += lpECB->lpszQueryString;
812     }
813
814     setRequestURI(uri.c_str());
815   }
816   ~ShibTargetIsapiE() { }
817
818   const char* getScheme() const {
819     return m_scheme.c_str();
820   }
821   const char* getHostname() const {
822     return m_hostname.c_str();
823   }
824   int getPort() const {
825     return m_port;
826   }
827   const char* getMethod() const {
828     return m_lpECB->lpszMethod;
829   }
830   string getContentType() const {
831     return m_lpECB->lpszContentType ? m_lpECB->lpszContentType : "";
832   }
833   long getContentLength() const {
834       return m_lpECB->cbTotalBytes;
835   }
836   string getRemoteUser() const {
837     if (m_remote_user.empty()) {
838         dynabuf var(16);
839         GetServerVariable("REMOTE_USER", var, 32, false);
840         if (!var.empty())
841             m_remote_user = var;
842     }
843     return m_remote_user;
844   }
845   string getRemoteAddr() const {
846     m_remote_addr = AbstractSPRequest::getRemoteAddr();
847     if (m_remote_addr.empty()) {
848         dynabuf var(16);
849         GetServerVariable("REMOTE_ADDR", var, 16, false);
850         if (!var.empty())
851             m_remote_addr = var;
852     }
853     return m_remote_addr;
854   }
855   void log(SPLogLevel level, const string& msg) const {
856       AbstractSPRequest::log(level,msg);
857       if (level >= SPCrit)
858           LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, msg.c_str());
859   }
860   string getHeader(const char* name) const {
861     string hdr("HTTP_");
862     for (; *name; ++name) {
863         if (*name=='-')
864             hdr += '_';
865         else
866             hdr += toupper(*name);
867     }
868     dynabuf buf(128);
869     GetServerVariable(const_cast<char*>(hdr.c_str()), buf, 128, false);
870     return buf.empty() ? "" : buf;
871   }
872   void setResponseHeader(const char* name, const char* value) {
873     HTTPResponse::setResponseHeader(name, value);
874     // Set for later.
875     if (value)
876         m_headers.insert(make_pair(name,value));
877     else
878         m_headers.erase(name);
879   }
880   const char* getQueryString() const {
881     return m_lpECB->lpszQueryString;
882   }
883   const char* getRequestBody() const {
884     if (m_gotBody)
885         return m_body.c_str();
886     if (m_lpECB->cbTotalBytes > 1024*1024) // 1MB?
887         throw opensaml::SecurityPolicyException("Size of request body exceeded 1M size limit.");
888     else if (m_lpECB->cbTotalBytes > m_lpECB->cbAvailable) {
889       m_gotBody=true;
890       DWORD datalen=m_lpECB->cbTotalBytes;
891       if (m_lpECB->cbAvailable > 0) {
892         m_body.assign(reinterpret_cast<char*>(m_lpECB->lpbData),m_lpECB->cbAvailable);
893         datalen-=m_lpECB->cbAvailable;
894       }
895       char buf[8192];
896       while (datalen) {
897         DWORD buflen=8192;
898         BOOL ret = m_lpECB->ReadClient(m_lpECB->ConnID, buf, &buflen);
899         if (!ret) {
900             char message[65];
901             _snprintf(message, 64, "Error reading request body from browser (%x).", GetLastError());
902             throw IOException(message);
903         }
904         else if (!buflen)
905             throw IOException("Socket closed while reading request body from browser.");
906         m_body.append(buf, buflen);
907         datalen-=buflen;
908       }
909     }
910     else if (m_lpECB->cbAvailable) {
911         m_gotBody=true;
912         m_body.assign(reinterpret_cast<char*>(m_lpECB->lpbData),m_lpECB->cbAvailable);
913     }
914     return m_body.c_str();
915   }
916   long sendResponse(istream& in, long status) {
917     string hdr = string("Connection: close\r\n");
918     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
919         hdr += i->first + ": " + i->second + "\r\n";
920     hdr += "\r\n";
921     const char* codestr="200 OK";
922     switch (status) {
923         case XMLTOOLING_HTTP_STATUS_NOTMODIFIED:    codestr="304 Not Modified"; break;
924         case XMLTOOLING_HTTP_STATUS_UNAUTHORIZED:   codestr="401 Authorization Required"; break;
925         case XMLTOOLING_HTTP_STATUS_FORBIDDEN:      codestr="403 Forbidden"; break;
926         case XMLTOOLING_HTTP_STATUS_NOTFOUND:       codestr="404 Not Found"; break;
927         case XMLTOOLING_HTTP_STATUS_ERROR:          codestr="500 Server Error"; break;
928     }
929     m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER, (void*)codestr, 0, (LPDWORD)hdr.c_str());
930     char buf[1024];
931     while (in) {
932         in.read(buf,1024);
933         DWORD resplen = in.gcount();
934         m_lpECB->WriteClient(m_lpECB->ConnID, buf, &resplen, HSE_IO_SYNC);
935     }
936     return HSE_STATUS_SUCCESS;
937   }
938   long sendRedirect(const char* url) {
939     HTTPResponse::sendRedirect(url);
940     string hdr=string("Location: ") + url + "\r\n"
941       "Content-Type: text/html\r\n"
942       "Content-Length: 40\r\n"
943       "Expires: Wed, 01 Jan 1997 12:00:00 GMT\r\n"
944       "Cache-Control: private,no-store,no-cache,max-age=0\r\n";
945     for (multimap<string,string>::const_iterator i=m_headers.begin(); i!=m_headers.end(); ++i)
946         hdr += i->first + ": " + i->second + "\r\n";
947     hdr += "\r\n";
948     m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER, "302 Moved", 0, (LPDWORD)hdr.c_str());
949     static const char* redmsg="<HTML><BODY>Redirecting...</BODY></HTML>";
950     DWORD resplen=40;
951     m_lpECB->WriteClient(m_lpECB->ConnID, (LPVOID)redmsg, &resplen, HSE_IO_SYNC);
952     return HSE_STATUS_SUCCESS;
953   }
954   // Decline happens in the POST processor if this isn't the shire url
955   // Note that it can also happen with HTAccess, but we don't support that, yet.
956   long returnDecline() {
957     return WriteClientError(
958         m_lpECB,
959         "ISAPI extension can only be invoked to process Shibboleth protocol requests."
960                 "Make sure the mapped file extension doesn't match actual content."
961         );
962   }
963   long returnOK() {
964       return HSE_STATUS_SUCCESS;
965   }
966
967   const vector<string>& getClientCertificates() const {
968       if (m_certs.empty()) {
969         char CertificateBuf[8192];
970         CERT_CONTEXT_EX ccex;
971         ccex.cbAllocated = sizeof(CertificateBuf);
972         ccex.CertContext.pbCertEncoded = (BYTE*)CertificateBuf;
973         DWORD dwSize = sizeof(ccex);
974
975         if (m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_GET_CERT_INFO_EX, (LPVOID)&ccex, (LPDWORD)dwSize, nullptr)) {
976             if (ccex.CertContext.cbCertEncoded) {
977                 xsecsize_t outlen;
978                 XMLByte* serialized = Base64::encode(reinterpret_cast<XMLByte*>(CertificateBuf), ccex.CertContext.cbCertEncoded, &outlen);
979                 m_certs.push_back(reinterpret_cast<char*>(serialized));
980 #ifdef SHIBSP_XERCESC_HAS_XMLBYTE_RELEASE
981                 XMLString::release(&serialized);
982 #else
983                 XMLString::release((char**)&serialized);
984 #endif
985             }
986         }
987       }
988       return m_certs;
989   }
990
991   // Not used in the extension.
992   void clearHeader(const char* rawname, const char* cginame) { throw runtime_error("clearHeader not implemented"); }
993   void setHeader(const char* name, const char* value) { throw runtime_error("setHeader not implemented"); }
994   void setRemoteUser(const char* user) { throw runtime_error("setRemoteUser not implemented"); }
995
996   void GetServerVariable(LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true) const {
997     s.reserve(size);
998     s.erase();
999     size=s.size();
1000
1001     while (!m_lpECB->GetServerVariable(m_lpECB->ConnID,lpszVariable,s,&size)) {
1002         // Grumble. Check the error.
1003         DWORD e=GetLastError();
1004         if (e==ERROR_INSUFFICIENT_BUFFER)
1005             s.reserve(size);
1006         else
1007             break;
1008     }
1009     if (bRequired && s.empty())
1010         log(SPRequest::SPError, string("missing required server variable: ") + lpszVariable);
1011   }
1012 };
1013
1014 void GetServerVariable(LPEXTENSION_CONTROL_BLOCK lpECB, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
1015 {
1016     s.reserve(size);
1017     s.erase();
1018     size=s.size();
1019
1020     while (!lpECB->GetServerVariable(lpECB->ConnID,lpszVariable,s,&size)) {
1021         // Grumble. Check the error.
1022         DWORD e=GetLastError();
1023         if (e==ERROR_INSUFFICIENT_BUFFER)
1024             s.reserve(size);
1025         else
1026             break;
1027     }
1028     if (bRequired && s.empty()) {
1029         string msg = string("Missing required server variable: ") + lpszVariable;
1030         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, msg.c_str());
1031     }
1032 }
1033
1034 extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB)
1035 {
1036     try {
1037         ostringstream threadid;
1038         threadid << "[" << getpid() << "] isapi_shib_extension" << '\0';
1039         xmltooling::NDC ndc(threadid.str().c_str());
1040
1041         // Determine web site number. This can't really fail, I don't think.
1042         dynabuf buf(128);
1043         GetServerVariable(lpECB,"INSTANCE_ID",buf,10);
1044         if (buf.empty())
1045             return WriteClientError(lpECB, "Shibboleth Extension failed to obtain INSTANCE_ID server variable.");
1046
1047         // Match site instance to host name, skip if no match.
1048         map<string,site_t>::const_iterator map_i=g_Sites.find(static_cast<char*>(buf));
1049         if (map_i==g_Sites.end())
1050             return WriteClientError(lpECB, "Shibboleth Extension not configured for web site (check ISAPI mappings in SP configuration).");
1051
1052         ShibTargetIsapiE ste(lpECB, map_i->second);
1053         pair<bool,long> res = ste.getServiceProvider().doHandler(ste);
1054         if (res.first) return res.second;
1055
1056         return WriteClientError(lpECB, "Shibboleth Extension failed to process request");
1057
1058     }
1059     catch(bad_alloc) {
1060         return WriteClientError(lpECB,"Out of Memory");
1061     }
1062     catch(long e) {
1063         if (e==ERROR_NO_DATA)
1064             return WriteClientError(lpECB,"A required variable or header was empty.");
1065         else
1066             return WriteClientError(lpECB,"Server detected unexpected IIS error.");
1067     }
1068     catch (exception& e) {
1069         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, e.what());
1070         return WriteClientError(lpECB,"Shibboleth Extension caught an exception, check Event Log for details.");
1071     }
1072     catch(...) {
1073         LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, "Shibboleth Extension threw an unknown exception.");
1074         if (g_catchAll)
1075             return WriteClientError(lpECB,"Shibboleth Extension threw an unknown exception.");
1076         throw;
1077     }
1078
1079     // If we get here we've got an error.
1080     return HSE_STATUS_ERROR;
1081 }