1/*
2 * Copyright (C) 2013 Apple Inc.  All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "CurlDownload.h"
28
29#include <WebCore/HTTPParsers.h>
30#include <WebCore/MainThreadTask.h>
31
32#include <wtf/MainThread.h>
33#include <wtf/text/CString.h>
34
35using namespace WebCore;
36
37template<> struct CrossThreadCopierBase<false, false, CurlDownload*> : public CrossThreadCopierPassThrough<CurlDownload*> {
38};
39
40// CurlDownloadManager -------------------------------------------------------------------
41
42CurlDownloadManager::CurlDownloadManager()
43: m_threadId(0)
44, m_curlMultiHandle(0)
45, m_activeDownloadCount(0)
46, m_runThread(false)
47{
48    curl_global_init(CURL_GLOBAL_ALL);
49    m_curlMultiHandle = curl_multi_init();
50}
51
52CurlDownloadManager::~CurlDownloadManager()
53{
54    stopThread();
55    curl_multi_cleanup(m_curlMultiHandle);
56    curl_global_cleanup();
57}
58
59bool CurlDownloadManager::add(CURL* curlHandle)
60{
61    MutexLocker locker(m_mutex);
62
63    m_pendingHandleList.append(curlHandle);
64    startThreadIfNeeded();
65
66    return true;
67}
68
69bool CurlDownloadManager::remove(CURL* curlHandle)
70{
71    MutexLocker locker(m_mutex);
72
73    m_removedHandleList.append(curlHandle);
74
75    return true;
76}
77
78int CurlDownloadManager::getActiveDownloadCount() const
79{
80    return m_activeDownloadCount;
81}
82
83int CurlDownloadManager::getPendingDownloadCount() const
84{
85    MutexLocker locker(m_mutex);
86    return m_pendingHandleList.size();
87}
88
89void CurlDownloadManager::startThreadIfNeeded()
90{
91    if (!m_runThread) {
92        if (m_threadId)
93            waitForThreadCompletion(m_threadId);
94        m_runThread = true;
95        m_threadId = createThread(downloadThread, this, "downloadThread");
96    }
97}
98
99void CurlDownloadManager::stopThread()
100{
101    m_runThread = false;
102
103    if (m_threadId) {
104        waitForThreadCompletion(m_threadId);
105        m_threadId = 0;
106    }
107}
108
109void CurlDownloadManager::stopThreadIfIdle()
110{
111    MutexLocker locker(m_mutex);
112
113    if (!getActiveDownloadCount() && !getPendingDownloadCount())
114        setRunThread(false);
115}
116
117void CurlDownloadManager::updateHandleList()
118{
119    MutexLocker locker(m_mutex);
120
121    // Add pending curl easy handles to multi list
122    int size = m_pendingHandleList.size();
123    for (int i = 0; i < size; i++) {
124        CURLMcode retval = curl_multi_add_handle(m_curlMultiHandle, m_pendingHandleList[0]);
125
126        if (retval == CURLM_OK)
127            m_pendingHandleList.remove(0);
128    }
129
130    // Remove curl easy handles from multi list
131    size = m_removedHandleList.size();
132    for (int i = 0; i < size; i++) {
133        CURLMcode retval = curl_multi_remove_handle(m_curlMultiHandle, m_removedHandleList[0]);
134
135        if (retval == CURLM_OK)
136            m_removedHandleList.remove(0);
137    }
138}
139
140void CurlDownloadManager::downloadThread(void* data)
141{
142    CurlDownloadManager* downloadManager = reinterpret_cast<CurlDownloadManager*>(data);
143
144    while (downloadManager->runThread()) {
145
146        downloadManager->updateHandleList();
147
148        // Retry 'select' if it was interrupted by a process signal.
149        int rc = 0;
150        do {
151            fd_set fdread;
152            fd_set fdwrite;
153            fd_set fdexcep;
154
155            int maxfd = 0;
156
157            const int selectTimeoutMS = 5;
158
159            struct timeval timeout;
160            timeout.tv_sec = 0;
161            timeout.tv_usec = selectTimeoutMS * 1000; // select waits microseconds
162
163            FD_ZERO(&fdread);
164            FD_ZERO(&fdwrite);
165            FD_ZERO(&fdexcep);
166            curl_multi_fdset(downloadManager->getMultiHandle(), &fdread, &fdwrite, &fdexcep, &maxfd);
167            // When the 3 file descriptors are empty, winsock will return -1
168            // and bail out, stopping the file download. So make sure we
169            // have valid file descriptors before calling select.
170            if (maxfd >= 0)
171                rc = ::select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout);
172        } while (rc == -1 && errno == EINTR);
173
174        while (curl_multi_perform(downloadManager->getMultiHandle(), &downloadManager->m_activeDownloadCount) == CURLM_CALL_MULTI_PERFORM) { }
175
176        int messagesInQueue = 0;
177        CURLMsg* msg = curl_multi_info_read(downloadManager->getMultiHandle(), &messagesInQueue);
178
179        if (!msg)
180            continue;
181
182        CurlDownload* download = 0;
183        CURLcode err = curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &download);
184
185        if (msg->msg == CURLMSG_DONE) {
186            if (msg->data.result == CURLE_OK)
187                callOnMainThread<CurlDownload*, CurlDownload*>(CurlDownload::downloadFinishedCallback, download);
188            else
189                callOnMainThread<CurlDownload*, CurlDownload*>(CurlDownload::downloadFailedCallback, download);
190
191            curl_multi_remove_handle(downloadManager->getMultiHandle(), msg->easy_handle);
192        }
193
194        downloadManager->stopThreadIfIdle();
195    }
196}
197
198// CurlDownload --------------------------------------------------------------------------
199
200CurlDownloadManager CurlDownload::m_downloadManager;
201
202CurlDownload::CurlDownload()
203: m_curlHandle(0)
204, m_url(0)
205, m_tempHandle(invalidPlatformFileHandle)
206, m_deletesFileUponFailure(false)
207, m_listener(0)
208{
209}
210
211CurlDownload::~CurlDownload()
212{
213    MutexLocker locker(m_mutex);
214
215    if (m_curlHandle)
216        curl_easy_cleanup(m_curlHandle);
217
218    if (m_url)
219        fastFree(m_url);
220
221    closeFile();
222    moveFileToDestination();
223}
224
225void CurlDownload::init(CurlDownloadListener* listener, const KURL& url)
226{
227    if (!listener)
228        return;
229
230    MutexLocker locker(m_mutex);
231
232    m_curlHandle = curl_easy_init();
233
234    String urlStr = url.string();
235    m_url = fastStrDup(urlStr.latin1().data());
236
237    curl_easy_setopt(m_curlHandle, CURLOPT_URL, m_url);
238    curl_easy_setopt(m_curlHandle, CURLOPT_PRIVATE, this);
239    curl_easy_setopt(m_curlHandle, CURLOPT_WRITEFUNCTION, writeCallback);
240    curl_easy_setopt(m_curlHandle, CURLOPT_WRITEDATA, this);
241    curl_easy_setopt(m_curlHandle, CURLOPT_HEADERFUNCTION, headerCallback);
242    curl_easy_setopt(m_curlHandle, CURLOPT_WRITEHEADER, this);
243    curl_easy_setopt(m_curlHandle, CURLOPT_FOLLOWLOCATION, 1);
244    curl_easy_setopt(m_curlHandle, CURLOPT_MAXREDIRS, 10);
245    curl_easy_setopt(m_curlHandle, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
246
247    m_listener = listener;
248}
249
250bool CurlDownload::start()
251{
252    return m_downloadManager.add(m_curlHandle);
253}
254
255bool CurlDownload::cancel()
256{
257    return m_downloadManager.remove(m_curlHandle);
258}
259
260String CurlDownload::getTempPath() const
261{
262    MutexLocker locker(m_mutex);
263    return m_tempPath;
264}
265
266String CurlDownload::getUrl() const
267{
268    MutexLocker locker(m_mutex);
269    return String(m_url);
270}
271
272ResourceResponse CurlDownload::getResponse() const
273{
274    MutexLocker locker(m_mutex);
275    return m_response;
276}
277
278void CurlDownload::closeFile()
279{
280    MutexLocker locker(m_mutex);
281
282    if (m_tempHandle) {
283        WebCore::closeFile(m_tempHandle);
284        m_tempHandle = invalidPlatformFileHandle;
285    }
286}
287
288void CurlDownload::moveFileToDestination()
289{
290    if (m_destination.isEmpty())
291        return;
292
293    ::MoveFile(m_tempPath.charactersWithNullTermination(), m_destination.charactersWithNullTermination());
294}
295
296void CurlDownload::didReceiveHeader(const String& header)
297{
298    MutexLocker locker(m_mutex);
299
300    if (header == "\r\n" || header == "\n") {
301
302        long httpCode = 0;
303        CURLcode err = curl_easy_getinfo(m_curlHandle, CURLINFO_RESPONSE_CODE, &httpCode);
304
305        if (httpCode >= 200 && httpCode < 300) {
306            const char* url = 0;
307            err = curl_easy_getinfo(m_curlHandle, CURLINFO_EFFECTIVE_URL, &url);
308            m_response.setURL(KURL(ParsedURLString, url));
309
310            m_response.setMimeType(extractMIMETypeFromMediaType(m_response.httpHeaderField("Content-Type")));
311            m_response.setTextEncodingName(extractCharsetFromMediaType(m_response.httpHeaderField("Content-Type")));
312            m_response.setSuggestedFilename(filenameFromHTTPContentDisposition(m_response.httpHeaderField("Content-Disposition")));
313
314            callOnMainThread<CurlDownload*, CurlDownload*>(receivedResponseCallback, this);
315        }
316    } else {
317        int splitPos = header.find(":");
318        if (splitPos != -1)
319            m_response.setHTTPHeaderField(header.left(splitPos), header.substring(splitPos+1).stripWhiteSpace());
320    }
321}
322
323void CurlDownload::didReceiveData(void* data, int size)
324{
325    MutexLocker locker(m_mutex);
326
327    callOnMainThread<CurlDownload*, CurlDownload*, int, int>(receivedDataCallback, this, size);
328
329    if (m_tempPath.isEmpty())
330        m_tempPath = openTemporaryFile("download", m_tempHandle);
331
332    if (m_tempHandle != invalidPlatformFileHandle) {
333        const char* fileData = static_cast<const char*>(data);
334        writeToFile(m_tempHandle, fileData, size);
335    }
336}
337
338void CurlDownload::didReceiveResponse()
339{
340    if (m_listener)
341        m_listener->didReceiveResponse();
342}
343
344void CurlDownload::didReceiveDataOfLength(int size)
345{
346    if (m_listener)
347        m_listener->didReceiveDataOfLength(size);
348}
349
350void CurlDownload::didFinish()
351{
352    closeFile();
353    moveFileToDestination();
354
355    if (m_listener)
356        m_listener->didFinish();
357}
358
359void CurlDownload::didFail()
360{
361    MutexLocker locker(m_mutex);
362
363    closeFile();
364
365    if (m_deletesFileUponFailure)
366        deleteFile(m_tempPath);
367
368    if (m_listener)
369        m_listener->didFail();
370}
371
372size_t CurlDownload::writeCallback(void* ptr, size_t size, size_t nmemb, void* data)
373{
374    size_t totalSize = size * nmemb;
375    CurlDownload* download = reinterpret_cast<CurlDownload*>(data);
376
377    if (download)
378        download->didReceiveData(ptr, totalSize);
379
380    return totalSize;
381}
382
383size_t CurlDownload::headerCallback(char* ptr, size_t size, size_t nmemb, void* data)
384{
385    size_t totalSize = size * nmemb;
386    CurlDownload* download = reinterpret_cast<CurlDownload*>(data);
387
388    String header(static_cast<const char*>(ptr), totalSize);
389
390    if (download)
391        download->didReceiveHeader(header);
392
393    return totalSize;
394}
395
396void CurlDownload::downloadFinishedCallback(CurlDownload* download)
397{
398    if (download)
399        download->didFinish();
400}
401
402void CurlDownload::downloadFailedCallback(CurlDownload* download)
403{
404    if (download)
405        download->didFail();
406}
407
408void CurlDownload::receivedDataCallback(CurlDownload* download, int size)
409{
410    if (download)
411        download->didReceiveDataOfLength(size);
412}
413
414void CurlDownload::receivedResponseCallback(CurlDownload* download)
415{
416    if (download)
417        download->didReceiveResponse();
418}
419