2 * The Shibboleth License, Version 1.
4 * University Corporation for Advanced Internet Development, Inc.
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions are met:
11 * Redistributions of source code must retain the above copyright notice, this
12 * list of conditions and the following disclaimer.
14 * Redistributions in binary form must reproduce the above copyright notice,
15 * this list of conditions and the following disclaimer in the documentation
16 * and/or other materials provided with the distribution, if any, must include
17 * the following acknowledgment: "This product includes software developed by
18 * the University Corporation for Advanced Internet Development
19 * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement
20 * may appear in the software itself, if and wherever such third-party
21 * acknowledgments normally appear.
23 * Neither the name of Shibboleth nor the names of its contributors, nor
24 * Internet2, nor the University Corporation for Advanced Internet Development,
25 * Inc., nor UCAID may be used to endorse or promote products derived from this
26 * software without specific prior written permission. For written permission,
27 * please contact shibboleth@shibboleth.org
29 * Products derived from this software may not be called Shibboleth, Internet2,
30 * UCAID, or the University Corporation for Advanced Internet Development, nor
31 * may Shibboleth appear in their name, without prior written permission of the
32 * University Corporation for Advanced Internet Development.
35 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36 * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
38 * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
39 * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
40 * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
41 * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT,
42 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
43 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
44 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
46 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
47 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
50 /* isapi_shib.cpp - Shibboleth ISAPI filter
57 #include <saml/saml.h>
58 #include <shib/shib.h>
59 #include <shib/shib-threads.h>
60 #include <shib-target/shib-target.h>
62 #include <log4cpp/Category.hh>
72 using namespace log4cpp;
74 using namespace shibboleth;
75 using namespace shibtarget;
80 settings_t(string& name) : m_name(name) {}
83 vector<string> m_mustContain;
89 ThreadKey* rpc_handle_key = NULL;
90 ShibTargetConfig* g_Config = NULL;
91 vector<settings_t> g_Sites;
94 void destroy_handle(void* data)
96 delete (RPCHandle*)data;
100 LPCSTR lpUNCServerName,
106 LPCSTR messages[] = {message, NULL};
108 HANDLE hElog = RegisterEventSource(lpUNCServerName, "Shibboleth ISAPI Filter");
109 BOOL res = ReportEvent(hElog, wType, 0, dwEventID, lpUserSid, 1, 0, messages, NULL);
110 return (DeregisterEventSource(hElog) && res);
113 extern "C" __declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
115 if (fdwReason==DLL_PROCESS_ATTACH)
120 extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer)
127 g_Config = &(ShibTargetConfig::init(SHIBTARGET_SHIRE, getenv("SHIBCONFIG")));
128 ShibINI& ini = g_Config->getINI();
130 // Create the RPC Handle TLS key.
131 rpc_handle_key=ThreadKey::create(destroy_handle);
133 // Read site-specific settings for each instance ID we can find.
136 sprintf(iid,"%u",i++);
138 while (ini.get_tag("isapi",iid,false,&hostname))
140 // If no section exists for the host, mark it as a "skip" site.
141 if (!ini.exists(hostname))
143 g_Sites.push_back(settings_t());
144 sprintf(iid,"%u",i++);
148 settings_t settings(hostname);
150 // Content matching string.
152 if (ini.get_tag(hostname,"mustContain",true,&mustcontain) && !mustcontain.empty())
154 char* buf=strdup(mustcontain.c_str());
157 while (char* sep=strchr(start,';'))
161 settings.m_mustContain.push_back(start);
165 settings.m_mustContain.push_back(start);
169 g_Sites.push_back(settings);
170 sprintf(iid,"%u",i++);
173 catch (SAMLException&)
175 LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
176 "Filter startup failed with SAML exception, check shire log for help.");
181 LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL,
182 "Filter startup failed with unexpected exception, check shire log for help.");
186 pVer->dwFilterVersion=HTTP_FILTER_REVISION;
187 strncpy(pVer->lpszFilterDesc,"Shibboleth ISAPI Filter",SF_MAX_FILTER_DESC_LEN);
188 pVer->dwFlags=(SF_NOTIFY_ORDER_HIGH |
189 SF_NOTIFY_SECURE_PORT |
190 SF_NOTIFY_NONSECURE_PORT |
191 SF_NOTIFY_PREPROC_HEADERS |
193 LogEvent(NULL, EVENTLOG_INFORMATION_TYPE, 7701, NULL, "Filter initialized...");
197 extern "C" BOOL WINAPI TerminateFilter(DWORD dwFlags)
199 delete rpc_handle_key;
201 g_Config->shutdown();
203 LogEvent(NULL, EVENTLOG_INFORMATION_TYPE, 7701, NULL, "Filter shut down...");
207 /* Next up, some suck-free versions of various APIs.
209 You DON'T require people to guess the buffer size and THEN tell them the right size.
210 Returning an LPCSTR is apparently way beyond their ken. Not to mention the fact that
211 constant strings aren't typed as such, making it just that much harder. These versions
212 are now updated to use a special growable buffer object, modeled after the standard
213 string class. The standard string won't work because they left out the option to
214 pre-allocate a non-constant buffer.
220 dynabuf() { bufptr=NULL; buflen=0; }
221 dynabuf(size_t s) { bufptr=new char[buflen=s]; *bufptr=0; }
222 ~dynabuf() { delete[] bufptr; }
223 size_t length() const { return bufptr ? strlen(bufptr) : 0; }
224 size_t size() const { return buflen; }
225 bool empty() const { return length()==0; }
226 void reserve(size_t s, bool keep=false);
227 void erase() { if (bufptr) *bufptr=0; }
228 operator char*() { return bufptr; }
229 bool operator ==(const char* s) const;
230 bool operator !=(const char* s) const { return !(*this==s); }
236 void dynabuf::reserve(size_t s, bool keep)
243 p[buflen]=bufptr[buflen];
249 bool dynabuf::operator==(const char* s) const
251 if (buflen==NULL || s==NULL)
252 return (buflen==NULL && s==NULL);
254 return strcmp(bufptr,s)==0;
257 void GetServerVariable(PHTTP_FILTER_CONTEXT pfc, LPSTR lpszVariable, dynabuf& s, DWORD size=80, bool bRequired=true)
258 throw (bad_alloc, DWORD)
264 while (!pfc->GetServerVariable(pfc,lpszVariable,s,&size))
266 // Grumble. Check the error.
267 DWORD e=GetLastError();
268 if (e==ERROR_INSUFFICIENT_BUFFER)
273 if (bRequired && s.empty())
277 void GetHeader(PHTTP_FILTER_PREPROC_HEADERS pn, PHTTP_FILTER_CONTEXT pfc,
278 LPSTR lpszName, dynabuf& s, DWORD size=80, bool bRequired=true)
279 throw (bad_alloc, DWORD)
285 while (!pn->GetHeader(pfc,lpszName,s,&size))
287 // Grumble. Check the error.
288 DWORD e=GetLastError();
289 if (e==ERROR_INSUFFICIENT_BUFFER)
294 if (bRequired && s.empty())
298 inline char hexchar(unsigned short s)
300 return (s<=9) ? ('0' + s) : ('A' + s - 10);
303 string url_encode(const char* url) throw (bad_alloc)
305 static char badchars[]="\"\\+<>#%{}|^~[]`;/?:@=&";
307 for (const char* pch=url; *pch; pch++)
309 if (strchr(badchars,*pch)!=NULL || *pch<=0x1F || *pch>=0x7F)
310 s=s + '%' + hexchar(*pch >> 4) + hexchar(*pch & 0x0F);
317 string get_target(PHTTP_FILTER_CONTEXT pfc, PHTTP_FILTER_PREPROC_HEADERS pn, settings_t& site)
319 // Reconstructing the requested URL is not fun. Apparently, the PREPROC_HEADERS
320 // event means way pre. As in, none of the usual CGI headers are in place yet.
321 // It's actually almost easier, in a way, because all the path-info and query
322 // stuff is in one place, the requested URL, which we can get. But we have to
323 // reconstruct the protocol/host pair using tweezers.
325 if (pfc->fIsSecurePort)
330 // We use the "normalizeRequest" tag to decide how to obtain the server's name.
333 if (g_Config->getINI().get_tag(site.m_name,"normalizeRequest",true,&tag) && ShibINI::boolean(tag))
339 GetServerVariable(pfc,"SERVER_NAME",buf);
343 GetServerVariable(pfc,"SERVER_PORT",buf,10);
344 if (buf!=(pfc->fIsSecurePort ? "443" : "80"))
345 s=s + ':' + static_cast<char*>(buf);
347 GetHeader(pn,pfc,"url",buf,256,false);
353 string get_shire_location(PHTTP_FILTER_CONTEXT pfc, settings_t& site, const char* target)
356 if (g_Config->getINI().get_tag(site.m_name,"shireURL",true,&shireURL) && !shireURL.empty())
358 if (shireURL[0]!='/')
360 const char* colon=strchr(target,':');
361 const char* slash=strchr(colon+3,'/');
362 string s(target,slash-target);
369 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg)
371 LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg);
372 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",0,0);
373 static const char* xmsg="<HTML><HEAD><TITLE>Shibboleth Filter Error</TITLE></HEAD><BODY>"
374 "<H1>Shibboleth Filter Error</H1>";
375 DWORD resplen=strlen(xmsg);
376 pfc->WriteClient(pfc,(LPVOID)xmsg,&resplen,0);
378 pfc->WriteClient(pfc,(LPVOID)msg,&resplen,0);
379 static const char* xmsg2="</BODY></HTML>";
380 resplen=strlen(xmsg2);
381 pfc->WriteClient(pfc,(LPVOID)xmsg2,&resplen,0);
382 return SF_STATUS_REQ_FINISHED;
385 DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* filename, ShibMLP& mlp)
387 ifstream infile(filename);
389 return WriteClientError(pfc,"Unable to open error template, check settings.");
391 string res = mlp.run(infile);
392 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"200 OK",0,0);
393 DWORD resplen=res.length();
394 pfc->WriteClient(pfc,(LPVOID)res.c_str(),&resplen,0);
395 return SF_STATUS_REQ_FINISHED;
398 extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification)
400 // Is this a log notification?
401 if (notificationType==SF_NOTIFY_LOG)
403 if (pfc->pFilterContext)
404 ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=static_cast<LPCSTR>(pfc->pFilterContext);
405 return SF_STATUS_REQ_NEXT_NOTIFICATION;
408 PHTTP_FILTER_PREPROC_HEADERS pn=(PHTTP_FILTER_PREPROC_HEADERS)pvNotification;
411 // Determine web site number. This can't really fail, I don't think.
414 GetServerVariable(pfc,"INSTANCE_ID",buf,10);
415 if ((site_id=strtoul(buf,NULL,10))==0)
416 return WriteClientError(pfc,"IIS site instance appears to be invalid.");
418 // Match site instance to site settings.
419 if (site_id>g_Sites.size() || g_Sites[site_id-1].m_name.length()==0)
420 return SF_STATUS_REQ_NEXT_NOTIFICATION;
421 settings_t& site=g_Sites[site_id-1];
423 string target_url=get_target(pfc,pn,site);
424 string shire_url=get_shire_location(pfc,site,target_url.c_str());
426 // If the user is accessing the SHIRE acceptance point, pass it on.
427 if (target_url.find(shire_url)!=string::npos)
428 return SF_STATUS_REQ_NEXT_NOTIFICATION;
430 // Get the url request and scan for the must-contain string.
431 if (!site.m_mustContain.empty())
433 char* upcased=new char[target_url.length()+1];
434 strcpy(upcased,target_url.c_str());
436 for (vector<string>::const_iterator index=site.m_mustContain.begin(); index!=site.m_mustContain.end(); index++)
437 if (strstr(upcased,index->c_str()))
440 if (index==site.m_mustContain.end())
441 return SF_STATUS_REQ_NEXT_NOTIFICATION;
444 // SSL content check.
445 ShibINI& ini=g_Config->getINI();
447 if (ini.get_tag(site.m_name,"contentSSLOnly",true,&tag) && ShibINI::boolean(tag) && !pfc->fIsSecurePort)
449 return WriteClientError(pfc,
450 "This server is configured to deny non-SSL requests for secure resources. "
451 "Try your request again using https instead of http.");
454 ostringstream threadid;
455 threadid << "[" << getpid() << "] shire" << '\0';
456 saml::NDC ndc(threadid.str().c_str());
458 // Set SHIRE policies.
460 config.checkIPAddress = (ini.get_tag(site.m_name,"checkIPAddress",true,&tag) && ShibINI::boolean(tag));
461 config.lifetime=config.timeout=0;
463 if (ini.get_tag(site.m_name, "authLifetime", true, &tag))
464 config.lifetime=strtoul(tag.c_str(),NULL,10);
466 if (ini.get_tag(site.m_name, "authTimeout", true, &tag))
467 config.timeout=strtoul(tag.c_str(),NULL,10);
469 // Pull the config data we need to handle the various possible conditions.
471 if (!ini.get_tag(site.m_name, "cookieName", true, &shib_cookie))
472 return WriteClientError(pfc,"The cookieName configuration setting is missing, check configuration.");
475 if (!ini.get_tag(site.m_name, "wayfURL", true, &wayfLocation))
476 return WriteClientError(pfc,"The wayfURL configuration setting is missing, check configuration.");
479 if (!ini.get_tag(site.m_name, "shireError", true, &shireError))
480 return WriteClientError(pfc,"The shireError configuration setting is missing, check configuration.");
483 if (!ini.get_tag(site.m_name, "accessError", true, &shireError))
484 return WriteClientError(pfc,"The accessError configuration setting is missing, check configuration.");
486 // Get an RPC handle and build the SHIRE object.
487 RPCHandle* rpc_handle = (RPCHandle*)rpc_handle_key->getData();
490 rpc_handle = new RPCHandle(shib_target_sockname(), SHIBRPC_PROG, SHIBRPC_VERS_1);
491 rpc_handle_key->setData(rpc_handle);
493 SHIRE shire(rpc_handle, config, shire_url);
495 // Check for authentication cookie.
496 const char* session_id=NULL;
497 GetHeader(pn,pfc,"Cookie:",buf,128,false);
498 if (buf.empty() || !(session_id=strstr(buf,shib_cookie.c_str())) || *(session_id+shib_cookie.length())!='=')
501 string wayf("Location: ");
502 wayf+=wayfLocation + "?shire=" + url_encode(shire_url.c_str()) + "&target=" + url_encode(target_url.c_str()) + "\r\n";
503 // Insert the headers.
504 pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
505 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
506 return SF_STATUS_REQ_FINISHED;
509 session_id+=shib_cookie.length() + 1; /* Skip over the '=' */
510 char* cookieend=strchr(session_id,';');
512 *cookieend = '\0'; /* Ignore anyting after a ; */
514 // Make sure this session is still valid.
515 RPCError* status = NULL;
516 ShibMLP markupProcessor;
517 bool has_tag = ini.get_tag(site.m_name, "supportContact", true, &tag);
518 markupProcessor.insert("supportContact", has_tag ? tag : "");
519 has_tag = ini.get_tag(site.m_name, "logoLocation", true, &tag);
520 markupProcessor.insert("logoLocation", has_tag ? tag : "");
521 markupProcessor.insert("requestURL", target_url);
523 GetServerVariable(pfc,"REMOTE_ADDR",buf,16);
525 status = shire.sessionIsValid(session_id, buf, target_url.c_str());
527 catch (ShibTargetException &e) {
528 markupProcessor.insert("errorType", "SHIRE Processing Error");
529 markupProcessor.insert("errorText", e.what());
530 markupProcessor.insert("errorDesc", "An error occurred while processing your request.");
531 return WriteClientError(pfc, shireError.c_str(), markupProcessor);
534 markupProcessor.insert("errorType", "SHIRE Processing Error");
535 markupProcessor.insert("errorText", "Unexpected Exception");
536 markupProcessor.insert("errorDesc", "An error occurred while processing your request.");
537 return WriteClientError(pfc, shireError.c_str(), markupProcessor);
541 if (status->isError()) {
542 if (status->isRetryable()) {
545 string wayf("Location: ");
546 wayf+=wayfLocation + "?shire=" + url_encode(shire_url.c_str()) + "&target=" + url_encode(target_url.c_str()) + "\r\n";
547 // Insert the headers.
548 pfc->AddResponseHeaders(pfc,const_cast<char*>(wayf.c_str()),0);
549 pfc->ServerSupportFunction(pfc,SF_REQ_SEND_RESPONSE_HEADER,"302 Please Wait",0,0);
550 return SF_STATUS_REQ_FINISHED;
553 // return the error page to the user
554 markupProcessor.insert(*status);
556 return WriteClientError(pfc, shireError.c_str(), markupProcessor);
563 rm_config.checkIPAddress = config.checkIPAddress;
564 RM rm(rpc_handle,rm_config);
566 // Get the attributes.
567 vector<SAMLAssertion*> assertions;
568 SAMLAuthenticationStatement* sso_statement=NULL;
569 status = rm.getAssertions(session_id, buf, target_url.c_str(), assertions, &sso_statement);
571 if (status->isError()) {
573 if (!ini.get_tag(site.m_name, "rmError", true, &shireError))
574 return WriteClientError(pfc,"The rmError configuration setting is missing, check configuration.");
576 markupProcessor.insert(*status);
578 return WriteClientError(pfc, rmError.c_str(), markupProcessor);
582 // Only allow a single assertion...
583 if (assertions.size() > 1) {
584 for (int k = 0; k < assertions.size(); k++)
585 delete assertions[k];
586 delete sso_statement;
587 return WriteClientError(pfc, accessError.c_str(), markupProcessor);
590 // Get the AAP providers, which contain the attribute policy info.
591 Iterator<IAAP*> provs=ShibConfig::getConfig().getAAPProviders();
593 // Clear out the list of mapped attributes
594 while (provs.hasNext())
596 IAAP* aap=provs.next();
600 Iterator<const IAttributeRule*> rules=aap->getAttributeRules();
601 while (rules.hasNext())
603 const char* header=rules.next()->getHeader();
605 pn->SetHeader(pfc,const_cast<char*>(header),"");
611 for (int k = 0; k < assertions.size(); k++)
612 delete assertions[k];
613 delete sso_statement;
620 // Clear relevant headers.
621 pn->SetHeader(pfc,"remote-user:","");
622 pn->SetHeader(pfc,"Shib-Attributes:","");
623 pn->SetHeader(pfc,"Shib-Origin-Site:","");
624 pn->SetHeader(pfc,"Shib-Authentication-Method:","");
626 // Maybe export the assertion.
627 if (ini.get_tag(site.m_name,"exportAssertion",true,&tag) && ShibINI::boolean(tag))
630 RM::serialize(*(assertions[0]), assertion);
631 // string::size_type lfeed;
632 // while ((lfeed=exp.find('\n'))!=string::npos)
633 // exp.erase(lfeed,1);
634 pn->SetHeader(pfc,"Shib-Attributes:",const_cast<char*>(assertion.c_str()));
639 auto_ptr<char> os(XMLString::transcode(sso_statement->getSubject()->getNameQualifier()));
640 auto_ptr<char> am(XMLString::transcode(sso_statement->getAuthMethod()));
641 pn->SetHeader(pfc,"Shib-Origin-Site:", os.get());
642 pn->SetHeader(pfc,"Shib-Authentication-Method:", am.get());
645 // Export the attributes. Only supports a single statement.
646 Iterator<SAMLAttribute*> j = assertions.size()==1 ? RM::getAttributes(*(assertions[0])) : EMPTY(SAMLAttribute*);
649 SAMLAttribute* attr=j.next();
651 // Are we supposed to export it?
652 const char* hname=NULL;
653 AAP wrapper(attr->getName(),attr->getNamespace());
655 hname=wrapper->getHeader();
658 Iterator<string> vals=attr->getSingleByteValues();
659 if (!strcmp(hname,"REMOTE_USER") && vals.hasNext())
661 char* principal=const_cast<char*>(vals.next().c_str());
662 pn->SetHeader(pfc,"remote-user:",principal);
663 pfc->pFilterContext=pfc->AllocMem(pfc,strlen(principal)+1,0);
664 if (pfc->pFilterContext)
665 strcpy(static_cast<char*>(pfc->pFilterContext),principal);
670 for (int it = 0; vals.hasNext(); it++) {
671 string value = vals.next();
672 for (string::size_type pos = value.find_first_of(";", string::size_type(0)); pos != string::npos; pos = value.find_first_of(";", pos)) {
673 value.insert(pos, "\\");
679 header=header + ';' + value;
681 pn->SetHeader(pfc,const_cast<char*>(hname),const_cast<char*>(header.c_str()));
687 for (int k = 0; k < assertions.size(); k++)
688 delete assertions[k];
689 delete sso_statement;
691 return SF_STATUS_REQ_NEXT_NOTIFICATION;
695 return WriteClientError(pfc,"Out of Memory");
699 if (e==ERROR_NO_DATA)
700 return WriteClientError(pfc,"A required variable or header was empty.");
702 WriteClientError(pfc,"Server detected unexpected IIS error.");
706 WriteClientError(pfc,"Server caught an unknown exception.");
709 return WriteClientError(pfc,"Server reached unreachable code!");