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