a6edc0edaa36c64c2a400fce81d7219fb8cae66e
[WebKit-https.git] / WebCore / platform / network / curl / ResourceHandleManager.cpp
1 /*
2  * Copyright (C) 2004, 2006 Apple Computer, Inc.  All rights reserved.
3  * Copyright (C) 2006 Michael Emmel mike.emmel@gmail.com 
4  * Copyright (C) 2007 Alp Toker <alp.toker@collabora.co.uk>
5  * Copyright (C) 2007 Holger Hans Peter Freyther
6  * All rights reserved.
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
11  * 1. Redistributions of source code must retain the above copyright
12  *    notice, this list of conditions and the following disclaimer.
13  * 2. Redistributions in binary form must reproduce the above copyright
14  *    notice, this list of conditions and the following disclaimer in the
15  *    documentation and/or other materials provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
21  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
25  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
28  */
29
30 #include "config.h"
31 #include "ResourceHandleManager.h"
32
33 #include "CString.h"
34 #include "NotImplemented.h"
35 #include "ResourceHandle.h"
36 #include "ResourceHandleInternal.h"
37 #include "HTTPParsers.h"
38 #include "Base64.h"
39
40 #include <wtf/Vector.h>
41
42 namespace WebCore {
43
44 const int selectTimeoutMS = 5;
45 const double pollTimeSeconds = 0.05;
46
47 ResourceHandleManager::ResourceHandleManager()
48     : m_downloadTimer(this, &ResourceHandleManager::downloadTimerCallback)
49     , m_cookieJarFileName(0)
50     , m_resourceHandleListHead(0)
51 {
52     curl_global_init(CURL_GLOBAL_ALL);
53     m_curlMultiHandle = curl_multi_init();
54     m_curlShareHandle = curl_share_init();
55     curl_share_setopt(m_curlShareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
56     curl_share_setopt(m_curlShareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
57 }
58
59 ResourceHandleManager::~ResourceHandleManager()
60 {
61     curl_multi_cleanup(m_curlMultiHandle);
62     curl_share_cleanup(m_curlShareHandle);
63     if (m_cookieJarFileName)
64         free(m_cookieJarFileName);
65 }
66
67 void ResourceHandleManager::setCookieJarFileName(const char* cookieJarFileName)
68
69     m_cookieJarFileName = strdup(cookieJarFileName);
70 }
71
72 ResourceHandleManager* ResourceHandleManager::sharedInstance()
73 {
74     static ResourceHandleManager* sharedInstance = 0;
75     if (!sharedInstance)
76         sharedInstance = new ResourceHandleManager();
77     return sharedInstance;
78 }
79
80 // called with data after all headers have been processed via headerCallback
81 static size_t writeCallback(void* ptr, size_t size, size_t nmemb, void* data)
82 {
83     ResourceHandle* job = static_cast<ResourceHandle*>(data);
84     ResourceHandleInternal* d = job->getInternal();
85     int totalSize = size * nmemb;
86
87     // this shouldn't be necessary but apparently is. CURL writes the data
88     // of html page even if it is a redirect that was handled internally
89     // can be observed e.g. on gmail.com
90     CURL* h = d->m_handle;
91     long httpCode = 0;
92     CURLcode err = curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &httpCode);
93     if (CURLE_OK == err && httpCode >= 300 && httpCode < 400)
94         return totalSize;
95
96     if (d->client())
97         d->client()->didReceiveData(job, static_cast<char*>(ptr), totalSize, 0);
98     return totalSize;
99 }
100
101 /*
102  * This is being called for each HTTP header in the response. This includes '\r\n'
103  * for the last line of the header.
104  *
105  * We will add each HTTP Header to the ResourceResponse and on the termination
106  * of the header (\r\n) we will parse Content-Type and Content-Disposition and
107  * update the ResourceResponse and then send it away.
108  *
109  */
110 static size_t headerCallback(char* ptr, size_t size, size_t nmemb, void* data)
111 {
112     ResourceHandle* job = static_cast<ResourceHandle*>(data);
113     ResourceHandleInternal* d = job->getInternal();
114
115     unsigned int totalSize = size * nmemb;
116     ResourceHandleClient* client = d->client();
117
118     String header(static_cast<const char*>(ptr), totalSize);
119
120     /*
121      * a) We can finish and send the ResourceResponse
122      * b) We will add the current header to the HTTPHeaderMap of the ResourceResponse 
123      */
124     if (header == String("\r\n")) {
125         CURL* h = d->m_handle;
126         CURLcode err;
127
128         double contentLength = 0;
129         err = curl_easy_getinfo(h, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &contentLength);
130         d->m_response.setExpectedContentLength(static_cast<long long int>(contentLength)); 
131
132         const char* hdr;
133         err = curl_easy_getinfo(h, CURLINFO_EFFECTIVE_URL, &hdr);
134         d->m_response.setUrl(KURL(hdr));
135
136         long httpCode = 0;
137         err = curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &httpCode);
138         d->m_response.setHTTPStatusCode(httpCode);
139     
140         d->m_response.setMimeType(extractMIMETypeFromMediaType(d->m_response.httpHeaderField("Content-Type")));
141         d->m_response.setTextEncodingName(extractCharsetFromMediaType(d->m_response.httpHeaderField("Content-Type")));
142         d->m_response.setSuggestedFilename(filenameFromHTTPContentDisposition(d->m_response.httpHeaderField("Content-Disposition")));
143
144         // HTTP redirection
145         if (httpCode >= 300 && httpCode < 400) {
146             String location = d->m_response.httpHeaderField("location");
147             if (!location.isEmpty()) {
148                 KURL newURL = KURL(job->request().url(), location.deprecatedString());
149
150                 ResourceRequest redirectedRequest = job->request();
151                 redirectedRequest.setURL(newURL);
152                 if (client)
153                     client->willSendRequest(job, redirectedRequest, d->m_response);
154
155                 d->m_request.setURL(newURL);
156
157                 return totalSize;
158             }
159         }
160
161         if (client)
162             client->didReceiveResponse(job, d->m_response);
163     } else {
164         int splitPos = header.find(":");
165         if (splitPos != -1)
166             d->m_response.setHTTPHeaderField(header.left(splitPos), header.substring(splitPos+1).stripWhiteSpace());
167     }
168
169     return totalSize;
170 }
171
172 void ResourceHandleManager::downloadTimerCallback(Timer<ResourceHandleManager>* timer)
173 {
174     startScheduledJobs();
175
176     fd_set fdread;
177     FD_ZERO(&fdread);
178     fd_set fdwrite;
179     FD_ZERO(&fdwrite);
180     fd_set fdexcep;
181     FD_ZERO(&fdexcep);
182     int maxfd = 0;
183     curl_multi_fdset(m_curlMultiHandle, &fdread, &fdwrite, &fdexcep, &maxfd);
184
185     struct timeval timeout;
186     timeout.tv_sec = 0;
187     timeout.tv_usec = selectTimeoutMS * 1000;       // select waits microseconds
188
189     // Temporarily disable timers since signals may interrupt select(), raising EINTR errors on some platforms
190     setDeferringTimers(true);
191     int rc = ::select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout);
192     setDeferringTimers(false);
193
194     if (-1 == rc) {
195 #ifndef NDEBUG
196         printf("bad: select() returned -1\n");
197 #endif
198         return;
199     }
200
201     int runningHandles = 0;
202     while (curl_multi_perform(m_curlMultiHandle, &runningHandles) == CURLM_CALL_MULTI_PERFORM) { }
203
204     // check the curl messages indicating completed transfers
205     // and free their resources
206     while (true) {
207         int messagesInQueue;
208         CURLMsg* msg = curl_multi_info_read(m_curlMultiHandle, &messagesInQueue);
209         if (!msg)
210             break;
211
212         if (CURLMSG_DONE != msg->msg)
213             continue;
214
215         // find the node which has same d->m_handle as completed transfer
216         CURL* handle = msg->easy_handle;
217         ASSERT(handle);
218         ResourceHandle* job = 0;
219         CURLcode err = curl_easy_getinfo(handle, CURLINFO_PRIVATE, &job);
220         ASSERT(CURLE_OK == err);
221         ASSERT(job);
222         if (!job)
223             continue;
224         ResourceHandleInternal* d = job->getInternal();
225         ASSERT(d->m_handle == handle);
226         if (CURLE_OK == msg->data.result) {
227             if (d->client())
228                 d->client()->didFinishLoading(job);
229         } else {
230 #ifndef NDEBUG
231             char* url = 0;
232             curl_easy_getinfo(d->m_handle, CURLINFO_EFFECTIVE_URL, &url);
233             printf("Curl ERROR for url='%s', error: '%s'\n", url, curl_easy_strerror(msg->data.result));
234 #endif
235             if (d->client())
236                 d->client()->didFail(job, ResourceError());
237         }
238
239         removeFromCurl(job);
240     }
241
242     bool started = startScheduledJobs(); // new jobs might have been added in the meantime
243
244     if (!m_downloadTimer.isActive() && (started || (runningHandles > 0)))
245         m_downloadTimer.startOneShot(pollTimeSeconds);
246 }
247
248 void ResourceHandleManager::removeFromCurl(ResourceHandle* job)
249 {
250     ResourceHandleInternal* d = job->getInternal();
251     ASSERT(d->m_handle);
252     if (!d->m_handle)
253         return;
254     curl_multi_remove_handle(m_curlMultiHandle, d->m_handle);
255     curl_easy_cleanup(d->m_handle);
256     d->m_handle = 0;
257 }
258
259 void ResourceHandleManager::setupPUT(ResourceHandle*)
260 {
261     notImplemented();
262 }
263
264 void ResourceHandleManager::setupPOST(ResourceHandle* job)
265 {
266     ResourceHandleInternal* d = job->getInternal();
267
268     curl_easy_setopt(d->m_handle, CURLOPT_POST, TRUE);
269
270     job->request().httpBody()->flatten(d->m_postBytes);
271     if (d->m_postBytes.size() != 0) {
272         curl_easy_setopt(d->m_handle, CURLOPT_POSTFIELDSIZE, d->m_postBytes.size());
273         curl_easy_setopt(d->m_handle, CURLOPT_POSTFIELDS, d->m_postBytes.data());
274     }
275
276     Vector<FormDataElement> elements = job->request().httpBody()->elements();
277     size_t size = elements.size();
278     struct curl_httppost* lastItem = 0;
279     struct curl_httppost* post = 0;
280     for (size_t i = 0; i < size; i++) {
281         if (elements[i].m_type != FormDataElement::encodedFile)
282             continue;
283         CString cstring = elements[i].m_filename.utf8();
284         ASSERT(!d->m_fileName);
285         d->m_fileName = strdup(cstring.data());
286
287         // Fill in the file upload field
288         curl_formadd(&post, &lastItem, CURLFORM_COPYNAME, "sendfile", CURLFORM_FILE, d->m_fileName, CURLFORM_END);
289
290         // Fill in the filename field
291         curl_formadd(&post, &lastItem, CURLFORM_COPYNAME, "filename", CURLFORM_COPYCONTENTS, d->m_fileName, CURLFORM_END);
292
293         // FIXME: We should not add a "submit" field for each file uploaded. Review this code.
294         // Fill in the submit field too, even if this is rarely needed
295         curl_formadd(&post, &lastItem, CURLFORM_COPYNAME, "submit", CURLFORM_COPYCONTENTS, "send", CURLFORM_END);
296
297         // FIXME: should we support more than one file?
298         break;
299     }
300
301     if (post)
302         curl_easy_setopt(d->m_handle, CURLOPT_HTTPPOST, post);
303 }
304
305 void ResourceHandleManager::add(ResourceHandle* job)
306 {
307     // we can be called from within curl, so to avoid re-entrancy issues
308     // schedule this job to be added the next time we enter curl download loop
309     m_resourceHandleListHead = new ResourceHandleList(job, m_resourceHandleListHead);
310     if (!m_downloadTimer.isActive())
311         m_downloadTimer.startOneShot(pollTimeSeconds);
312 }
313
314 bool ResourceHandleManager::removeScheduledJob(ResourceHandle* job)
315 {
316     ResourceHandleList* node = m_resourceHandleListHead;
317     while (node) {
318         ResourceHandleList* next = node->next();
319         if (job == node->job()) {
320             node->setRemoved(true);
321             return true;
322         }
323         node = next;
324     }
325     return false;
326 }
327
328 bool ResourceHandleManager::startScheduledJobs()
329 {
330     bool started = false;
331     ResourceHandleList* node = m_resourceHandleListHead;
332     while (node) {
333         ResourceHandleList* next = node->next();
334         if (!node->removed()) {
335             startJob(node->job());
336             started = true;
337         }
338         delete node;
339         node = next;
340     }
341     m_resourceHandleListHead = 0;
342     return started;
343 }
344
345 static void parseDataUrl(ResourceHandle* handle)
346 {
347     DeprecatedString data = handle->request().url().url();
348
349     ASSERT(data.startsWith("data:", false));
350
351     DeprecatedString header;
352     bool base64 = false;
353
354     int index = data.find(',');
355     if (index != -1) {
356         header = data.mid(5, index - 5).lower();
357         data = data.mid(index + 1);
358
359         if (header.endsWith(";base64")) {
360             base64 = true;
361             header = header.left(header.length() - 7);
362         }
363     } else
364         data = DeprecatedString();
365
366     data = KURL::decode_string(data);
367
368     if (base64) {
369         Vector<char> out;
370         if (base64Decode(data.ascii(), data.length(), out))
371             data = DeprecatedString(out.data(), out.size());
372         else
373             data = DeprecatedString();
374     }
375
376     if (header.isEmpty())
377         header = "text/plain;charset=US-ASCII";
378
379     ResourceHandleClient* client = handle->getInternal()->client();
380
381     ResourceResponse response;
382
383     response.setMimeType(extractMIMETypeFromMediaType(header));
384     response.setTextEncodingName(extractCharsetFromMediaType(header));
385     response.setExpectedContentLength(data.length());
386     response.setHTTPStatusCode(200);
387
388     client->didReceiveResponse(handle, response);
389
390     if (!data.isEmpty())
391         client->didReceiveData(handle, data.ascii(), data.length(), 0);
392
393     client->didFinishLoading(handle);
394 }
395
396 void ResourceHandleManager::startJob(ResourceHandle* job)
397 {
398     KURL kurl = job->request().url();
399     String protocol = kurl.protocol();
400
401     if (equalIgnoringCase(protocol, "data")) {
402         parseDataUrl(job);
403         return;
404     }
405
406     ResourceHandleInternal* d = job->getInternal();
407     DeprecatedString url = kurl.url();
408     d->m_handle = curl_easy_init();
409 #ifndef NDEBUG
410     if (getenv("DEBUG_CURL"))
411         curl_easy_setopt(d->m_handle, CURLOPT_VERBOSE, 1);
412 #endif
413     curl_easy_setopt(d->m_handle, CURLOPT_PRIVATE, job);
414     curl_easy_setopt(d->m_handle, CURLOPT_ERRORBUFFER, m_curlErrorBuffer);
415     curl_easy_setopt(d->m_handle, CURLOPT_WRITEFUNCTION, writeCallback);
416     curl_easy_setopt(d->m_handle, CURLOPT_WRITEDATA, job);
417     curl_easy_setopt(d->m_handle, CURLOPT_HEADERFUNCTION, headerCallback);
418     curl_easy_setopt(d->m_handle, CURLOPT_WRITEHEADER, job);
419     curl_easy_setopt(d->m_handle, CURLOPT_AUTOREFERER, 1);
420     curl_easy_setopt(d->m_handle, CURLOPT_FOLLOWLOCATION, 1);
421     curl_easy_setopt(d->m_handle, CURLOPT_MAXREDIRS, 10);
422     curl_easy_setopt(d->m_handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
423     curl_easy_setopt(d->m_handle, CURLOPT_SHARE, m_curlShareHandle);
424     curl_easy_setopt(d->m_handle, CURLOPT_DNS_CACHE_TIMEOUT, 60 * 5); // 5 minutes
425     // enable gzip and deflate through Accept-Encoding:
426     curl_easy_setopt(d->m_handle, CURLOPT_ENCODING, "");
427
428     // url must remain valid through the request
429     ASSERT(!d->m_url);
430     d->m_url = strdup(url.ascii());
431     curl_easy_setopt(d->m_handle, CURLOPT_URL, d->m_url);
432
433     if (m_cookieJarFileName) {
434         curl_easy_setopt(d->m_handle, CURLOPT_COOKIEFILE, m_cookieJarFileName);
435         curl_easy_setopt(d->m_handle, CURLOPT_COOKIEJAR, m_cookieJarFileName);
436     }
437
438     if (job->request().httpHeaderFields().size() > 0) {
439         struct curl_slist* headers = 0;
440         HTTPHeaderMap customHeaders = job->request().httpHeaderFields();
441         HTTPHeaderMap::const_iterator end = customHeaders.end();
442         for (HTTPHeaderMap::const_iterator it = customHeaders.begin(); it != end; ++it) {
443             String key = it->first;
444             String value = it->second;
445             String headerString(key);
446             headerString.append(": ");
447             headerString.append(value);
448             CString headerLatin1 = headerString.latin1();
449             headers = curl_slist_append(headers, headerLatin1.data());
450         }
451         curl_easy_setopt(d->m_handle, CURLOPT_HTTPHEADER, headers);
452         d->m_customHeaders = headers;
453     }
454
455     if ("GET" == job->request().httpMethod())
456         curl_easy_setopt(d->m_handle, CURLOPT_HTTPGET, TRUE);
457     else if ("POST" == job->request().httpMethod())
458         setupPOST(job);
459     else if ("PUT" == job->request().httpMethod())
460         setupPUT(job);
461     else if ("HEAD" == job->request().httpMethod())
462         curl_easy_setopt(d->m_handle, CURLOPT_NOBODY, TRUE);
463
464     CURLMcode ret = curl_multi_add_handle(m_curlMultiHandle, d->m_handle);
465     // don't call perform, because events must be async
466     // timeout will occur and do curl_multi_perform
467     if (ret && ret != CURLM_CALL_MULTI_PERFORM) {
468 #ifndef NDEBUG
469         printf("Error %d starting job %s\n", ret, job->request().url().url().ascii());
470 #endif
471         job->cancel();
472         return;
473     }
474 }
475
476 void ResourceHandleManager::cancel(ResourceHandle* job)
477 {
478     if (removeScheduledJob(job))
479         return;
480     removeFromCurl(job);
481     // FIXME: report an error?
482 }
483
484 } // namespace WebCore