2008-01-10 Luca Bruno <lethalman88@gmail.com>
[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 "MIMETypeRegistry.h"
35 #include "NotImplemented.h"
36 #include "ResourceHandle.h"
37 #include "ResourceHandleInternal.h"
38 #include "HTTPParsers.h"
39 #include "Base64.h"
40
41 #include <errno.h>
42 #include <wtf/Vector.h>
43
44 namespace WebCore {
45
46 const int selectTimeoutMS = 5;
47 const double pollTimeSeconds = 0.05;
48 const int maxRunningJobs = 5;
49
50 ResourceHandleManager::ResourceHandleManager()
51     : m_downloadTimer(this, &ResourceHandleManager::downloadTimerCallback)
52     , m_cookieJarFileName(0)
53     , m_runningJobs(0)
54 {
55     curl_global_init(CURL_GLOBAL_ALL);
56     m_curlMultiHandle = curl_multi_init();
57     m_curlShareHandle = curl_share_init();
58     curl_share_setopt(m_curlShareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
59     curl_share_setopt(m_curlShareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
60 }
61
62 ResourceHandleManager::~ResourceHandleManager()
63 {
64     curl_multi_cleanup(m_curlMultiHandle);
65     curl_share_cleanup(m_curlShareHandle);
66     if (m_cookieJarFileName)
67         free(m_cookieJarFileName);
68 }
69
70 void ResourceHandleManager::setCookieJarFileName(const char* cookieJarFileName)
71 {
72     m_cookieJarFileName = strdup(cookieJarFileName);
73 }
74
75 ResourceHandleManager* ResourceHandleManager::sharedInstance()
76 {
77     static ResourceHandleManager* sharedInstance = 0;
78     if (!sharedInstance)
79         sharedInstance = new ResourceHandleManager();
80     return sharedInstance;
81 }
82
83 // called with data after all headers have been processed via headerCallback
84 static size_t writeCallback(void* ptr, size_t size, size_t nmemb, void* data)
85 {
86     ResourceHandle* job = static_cast<ResourceHandle*>(data);
87     ResourceHandleInternal* d = job->getInternal();
88     if (d->m_cancelled)
89         return 0;
90     size_t totalSize = size * nmemb;
91
92     // this shouldn't be necessary but apparently is. CURL writes the data
93     // of html page even if it is a redirect that was handled internally
94     // can be observed e.g. on gmail.com
95     CURL* h = d->m_handle;
96     long httpCode = 0;
97     CURLcode err = curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &httpCode);
98     if (CURLE_OK == err && httpCode >= 300 && httpCode < 400)
99         return totalSize;
100
101     // since the code in headerCallback will not have run for local files
102     // the code to set the URL and fire didReceiveResponse is never run,
103     // which means the ResourceLoader's response does not contain the URL.
104     // Run the code here for local files to resolve the issue.
105     // TODO: See if there is a better approach for handling this.
106     if (!d->m_response.responseFired()) {
107         const char* hdr;
108         err = curl_easy_getinfo(h, CURLINFO_EFFECTIVE_URL, &hdr);
109         d->m_response.setUrl(KURL(hdr));
110         if (d->client())
111             d->client()->didReceiveResponse(job, d->m_response);
112         d->m_response.setResponseFired(true);
113     }
114
115     if (d->client())
116         d->client()->didReceiveData(job, static_cast<char*>(ptr), totalSize, 0);
117     return totalSize;
118 }
119
120 /*
121  * This is being called for each HTTP header in the response. This includes '\r\n'
122  * for the last line of the header.
123  *
124  * We will add each HTTP Header to the ResourceResponse and on the termination
125  * of the header (\r\n) we will parse Content-Type and Content-Disposition and
126  * update the ResourceResponse and then send it away.
127  *
128  */
129 static size_t headerCallback(char* ptr, size_t size, size_t nmemb, void* data)
130 {
131     ResourceHandle* job = static_cast<ResourceHandle*>(data);
132     ResourceHandleInternal* d = job->getInternal();
133     if (d->m_cancelled)
134         return 0;
135     size_t totalSize = size * nmemb;
136     ResourceHandleClient* client = d->client();
137
138     String header(static_cast<const char*>(ptr), totalSize);
139
140     /*
141      * a) We can finish and send the ResourceResponse
142      * b) We will add the current header to the HTTPHeaderMap of the ResourceResponse
143      */
144     if (header == String("\r\n")) {
145         CURL* h = d->m_handle;
146         CURLcode err;
147
148         double contentLength = 0;
149         err = curl_easy_getinfo(h, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &contentLength);
150         d->m_response.setExpectedContentLength(static_cast<long long int>(contentLength));
151
152         const char* hdr;
153         err = curl_easy_getinfo(h, CURLINFO_EFFECTIVE_URL, &hdr);
154         d->m_response.setUrl(KURL(hdr));
155
156         long httpCode = 0;
157         err = curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &httpCode);
158         d->m_response.setHTTPStatusCode(httpCode);
159
160         d->m_response.setMimeType(extractMIMETypeFromMediaType(d->m_response.httpHeaderField("Content-Type")));
161         d->m_response.setTextEncodingName(extractCharsetFromMediaType(d->m_response.httpHeaderField("Content-Type")));
162         d->m_response.setSuggestedFilename(filenameFromHTTPContentDisposition(d->m_response.httpHeaderField("Content-Disposition")));
163
164         // HTTP redirection
165         if (httpCode >= 300 && httpCode < 400) {
166             String location = d->m_response.httpHeaderField("location");
167             if (!location.isEmpty()) {
168                 KURL newURL = KURL(job->request().url(), location.deprecatedString());
169
170                 ResourceRequest redirectedRequest = job->request();
171                 redirectedRequest.setURL(newURL);
172                 if (client)
173                     client->willSendRequest(job, redirectedRequest, d->m_response);
174
175                 d->m_request.setURL(newURL);
176
177                 return totalSize;
178             }
179         }
180
181         if (client)
182             client->didReceiveResponse(job, d->m_response);
183         d->m_response.setResponseFired(true);
184
185     } else {
186         int splitPos = header.find(":");
187         if (splitPos != -1)
188             d->m_response.setHTTPHeaderField(header.left(splitPos), header.substring(splitPos+1).stripWhiteSpace());
189     }
190
191     return totalSize;
192 }
193
194 void ResourceHandleManager::downloadTimerCallback(Timer<ResourceHandleManager>* timer)
195 {
196     startScheduledJobs();
197
198     fd_set fdread;
199     fd_set fdwrite;
200     fd_set fdexcep;
201     int maxfd = 0;
202
203     struct timeval timeout;
204     timeout.tv_sec = 0;
205     timeout.tv_usec = selectTimeoutMS * 1000;       // select waits microseconds
206
207     // Temporarily disable timers since signals may interrupt select(), raising EINTR errors on some platforms
208     setDeferringTimers(true);
209     int rc;
210     do {
211         FD_ZERO(&fdread);
212         FD_ZERO(&fdwrite);
213         FD_ZERO(&fdexcep);
214         curl_multi_fdset(m_curlMultiHandle, &fdread, &fdwrite, &fdexcep, &maxfd);
215         rc = ::select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout);
216     } while (rc == -1 && errno == EINTR);
217     setDeferringTimers(false);
218
219     if (-1 == rc) {
220 #ifndef NDEBUG
221         perror("bad: select() returned -1: ");
222 #endif
223         return;
224     }
225
226     int runningHandles = 0;
227     while (curl_multi_perform(m_curlMultiHandle, &runningHandles) == CURLM_CALL_MULTI_PERFORM) { }
228
229     // check the curl messages indicating completed transfers
230     // and free their resources
231     while (true) {
232         int messagesInQueue;
233         CURLMsg* msg = curl_multi_info_read(m_curlMultiHandle, &messagesInQueue);
234         if (!msg)
235             break;
236
237         // find the node which has same d->m_handle as completed transfer
238         CURL* handle = msg->easy_handle;
239         ASSERT(handle);
240         ResourceHandle* job = 0;
241         CURLcode err = curl_easy_getinfo(handle, CURLINFO_PRIVATE, &job);
242         ASSERT(CURLE_OK == err);
243         ASSERT(job);
244         if (!job)
245             continue;
246         ResourceHandleInternal* d = job->getInternal();
247         ASSERT(d->m_handle == handle);
248
249         if (d->m_cancelled) {
250             removeFromCurl(job);
251             continue;
252         }
253
254         if (CURLMSG_DONE != msg->msg)
255             continue;
256
257         if (CURLE_OK == msg->data.result) {
258             if (d->client())
259                 d->client()->didFinishLoading(job);
260         } else {
261 #ifndef NDEBUG
262             char* url = 0;
263             curl_easy_getinfo(d->m_handle, CURLINFO_EFFECTIVE_URL, &url);
264             printf("Curl ERROR for url='%s', error: '%s'\n", url, curl_easy_strerror(msg->data.result));
265 #endif
266             if (d->client())
267                 d->client()->didFail(job, ResourceError());
268         }
269
270         removeFromCurl(job);
271     }
272
273     bool started = startScheduledJobs(); // new jobs might have been added in the meantime
274
275     if (!m_downloadTimer.isActive() && (started || (runningHandles > 0)))
276         m_downloadTimer.startOneShot(pollTimeSeconds);
277 }
278
279 void ResourceHandleManager::removeFromCurl(ResourceHandle* job)
280 {
281     ResourceHandleInternal* d = job->getInternal();
282     ASSERT(d->m_handle);
283     if (!d->m_handle)
284         return;
285     m_runningJobs--;
286     curl_multi_remove_handle(m_curlMultiHandle, d->m_handle);
287     curl_easy_cleanup(d->m_handle);
288     d->m_handle = 0;
289 }
290
291 void ResourceHandleManager::setupPUT(ResourceHandle*)
292 {
293     notImplemented();
294 }
295
296 void ResourceHandleManager::setupPOST(ResourceHandle* job)
297 {
298     ResourceHandleInternal* d = job->getInternal();
299
300     job->request().httpBody()->flatten(d->m_postBytes);
301     if (d->m_postBytes.size() != 0) {
302         curl_easy_setopt(d->m_handle, CURLOPT_POST, TRUE);
303         curl_easy_setopt(d->m_handle, CURLOPT_POSTFIELDSIZE, d->m_postBytes.size());
304         curl_easy_setopt(d->m_handle, CURLOPT_POSTFIELDS, d->m_postBytes.data());
305     }
306
307     Vector<FormDataElement> elements = job->request().httpBody()->elements();
308     size_t size = elements.size();
309     struct curl_httppost* lastItem = 0;
310     struct curl_httppost* post = 0;
311     for (size_t i = 0; i < size; i++) {
312         if (elements[i].m_type != FormDataElement::encodedFile)
313             continue;
314         CString cstring = elements[i].m_filename.utf8();
315         ASSERT(!d->m_fileName);
316         d->m_fileName = strdup(cstring.data());
317
318         // Fill in the file upload field
319         curl_formadd(&post, &lastItem, CURLFORM_COPYNAME, "sendfile", CURLFORM_FILE, d->m_fileName, CURLFORM_END);
320
321         // Fill in the filename field
322         curl_formadd(&post, &lastItem, CURLFORM_COPYNAME, "filename", CURLFORM_COPYCONTENTS, d->m_fileName, CURLFORM_END);
323
324         // FIXME: We should not add a "submit" field for each file uploaded. Review this code.
325         // Fill in the submit field too, even if this is rarely needed
326         curl_formadd(&post, &lastItem, CURLFORM_COPYNAME, "submit", CURLFORM_COPYCONTENTS, "send", CURLFORM_END);
327
328         // FIXME: should we support more than one file?
329         break;
330     }
331
332     if (post)
333         curl_easy_setopt(d->m_handle, CURLOPT_HTTPPOST, post);
334 }
335
336 void ResourceHandleManager::add(ResourceHandle* job)
337 {
338     // we can be called from within curl, so to avoid re-entrancy issues
339     // schedule this job to be added the next time we enter curl download loop
340     m_resourceHandleList.append(job);
341     if (!m_downloadTimer.isActive())
342         m_downloadTimer.startOneShot(pollTimeSeconds);
343 }
344
345 bool ResourceHandleManager::removeScheduledJob(ResourceHandle* job)
346 {
347     int size = m_resourceHandleList.size();
348     for (int i=0; i < size; i++) {
349         if (job == m_resourceHandleList[i]) {
350             m_resourceHandleList.remove(i);
351             return true;
352         }
353     }
354     return false;
355 }
356
357 bool ResourceHandleManager::startScheduledJobs()
358 {
359     // TODO: Create a separate stack of jobs for each domain.
360
361     bool started = false;
362     while (!m_resourceHandleList.isEmpty() && m_runningJobs < maxRunningJobs) {
363         ResourceHandle* job = m_resourceHandleList[0];
364         startJob(job);
365         m_resourceHandleList.remove(0);
366         started = true;
367     }
368     return started;
369 }
370
371 static void parseDataUrl(ResourceHandle* handle)
372 {
373     DeprecatedString data = handle->request().url().deprecatedString();
374
375     ASSERT(data.startsWith("data:", false));
376
377     DeprecatedString header;
378     bool base64 = false;
379
380     int index = data.find(',');
381     if (index != -1) {
382         header = data.mid(5, index - 5).lower();
383         data = data.mid(index + 1);
384
385         if (header.endsWith(";base64")) {
386             base64 = true;
387             header = header.left(header.length() - 7);
388         }
389     } else
390         data = DeprecatedString();
391
392     data = KURL::decode_string(data);
393
394     if (base64) {
395         Vector<char> out;
396         if (base64Decode(data.ascii(), data.length(), out))
397             data = DeprecatedString(out.data(), out.size());
398         else
399             data = DeprecatedString();
400     }
401
402     if (header.isEmpty())
403         header = "text/plain;charset=US-ASCII";
404
405     ResourceHandleClient* client = handle->getInternal()->client();
406
407     ResourceResponse response;
408
409     response.setMimeType(extractMIMETypeFromMediaType(header));
410     response.setTextEncodingName(extractCharsetFromMediaType(header));
411     response.setExpectedContentLength(data.length());
412     response.setHTTPStatusCode(200);
413
414     client->didReceiveResponse(handle, response);
415
416     if (!data.isEmpty())
417         client->didReceiveData(handle, data.ascii(), data.length(), 0);
418
419     client->didFinishLoading(handle);
420 }
421
422 void ResourceHandleManager::startJob(ResourceHandle* job)
423 {
424     KURL kurl = job->request().url();
425     String protocol = kurl.protocol();
426
427     if (equalIgnoringCase(protocol, "data")) {
428         parseDataUrl(job);
429         return;
430     }
431
432     ResourceHandleInternal* d = job->getInternal();
433     DeprecatedString url = kurl.deprecatedString();
434
435     if (kurl.isLocalFile()) {
436         DeprecatedString query = kurl.query();
437         // Remove any query part sent to a local file.
438         if (!query.isEmpty())
439             url = url.left(url.find(query));
440         // Determine the MIME type based on the path.
441         d->m_response.setMimeType(MIMETypeRegistry::getMIMETypeForPath(String(url)));
442     }
443
444     d->m_handle = curl_easy_init();
445 #ifndef NDEBUG
446     if (getenv("DEBUG_CURL"))
447         curl_easy_setopt(d->m_handle, CURLOPT_VERBOSE, 1);
448 #endif
449     curl_easy_setopt(d->m_handle, CURLOPT_PRIVATE, job);
450     curl_easy_setopt(d->m_handle, CURLOPT_ERRORBUFFER, m_curlErrorBuffer);
451     curl_easy_setopt(d->m_handle, CURLOPT_WRITEFUNCTION, writeCallback);
452     curl_easy_setopt(d->m_handle, CURLOPT_WRITEDATA, job);
453     curl_easy_setopt(d->m_handle, CURLOPT_HEADERFUNCTION, headerCallback);
454     curl_easy_setopt(d->m_handle, CURLOPT_WRITEHEADER, job);
455     curl_easy_setopt(d->m_handle, CURLOPT_AUTOREFERER, 1);
456     curl_easy_setopt(d->m_handle, CURLOPT_FOLLOWLOCATION, 1);
457     curl_easy_setopt(d->m_handle, CURLOPT_MAXREDIRS, 10);
458     curl_easy_setopt(d->m_handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
459     curl_easy_setopt(d->m_handle, CURLOPT_SHARE, m_curlShareHandle);
460     curl_easy_setopt(d->m_handle, CURLOPT_DNS_CACHE_TIMEOUT, 60 * 5); // 5 minutes
461     // enable gzip and deflate through Accept-Encoding:
462     curl_easy_setopt(d->m_handle, CURLOPT_ENCODING, "");
463
464     // url must remain valid through the request
465     ASSERT(!d->m_url);
466     d->m_url = strdup(url.ascii());
467     curl_easy_setopt(d->m_handle, CURLOPT_URL, d->m_url);
468
469     if (m_cookieJarFileName) {
470         curl_easy_setopt(d->m_handle, CURLOPT_COOKIEFILE, m_cookieJarFileName);
471         curl_easy_setopt(d->m_handle, CURLOPT_COOKIEJAR, m_cookieJarFileName);
472     }
473
474     if (job->request().httpHeaderFields().size() > 0) {
475         struct curl_slist* headers = 0;
476         HTTPHeaderMap customHeaders = job->request().httpHeaderFields();
477         HTTPHeaderMap::const_iterator end = customHeaders.end();
478         for (HTTPHeaderMap::const_iterator it = customHeaders.begin(); it != end; ++it) {
479             String key = it->first;
480             String value = it->second;
481             String headerString(key);
482             headerString.append(": ");
483             headerString.append(value);
484             CString headerLatin1 = headerString.latin1();
485             headers = curl_slist_append(headers, headerLatin1.data());
486         }
487         curl_easy_setopt(d->m_handle, CURLOPT_HTTPHEADER, headers);
488         d->m_customHeaders = headers;
489     }
490
491     if ("GET" == job->request().httpMethod())
492         curl_easy_setopt(d->m_handle, CURLOPT_HTTPGET, TRUE);
493     else if ("POST" == job->request().httpMethod())
494         setupPOST(job);
495     else if ("PUT" == job->request().httpMethod())
496         setupPUT(job);
497     else if ("HEAD" == job->request().httpMethod())
498         curl_easy_setopt(d->m_handle, CURLOPT_NOBODY, TRUE);
499
500     m_runningJobs++;
501     CURLMcode ret = curl_multi_add_handle(m_curlMultiHandle, d->m_handle);
502     // don't call perform, because events must be async
503     // timeout will occur and do curl_multi_perform
504     if (ret && ret != CURLM_CALL_MULTI_PERFORM) {
505 #ifndef NDEBUG
506         printf("Error %d starting job %s\n", ret, job->request().url().deprecatedString().ascii());
507 #endif
508         job->cancel();
509         return;
510     }
511 }
512
513 void ResourceHandleManager::cancel(ResourceHandle* job)
514 {
515     if (removeScheduledJob(job))
516         return;
517
518     ResourceHandleInternal* d = job->getInternal();
519     d->m_cancelled = true;
520     if (!m_downloadTimer.isActive())
521         m_downloadTimer.startOneShot(pollTimeSeconds);
522 }
523
524 } // namespace WebCore