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