1 /*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2020 - Raw Material Software Limited
6
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
9
10 The code included in this file is provided under the terms of the ISC license
11 http://www.isc.org/downloads/software-support-policy/isc-license. Permission
12 To use, copy, modify, and/or distribute this software for any purpose with or
13 without fee is hereby granted provided that the above copyright notice and
14 this permission notice appear in all copies.
15
16 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
17 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
18 DISCLAIMED.
19
20 ==============================================================================
21 */
22
23 namespace juce
24 {
25
26 struct CURLSymbols
27 {
28 CURL* (*curl_easy_init) (void);
29 CURLcode (*curl_easy_setopt) (CURL *curl, CURLoption option, ...);
30 void (*curl_easy_cleanup) (CURL *curl);
31 CURLcode (*curl_easy_getinfo) (CURL *curl, CURLINFO info, ...);
32 CURLMcode (*curl_multi_add_handle) (CURLM *multi_handle, CURL *curl_handle);
33 CURLMcode (*curl_multi_cleanup) (CURLM *multi_handle);
34 CURLMcode (*curl_multi_fdset) (CURLM *multi_handle, fd_set *read_fd_set, fd_set *write_fd_set, fd_set *exc_fd_set, int *max_fd);
35 CURLMsg* (*curl_multi_info_read) (CURLM *multi_handle, int *msgs_in_queue);
36 CURLM* (*curl_multi_init) (void);
37 CURLMcode (*curl_multi_perform) (CURLM *multi_handle, int *running_handles);
38 CURLMcode (*curl_multi_remove_handle) (CURLM *multi_handle, CURL *curl_handle);
39 CURLMcode (*curl_multi_timeout) (CURLM *multi_handle, long *milliseconds);
40 struct curl_slist* (*curl_slist_append) (struct curl_slist *, const char *);
41 void (*curl_slist_free_all) (struct curl_slist *);
42 curl_version_info_data* (*curl_version_info) (CURLversion);
43
createjuce::CURLSymbols44 static std::unique_ptr<CURLSymbols> create()
45 {
46 std::unique_ptr<CURLSymbols> symbols (new CURLSymbols);
47
48 #if JUCE_LOAD_CURL_SYMBOLS_LAZILY
49 const ScopedLock sl (getLibcurlLock());
50 #define JUCE_INIT_CURL_SYMBOL(name) if (! symbols->loadSymbol (symbols->name, #name)) return nullptr;
51 #else
52 #define JUCE_INIT_CURL_SYMBOL(name) symbols->name = ::name;
53 #endif
54
55 JUCE_INIT_CURL_SYMBOL (curl_easy_init)
56 JUCE_INIT_CURL_SYMBOL (curl_easy_setopt)
57 JUCE_INIT_CURL_SYMBOL (curl_easy_cleanup)
58 JUCE_INIT_CURL_SYMBOL (curl_easy_getinfo)
59 JUCE_INIT_CURL_SYMBOL (curl_multi_add_handle)
60 JUCE_INIT_CURL_SYMBOL (curl_multi_cleanup)
61 JUCE_INIT_CURL_SYMBOL (curl_multi_fdset)
62 JUCE_INIT_CURL_SYMBOL (curl_multi_info_read)
63 JUCE_INIT_CURL_SYMBOL (curl_multi_init)
64 JUCE_INIT_CURL_SYMBOL (curl_multi_perform)
65 JUCE_INIT_CURL_SYMBOL (curl_multi_remove_handle)
66 JUCE_INIT_CURL_SYMBOL (curl_multi_timeout)
67 JUCE_INIT_CURL_SYMBOL (curl_slist_append)
68 JUCE_INIT_CURL_SYMBOL (curl_slist_free_all)
69 JUCE_INIT_CURL_SYMBOL (curl_version_info)
70
71 return symbols;
72 }
73
74 // liburl's curl_multi_init calls curl_global_init which is not thread safe
75 // so we need to get a lock during calls to curl_multi_init and curl_multi_cleanup
getLibcurlLockjuce::CURLSymbols76 static CriticalSection& getLibcurlLock() noexcept
77 {
78 static CriticalSection cs;
79 return cs;
80 }
81
82 private:
83 CURLSymbols() = default;
84
85 #if JUCE_LOAD_CURL_SYMBOLS_LAZILY
getLibcurljuce::CURLSymbols86 static DynamicLibrary& getLibcurl()
87 {
88 const ScopedLock sl (getLibcurlLock());
89 static DynamicLibrary libcurl;
90
91 if (libcurl.getNativeHandle() == nullptr)
92 for (auto libName : { "libcurl.so", "libcurl.so.4", "libcurl.so.3" })
93 if (libcurl.open (libName))
94 break;
95
96 return libcurl;
97 }
98
99 template <typename FuncPtr>
loadSymboljuce::CURLSymbols100 bool loadSymbol (FuncPtr& dst, const char* name)
101 {
102 dst = reinterpret_cast<FuncPtr> (getLibcurl().getFunction (name));
103 return (dst != nullptr);
104 }
105 #endif
106 };
107
108
109 //==============================================================================
110 class WebInputStream::Pimpl
111 {
112 public:
Pimpl(WebInputStream & ownerStream,const URL & urlToCopy,bool shouldUsePost)113 Pimpl (WebInputStream& ownerStream, const URL& urlToCopy, bool shouldUsePost)
114 : owner (ownerStream), url (urlToCopy), isPost (shouldUsePost),
115 httpRequest (isPost ? "POST" : "GET")
116 {
117 jassert (symbols); // Unable to load libcurl!
118
119 {
120 const ScopedLock sl (CURLSymbols::getLibcurlLock());
121 multi = symbols->curl_multi_init();
122 }
123
124 if (multi != nullptr)
125 {
126 curl = symbols->curl_easy_init();
127
128 if (curl != nullptr)
129 if (symbols->curl_multi_add_handle (multi, curl) == CURLM_OK)
130 return;
131 }
132
133 cleanup();
134 }
135
~Pimpl()136 ~Pimpl()
137 {
138 cleanup();
139 }
140
141 //==============================================================================
142 // Input Stream overrides
isError() const143 bool isError() const { return curl == nullptr || lastError != CURLE_OK; }
isExhausted()144 bool isExhausted() { return (isError() || finished) && curlBuffer.getSize() == 0; }
getPosition()145 int64 getPosition() { return streamPos; }
getTotalLength()146 int64 getTotalLength() { return contentLength; }
147
read(void * buffer,int bytesToRead)148 int read (void* buffer, int bytesToRead)
149 {
150 return readOrSkip (buffer, bytesToRead, false);
151 }
152
setPosition(int64 wantedPos)153 bool setPosition (int64 wantedPos)
154 {
155 const int amountToSkip = static_cast<int> (wantedPos - getPosition());
156
157 if (amountToSkip < 0)
158 return false;
159
160 if (amountToSkip == 0)
161 return true;
162
163 const int actuallySkipped = readOrSkip (nullptr, amountToSkip, true);
164
165 return actuallySkipped == amountToSkip;
166 }
167
168 //==============================================================================
169 // WebInputStream methods
withExtraHeaders(const String & extraHeaders)170 void withExtraHeaders (const String& extraHeaders)
171 {
172 if (! requestHeaders.endsWithChar ('\n') && requestHeaders.isNotEmpty())
173 requestHeaders << "\r\n";
174
175 requestHeaders << extraHeaders;
176
177 if (! requestHeaders.endsWithChar ('\n') && requestHeaders.isNotEmpty())
178 requestHeaders << "\r\n";
179 }
180
withCustomRequestCommand(const String & customRequestCommand)181 void withCustomRequestCommand (const String& customRequestCommand) { httpRequest = customRequestCommand; }
withConnectionTimeout(int timeoutInMs)182 void withConnectionTimeout (int timeoutInMs) { timeOutMs = timeoutInMs; }
withNumRedirectsToFollow(int maxRedirectsToFollow)183 void withNumRedirectsToFollow (int maxRedirectsToFollow) { maxRedirects = maxRedirectsToFollow; }
getRequestHeaders() const184 StringPairArray getRequestHeaders() const { return WebInputStream::parseHttpHeaders (requestHeaders); }
getResponseHeaders() const185 StringPairArray getResponseHeaders() const { return WebInputStream::parseHttpHeaders (responseHeaders); }
getStatusCode() const186 int getStatusCode() const { return statusCode; }
187
188 //==============================================================================
cleanup()189 void cleanup()
190 {
191 const ScopedLock lock (cleanupLock);
192 const ScopedLock sl (CURLSymbols::getLibcurlLock());
193
194 if (curl != nullptr)
195 {
196 symbols->curl_multi_remove_handle (multi, curl);
197
198 if (headerList != nullptr)
199 {
200 symbols->curl_slist_free_all (headerList);
201 headerList = nullptr;
202 }
203
204 symbols->curl_easy_cleanup (curl);
205 curl = nullptr;
206 }
207
208 if (multi != nullptr)
209 {
210 symbols->curl_multi_cleanup (multi);
211 multi = nullptr;
212 }
213 }
214
cancel()215 void cancel()
216 {
217 cleanup();
218 }
219
220 //==============================================================================
setOptions()221 bool setOptions()
222 {
223 auto address = url.toString (! isPost);
224
225 curl_version_info_data* data = symbols->curl_version_info (CURLVERSION_NOW);
226 jassert (data != nullptr);
227
228 if (! requestHeaders.endsWithChar ('\n'))
229 requestHeaders << "\r\n";
230
231 if (isPost)
232 WebInputStream::createHeadersAndPostData (url, requestHeaders, headersAndPostData);
233
234 if (! requestHeaders.endsWithChar ('\n'))
235 requestHeaders << "\r\n";
236
237 auto userAgent = String ("curl/") + data->version;
238
239 if (symbols->curl_easy_setopt (curl, CURLOPT_URL, address.toRawUTF8()) == CURLE_OK
240 && symbols->curl_easy_setopt (curl, CURLOPT_WRITEDATA, this) == CURLE_OK
241 && symbols->curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, StaticCurlWrite) == CURLE_OK
242 && symbols->curl_easy_setopt (curl, CURLOPT_NOSIGNAL, 1) == CURLE_OK
243 && symbols->curl_easy_setopt (curl, CURLOPT_MAXREDIRS, static_cast<long> (maxRedirects)) == CURLE_OK
244 && symbols->curl_easy_setopt (curl, CURLOPT_USERAGENT, userAgent.toRawUTF8()) == CURLE_OK
245 && symbols->curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, (maxRedirects > 0 ? 1 : 0)) == CURLE_OK)
246 {
247 if (isPost)
248 {
249 if (symbols->curl_easy_setopt (curl, CURLOPT_READDATA, this) != CURLE_OK
250 || symbols->curl_easy_setopt (curl, CURLOPT_READFUNCTION, StaticCurlRead) != CURLE_OK)
251 return false;
252
253 if (symbols->curl_easy_setopt (curl, CURLOPT_POST, 1) != CURLE_OK
254 || symbols->curl_easy_setopt (curl, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t> (headersAndPostData.getSize())) != CURLE_OK)
255 return false;
256 }
257
258 // handle special http request commands
259 bool hasSpecialRequestCmd = isPost ? (httpRequest != "POST") : (httpRequest != "GET");
260
261 if (hasSpecialRequestCmd)
262 if (symbols->curl_easy_setopt (curl, CURLOPT_CUSTOMREQUEST, httpRequest.toRawUTF8()) != CURLE_OK)
263 return false;
264
265 if (symbols->curl_easy_setopt (curl, CURLOPT_HEADERDATA, this) != CURLE_OK
266 || symbols->curl_easy_setopt (curl, CURLOPT_HEADERFUNCTION, StaticCurlHeader) != CURLE_OK)
267 return false;
268
269 if (timeOutMs > 0)
270 {
271 auto timeOutSecs = ((long) timeOutMs + 999) / 1000;
272
273 if (symbols->curl_easy_setopt (curl, CURLOPT_CONNECTTIMEOUT, timeOutSecs) != CURLE_OK
274 || symbols->curl_easy_setopt (curl, CURLOPT_LOW_SPEED_LIMIT, 100) != CURLE_OK
275 || symbols->curl_easy_setopt (curl, CURLOPT_LOW_SPEED_TIME, timeOutSecs) != CURLE_OK)
276 return false;
277 }
278
279 return true;
280 }
281
282 return false;
283 }
284
connect(WebInputStream::Listener * webInputListener)285 bool connect (WebInputStream::Listener* webInputListener)
286 {
287 {
288 const ScopedLock lock (cleanupLock);
289
290 if (curl == nullptr)
291 return false;
292
293 if (! setOptions())
294 {
295 cleanup();
296 return false;
297 }
298
299 if (requestHeaders.isNotEmpty())
300 {
301 const StringArray headerLines = StringArray::fromLines (requestHeaders);
302
303 // fromLines will always return at least one line if the string is not empty
304 jassert (headerLines.size() > 0);
305 headerList = symbols->curl_slist_append (headerList, headerLines [0].toRawUTF8());
306
307 for (int i = 1; (i < headerLines.size() && headerList != nullptr); ++i)
308 headerList = symbols->curl_slist_append (headerList, headerLines [i].toRawUTF8());
309
310 if (headerList == nullptr)
311 {
312 cleanup();
313 return false;
314 }
315
316 if (symbols->curl_easy_setopt (curl, CURLOPT_HTTPHEADER, headerList) != CURLE_OK)
317 {
318 cleanup();
319 return false;
320 }
321 }
322 }
323
324 listener = webInputListener;
325
326 if (isPost)
327 postBuffer = &headersAndPostData;
328
329 size_t lastPos = static_cast<size_t> (-1);
330
331 // step until either: 1) there is an error 2) the transaction is complete
332 // or 3) data is in the in buffer
333 while ((! finished) && curlBuffer.getSize() == 0)
334 {
335 {
336 const ScopedLock lock (cleanupLock);
337
338 if (curl == nullptr)
339 return false;
340 }
341
342 singleStep();
343
344 // call callbacks if this is a post request
345 if (isPost && listener != nullptr && lastPos != postPosition)
346 {
347 lastPos = postPosition;
348
349 if (! listener->postDataSendProgress (owner, static_cast<int> (lastPos), static_cast<int> (headersAndPostData.getSize())))
350 {
351 // user has decided to abort the transaction
352 cleanup();
353 return false;
354 }
355 }
356 }
357
358 {
359 const ScopedLock lock (cleanupLock);
360
361 if (curl == nullptr)
362 return false;
363
364 long responseCode;
365 if (symbols->curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &responseCode) == CURLE_OK)
366 statusCode = static_cast<int> (responseCode);
367
368 // get content length size
369 double curlLength;
370 if (symbols->curl_easy_getinfo (curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &curlLength) == CURLE_OK)
371 contentLength = static_cast<int64> (curlLength);
372 }
373
374 return true;
375 }
376
finish()377 void finish()
378 {
379 const ScopedLock lock (cleanupLock);
380
381 if (curl == nullptr)
382 return;
383
384 for (;;)
385 {
386 int cnt = 0;
387
388 if (CURLMsg* msg = symbols->curl_multi_info_read (multi, &cnt))
389 {
390 if (msg->msg == CURLMSG_DONE && msg->easy_handle == curl)
391 {
392 lastError = msg->data.result; // this is the error that stopped our process from continuing
393 break;
394 }
395 }
396 else
397 {
398 break;
399 }
400 }
401
402 finished = true;
403 }
404
405 //==============================================================================
singleStep()406 void singleStep()
407 {
408 if (lastError != CURLE_OK)
409 return;
410
411 fd_set fdread, fdwrite, fdexcep;
412 int maxfd = -1;
413 long curl_timeo;
414
415 {
416 const ScopedLock lock (cleanupLock);
417
418 if (multi == nullptr)
419 return;
420
421 if ((lastError = (int) symbols->curl_multi_timeout (multi, &curl_timeo)) != CURLM_OK)
422 return;
423 }
424
425 // why 980? see http://curl.haxx.se/libcurl/c/curl_multi_timeout.html
426 if (curl_timeo < 0)
427 curl_timeo = 980;
428
429 struct timeval tv;
430 tv.tv_sec = curl_timeo / 1000;
431 tv.tv_usec = (curl_timeo % 1000) * 1000;
432
433 FD_ZERO (&fdread);
434 FD_ZERO (&fdwrite);
435 FD_ZERO (&fdexcep);
436
437 {
438 const ScopedLock lock (cleanupLock);
439
440 if (multi == nullptr)
441 return;
442
443 if ((lastError = (int) symbols->curl_multi_fdset (multi, &fdread, &fdwrite, &fdexcep, &maxfd)) != CURLM_OK)
444 return;
445 }
446
447 if (maxfd != -1)
448 {
449 if (select (maxfd + 1, &fdread, &fdwrite, &fdexcep, &tv) < 0)
450 {
451 lastError = -1;
452 return;
453 }
454 }
455 else
456 {
457 // if curl does not return any sockets for to wait on, then the doc says to wait 100 ms
458 Thread::sleep (100);
459 }
460
461 int still_running = 0;
462 int curlRet;
463
464 {
465 const ScopedLock lock (cleanupLock);
466
467 while ((curlRet = (int) symbols->curl_multi_perform (multi, &still_running)) == CURLM_CALL_MULTI_PERFORM)
468 {}
469 }
470
471 if ((lastError = curlRet) != CURLM_OK)
472 return;
473
474 if (still_running <= 0)
475 finish();
476 }
477
readOrSkip(void * buffer,int bytesToRead,bool skip)478 int readOrSkip (void* buffer, int bytesToRead, bool skip)
479 {
480 if (bytesToRead <= 0)
481 return 0;
482
483 size_t pos = 0;
484 size_t len = static_cast<size_t> (bytesToRead);
485
486 while (len > 0)
487 {
488 size_t bufferBytes = curlBuffer.getSize();
489 bool removeSection = true;
490
491 if (bufferBytes == 0)
492 {
493 // do not call curl again if we are finished
494 {
495 const ScopedLock lock (cleanupLock);
496
497 if (finished || curl == nullptr)
498 return static_cast<int> (pos);
499 }
500
501 skipBytes = skip ? len : 0;
502 singleStep();
503
504 // update the amount that was read/skipped from curl
505 bufferBytes = skip ? len - skipBytes : curlBuffer.getSize();
506 removeSection = ! skip;
507 }
508
509 // can we copy data from the internal buffer?
510 if (bufferBytes > 0)
511 {
512 size_t max = jmin (len, bufferBytes);
513
514 if (! skip)
515 memcpy (addBytesToPointer (buffer, pos), curlBuffer.getData(), max);
516
517 pos += max;
518 streamPos += static_cast<int64> (max);
519 len -= max;
520
521 if (removeSection)
522 curlBuffer.removeSection (0, max);
523 }
524 }
525
526 return static_cast<int> (pos);
527 }
528
529 //==============================================================================
530 // CURL callbacks
curlWriteCallback(char * ptr,size_t size,size_t nmemb)531 size_t curlWriteCallback (char* ptr, size_t size, size_t nmemb)
532 {
533 if (curl == nullptr || lastError != CURLE_OK)
534 return 0;
535
536 const size_t len = size * nmemb;
537
538 // skip bytes if necessary
539 size_t max = jmin (skipBytes, len);
540 skipBytes -= max;
541
542 if (len > max)
543 curlBuffer.append (ptr + max, len - max);
544
545 return len;
546 }
547
curlReadCallback(char * ptr,size_t size,size_t nmemb)548 size_t curlReadCallback (char* ptr, size_t size, size_t nmemb)
549 {
550 if (curl == nullptr || postBuffer == nullptr || lastError != CURLE_OK)
551 return 0;
552
553 const size_t len = size * nmemb;
554
555 size_t max = jmin (postBuffer->getSize() - postPosition, len);
556 memcpy (ptr, (char*)postBuffer->getData() + postPosition, max);
557 postPosition += max;
558
559 return max;
560 }
561
curlHeaderCallback(char * ptr,size_t size,size_t nmemb)562 size_t curlHeaderCallback (char* ptr, size_t size, size_t nmemb)
563 {
564 if (curl == nullptr || lastError != CURLE_OK)
565 return 0;
566
567 size_t len = size * nmemb;
568
569 String header (ptr, len);
570
571 if (! header.contains (":") && header.startsWithIgnoreCase ("HTTP/"))
572 responseHeaders.clear();
573 else
574 responseHeaders += header;
575
576 return len;
577 }
578
579
580 //==============================================================================
581 // Static method wrappers
StaticCurlWrite(char * ptr,size_t size,size_t nmemb,void * userdata)582 static size_t StaticCurlWrite (char* ptr, size_t size, size_t nmemb, void* userdata)
583 {
584 WebInputStream::Pimpl* wi = reinterpret_cast<WebInputStream::Pimpl*> (userdata);
585 return wi->curlWriteCallback (ptr, size, nmemb);
586 }
587
StaticCurlRead(char * ptr,size_t size,size_t nmemb,void * userdata)588 static size_t StaticCurlRead (char* ptr, size_t size, size_t nmemb, void* userdata)
589 {
590 WebInputStream::Pimpl* wi = reinterpret_cast<WebInputStream::Pimpl*> (userdata);
591 return wi->curlReadCallback (ptr, size, nmemb);
592 }
593
StaticCurlHeader(char * ptr,size_t size,size_t nmemb,void * userdata)594 static size_t StaticCurlHeader (char* ptr, size_t size, size_t nmemb, void* userdata)
595 {
596 WebInputStream::Pimpl* wi = reinterpret_cast<WebInputStream::Pimpl*> (userdata);
597 return wi->curlHeaderCallback (ptr, size, nmemb);
598 }
599
600 //==============================================================================
601 WebInputStream& owner;
602 const URL url;
603 std::unique_ptr<CURLSymbols> symbols { CURLSymbols::create() };
604
605 //==============================================================================
606 // curl stuff
607 CURLM* multi = nullptr;
608 CURL* curl = nullptr;
609 struct curl_slist* headerList = nullptr;
610 int lastError = CURLE_OK;
611
612 //==============================================================================
613 // Options
614 int timeOutMs = 0;
615 int maxRedirects = 5;
616 const bool isPost;
617 String httpRequest;
618
619 //==============================================================================
620 // internal buffers and buffer positions
621 int64 contentLength = -1, streamPos = 0;
622 MemoryBlock curlBuffer;
623 MemoryBlock headersAndPostData;
624 String responseHeaders, requestHeaders;
625 int statusCode = -1;
626
627 //==============================================================================
628 bool finished = false;
629 size_t skipBytes = 0;
630
631 //==============================================================================
632 // Http POST variables
633 const MemoryBlock* postBuffer = nullptr;
634 size_t postPosition = 0;
635
636 //==============================================================================
637 WebInputStream::Listener* listener = nullptr;
638
639 //==============================================================================
640 CriticalSection cleanupLock;
641
642 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
643 };
644
downloadToFile(const File & targetLocation,String extraHeaders,DownloadTask::Listener * listener,bool shouldUsePost)645 std::unique_ptr<URL::DownloadTask> URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener, bool shouldUsePost)
646 {
647 return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener, shouldUsePost);
648 }
649
650 } // namespace juce
651