https://issues.shibboleth.net/jira/browse/CPPXT-54
[shibboleth/cpp-xmltooling.git] / xmltooling / util / CurlURLInputStream.cpp
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 /**
19  * xmltooling/util/CurlURLInputStream.cpp
20  *
21  * Asynchronous use of curl to fetch data from a URL.
22  */
23
24 #include "internal.h"
25
26 #include <xmltooling/util/CurlURLInputStream.h>
27 #include <xmltooling/util/ParserPool.h>
28 #include <xmltooling/util/XMLHelper.h>
29
30 #include <openssl/ssl.h>
31 #include <xercesc/util/XercesDefs.hpp>
32 #include <xercesc/util/XMLNetAccessor.hpp>
33 #include <xercesc/util/XMLString.hpp>
34 #include <xercesc/util/XMLExceptMsgs.hpp>
35 #include <xercesc/util/Janitor.hpp>
36 #include <xercesc/util/XMLUniDefs.hpp>
37 #include <xercesc/util/TransService.hpp>
38 #include <xercesc/util/TranscodingException.hpp>
39 #include <xercesc/util/PlatformUtils.hpp>
40
41 using namespace xmltooling;
42 using namespace xercesc;
43 using namespace std;
44
45 namespace {
46     static const XMLCh _CURL[] =            UNICODE_LITERAL_4(C,U,R,L);
47     static const XMLCh _OpenSSL[] =         UNICODE_LITERAL_7(O,p,e,n,S,S,L);
48     static const XMLCh _option[] =          UNICODE_LITERAL_6(o,p,t,i,o,n);
49     static const XMLCh _provider[] =        UNICODE_LITERAL_8(p,r,o,v,i,d,e,r);
50     static const XMLCh TransportOption[] =  UNICODE_LITERAL_15(T,r,a,n,s,p,o,r,t,O,p,t,i,o,n);
51     static const XMLCh uri[] =              UNICODE_LITERAL_3(u,r,i);
52     static const XMLCh url[] =              UNICODE_LITERAL_3(u,r,l);
53     static const XMLCh verifyHost[] =       UNICODE_LITERAL_10(v,e,r,i,f,y,H,o,s,t);
54
55     // callback to invoke a caller-defined SSL callback
56     CURLcode ssl_ctx_callback(CURL* curl, SSL_CTX* ssl_ctx, void* userptr)
57     {
58         CurlURLInputStream* str = reinterpret_cast<CurlURLInputStream*>(userptr);
59
60         // Default flags manually disable SSLv2 so we're not dependent on libcurl to do it.
61         // Also disable the ticket option where implemented, since this breaks a variety
62         // of servers. Newer libcurl also does this for us.
63 #ifdef SSL_OP_NO_TICKET
64         SSL_CTX_set_options(ssl_ctx, str->getOpenSSLOps()|SSL_OP_NO_TICKET);
65 #else
66         SSL_CTX_set_options(ssl_ctx, str->getOpenSSLOps());
67 #endif
68
69         return CURLE_OK;
70     }
71
72     size_t curl_header_hook(void* ptr, size_t size, size_t nmemb, void* stream)
73     {
74         // only handle single-byte data
75         if (size!=1 || nmemb<5 || !stream)
76             return nmemb;
77         string* cacheTag = reinterpret_cast<string*>(stream);
78         const char* hdr = reinterpret_cast<char*>(ptr);
79         if (strncmp(hdr, "ETag:", 5) == 0) {
80             hdr += 5;
81             size_t remaining = nmemb - 5;
82             // skip leading spaces
83             while (remaining > 0) {
84                 if (*hdr == ' ') {
85                     ++hdr;
86                     --remaining;
87                     continue;
88                 }
89                 break;
90             }
91             // append until whitespace
92             while (remaining > 0) {
93                 if (!isspace(*hdr)) {
94                     (*cacheTag) += *hdr++;
95                     --remaining;
96                     continue;
97                 }
98                 break;
99             }
100         }
101
102         return nmemb;
103     }
104 }
105
106 CurlURLInputStream::CurlURLInputStream(const char* url, string* cacheTag)
107     : fLog(logging::Category::getInstance(XMLTOOLING_LOGCAT".libcurl.InputStream"))
108     , fCacheTag(cacheTag)
109     , fOpenSSLOps(SSL_OP_ALL|SSL_OP_NO_SSLv2)
110     , fURL(url ? url : "")
111     , fMulti(0)
112     , fEasy(0)
113     , fHeaders(0)
114     , fTotalBytesRead(0)
115     , fWritePtr(0)
116     , fBytesRead(0)
117     , fBytesToRead(0)
118     , fDataAvailable(false)
119     , fBufferHeadPtr(fBuffer)
120     , fBufferTailPtr(fBuffer)
121     , fContentType(0)
122     , fStatusCode(200)
123 {
124     if (fURL.empty())
125         throw IOException("No URL supplied to CurlURLInputStream constructor.");
126     init();
127 }
128
129 CurlURLInputStream::CurlURLInputStream(const XMLCh* url, string* cacheTag)
130     : fLog(logging::Category::getInstance(XMLTOOLING_LOGCAT".libcurl.InputStream"))
131     , fCacheTag(cacheTag)
132     , fOpenSSLOps(SSL_OP_ALL|SSL_OP_NO_SSLv2)
133     , fMulti(0)
134     , fEasy(0)
135     , fHeaders(0)
136     , fTotalBytesRead(0)
137     , fWritePtr(0)
138     , fBytesRead(0)
139     , fBytesToRead(0)
140     , fDataAvailable(false)
141     , fBufferHeadPtr(fBuffer)
142     , fBufferTailPtr(fBuffer)
143     , fContentType(0)
144     , fStatusCode(200)
145 {
146     if (url) {
147         auto_ptr_char temp(url);
148         fURL = temp.get();
149     }
150     if (fURL.empty())
151         throw IOException("No URL supplied to CurlURLInputStream constructor.");
152     init();
153 }
154
155 CurlURLInputStream::CurlURLInputStream(const DOMElement* e, string* cacheTag)
156     : fLog(logging::Category::getInstance(XMLTOOLING_LOGCAT".libcurl.InputStream"))
157     , fCacheTag(cacheTag)
158     , fOpenSSLOps(SSL_OP_ALL|SSL_OP_NO_SSLv2)
159     , fMulti(0)
160     , fEasy(0)
161     , fHeaders(0)
162     , fTotalBytesRead(0)
163     , fWritePtr(0)
164     , fBytesRead(0)
165     , fBytesToRead(0)
166     , fDataAvailable(false)
167     , fBufferHeadPtr(fBuffer)
168     , fBufferTailPtr(fBuffer)
169     , fContentType(0)
170     , fStatusCode(200)
171 {
172     const XMLCh* attr = e->getAttributeNS(NULL, url);
173     if (!attr || !*attr) {
174         attr = e->getAttributeNS(NULL, uri);
175         if (!attr || !*attr)
176             throw IOException("No URL supplied via DOM to CurlURLInputStream constructor.");
177     }
178
179     auto_ptr_char temp(attr);
180     fURL = temp.get();
181     init(e);
182 }
183
184 CurlURLInputStream::~CurlURLInputStream()
185 {
186     if (fEasy) {
187         // Remove the easy handle from the multi stack
188         curl_multi_remove_handle(fMulti, fEasy);
189
190         // Cleanup the easy handle
191         curl_easy_cleanup(fEasy);
192     }
193
194     if (fMulti) {
195         // Cleanup the multi handle
196         curl_multi_cleanup(fMulti);
197     }
198
199     if (fHeaders) {
200         curl_slist_free_all(fHeaders);
201     }
202
203     XMLString::release(&fContentType);
204 }
205
206 void CurlURLInputStream::init(const DOMElement* e)
207 {
208     // Allocate the curl multi handle
209     fMulti = curl_multi_init();
210
211     // Allocate the curl easy handle
212     fEasy = curl_easy_init();
213
214     if (!fMulti || !fEasy)
215         throw IOException("Failed to allocate libcurl handles.");
216
217     curl_easy_setopt(fEasy, CURLOPT_URL, fURL.c_str());
218
219     // Set up a way to recieve the data
220     curl_easy_setopt(fEasy, CURLOPT_WRITEDATA, this);                       // Pass this pointer to write function
221     curl_easy_setopt(fEasy, CURLOPT_WRITEFUNCTION, staticWriteCallback);    // Our static write function
222
223     // Do redirects
224     curl_easy_setopt(fEasy, CURLOPT_FOLLOWLOCATION, 1);
225     curl_easy_setopt(fEasy, CURLOPT_MAXREDIRS, 6);
226
227     // Default settings.
228     curl_easy_setopt(fEasy, CURLOPT_CONNECTTIMEOUT,10);
229     curl_easy_setopt(fEasy, CURLOPT_TIMEOUT,60);
230     curl_easy_setopt(fEasy, CURLOPT_HTTPAUTH,0);
231     curl_easy_setopt(fEasy, CURLOPT_USERPWD,NULL);
232     curl_easy_setopt(fEasy, CURLOPT_SSL_VERIFYHOST, 2);
233     curl_easy_setopt(fEasy, CURLOPT_SSL_VERIFYPEER, 0);
234     curl_easy_setopt(fEasy, CURLOPT_CAINFO, NULL);
235     curl_easy_setopt(fEasy, CURLOPT_SSL_CIPHER_LIST, "ALL:!aNULL:!LOW:!EXPORT:!SSLv2");
236     curl_easy_setopt(fEasy, CURLOPT_NOPROGRESS, 1);
237     curl_easy_setopt(fEasy, CURLOPT_NOSIGNAL, 1);
238     curl_easy_setopt(fEasy, CURLOPT_FAILONERROR, 1);
239
240     // Install SSL callback.
241     curl_easy_setopt(fEasy, CURLOPT_SSL_CTX_FUNCTION, ssl_ctx_callback);
242     curl_easy_setopt(fEasy, CURLOPT_SSL_CTX_DATA, this);
243
244     fError[0] = 0;
245     curl_easy_setopt(fEasy, CURLOPT_ERRORBUFFER, fError);
246
247     // Check for cache tag.
248     if (fCacheTag) {
249         // Outgoing tag.
250         if (!fCacheTag->empty()) {
251             string hdr("If-None-Match: ");
252             hdr += *fCacheTag;
253             fHeaders = curl_slist_append(fHeaders, hdr.c_str());
254             curl_easy_setopt(fEasy, CURLOPT_HTTPHEADER, fHeaders);
255         }
256         // Incoming tag.
257         curl_easy_setopt(fEasy, CURLOPT_HEADERFUNCTION, curl_header_hook);
258         curl_easy_setopt(fEasy, CURLOPT_HEADERDATA, fCacheTag);
259     }
260
261     if (e) {
262         const XMLCh* flag = e->getAttributeNS(NULL, verifyHost);
263         if (flag && (*flag == chLatin_f || *flag == chDigit_0))
264             curl_easy_setopt(fEasy, CURLOPT_SSL_VERIFYHOST, 0);
265
266         // Process TransportOption elements.
267         bool success;
268         DOMElement* child = XMLHelper::getLastChildElement(e, TransportOption);
269         while (child) {
270             if (child->hasChildNodes() && XMLString::equals(child->getAttributeNS(NULL,_provider), _OpenSSL)) {
271                 auto_ptr_char option(child->getAttributeNS(NULL,_option));
272                 auto_ptr_char value(child->getFirstChild()->getNodeValue());
273                 if (option.get() && value.get() && !strcmp(option.get(), "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION") &&
274                     (*value.get()=='1' || *value.get()=='t')) {
275                     // If the new option to enable buggy rengotiation is available, set it.
276                     // Otherwise, signal false if this is newer than 0.9.8k, because that
277                     // means it's 0.9.8l, which blocks renegotiation, and therefore will
278                     // not honor this request. Older versions are buggy, so behave as though
279                     // the flag was set anyway, so we signal true.
280 #if defined(SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION)
281                     fOpenSSLOps |= SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION;
282                     success = true;
283 #elif (OPENSSL_VERSION_NUMBER > 0x009080bfL)
284                     success = false;
285 #else
286                     success = true;
287 #endif
288                 }
289                 else {
290                     success = false;
291                 }
292                 if (!success)
293                     fLog.error("failed to set OpenSSL transport option (%s)", option.get());
294             }
295             else if (child->hasChildNodes() && XMLString::equals(child->getAttributeNS(NULL,_provider), _CURL)) {
296                 auto_ptr_char option(child->getAttributeNS(NULL,_option));
297                 auto_ptr_char value(child->getFirstChild()->getNodeValue());
298                 if (option.get() && *option.get() && value.get() && *value.get()) {
299                     // For libcurl, the option is an enum and the value type depends on the option.
300                     CURLoption opt = static_cast<CURLoption>(strtol(option.get(), NULL, 10));
301                     if (opt < CURLOPTTYPE_OBJECTPOINT)
302                         success = (curl_easy_setopt(fEasy, opt, strtol(value.get(), NULL, 10)) == CURLE_OK);
303 #ifdef CURLOPTTYPE_OFF_T
304                     else if (opt < CURLOPTTYPE_OFF_T) {
305                         fSavedOptions.push_back(value.get());
306                         success = (curl_easy_setopt(fEasy, opt, fSavedOptions.back().c_str()) == CURLE_OK);
307                     }
308 # ifdef HAVE_CURL_OFF_T
309                     else if (sizeof(curl_off_t) == sizeof(long))
310                         success = (curl_easy_setopt(fEasy, opt, strtol(value.get(), NULL, 10)) == CURLE_OK);
311 # else
312                     else if (sizeof(off_t) == sizeof(long))
313                         success = (curl_easy_setopt(fEasy, opt, strtol(value.get(), NULL, 10)) == CURLE_OK);
314 # endif
315                     else
316                         success = false;
317 #else
318                     else {
319                         fSavedOptions.push_back(value.get());
320                         success = (curl_easy_setopt(fEasy, opt, fSavedOptions.back().c_str()) == CURLE_OK);
321                     }
322 #endif
323                     if (!success)
324                         fLog.error("failed to set CURL transport option (%s)", option.get());
325                 }
326             }
327             child = XMLHelper::getPreviousSiblingElement(child, TransportOption);
328         }
329     }
330
331     // Add easy handle to the multi stack
332     curl_multi_add_handle(fMulti, fEasy);
333
334     fLog.debug("libcurl trying to fetch %s", fURL.c_str());
335
336     // Start reading, to get the content type
337     while(fBufferHeadPtr == fBuffer) {
338         int runningHandles = 0;
339         try {
340             readMore(&runningHandles);
341         }
342         catch (XMLException&) {
343             curl_multi_remove_handle(fMulti, fEasy);
344             curl_easy_cleanup(fEasy);
345             fEasy = NULL;
346             curl_multi_cleanup(fMulti);
347             fMulti = NULL;
348             throw;
349         }
350         if(runningHandles == 0) break;
351     }
352
353     // Check for a response code.
354     if (curl_easy_getinfo(fEasy, CURLINFO_RESPONSE_CODE, &fStatusCode) == CURLE_OK) {
355         if (fStatusCode >= 300 ) {
356             // Short-circuit usual processing by storing a special XML document in the buffer.
357             ostringstream specialdoc;
358             specialdoc << '<' << URLInputSource::asciiStatusCodeElementName << " xmlns=\"http://www.opensaml.org/xmltooling\">"
359                 << fStatusCode
360                 << "</" << URLInputSource::asciiStatusCodeElementName << '>';
361             string specialxml = specialdoc.str();
362             memcpy(fBuffer, specialxml.c_str(), specialxml.length());
363             fBufferHeadPtr += specialxml.length();
364         }
365     }
366     else {
367         fStatusCode = 200;  // reset to 200 to ensure no special processing occurs
368     }
369
370     // Find the content type
371     char* contentType8 = NULL;
372     if(curl_easy_getinfo(fEasy, CURLINFO_CONTENT_TYPE, &contentType8) == CURLE_OK && contentType8)
373         fContentType = XMLString::transcode(contentType8);
374 }
375
376
377 size_t CurlURLInputStream::staticWriteCallback(char* buffer, size_t size, size_t nitems, void* outstream)
378 {
379     return ((CurlURLInputStream*)outstream)->writeCallback(buffer, size, nitems);
380 }
381
382 size_t CurlURLInputStream::writeCallback(char* buffer, size_t size, size_t nitems)
383 {
384     size_t cnt = size * nitems;
385     size_t totalConsumed = 0;
386
387     // Consume as many bytes as possible immediately into the buffer
388     size_t consume = (cnt > fBytesToRead) ? fBytesToRead : cnt;
389     memcpy(fWritePtr, buffer, consume);
390     fWritePtr       += consume;
391     fBytesRead      += consume;
392     fTotalBytesRead += consume;
393     fBytesToRead    -= consume;
394
395     //fLog.debug("write callback consuming %d bytes", consume);
396
397     // If bytes remain, rebuffer as many as possible into our holding buffer
398     buffer          += consume;
399     totalConsumed   += consume;
400     cnt             -= consume;
401     if (cnt > 0)
402     {
403         size_t bufAvail = sizeof(fBuffer) - (fBufferHeadPtr - fBuffer);
404         consume = (cnt > bufAvail) ? bufAvail : cnt;
405         memcpy(fBufferHeadPtr, buffer, consume);
406         fBufferHeadPtr  += consume;
407         buffer          += consume;
408         totalConsumed   += consume;
409         //fLog.debug("write callback rebuffering %d bytes", consume);
410     }
411
412     // Return the total amount we've consumed. If we don't consume all the bytes
413     // then an error will be generated. Since our buffer size is equal to the
414     // maximum size that curl will write, this should never happen unless there
415     // is a logic error somewhere here.
416     return totalConsumed;
417 }
418
419 bool CurlURLInputStream::readMore(int* runningHandles)
420 {
421     // Ask the curl to do some work
422     CURLMcode curlResult = curl_multi_perform(fMulti, runningHandles);
423
424     // Process messages from curl
425     int msgsInQueue = 0;
426     for (CURLMsg* msg = NULL; (msg = curl_multi_info_read(fMulti, &msgsInQueue)) != NULL; )
427     {
428         //fLog.debug("msg %d, %d from curl", msg->msg, msg->data.result);
429
430         if (msg->msg != CURLMSG_DONE)
431             return true;
432
433         switch (msg->data.result)
434         {
435         case CURLE_OK:
436             // We completed successfully. runningHandles should have dropped to zero, so we'll bail out below...
437             break;
438
439         case CURLE_UNSUPPORTED_PROTOCOL:
440             ThrowXML(MalformedURLException, XMLExcepts::URL_UnsupportedProto);
441             break;
442
443         case CURLE_COULDNT_RESOLVE_HOST:
444         case CURLE_COULDNT_RESOLVE_PROXY:
445             ThrowXML1(NetAccessorException,  XMLExcepts::NetAcc_TargetResolution, fURL.c_str());
446             break;
447
448         case CURLE_COULDNT_CONNECT:
449             ThrowXML1(NetAccessorException, XMLExcepts::NetAcc_ConnSocket, fURL.c_str());
450             break;
451
452         case CURLE_OPERATION_TIMEDOUT:
453             ThrowXML1(NetAccessorException, XMLExcepts::NetAcc_ConnSocket, fURL.c_str());
454             break;
455
456         case CURLE_RECV_ERROR:
457             ThrowXML1(NetAccessorException, XMLExcepts::NetAcc_ReadSocket, fURL.c_str());
458             break;
459
460         default:
461             fLog.error("error while fetching %s: (%d) %s", fURL.c_str(), msg->data.result, fError);
462             ThrowXML1(NetAccessorException, XMLExcepts::NetAcc_InternalError, fURL.c_str());
463             break;
464         }
465     }
466
467     // If nothing is running any longer, bail out
468     if(*runningHandles == 0)
469         return false;
470
471     // If there is no further data to read, and we haven't
472     // read any yet on this invocation, call select to wait for data
473     if (curlResult != CURLM_CALL_MULTI_PERFORM && fBytesRead == 0)
474     {
475         fd_set readSet;
476         fd_set writeSet;
477         fd_set exceptSet;
478         int fdcnt=0;
479
480         FD_ZERO(&readSet);
481         FD_ZERO(&writeSet);
482         FD_ZERO(&exceptSet);
483
484         // Ask curl for the file descriptors to wait on
485         curl_multi_fdset(fMulti, &readSet, &writeSet, &exceptSet, &fdcnt);
486
487         // Wait on the file descriptors
488         timeval tv;
489         tv.tv_sec  = 2;
490         tv.tv_usec = 0;
491         select(fdcnt+1, &readSet, &writeSet, &exceptSet, &tv);
492     }
493
494     return curlResult == CURLM_CALL_MULTI_PERFORM;
495 }
496
497 xsecsize_t CurlURLInputStream::readBytes(XMLByte* const toFill, const xsecsize_t maxToRead)
498 {
499     fBytesRead = 0;
500     fBytesToRead = maxToRead;
501     fWritePtr = toFill;
502
503     for (bool tryAgain = true; fBytesToRead > 0 && (tryAgain || fBytesRead == 0); )
504     {
505         // First, any buffered data we have available
506         size_t bufCnt = fBufferHeadPtr - fBufferTailPtr;
507         bufCnt = (bufCnt > fBytesToRead) ? fBytesToRead : bufCnt;
508         if (bufCnt > 0)
509         {
510             memcpy(fWritePtr, fBufferTailPtr, bufCnt);
511             fWritePtr       += bufCnt;
512             fBytesRead      += bufCnt;
513             fTotalBytesRead += bufCnt;
514             fBytesToRead    -= bufCnt;
515
516             fBufferTailPtr  += bufCnt;
517             if (fBufferTailPtr == fBufferHeadPtr)
518                 fBufferHeadPtr = fBufferTailPtr = fBuffer;
519
520             //fLog.debug("consuming %d buffered bytes", bufCnt);
521
522             tryAgain = true;
523             continue;
524         }
525
526         // Check for a non-2xx status that means to ignore the curl response.
527         if (fStatusCode >= 300)
528             break;
529
530         // Ask the curl to do some work
531         int runningHandles = 0;
532         tryAgain = readMore(&runningHandles);
533
534         // If nothing is running any longer, bail out
535         if (runningHandles == 0)
536             break;
537     }
538
539     return fBytesRead;
540 }