Handle null TARGET value.
[shibboleth/cpp-sp.git] / shib-target / shib-handlers.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  * shib-handlers.cpp -- profile handlers that plug into SP
52  *
53  * Scott Cantor
54  * 5/17/2005
55  */
56
57 #include "internal.h"
58
59 #ifdef HAVE_UNISTD_H
60 # include <unistd.h>
61 #endif
62
63 #include <shib/shib-threads.h>
64 #include <xercesc/util/Base64.hpp>
65
66 #ifndef HAVE_STRCASECMP
67 # define strcasecmp stricmp
68 #endif
69
70 using namespace std;
71 using namespace saml;
72 using namespace shibboleth;
73 using namespace shibtarget;
74 using namespace log4cpp;
75
76 namespace {
77   class CgiParse
78   {
79   public:
80     CgiParse(const char* data, unsigned int len);
81     ~CgiParse();
82     const char* get_value(const char* name) const;
83     
84     static char x2c(char *what);
85     static void url_decode(char *url);
86     static string url_encode(const char* s);
87   private:
88     char * fmakeword(char stop, unsigned int *cl, const char** ppch);
89     char * makeword(char *line, char stop);
90     void plustospace(char *str);
91
92     map<string,char*> kvp_map;
93   };
94
95     // Helper class for SAML 2.0 Common Domain Cookie operations
96     class CommonDomainCookie
97     {
98     public:
99         CommonDomainCookie(const char* cookie);
100         ~CommonDomainCookie() {}
101         saml::Iterator<std::string> get() {return m_list;}
102         const char* set(const char* providerId);
103         static const char CDCName[];
104     private:
105         std::string m_encoded;
106         std::vector<std::string> m_list;
107     };
108
109   class SessionInitiator : virtual public IHandler
110   {
111   public:
112     SessionInitiator(const DOMElement* e) {}
113     ~SessionInitiator() {}
114     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
115     pair<bool,void*> ShibAuthnRequest(
116         ShibTarget* st,
117         const IPropertySet* shire,
118         const char* dest,
119         const char* target,
120         const char* providerId
121         );
122   };
123
124   class SAML1Consumer : virtual public IHandler
125   {
126   public:
127     SAML1Consumer(const DOMElement* e) {}
128     ~SAML1Consumer() {}
129     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
130   };
131
132   class ShibLogout : virtual public IHandler
133   {
134   public:
135     ShibLogout(const DOMElement* e) {}
136     ~ShibLogout() {}
137     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
138   };
139 }
140
141
142 IPlugIn* ShibSessionInitiatorFactory(const DOMElement* e)
143 {
144     return new SessionInitiator(e);
145 }
146
147 IPlugIn* SAML1POSTFactory(const DOMElement* e)
148 {
149     return new SAML1Consumer(e);
150 }
151
152 IPlugIn* SAML1ArtifactFactory(const DOMElement* e)
153 {
154     return new SAML1Consumer(e);
155 }
156
157 IPlugIn* ShibLogoutFactory(const DOMElement* e)
158 {
159     return new ShibLogout(e);
160 }
161
162 pair<bool,void*> SessionInitiator::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
163 {
164     string dupresource;
165     const char* resource=NULL;
166     const IPropertySet* ACS=NULL;
167     const IApplication* app=st->getApplication();
168     
169     if (isHandler) {
170         /* 
171          * Binding is CGI query string with:
172          *  target      the resource to direct back to later
173          *  acsIndex    optional index of an ACS to use on the way back in
174          *  providerId  optional direct invocation of a specific IdP
175          */
176         string query=st->getArgs();
177         CgiParse parser(query.c_str(),query.length());
178
179         const char* option=parser.get_value("acsIndex");
180         if (option)
181             ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
182         option=parser.get_value("providerId");
183         
184         resource=parser.get_value("target");
185         if (!resource || !*resource) {
186             pair<bool,const char*> home=app->getString("homeURL");
187             if (home.first)
188                 resource=home.second;
189             else
190                 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
191         }
192         else if (!option) {
193             dupresource=resource;
194             resource=dupresource.c_str();
195         }
196         
197         if (option) {
198             // Here we actually use metadata to invoke the SSO service directly.
199             // The only currently understood binding is the Shibboleth profile.
200             Metadata m(app->getMetadataProviders());
201             const IEntityDescriptor* entity=m.lookup(option);
202             if (!entity)
203                 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
204             const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(Constants::SHIB_NS);
205             if (!role)
206                 throw MetadataException(
207                     "Session initiator unable to locate a Shibboleth-aware identity provider role for provider ($1).", params(1,option)
208                     );
209             const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
210             const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI);
211             if (!ep)
212                 throw MetadataException(
213                     "Session initiator unable to locate compatible SSO service for provider ($1).", params(1,option)
214                     );
215             auto_ptr_char dest(ep->getLocation());
216             return ShibAuthnRequest(
217                 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
218                 );
219         }
220     }
221     else {
222         // We're running as a "virtual handler" from within the filter.
223         // The target resource is the current one and everything else is defaulted.
224         resource=st->getRequestURL();
225     }
226     
227     if (!ACS) ACS=app->getDefaultAssertionConsumerService();
228     
229     // For now, we only support external session initiation via a wayfURL
230     pair<bool,const char*> wayfURL=handler->getString("wayfURL");
231     if (!wayfURL.first)
232         throw ConfigurationException("Session initiator is missing wayfURL property.");
233
234     pair<bool,const XMLCh*> wayfBinding=handler->getXMLString("wayfBinding");
235     if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI))
236         // Standard Shib 1.x
237         return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
238     else if (!XMLString::compareString(wayfBinding.second,Constants::SHIB_LEGACY_AUTHNREQUEST_PROFILE_URI))
239         // Shib pre-1.2
240         return ShibAuthnRequest(st,ACS,wayfURL.second,resource,NULL);
241     else if (!strcmp(handler->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
242         // TODO: Finalize E-Auth profile URI
243         pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
244         if (!localRelayState.first || !localRelayState.second)
245             throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
246
247         // Here we store the state in a cookie.
248         pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
249         st->setCookie(shib_cookie.first,CgiParse::url_encode(resource) + shib_cookie.second);
250         return make_pair(true, st->sendRedirect(wayfURL.second));
251     }
252    
253     throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,handler->getString("wayfBinding").second));
254 }
255
256 // Handles Shib 1.x AuthnRequest profile.
257 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
258     ShibTarget* st,
259     const IPropertySet* shire,
260     const char* dest,
261     const char* target,
262     const char* providerId
263     )
264 {
265     // Compute the ACS URL. We add the ACS location to the handler baseURL.
266     // Legacy configs will not have an ACS specified, so no suffix will be added.
267     string ACSloc=st->getHandlerURL(target);
268     if (shire) ACSloc+=shire->getString("Location").second;
269     
270     char timebuf[16];
271     sprintf(timebuf,"%u",time(NULL));
272     string req=string(dest) + "?shire=" + CgiParse::url_encode(ACSloc.c_str()) + "&time=" + timebuf;
273
274     // How should the resource value be preserved?
275     pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
276     if (!localRelayState.first || !localRelayState.second) {
277         // The old way, just send it along.
278         req+="&target=" + CgiParse::url_encode(target);
279     }
280     else {
281         // Here we store the state in a cookie and send a fixed
282         // value to the IdP so we can recognize it on the way back.
283         pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
284         st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second);
285         req+="&target=cookie";
286     }
287     
288     // Only omitted for 1.1 style requests.
289     if (providerId)
290         req+="&providerId=" + CgiParse::url_encode(providerId);
291
292     return make_pair(true, st->sendRedirect(req));
293 }
294
295 pair<bool,void*> SAML1Consumer::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
296 {
297     int profile=0;
298     string input,cookie,target,providerId;
299     const IApplication* app=st->getApplication();
300     
301     // Supports either version...
302     pair<bool,unsigned int> version=handler->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
303     if (!version.first)
304         version.second=1;
305
306     pair<bool,const XMLCh*> binding=handler->getXMLString("Binding");
307     if (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
308         if (strcasecmp(st->getRequestMethod(), "POST"))
309             throw FatalProfileException(
310                 "SAML 1.x Browser/POST handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
311                 );
312         
313         if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded"))
314             throw FatalProfileException(
315                 "Blocked invalid content-type ($1) submitted to SAML 1.x Browser/POST handler.", params(1,st->getContentType())
316                 );
317         input=st->getPostData();
318         profile|=(version.second==1 ? SAML11_POST : SAML10_POST);
319     }
320     else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
321         if (strcasecmp(st->getRequestMethod(), "GET"))
322             throw FatalProfileException(
323                 "SAML 1.x Browser/Artifact handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
324                 );
325         input=st->getArgs();
326         profile|=(version.second==1 ? SAML11_ARTIFACT : SAML10_ARTIFACT);
327     }
328     
329     if (input.empty())
330         throw FatalProfileException("SAML 1.x Browser Profile handler received no data from browser.");
331     
332     string hURL=st->getHandlerURL(st->getRequestURL());
333     pair<bool,const char*> loc=handler->getString("Location");
334     string recipient=loc.first ? hURL + loc.second : hURL;
335     st->getConfig()->getListener()->sessionNew(
336         app,
337         profile,
338         recipient.c_str(),
339         input.c_str(),
340         st->getRemoteAddr(),
341         target,
342         cookie,
343         providerId
344         );
345
346     st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + cookie + ")");
347
348     if (target=="default") {
349         pair<bool,const char*> homeURL=app->getString("homeURL");
350         target=homeURL.first ? homeURL.second : "/";
351     }
352     else if (target=="cookie" || target.empty()) {
353         // Pull the target value from the "relay state" cookie.
354         pair<string,const char*> relay_cookie = st->getCookieNameProps("_shibstate_");
355         const char* relay_state = st->getCookie(relay_cookie.first);
356         if (!relay_state || !*relay_state) {
357             // No apparent relay state value to use, so fall back on the default.
358             pair<bool,const char*> homeURL=app->getString("homeURL");
359             target=homeURL.first ? homeURL.second : "/";
360         }
361         else {
362             char* rscopy=strdup(relay_state);
363             CgiParse::url_decode(rscopy);
364             target=rscopy;
365             free(rscopy);
366         }
367     }
368
369     // We've got a good session, set the session cookie.
370     pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibsession_");
371     st->setCookie(shib_cookie.first, cookie + shib_cookie.second);
372
373     const IPropertySet* sessionProps=app->getPropertySet("Sessions");
374     pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
375     if (!idpHistory.first || idpHistory.second) {
376         // Set an IdP history cookie locally (essentially just a CDC).
377         CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
378
379         // Either leave in memory or set an expiration.
380         pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
381             if (!days.first || days.second==0)
382                 st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId.c_str())) + shib_cookie.second);
383             else {
384                 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
385 #ifdef HAVE_GMTIME_R
386                 struct tm res;
387                 struct tm* ptime=gmtime_r(&now,&res);
388 #else
389                 struct tm* ptime=gmtime(&now);
390 #endif
391                 char timebuf[64];
392                 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
393                 st->setCookie(
394                     CommonDomainCookie::CDCName,
395                     string(cdc.set(providerId.c_str())) + shib_cookie.second + "; expires=" + timebuf
396                     );
397         }
398     }
399
400     // Now redirect to the target.
401     return make_pair(true, st->sendRedirect(target));
402 }
403
404 pair<bool,void*> ShibLogout::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
405 {
406     // Recover the session key.
407     pair<string,const char*> shib_cookie = st->getCookieNameProps("_shibsession_");
408     const char* session_id = st->getCookie(shib_cookie.first);
409     
410     // Logout is best effort.
411     if (session_id && *session_id) {
412         try {
413             st->getConfig()->getListener()->sessionEnd(st->getApplication(),session_id);
414         }
415         catch (SAMLException& e) {
416             st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
417         }
418 #ifndef _DEBUG
419         catch (...) {
420             st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
421         }
422 #endif
423         st->setCookie(shib_cookie.first,"");
424     }
425     
426     string query=st->getArgs();
427     CgiParse parser(query.c_str(),query.length());
428
429     const char* ret=parser.get_value("return");
430     if (!ret)
431         ret=handler->getString("ResponseLocation").second;
432     if (!ret)
433         ret=st->getApplication()->getString("homeURL").second;
434     if (!ret)
435         ret="/";
436     return make_pair(true, st->sendRedirect(ret));
437 }
438
439 /*************************************************************************
440  * CGI Parser implementation
441  */
442
443 CgiParse::CgiParse(const char* data, unsigned int len)
444 {
445     const char* pch = data;
446     unsigned int cl = len;
447         
448     while (cl && pch) {
449         char *name;
450         char *value;
451         value=fmakeword('&',&cl,&pch);
452         plustospace(value);
453         url_decode(value);
454         name=makeword(value,'=');
455         kvp_map[name]=value;
456         free(name);
457     }
458 }
459
460 CgiParse::~CgiParse()
461 {
462     for (map<string,char*>::iterator i=kvp_map.begin(); i!=kvp_map.end(); i++)
463         free(i->second);
464 }
465
466 const char*
467 CgiParse::get_value(const char* name) const
468 {
469     map<string,char*>::const_iterator i=kvp_map.find(name);
470     if (i==kvp_map.end())
471         return NULL;
472     return i->second;
473 }
474
475 /* Parsing routines modified from NCSA source. */
476 char *
477 CgiParse::makeword(char *line, char stop)
478 {
479     int x = 0,y;
480     char *word = (char *) malloc(sizeof(char) * (strlen(line) + 1));
481
482     for(x=0;((line[x]) && (line[x] != stop));x++)
483         word[x] = line[x];
484
485     word[x] = '\0';
486     if(line[x])
487         ++x;
488     y=0;
489
490     while(line[x])
491       line[y++] = line[x++];
492     line[y] = '\0';
493     return word;
494 }
495
496 char *
497 CgiParse::fmakeword(char stop, unsigned int *cl, const char** ppch)
498 {
499     int wsize;
500     char *word;
501     int ll;
502
503     wsize = 1024;
504     ll=0;
505     word = (char *) malloc(sizeof(char) * (wsize + 1));
506
507     while(1)
508     {
509         word[ll] = *((*ppch)++);
510         if(ll==wsize-1)
511         {
512             word[ll+1] = '\0';
513             wsize+=1024;
514             word = (char *)realloc(word,sizeof(char)*(wsize+1));
515         }
516         --(*cl);
517         if((word[ll] == stop) || word[ll] == EOF || (!(*cl)))
518         {
519             if(word[ll] != stop)
520                 ll++;
521             word[ll] = '\0';
522             return word;
523         }
524         ++ll;
525     }
526 }
527
528 void
529 CgiParse::plustospace(char *str)
530 {
531     register int x;
532
533     for(x=0;str[x];x++)
534         if(str[x] == '+') str[x] = ' ';
535 }
536
537 char
538 CgiParse::x2c(char *what)
539 {
540     register char digit;
541
542     digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
543     digit *= 16;
544     digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));
545     return(digit);
546 }
547
548 void
549 CgiParse::url_decode(char *url)
550 {
551     register int x,y;
552
553     for(x=0,y=0;url[y];++x,++y)
554     {
555         if((url[x] = url[y]) == '%')
556         {
557             url[x] = x2c(&url[y+1]);
558             y+=2;
559         }
560     }
561     url[x] = '\0';
562 }
563
564 static inline char hexchar(unsigned short s)
565 {
566     return (s<=9) ? ('0' + s) : ('A' + s - 10);
567 }
568
569 string CgiParse::url_encode(const char* s)
570 {
571     static char badchars[]="\"\\+<>#%{}|^~[]`;/?:@=&";
572
573     string ret;
574     for (; *s; s++) {
575         if (strchr(badchars,*s) || *s<=0x1F || *s>=0x7F) {
576             ret+='%';
577         ret+=hexchar(*s >> 4);
578         ret+=hexchar(*s & 0x0F);
579         }
580         else
581             ret+=*s;
582     }
583     return ret;
584 }
585
586 // CDC implementation
587
588 const char CommonDomainCookie::CDCName[] = "_saml_idp";
589
590 CommonDomainCookie::CommonDomainCookie(const char* cookie)
591 {
592     if (!cookie)
593         return;
594
595     Category& log=Category::getInstance(SHIBT_LOGCAT".CommonDomainCookie");
596
597     // Copy it so we can URL-decode it.
598     char* b64=strdup(cookie);
599     CgiParse::url_decode(b64);
600
601     // Chop it up and save off elements.
602     vector<string> templist;
603     char* ptr=b64;
604     while (*ptr) {
605         while (*ptr && isspace(*ptr)) ptr++;
606         char* end=ptr;
607         while (*end && !isspace(*end)) end++;
608         templist.push_back(string(ptr,end-ptr));
609         ptr=end;
610     }
611     free(b64);
612
613     // Now Base64 decode the list.
614     for (vector<string>::iterator i=templist.begin(); i!=templist.end(); i++) {
615         unsigned int len;
616         XMLByte* decoded=Base64::decode(reinterpret_cast<const XMLByte*>(i->c_str()),&len);
617         if (decoded && *decoded) {
618             m_list.push_back(reinterpret_cast<char*>(decoded));
619             XMLString::release(&decoded);
620         }
621         else
622             log.warn("cookie element does not appear to be base64-encoded");
623     }
624 }
625
626 const char* CommonDomainCookie::set(const char* providerId)
627 {
628     // First scan the list for this IdP.
629     for (vector<string>::iterator i=m_list.begin(); i!=m_list.end(); i++) {
630         if (*i == providerId) {
631             m_list.erase(i);
632             break;
633         }
634     }
635     
636     // Append it to the end.
637     m_list.push_back(providerId);
638     
639     // Now rebuild the delimited list.
640     string delimited;
641     for (vector<string>::const_iterator j=m_list.begin(); j!=m_list.end(); j++) {
642         if (!delimited.empty()) delimited += ' ';
643         
644         unsigned int len;
645         XMLByte* b64=Base64::encode(reinterpret_cast<const XMLByte*>(j->c_str()),j->length(),&len);
646         XMLByte *pos, *pos2;
647         for (pos=b64, pos2=b64; *pos2; pos2++)
648             if (isgraph(*pos2))
649                 *pos++=*pos2;
650         *pos=0;
651         
652         delimited += reinterpret_cast<char*>(b64);
653         XMLString::release(&b64);
654     }
655     
656     m_encoded=CgiParse::url_encode(delimited.c_str());
657     return m_encoded.c_str();
658 }