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.
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
12 * http://www.apache.org/licenses/LICENSE-2.0
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.
24 * Shibboleth ISAPI filter.
28 #include "config_win32.h"
30 #define _CRT_NONSTDC_NO_DEPRECATE 1
31 #define _CRT_SECURE_NO_DEPRECATE 1
34 #include <shibsp/exceptions.h>
35 #include <shibsp/AbstractSPRequest.h>
36 #include <shibsp/SPConfig.h>
37 #include <shibsp/ServiceProvider.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>
56 using namespace shibsp;
57 using namespace xmltooling;
58 using namespace xercesc;
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);
74 site_t(const DOMElement* e)
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);
86 if (e->hasChildNodes()) {
87 auto_ptr_char alias(e->getFirstChild()->getNodeValue());
88 m_aliases.insert(alias.get());
90 e = XMLHelper::getNextSiblingElement(e, Alias);
93 string m_scheme,m_port,m_sslport,m_name;
94 set<string> m_aliases;
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;
109 LPCSTR lpUNCServerName,
115 LPCSTR messages[] = {message, nullptr};
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);
122 void _my_invalid_parameter_handler(
123 const wchar_t * expression,
124 const wchar_t * function,
125 const wchar_t * file,
133 extern "C" __declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
135 if (fdwReason==DLL_PROCESS_ATTACH)
140 extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer)
146 LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr,
147 "Extension mode startup not possible, is the DLL loaded as a filter?");
151 pVer->dwExtensionVersion=HSE_VERSION;
152 strncpy(pVer->lpszExtensionDesc,"Shibboleth ISAPI Extension",HSE_MAX_EXT_DLL_NAME_LEN-1);
156 extern "C" BOOL WINAPI TerminateExtension(DWORD)
158 return TRUE; // cleanup should happen when filter unloads
161 extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer)
166 LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr,
167 "Reentrant filter initialization, ignoring...");
171 g_Config=&SPConfig::getConfig();
172 g_Config->setFeatures(
175 SPConfig::RequestMapping |
176 SPConfig::InProcess |
180 if (!g_Config->init()) {
182 LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr,
183 "Filter startup failed during library initialization, check native log for help.");
188 if (!g_Config->instantiate(nullptr, true))
189 throw runtime_error("unknown error");
191 catch (exception& ex) {
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.");
200 // Access implementation-specifics and site mappings.
201 ServiceProvider* sp=g_Config->getServiceProvider();
203 const PropertySet* props=sp->getPropertySet("InProcess");
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;
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;
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();
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
238 props = props->getPropertySet("ISAPI");
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);
246 auto_ptr_char id(child->getAttributeNS(nullptr,id));
248 g_Sites.insert(pair<string,site_t>(id.get(),site_t(child)));
249 child=XMLHelper::getNextSiblingElement(child,Site);
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 |
261 LogEvent(nullptr, EVENTLOG_INFORMATION_TYPE, 7701, nullptr, "Filter initialized...");
265 extern "C" BOOL WINAPI TerminateFilter(DWORD)
270 LogEvent(nullptr, EVENTLOG_INFORMATION_TYPE, 7701, nullptr, "Filter shut down...");
274 /* Next up, some suck-free versions of various APIs.
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.
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); }
303 void dynabuf::reserve(size_t s, bool keep)
310 p[buflen]=bufptr[buflen];
316 bool dynabuf::operator==(const char* s) const
318 if (buflen==0 || s==nullptr)
319 return (buflen==0 && s==nullptr);
321 return strcmp(bufptr,s)==0;
324 /****************************************************************************/
327 class ShibTargetIsapiF : public AbstractSPRequest
329 PHTTP_FILTER_CONTEXT m_pfc;
330 PHTTP_FILTER_PREPROC_HEADERS m_pn;
331 multimap<string,string> m_headers;
333 string m_scheme,m_hostname;
334 mutable string m_remote_addr,m_content_type,m_method;
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) {
342 // URL path always come from IIS.
344 GetHeader("url",var,256,false);
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);
351 m_port = pfc->fIsSecurePort ? 443 : 80;
357 else if (pfc->fIsSecurePort) {
358 m_port = atoi(site.m_sslport.c_str());
361 m_port = atoi(site.m_port.c_str());
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";
369 GetServerVariable("SERVER_NAME",var,32);
371 // Make sure SERVER_NAME is "authorized" for use on this site. If not, or empty, set to canonical name.
373 m_hostname = site.m_name;
377 if (site.m_name!=m_hostname && site.m_aliases.find(m_hostname)==site.m_aliases.end())
378 m_hostname=site.m_name;
381 if (!g_spoofKey.empty()) {
382 GetHeader("ShibSpoofCheck:", var, 32, false);
383 if (!var.empty() && g_spoofKey == (char*)var)
388 log(SPDebug, "ISAPI filter running more than once");
390 ~ShibTargetIsapiF() { }
392 const char* getScheme() const {
393 return m_scheme.c_str();
395 const char* getHostname() const {
396 return m_hostname.c_str();
398 int getPort() const {
401 const char* getQueryString() const {
402 const char* uri = getRequestURI();
403 uri = (uri ? strchr(uri, '?') : nullptr);
404 return uri ? (uri + 1) : nullptr;
406 const char* getMethod() const {
407 if (m_method.empty()) {
409 GetServerVariable("HTTP_METHOD",var,5,false);
413 return m_method.c_str();
415 string getContentType() const {
416 if (m_content_type.empty()) {
418 GetServerVariable("HTTP_CONTENT_TYPE",var,32,false);
420 m_content_type = var;
422 return m_content_type;
424 string getRemoteAddr() const {
425 m_remote_addr = AbstractSPRequest::getRemoteAddr();
426 if (m_remote_addr.empty()) {
428 GetServerVariable("REMOTE_ADDR",var,16,false);
432 return m_remote_addr;
434 void log(SPLogLevel level, const string& msg) const {
435 AbstractSPRequest::log(level,msg);
437 LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, msg.c_str());
439 string makeSafeHeader(const char* rawname) const {
441 for (; *rawname; ++rawname) {
442 if (isalnum(*rawname))
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()));
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()));
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()));
466 string hdr = string(rawname) + ':';
467 m_pn->SetHeader(m_pfc, const_cast<char*>(hdr.c_str()), const_cast<char*>(g_unsetHeaderValue.c_str()));
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));
474 string getSecureHeader(const char* name) const {
475 string hdr = g_bSafeHeaderNames ? makeSafeHeader(name) : (string(name) + ':');
477 GetHeader(const_cast<char*>(hdr.c_str()), buf, 256, false);
478 return string(buf.empty() ? "" : buf);
480 string getHeader(const char* name) const {
484 GetHeader(const_cast<char*>(hdr.c_str()), buf, 256, false);
485 return string(buf.empty() ? "" : buf);
487 void setRemoteUser(const char* user) {
488 setHeader("remote-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);
494 string getRemoteUser() const {
495 return getSecureHeader("remote-user");
497 void setResponseHeader(const char* name, const char* value) {
498 HTTPResponse::setResponseHeader(name, value);
501 m_headers.insert(make_pair(name,value));
503 m_headers.erase(name);
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";
510 const char* codestr="200 OK";
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;
518 m_pfc->ServerSupportFunction(m_pfc, SF_REQ_SEND_RESPONSE_HEADER, (void*)codestr, (ULONG_PTR)hdr.c_str(), 0);
522 DWORD resplen = in.gcount();
523 m_pfc->WriteClient(m_pfc, buf, &resplen, 0);
525 return SF_STATUS_REQ_FINISHED;
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";
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>";
540 m_pfc->WriteClient(m_pfc, (LPVOID)redmsg, &resplen, 0);
541 return SF_STATUS_REQ_FINISHED;
543 long returnDecline() {
544 return SF_STATUS_REQ_NEXT_NOTIFICATION;
547 return SF_STATUS_REQ_NEXT_NOTIFICATION;
550 const vector<string>& getClientCertificates() const {
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."); }
558 void GetServerVariable(LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true) const {
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)
571 if (bRequired && s.empty())
572 log(SPRequest::SPError, string("missing required server variable: ") + lpszVariable);
575 void GetHeader(LPSTR lpszName, dynabuf& s, DWORD size=80, bool bRequired=true) const {
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)
588 if (bRequired && s.empty())
589 log(SPRequest::SPError, string("missing required header: ") + lpszName);
593 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg)
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);
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;
610 void GetServerVariable(PHTTP_FILTER_CONTEXT pfc, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
616 while (!pfc->GetServerVariable(pfc,lpszVariable,s,&size)) {
617 // Grumble. Check the error.
618 DWORD e=GetLastError();
619 if (e==ERROR_INSUFFICIENT_BUFFER)
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());
631 extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification)
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;
640 PHTTP_FILTER_PREPROC_HEADERS pn=(PHTTP_FILTER_PREPROC_HEADERS)pvNotification;
643 // Determine web site number. This can't really fail, I don't think.
645 GetServerVariable(pfc,"INSTANCE_ID",buf,10);
647 return WriteClientError(pfc, "Shibboleth Filter failed to obtain INSTANCE_ID server variable.");
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;
654 ostringstream threadid;
655 threadid << "[" << getpid() << "] isapi_shib" << '\0';
656 xmltooling::NDC ndc(threadid.str().c_str());
658 ShibTargetIsapiF stf(pfc, pn, map_i->second);
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;
665 res = stf.getServiceProvider().doExport(stf);
666 if (res.first) return res.second;
668 res = stf.getServiceProvider().doAuthorization(stf);
669 if (res.first) return res.second;
671 return SF_STATUS_REQ_NEXT_NOTIFICATION;
674 return WriteClientError(pfc,"Out of Memory");
677 if (e==ERROR_NO_DATA)
678 return WriteClientError(pfc,"A required variable or header was empty.");
680 return WriteClientError(pfc,"Shibboleth Filter detected unexpected IIS error.");
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.");
687 LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, "Shibboleth Filter threw an unknown exception.");
689 return WriteClientError(pfc,"Shibboleth Filter threw an unknown exception.");
693 return WriteClientError(pfc,"Shibboleth Filter reached unreachable code, save my walrus!");
697 /****************************************************************************/
700 DWORD WriteClientError(LPEXTENSION_CONTROL_BLOCK lpECB, const char* msg)
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);
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;
717 class ShibTargetIsapiE : public AbstractSPRequest
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;
725 string m_scheme,m_hostname,m_uri;
726 mutable string m_remote_addr,m_remote_user;
729 ShibTargetIsapiE(LPEXTENSION_CONTROL_BLOCK lpECB, const site_t& site)
730 : AbstractSPRequest(SHIBSP_LOGCAT".ISAPI"), m_lpECB(lpECB), m_gotBody(false) {
732 GetServerVariable("HTTPS",ssl,5);
733 bool SSL=(ssl=="on" || ssl=="ON");
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";
740 // URL path always come from IIS.
742 GetServerVariable("URL",url,255);
744 // Port may come from IIS or from site def.
745 if (!g_bNormalizeRequest || (SSL && site.m_sslport.empty()) || (!SSL && site.m_port.empty())) {
747 GetServerVariable("SERVER_PORT",port,10);
749 m_port = SSL ? 443 : 80;
756 m_port = atoi(site.m_sslport.c_str());
759 m_port = atoi(site.m_port.c_str());
763 GetServerVariable("SERVER_NAME", var, 32);
765 m_hostname = site.m_name;
768 // Make sure SERVER_NAME is "authorized" for use on this site. If not, set to canonical name.
770 if (site.m_name!=m_hostname && site.m_aliases.find(m_hostname)==site.m_aliases.end())
771 m_hostname=site.m_name;
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:
780 * e.g. for /Shibboleth.sso/SAML/POST
782 * Bad mode (default):
783 * URL: /Shibboleth.sso
784 * PathInfo: /Shibboleth.sso/SAML/POST
787 * URL: /Shibboleth.sso
788 * PathInfo: /SAML/POST
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;
801 uri += lpECB->lpszPathInfo;
804 else if (!url.empty()) {
808 // For consistency with Apache, let's add the query string.
809 if (lpECB->lpszQueryString && *(lpECB->lpszQueryString)) {
811 uri += lpECB->lpszQueryString;
814 setRequestURI(uri.c_str());
816 ~ShibTargetIsapiE() { }
818 const char* getScheme() const {
819 return m_scheme.c_str();
821 const char* getHostname() const {
822 return m_hostname.c_str();
824 int getPort() const {
827 const char* getMethod() const {
828 return m_lpECB->lpszMethod;
830 string getContentType() const {
831 return m_lpECB->lpszContentType ? m_lpECB->lpszContentType : "";
833 long getContentLength() const {
834 return m_lpECB->cbTotalBytes;
836 string getRemoteUser() const {
837 if (m_remote_user.empty()) {
839 GetServerVariable("REMOTE_USER", var, 32, false);
843 return m_remote_user;
845 string getRemoteAddr() const {
846 m_remote_addr = AbstractSPRequest::getRemoteAddr();
847 if (m_remote_addr.empty()) {
849 GetServerVariable("REMOTE_ADDR", var, 16, false);
853 return m_remote_addr;
855 void log(SPLogLevel level, const string& msg) const {
856 AbstractSPRequest::log(level,msg);
858 LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, msg.c_str());
860 string getHeader(const char* name) const {
862 for (; *name; ++name) {
866 hdr += toupper(*name);
869 GetServerVariable(const_cast<char*>(hdr.c_str()), buf, 128, false);
870 return buf.empty() ? "" : buf;
872 void setResponseHeader(const char* name, const char* value) {
873 HTTPResponse::setResponseHeader(name, value);
876 m_headers.insert(make_pair(name,value));
878 m_headers.erase(name);
880 const char* getQueryString() const {
881 return m_lpECB->lpszQueryString;
883 const char* getRequestBody() const {
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) {
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;
898 BOOL ret = m_lpECB->ReadClient(m_lpECB->ConnID, buf, &buflen);
901 _snprintf(message, 64, "Error reading request body from browser (%x).", GetLastError());
902 throw IOException(message);
905 throw IOException("Socket closed while reading request body from browser.");
906 m_body.append(buf, buflen);
910 else if (m_lpECB->cbAvailable) {
912 m_body.assign(reinterpret_cast<char*>(m_lpECB->lpbData),m_lpECB->cbAvailable);
914 return m_body.c_str();
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";
921 const char* codestr="200 OK";
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;
929 m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER, (void*)codestr, 0, (LPDWORD)hdr.c_str());
933 DWORD resplen = in.gcount();
934 m_lpECB->WriteClient(m_lpECB->ConnID, buf, &resplen, HSE_IO_SYNC);
936 return HSE_STATUS_SUCCESS;
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";
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>";
951 m_lpECB->WriteClient(m_lpECB->ConnID, (LPVOID)redmsg, &resplen, HSE_IO_SYNC);
952 return HSE_STATUS_SUCCESS;
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(
959 "ISAPI extension can only be invoked to process Shibboleth protocol requests."
960 "Make sure the mapped file extension doesn't match actual content."
964 return HSE_STATUS_SUCCESS;
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);
975 if (m_lpECB->ServerSupportFunction(m_lpECB->ConnID, HSE_REQ_GET_CERT_INFO_EX, (LPVOID)&ccex, (LPDWORD)dwSize, nullptr)) {
976 if (ccex.CertContext.cbCertEncoded) {
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);
983 XMLString::release((char**)&serialized);
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"); }
996 void GetServerVariable(LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true) const {
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)
1009 if (bRequired && s.empty())
1010 log(SPRequest::SPError, string("missing required server variable: ") + lpszVariable);
1014 void GetServerVariable(LPEXTENSION_CONTROL_BLOCK lpECB, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
1020 while (!lpECB->GetServerVariable(lpECB->ConnID,lpszVariable,s,&size)) {
1021 // Grumble. Check the error.
1022 DWORD e=GetLastError();
1023 if (e==ERROR_INSUFFICIENT_BUFFER)
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());
1034 extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB)
1037 ostringstream threadid;
1038 threadid << "[" << getpid() << "] isapi_shib_extension" << '\0';
1039 xmltooling::NDC ndc(threadid.str().c_str());
1041 // Determine web site number. This can't really fail, I don't think.
1043 GetServerVariable(lpECB,"INSTANCE_ID",buf,10);
1045 return WriteClientError(lpECB, "Shibboleth Extension failed to obtain INSTANCE_ID server variable.");
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).");
1052 ShibTargetIsapiE ste(lpECB, map_i->second);
1053 pair<bool,long> res = ste.getServiceProvider().doHandler(ste);
1054 if (res.first) return res.second;
1056 return WriteClientError(lpECB, "Shibboleth Extension failed to process request");
1060 return WriteClientError(lpECB,"Out of Memory");
1063 if (e==ERROR_NO_DATA)
1064 return WriteClientError(lpECB,"A required variable or header was empty.");
1066 return WriteClientError(lpECB,"Server detected unexpected IIS error.");
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.");
1073 LogEvent(nullptr, EVENTLOG_ERROR_TYPE, 2100, nullptr, "Shibboleth Extension threw an unknown exception.");
1075 return WriteClientError(lpECB,"Shibboleth Extension threw an unknown exception.");
1079 // If we get here we've got an error.
1080 return HSE_STATUS_ERROR;