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