Consolidated exception and status handling into a single class.
[shibboleth/sp.git] / shib-target / shibrpc-server.cpp
1 /*
2  * The Shibboleth License, Version 1.
3  * Copyright (c) 2002
4  * University Corporation for Advanced Internet Development, Inc.
5  * All rights reserved
6  *
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions are met:
10  *
11  * Redistributions of source code must retain the above copyright notice, this
12  * list of conditions and the following disclaimer.
13  *
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.
22  *
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
28  *
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.
33  *
34  *
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.
48  */
49
50 /*
51  * shibrpc-server.cpp -- SHIBRPC Server implementation.  Originally created
52  *                       as shibrpc-server-stubs.c; make sure that the function
53  *                       prototypes here match those in shibrpc.x.
54  *
55  * Created by:  Derek Atkins <derek@ihtfp.com>
56  *
57  * $Id$
58  */
59
60 #include "internal.h"
61
62 #include "shibrpc.h"
63
64 #include <sstream>
65
66 #ifdef HAVE_LIBDMALLOCXX
67 #include <dmalloc.h>
68 #endif
69
70 #include <log4cpp/Category.hh>
71
72 using namespace std;
73 using namespace log4cpp;
74 using namespace saml;
75 using namespace shibboleth;
76 using namespace shibtarget;
77
78 static string get_threadid (const char* proc)
79 {
80   static u_long counter = 0;
81   ostringstream buf;
82   buf << "[" << counter++ << "] " << proc;
83   return buf.str();
84 }
85
86 static Category& get_category (void)
87 {
88   return Category::getInstance("shibtarget.rpc-server");
89 }
90
91 extern "C" bool_t
92 shibrpc_ping_2_svc(int *argp, int *result, struct svc_req *rqstp)
93 {
94   *result = (*argp)+1;
95   return TRUE;
96 }
97
98 extern "C" bool_t
99 shibrpc_get_session_2_svc(
100     shibrpc_get_session_args_2 *argp,
101     shibrpc_get_session_ret_2 *result,
102     struct svc_req *rqstp
103     )
104 {
105     Category& log = get_category();
106     string ctx = get_threadid("get_session");
107     saml::NDC ndc(ctx);
108
109     if (!argp || !result) {
110         log.error ("RPC Argument Error");
111         return FALSE;
112     }
113
114     memset (result, 0, sizeof (*result));
115     result->provider_id = strdup("");
116     result->auth_statement = strdup("");
117     result->attr_response_pre = strdup("");
118     result->attr_response_post = strdup("");
119
120     log.debug ("checking: %s@%s", argp->cookie, argp->client_addr);
121
122     // See if the session exists...
123   
124     IConfig* conf=ShibTargetConfig::getConfig().getINI();
125     Locker locker(conf);
126     log.debug ("application: %s", argp->application_id);
127     const IApplication* app=conf->getApplication(argp->application_id);
128     if (!app) {
129         // Something's horribly wrong.
130         log.error("couldn't find application for session");
131         SAMLException ex("Unable to locate application for session, deleted?");
132         ostringstream os;
133         os << ex;
134         result->status=strdup(os.str().c_str());
135         return TRUE;
136     }
137
138     bool checkIPAddress=true;
139     int lifetime=0,timeout=0;
140     const IPropertySet* props=app->getPropertySet("Sessions");
141     if (props) {
142         pair<bool,unsigned int> p=props->getUnsignedInt("lifetime");
143         if (p.first)
144             lifetime = p.second;
145         p=props->getUnsignedInt("timeout");
146         if (p.first)
147             timeout = p.second;
148         pair<bool,bool> pcheck=props->getBool("checkAddress");
149         if (pcheck.first)
150             checkIPAddress = pcheck.second;
151     }
152     
153     ISessionCacheEntry* entry = conf->getSessionCache()->find(argp->cookie,app);
154
155     // If not, leave now..
156     if (!entry) {
157         log.debug("session not found");
158         InvalidSessionException ex("No session exists for key value ($session_id)",namedparams(1,"session_id",argp->cookie));
159         ostringstream os;
160         os << ex;
161         result->status=strdup(os.str().c_str());
162         return TRUE;
163     }
164
165     // TEST the session...
166     try {
167         // Verify the address is the same
168         if (checkIPAddress) {
169             log.debug("Checking address against %s", entry->getClientAddress());
170             if (strcmp(argp->client_addr, entry->getClientAddress())) {
171                 log.debug("client address mismatch");
172                 InvalidSessionException ex(
173                     SESSION_E_ADDRESSMISMATCH,
174                     "Your IP address (%1) does not match the address recorded at the time the session was established.",
175                     params(1,argp->client_addr)
176                     );
177                 Metadata m(app->getMetadataProviders());
178                 annotateException(ex,m.lookup(entry->getProviderId())); // throws it
179             }
180         }
181
182         // and that the session is still valid...
183         if (!entry->isValid(lifetime,timeout)) {
184             log.debug("session expired");
185             InvalidSessionException ex(SESSION_E_EXPIRED, "Your session has expired, and you must re-authenticate.");
186             Metadata m(app->getMetadataProviders());
187             annotateException(ex,m.lookup(entry->getProviderId())); // throws it
188         }
189
190         // Set profile and provider
191         result->profile = entry->getProfile();
192         free(result->provider_id);
193         result->provider_id = strdup(entry->getProviderId());
194      
195         // Now grab the serialized authentication statement and responses
196         ostringstream os;
197         os << *(entry->getAuthnStatement());
198         free(result->auth_statement);
199         result->auth_statement = strdup(os.str().c_str());
200       
201         ISessionCacheEntry::CachedResponse responses=entry->getResponse();
202         if (!responses.empty()) {
203             os.str("");
204             os << *responses.unfiltered;
205             free(result->attr_response_pre);
206             result->attr_response_pre = strdup(os.str().c_str());
207
208             os.str("");
209             os << *responses.filtered;
210             free(result->attr_response_post);
211             result->attr_response_post = strdup(os.str().c_str());
212         }
213     }
214     catch (SAMLException &e) {
215         entry->unlock();
216         log.error("caught SAML exception: %s", e.what());
217         conf->getSessionCache()->remove(argp->cookie);
218         ostringstream os;
219         os << e;
220         result->status = strdup(os.str().c_str());
221       
222         // Transaction Logging
223         STConfig& stc=static_cast<STConfig&>(ShibTargetConfig::getConfig());
224         stc.getTransactionLog().infoStream() <<
225             "Destroyed invalid session (ID: " <<
226                 argp->cookie <<
227             ") with (applicationId: " <<
228                 argp->application_id <<
229             "), request was from (ClientAddress: " <<
230                 argp->client_addr <<
231             ")";
232         stc.releaseTransactionLog();
233         return TRUE;
234     }
235 #ifndef _DEBUG
236     catch (...) {
237         log.error("caught unknown exception");
238         InvalidSessionException ex("An unexpected error occurred while validating your session, and you must re-authenticate.");
239         Metadata m(app->getMetadataProviders());
240         annotateException(ex,m.lookup(entry->getProviderId()),false);
241         entry->unlock();
242         conf->getSessionCache()->remove(argp->cookie);
243         ostringstream os;
244         os << ex;
245         result->status = strdup(os.str().c_str());
246
247         // Transaction Logging
248         STConfig& stc=static_cast<STConfig&>(ShibTargetConfig::getConfig());
249         stc.getTransactionLog().infoStream() <<
250             "Destroyed invalid session (ID: " <<
251                 argp->cookie <<
252             ") with (applicationId: " <<
253                 argp->application_id <<
254             "), request was from (ClientAddress: " <<
255                 argp->client_addr <<
256             ")";
257         stc.releaseTransactionLog();
258         return TRUE;
259     }
260 #endif
261
262     // Ok, just release it.
263     entry->unlock();
264     log.debug ("session ok");
265     result->status=strdup("");
266     return TRUE;
267 }
268
269 extern "C" bool_t
270 shibrpc_new_session_2_svc(
271     shibrpc_new_session_args_2 *argp,
272     shibrpc_new_session_ret_2 *result,
273     struct svc_req *rqstp
274     )
275 {
276     Category& log = get_category();
277     string ctx=get_threadid("new_session");
278     saml::NDC ndc(ctx);
279
280     if (!argp || !result) {
281         log.error ("Invalid RPC Arguments");
282         return FALSE;
283     }
284
285     // Initialize the result structure
286     memset (result, 0, sizeof(*result));
287     result->cookie = strdup ("");
288     result->target = strdup ("");
289
290     log.debug ("creating session for %s", argp->client_addr);
291     log.debug ("recipient: %s", argp->recipient);
292     log.debug ("application: %s", argp->application_id);
293
294     auto_ptr_XMLCh recipient(argp->recipient);
295
296     // Access the application config.
297     IConfig* conf=ShibTargetConfig::getConfig().getINI();
298     Locker locker(conf);
299     const IApplication* app=conf->getApplication(argp->application_id);
300     if (!app) {
301         // Something's horribly wrong. Flush the session.
302         log.error ("couldn't find application for session");
303         SAMLException ex("Unable to locate application for session, deleted?");
304         ostringstream os;
305         os << ex;
306         result->status=strdup(os.str().c_str());
307         return TRUE;
308     }
309
310     bool checkIPAddress=true;
311     const IPropertySet* props=app->getPropertySet("Sessions");
312     if (props) {
313         pair<bool,bool> pcheck=props->getBool("checkAddress");
314         if (pcheck.first)
315             checkIPAddress = pcheck.second;
316     }
317
318     pair<bool,bool> checkReplay=pair<bool,bool>(false,false);
319     props=app->getPropertySet("Sessions");
320     if (props)
321         checkReplay=props->getBool("checkReplay");
322  
323     const IRoleDescriptor* role=NULL;
324     Metadata m(app->getMetadataProviders());
325     SAMLBrowserProfile::BrowserProfileResponse bpr;
326     try {
327         auto_ptr<SAMLBrowserProfile::ArtifactMapper> artifactMapper(app->getArtifactMapper());
328       
329         // Try and run the profile.
330         log.debug("executing browser profile...");
331         int allowed = 0;
332         if (argp->supported_profiles & SAML11_POST)
333             allowed |= SAMLBrowserProfile::Post;
334         if (argp->supported_profiles & SAML11_ARTIFACT)
335             allowed |= SAMLBrowserProfile::Artifact;
336         bpr=app->getBrowserProfile()->receive(
337             argp->packet,
338             recipient.get(),
339             allowed,
340             (!checkReplay.first || checkReplay.second) ? conf->getReplayCache() : NULL,
341             artifactMapper.get()
342             );
343
344         // Blow it away to clear any locks that might be held.
345         delete artifactMapper.release();
346
347         // Try and map to metadata (again).
348         // Once the metadata layer is in the SAML core, the repetition should be fixed.
349         const IEntityDescriptor* provider=m.lookup(bpr.assertion->getIssuer());
350         if (!provider && bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier())
351             provider=m.lookup(bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier());
352         if (provider) {
353             const IIDPSSODescriptor* IDP=provider->getIDPSSODescriptor(saml::XML::SAML11_PROTOCOL_ENUM);
354             role=IDP;
355         }
356         
357         // This isn't likely, since the profile must have found a role.
358         if (!role) {
359             MetadataException ex("Unable to locate role-specific metadata for identity provider.");
360             annotateException(ex,provider); // throws it
361         }
362     
363         // Maybe verify the origin address....
364         if (checkIPAddress) {
365             log.debug("verify client address");
366             // Verify the client address exists
367             const XMLCh* ip = bpr.authnStatement->getSubjectIP();
368             if (ip && *ip) {
369                 // Verify the client address matches authentication
370                 auto_ptr_char this_ip(ip);
371                 if (strcmp(argp->client_addr, this_ip.get())) {
372                     FatalProfileException ex(
373                         SESSION_E_ADDRESSMISMATCH,
374                         "Your client's current address ($1) differs from the one used when you authenticated "
375                         "to your identity provider. To correct this problem, you may need to bypass a proxy server. "
376                         "Please contact your local support staff or help desk for assistance.",
377                         params(1,argp->client_addr)
378                         );
379                     annotateException(ex,role); // throws it
380                 }
381             }
382         }
383       
384         // Verify condition(s) on authentication assertion.
385         // Attribute assertions get filtered later, essentially just like an AAP.
386         Iterator<SAMLCondition*> conditions=bpr.assertion->getConditions();
387         while (conditions.hasNext()) {
388             SAMLCondition* cond=conditions.next();
389             const SAMLAudienceRestrictionCondition* ac=dynamic_cast<const SAMLAudienceRestrictionCondition*>(cond);
390             if (!ac) {
391                 ostringstream os;
392                 os << *cond;
393                 log.error("Unrecognized Condition in authentication assertion (%s), tossing it.",os.str().c_str());
394                 FatalProfileException ex("Unable to create session due to unrecognized condition in authentication assertion.");
395                 annotateException(ex,role); // throws it
396             }
397             else if (!ac->eval(app->getAudiences())) {
398                 ostringstream os;
399                 os << *ac;
400                 log.error("Unacceptable AudienceRestrictionCondition in authentication assertion (%s), tossing it.",os.str().c_str());
401                 FatalProfileException ex("Unable to create session due to unacceptable AudienceRestrictionCondition in authentication assertion.");
402                 annotateException(ex,role); // throws it
403             }
404         }
405     }
406     catch (SAMLException& e) {
407         bpr.clear();
408         log.error("caught SAML exception: %s", e.what());
409         ostringstream os;
410         os << e;
411         result->status = strdup(os.str().c_str());
412         return TRUE;
413     }
414 #ifndef _DEBUG
415     catch (...) {
416         log.error("unknown error");
417         bpr.clear();
418         SAMLException e("An unexpected error occurred while creating your session.");
419         annotateException(e,role,false);
420         ostringstream os;
421         os << e;
422         result->status = strdup(os.str().c_str());
423         return TRUE;
424     }
425 #endif
426
427     // It passes all our tests -- create a new session.
428     log.info("creating new session");
429
430     // Create a new session key.
431     string cookie = conf->getSessionCache()->generateKey();
432
433     // Are attributes present?
434     bool attributesPushed=false;
435     Iterator<SAMLAssertion*> assertions=bpr.response->getAssertions();
436     while (!attributesPushed && assertions.hasNext()) {
437         Iterator<SAMLStatement*> statements=assertions.next()->getStatements();
438         while (!attributesPushed && statements.hasNext()) {
439             if (dynamic_cast<SAMLAttributeStatement*>(statements.next()))
440                 attributesPushed=true;
441         }
442     }
443
444     auto_ptr_char oname(role->getEntityDescriptor()->getId());
445     auto_ptr_char hname(bpr.authnStatement->getSubject()->getNameIdentifier()->getName());
446
447     try {
448         // Insert into cache.
449         auto_ptr<SAMLAuthenticationStatement> as(static_cast<SAMLAuthenticationStatement*>(bpr.authnStatement->clone()));
450         conf->getSessionCache()->insert(
451             cookie.c_str(),
452             app,
453             argp->client_addr,
454             (bpr.profile==SAMLBrowserProfile::Post) ? SAML11_POST : SAML11_ARTIFACT,
455             oname.get(),
456             as.get(),
457             (attributesPushed ? bpr.response : NULL),
458             role
459             );
460         as.release();   // owned by cache now
461     }
462     catch (SAMLException& e) {
463         bpr.clear();
464         log.error("caught SAML exception: %s", e.what());
465         ostringstream os;
466         os << e;
467         result->status = strdup(os.str().c_str());
468         return TRUE;
469     }
470 #ifndef _DEBUG
471     catch (...) {
472         log.error("unknown error");
473         bpr.clear();
474         SAMLException e("An unexpected error occurred while creating your session.");
475         annotateException(e,role,false);
476         ostringstream os;
477         os << e;
478         result->status = strdup(os.str().c_str());
479         return TRUE;
480     }
481 #endif
482
483     // And let the user know.
484     if (result->cookie) free(result->cookie);
485     if (result->target) free(result->target);
486     result->cookie = strdup(cookie.c_str());
487     result->target = strdup(bpr.TARGET.c_str());
488     result->status = strdup("");
489
490     // Maybe delete the response...
491     if (!attributesPushed)
492         bpr.clear();
493
494     log.debug("new session id: %s", cookie.c_str());
495   
496     // Transaction Logging
497     STConfig& stc=static_cast<STConfig&>(ShibTargetConfig::getConfig());
498     stc.getTransactionLog().infoStream() <<
499         "New session (ID: " <<
500             result->cookie <<
501         ") with (applicationId: " <<
502             argp->application_id <<
503         ") for principal from (IdP: " <<
504             oname.get() <<
505         ") at (ClientAddress: " <<
506             argp->client_addr <<
507         ") with (NameIdentifier: " <<
508             hname.get() <<
509         ")";
510
511     stc.releaseTransactionLog();
512     return TRUE;
513 }
514
515 extern "C" int
516 shibrpc_prog_2_freeresult (SVCXPRT *transp, xdrproc_t xdr_result, caddr_t result)
517 {
518         xdr_free (xdr_result, result);
519
520         /*
521          * Insert additional freeing code here, if needed
522          */
523
524         return 1;
525 }