source: trunk/libtransmission/web.c @ 14320

Last change on this file since 14320 was 14320, checked in by jordan, 8 years ago

(trunk, libT) #4160 'foreign character support' -- merge mike.dld's 4160-02b-path.patch, which updates the codebase to use the new tr_sys_path_*() portability wrappers introduced in 4160-02a

  • Property svn:keywords set to Date Rev Author Id
File size: 18.7 KB
Line 
1/*
2 * This file Copyright (C) 2008-2014 Mnemosyne LLC
3 *
4 * It may be used under the GNU GPL versions 2 or 3
5 * or any future license endorsed by Mnemosyne LLC.
6 *
7 * $Id: web.c 14320 2014-07-08 00:08:43Z jordan $
8 */
9
10#include <assert.h>
11#include <string.h> /* strlen (), strstr () */
12#include <stdlib.h> /* getenv () */
13
14#ifdef _WIN32
15  #include <ws2tcpip.h>
16#else
17  #include <sys/select.h>
18#endif
19
20#include <curl/curl.h>
21
22#include <event2/buffer.h>
23
24#include "transmission.h"
25#include "file.h"
26#include "list.h"
27#include "log.h"
28#include "net.h" /* tr_address */
29#include "torrent.h"
30#include "platform.h" /* mutex */
31#include "session.h"
32#include "trevent.h" /* tr_runInEventThread () */
33#include "utils.h"
34#include "version.h" /* User-Agent */
35#include "web.h"
36
37#if LIBCURL_VERSION_NUM >= 0x070F06 /* CURLOPT_SOCKOPT* was added in 7.15.6 */
38 #define USE_LIBCURL_SOCKOPT
39#endif
40
41enum
42{
43  THREADFUNC_MAX_SLEEP_MSEC = 200,
44};
45
46#if 0
47#define dbgmsg(...) \
48  do { \
49    fprintf (stderr, __VA_ARGS__); \
50    fprintf (stderr, "\n"); \
51  } while (0)
52#else
53#define dbgmsg(...) \
54  do { \
55    if (tr_logGetDeepEnabled ()) \
56      tr_logAddDeep (__FILE__, __LINE__, "web", __VA_ARGS__); \
57  } while (0)
58#endif
59
60/***
61****
62***/
63
64struct tr_web_task
65{
66  int torrentId;
67  long code;
68  long timeout_secs;
69  bool did_connect;
70  bool did_timeout;
71  struct evbuffer * response;
72  struct evbuffer * freebuf;
73  char * url;
74  char * range;
75  char * cookies;
76  tr_session * session;
77  tr_web_done_func done_func;
78  void * done_func_user_data;
79  CURL * curl_easy;
80  struct tr_web_task * next;
81};
82
83static void
84task_free (struct tr_web_task * task)
85{
86  if (task->freebuf)
87    evbuffer_free (task->freebuf);
88  tr_free (task->cookies);
89  tr_free (task->range);
90  tr_free (task->url);
91  tr_free (task);
92}
93
94/***
95****
96***/
97
98static tr_list  * paused_easy_handles = NULL;
99
100struct tr_web
101{
102  bool curl_verbose;
103  bool curl_ssl_verify;
104  const char * curl_ca_bundle;
105  int close_mode;
106  struct tr_web_task * tasks;
107  tr_lock * taskLock;
108  char * cookie_filename;
109};
110
111/***
112****
113***/
114
115static size_t
116writeFunc (void * ptr, size_t size, size_t nmemb, void * vtask)
117{
118  const size_t byteCount = size * nmemb;
119  struct tr_web_task * task = vtask;
120
121  /* webseed downloads should be speed limited */
122  if (task->torrentId != -1)
123    {
124      tr_torrent * tor = tr_torrentFindFromId (task->session, task->torrentId);
125
126      if (tor && !tr_bandwidthClamp (&tor->bandwidth, TR_DOWN, nmemb))
127        {
128          tr_list_append (&paused_easy_handles, task->curl_easy);
129          return CURL_WRITEFUNC_PAUSE;
130        }
131    }
132
133  evbuffer_add (task->response, ptr, byteCount);
134  dbgmsg ("wrote %"TR_PRIuSIZE" bytes to task %p's buffer", byteCount, (void*)task);
135  return byteCount;
136}
137
138#ifdef USE_LIBCURL_SOCKOPT
139static int
140sockoptfunction (void * vtask, curl_socket_t fd, curlsocktype purpose UNUSED)
141{
142  struct tr_web_task * task = vtask;
143  const bool isScrape = strstr (task->url, "scrape") != NULL;
144  const bool isAnnounce = strstr (task->url, "announce") != NULL;
145
146  /* announce and scrape requests have tiny payloads. */
147  if (isScrape || isAnnounce)
148    {
149      const int sndbuf = isScrape ? 4096 : 1024;
150      const int rcvbuf = isScrape ? 4096 : 3072;
151      setsockopt (fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof (sndbuf));
152      setsockopt (fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof (rcvbuf));
153    }
154
155  /* return nonzero if this function encountered an error */
156  return 0;
157}
158#endif
159
160static long
161getTimeoutFromURL (const struct tr_web_task * task)
162{
163  long timeout;
164  const tr_session * session = task->session;
165
166  if (!session || session->isClosed) timeout = 20L;
167  else if (strstr (task->url, "scrape") != NULL) timeout = 30L;
168  else if (strstr (task->url, "announce") != NULL) timeout = 90L;
169  else timeout = 240L;
170
171  return timeout;
172}
173
174static CURL *
175createEasy (tr_session * s, struct tr_web * web, struct tr_web_task * task)
176{
177  bool is_default_value;
178  const tr_address * addr;
179  CURL * e = task->curl_easy = curl_easy_init ();
180
181  task->timeout_secs = getTimeoutFromURL (task);
182
183  curl_easy_setopt (e, CURLOPT_AUTOREFERER, 1L);
184  curl_easy_setopt (e, CURLOPT_ENCODING, "gzip;q=1.0, deflate, identity");
185  curl_easy_setopt (e, CURLOPT_FOLLOWLOCATION, 1L);
186  curl_easy_setopt (e, CURLOPT_MAXREDIRS, -1L);
187  curl_easy_setopt (e, CURLOPT_NOSIGNAL, 1L);
188  curl_easy_setopt (e, CURLOPT_PRIVATE, task);
189#ifdef USE_LIBCURL_SOCKOPT
190  curl_easy_setopt (e, CURLOPT_SOCKOPTFUNCTION, sockoptfunction);
191  curl_easy_setopt (e, CURLOPT_SOCKOPTDATA, task);
192#endif
193  if (web->curl_ssl_verify)
194    {
195      curl_easy_setopt (e, CURLOPT_CAINFO, web->curl_ca_bundle);
196    }
197  else
198    {
199      curl_easy_setopt (e, CURLOPT_SSL_VERIFYHOST, 0L);
200      curl_easy_setopt (e, CURLOPT_SSL_VERIFYPEER, 0L);
201    }
202  curl_easy_setopt (e, CURLOPT_TIMEOUT, task->timeout_secs);
203  curl_easy_setopt (e, CURLOPT_URL, task->url);
204  curl_easy_setopt (e, CURLOPT_USERAGENT, TR_NAME "/" SHORT_VERSION_STRING);
205  curl_easy_setopt (e, CURLOPT_VERBOSE, (long)(web->curl_verbose?1:0));
206  curl_easy_setopt (e, CURLOPT_WRITEDATA, task);
207  curl_easy_setopt (e, CURLOPT_WRITEFUNCTION, writeFunc);
208
209  if (((addr = tr_sessionGetPublicAddress (s, TR_AF_INET, &is_default_value))) && !is_default_value)
210    curl_easy_setopt (e, CURLOPT_INTERFACE, tr_address_to_string (addr));
211  else if (((addr = tr_sessionGetPublicAddress (s, TR_AF_INET6, &is_default_value))) && !is_default_value)
212    curl_easy_setopt (e, CURLOPT_INTERFACE, tr_address_to_string (addr));
213
214  if (task->cookies != NULL)
215    curl_easy_setopt (e, CURLOPT_COOKIE, task->cookies);
216
217  if (web->cookie_filename != NULL)
218    curl_easy_setopt (e, CURLOPT_COOKIEFILE, web->cookie_filename);
219
220  if (task->range != NULL)
221    {
222      curl_easy_setopt (e, CURLOPT_RANGE, task->range);
223      /* don't bother asking the server to compress webseed fragments */
224      curl_easy_setopt (e, CURLOPT_ENCODING, "identity");
225    }
226
227  return e;
228}
229
230/***
231****
232***/
233
234static void
235task_finish_func (void * vtask)
236{
237  struct tr_web_task * task = vtask;
238  dbgmsg ("finished web task %p; got %ld", (void*)task, task->code);
239
240  if (task->done_func != NULL)
241    task->done_func (task->session,
242                     task->did_connect,
243                     task->did_timeout,
244                     task->code,
245                     evbuffer_pullup (task->response, -1),
246                     evbuffer_get_length (task->response),
247                     task->done_func_user_data);
248
249  task_free (task);
250}
251
252/****
253*****
254****/
255
256static void tr_webThreadFunc (void * vsession);
257
258static struct tr_web_task *
259tr_webRunImpl (tr_session         * session,
260               int                  torrentId,
261               const char         * url,
262               const char         * range,
263               const char         * cookies,
264               tr_web_done_func     done_func,
265               void               * done_func_user_data,
266               struct evbuffer    * buffer)
267{
268  struct tr_web_task * task = NULL;
269
270  if (!session->isClosing)
271    {
272      if (session->web == NULL)
273        {
274          tr_threadNew (tr_webThreadFunc, session);
275
276          while (session->web == NULL)
277            tr_wait_msec (20);
278        }
279     
280      task = tr_new0 (struct tr_web_task, 1);
281      task->session = session;
282      task->torrentId = torrentId;
283      task->url = tr_strdup (url);
284      task->range = tr_strdup (range);
285      task->cookies = tr_strdup (cookies);
286      task->done_func = done_func;
287      task->done_func_user_data = done_func_user_data;
288      task->response = buffer ? buffer : evbuffer_new ();
289      task->freebuf = buffer ? NULL : task->response;
290
291      tr_lockLock (session->web->taskLock);
292      task->next = session->web->tasks;
293      session->web->tasks = task;
294      tr_lockUnlock (session->web->taskLock);
295    }
296
297  return task;
298}
299
300struct tr_web_task *
301tr_webRunWithCookies (tr_session        * session,
302                      const char        * url,
303                      const char        * cookies,
304                      tr_web_done_func    done_func,
305                      void              * done_func_user_data)
306{
307  return tr_webRunImpl (session, -1, url,
308                        NULL, cookies,
309                        done_func, done_func_user_data,
310                        NULL);
311}
312
313struct tr_web_task *
314tr_webRun (tr_session         * session,
315           const char         * url,
316           tr_web_done_func     done_func,
317           void               * done_func_user_data)
318{
319  return tr_webRunWithCookies (session, url, NULL,
320                               done_func, done_func_user_data);
321}
322
323
324struct tr_web_task *
325tr_webRunWebseed (tr_torrent        * tor,
326                  const char        * url,
327                  const char        * range,
328                  tr_web_done_func    done_func,
329                  void              * done_func_user_data,
330                  struct evbuffer   * buffer)
331{
332  return tr_webRunImpl (tor->session, tr_torrentId (tor), url,
333                        range, NULL,
334                        done_func, done_func_user_data,
335                        buffer);
336}
337
338/**
339 * Portability wrapper for select ().
340 *
341 * http://msdn.microsoft.com/en-us/library/ms740141%28VS.85%29.aspx
342 * On win32, any two of the parameters, readfds, writefds, or exceptfds,
343 * can be given as null. At least one must be non-null, and any non-null
344 * descriptor set must contain at least one handle to a socket.
345 */
346static void
347tr_select (int nfds,
348           fd_set * r_fd_set, fd_set * w_fd_set, fd_set * c_fd_set,
349           struct timeval  * t)
350{
351#ifdef _WIN32
352  if (!r_fd_set->fd_count && !w_fd_set->fd_count && !c_fd_set->fd_count)
353    {
354      const long int msec = t->tv_sec*1000 + t->tv_usec/1000;
355      tr_wait_msec (msec);
356    }
357  else if (select (0, r_fd_set->fd_count ? r_fd_set : NULL,
358                      w_fd_set->fd_count ? w_fd_set : NULL,
359                      c_fd_set->fd_count ? c_fd_set : NULL, t) < 0)
360    {
361      char errstr[512];
362      const int e = EVUTIL_SOCKET_ERROR ();
363      tr_net_strerror (errstr, sizeof (errstr), e);
364      dbgmsg ("Error: select (%d) %s", e, errstr);
365    }
366#else
367  select (nfds, r_fd_set, w_fd_set, c_fd_set, t);
368#endif
369}
370
371static void
372tr_webThreadFunc (void * vsession)
373{
374  char * str;
375  CURLM * multi;
376  struct tr_web * web;
377  int taskCount = 0;
378  struct tr_web_task * task;
379  tr_session * session = vsession;
380
381  /* try to enable ssl for https support; but if that fails,
382   * try a plain vanilla init */
383  if (curl_global_init (CURL_GLOBAL_SSL))
384    curl_global_init (0);
385
386  web = tr_new0 (struct tr_web, 1);
387  web->close_mode = ~0;
388  web->taskLock = tr_lockNew ();
389  web->tasks = NULL;
390  web->curl_verbose = getenv ("TR_CURL_VERBOSE") != NULL;
391  web->curl_ssl_verify = getenv ("TR_CURL_SSL_VERIFY") != NULL;
392  web->curl_ca_bundle = getenv ("CURL_CA_BUNDLE");
393  if (web->curl_ssl_verify)
394    {
395      tr_logAddNamedInfo ("web", "will verify tracker certs using envvar CURL_CA_BUNDLE: %s",
396               web->curl_ca_bundle == NULL ? "none" : web->curl_ca_bundle);
397      tr_logAddNamedInfo ("web", "NB: this only works if you built against libcurl with openssl or gnutls, NOT nss");
398      tr_logAddNamedInfo ("web", "NB: invalid certs will show up as 'Could not connect to tracker' like many other errors");
399    }
400
401  str = tr_buildPath (session->configDir, "cookies.txt", NULL);
402  if (tr_sys_path_exists (str, NULL))
403    web->cookie_filename = tr_strdup (str);
404  tr_free (str);
405
406  multi = curl_multi_init ();
407  session->web = web;
408
409  for (;;)
410    {
411      long msec;
412      int unused;
413      CURLMsg * msg;
414      CURLMcode mcode;
415
416      if (web->close_mode == TR_WEB_CLOSE_NOW)
417        break;
418      if ((web->close_mode == TR_WEB_CLOSE_WHEN_IDLE) && (web->tasks == NULL))
419        break;
420
421      /* add tasks from the queue */
422      tr_lockLock (web->taskLock);
423      while (web->tasks != NULL)
424        {
425          /* pop the task */
426          task = web->tasks;
427          web->tasks = task->next;
428          task->next = NULL;
429
430          dbgmsg ("adding task to curl: [%s]", task->url);
431          curl_multi_add_handle (multi, createEasy (session, web, task));
432          /*fprintf (stderr, "adding a task.. taskCount is now %d\n", taskCount);*/
433          ++taskCount;
434        }
435      tr_lockUnlock (web->taskLock);
436
437      /* unpause any paused curl handles */
438      if (paused_easy_handles != NULL)
439        {
440          CURL * handle;
441          tr_list * tmp;
442
443          /* swap paused_easy_handles to prevent oscillation
444             between writeFunc this while loop */
445          tmp = paused_easy_handles;
446          paused_easy_handles = NULL;
447
448          while ((handle = tr_list_pop_front (&tmp)))
449            curl_easy_pause (handle, CURLPAUSE_CONT);
450        }
451
452      /* maybe wait a little while before calling curl_multi_perform () */
453      msec = 0;
454      curl_multi_timeout (multi, &msec);
455      if (msec < 0)
456        msec = THREADFUNC_MAX_SLEEP_MSEC;
457      if (session->isClosed)
458        msec = 100; /* on shutdown, call perform () more frequently */
459      if (msec > 0)
460        {
461          int usec;
462          int max_fd;
463          struct timeval t;
464          fd_set r_fd_set, w_fd_set, c_fd_set;
465
466          max_fd = 0;
467          FD_ZERO (&r_fd_set);
468          FD_ZERO (&w_fd_set);
469          FD_ZERO (&c_fd_set);
470          curl_multi_fdset (multi, &r_fd_set, &w_fd_set, &c_fd_set, &max_fd);
471
472          if (msec > THREADFUNC_MAX_SLEEP_MSEC)
473            msec = THREADFUNC_MAX_SLEEP_MSEC;
474
475          usec = msec * 1000;
476          t.tv_sec =  usec / 1000000;
477          t.tv_usec = usec % 1000000;
478          tr_select (max_fd+1, &r_fd_set, &w_fd_set, &c_fd_set, &t);
479        }
480
481      /* call curl_multi_perform () */
482      do
483        mcode = curl_multi_perform (multi, &unused);
484      while (mcode == CURLM_CALL_MULTI_PERFORM);
485
486      /* pump completed tasks from the multi */
487      while ((msg = curl_multi_info_read (multi, &unused)))
488        {
489          if ((msg->msg == CURLMSG_DONE) && (msg->easy_handle != NULL))
490            {
491              double total_time;
492              struct tr_web_task * task;
493              long req_bytes_sent;
494              CURL * e = msg->easy_handle;
495              curl_easy_getinfo (e, CURLINFO_PRIVATE, (void*)&task);
496              assert (e == task->curl_easy);
497              curl_easy_getinfo (e, CURLINFO_RESPONSE_CODE, &task->code);
498              curl_easy_getinfo (e, CURLINFO_REQUEST_SIZE, &req_bytes_sent);
499              curl_easy_getinfo (e, CURLINFO_TOTAL_TIME, &total_time);
500              task->did_connect = task->code>0 || req_bytes_sent>0;
501              task->did_timeout = !task->code && (total_time >= task->timeout_secs);
502              curl_multi_remove_handle (multi, e);
503              tr_list_remove_data (&paused_easy_handles, e);
504              curl_easy_cleanup (e);
505              tr_runInEventThread (task->session, task_finish_func, task);
506              --taskCount;
507            }
508        }
509    }
510
511  /* Discard any remaining tasks.
512   * This is rare, but can happen on shutdown with unresponsive trackers. */
513  while (web->tasks != NULL)
514    {
515      task = web->tasks;
516      web->tasks = task->next;
517      dbgmsg ("Discarding task \"%s\"", task->url);
518      task_free (task);
519    }
520
521  /* cleanup */
522  tr_list_free (&paused_easy_handles, NULL);
523  curl_multi_cleanup (multi);
524  tr_lockFree (web->taskLock);
525  tr_free (web->cookie_filename);
526  tr_free (web);
527  session->web = NULL;
528}
529
530
531void
532tr_webClose (tr_session * session, tr_web_close_mode close_mode)
533{
534  if (session->web != NULL)
535    {
536      session->web->close_mode = close_mode;
537
538      if (close_mode == TR_WEB_CLOSE_NOW)
539        while (session->web != NULL)
540          tr_wait_msec (100);
541    }
542}
543
544void
545tr_webGetTaskInfo (struct tr_web_task * task, tr_web_task_info info, void * dst)
546{
547  curl_easy_getinfo (task->curl_easy, (CURLINFO) info, dst);
548}
549
550/*****
551******
552******
553*****/
554
555const char *
556tr_webGetResponseStr (long code)
557{
558  switch (code)
559    {
560      case   0: return "No Response";
561      case 101: return "Switching Protocols";
562      case 200: return "OK";
563      case 201: return "Created";
564      case 202: return "Accepted";
565      case 203: return "Non-Authoritative Information";
566      case 204: return "No Content";
567      case 205: return "Reset Content";
568      case 206: return "Partial Content";
569      case 300: return "Multiple Choices";
570      case 301: return "Moved Permanently";
571      case 302: return "Found";
572      case 303: return "See Other";
573      case 304: return "Not Modified";
574      case 305: return "Use Proxy";
575      case 306: return " (Unused)";
576      case 307: return "Temporary Redirect";
577      case 400: return "Bad Request";
578      case 401: return "Unauthorized";
579      case 402: return "Payment Required";
580      case 403: return "Forbidden";
581      case 404: return "Not Found";
582      case 405: return "Method Not Allowed";
583      case 406: return "Not Acceptable";
584      case 407: return "Proxy Authentication Required";
585      case 408: return "Request Timeout";
586      case 409: return "Conflict";
587      case 410: return "Gone";
588      case 411: return "Length Required";
589      case 412: return "Precondition Failed";
590      case 413: return "Request Entity Too Large";
591      case 414: return "Request-URI Too Long";
592      case 415: return "Unsupported Media Type";
593      case 416: return "Requested Range Not Satisfiable";
594      case 417: return "Expectation Failed";
595      case 500: return "Internal Server Error";
596      case 501: return "Not Implemented";
597      case 502: return "Bad Gateway";
598      case 503: return "Service Unavailable";
599      case 504: return "Gateway Timeout";
600      case 505: return "HTTP Version Not Supported";
601      default:  return "Unknown Error";
602    }
603}
604
605void
606tr_http_escape (struct evbuffer  * out,
607                const char       * str,
608                int                len,
609                bool               escape_slashes)
610{
611  const char * end;
612
613  if ((len < 0) && (str != NULL))
614    len = strlen (str);
615
616  for (end=str+len; str && str!=end; ++str)
617    {
618      if ((*str == ',') || (*str == '-')
619                        || (*str == '.')
620                        || (('0' <= *str) && (*str <= '9'))
621                        || (('A' <= *str) && (*str <= 'Z'))
622                        || (('a' <= *str) && (*str <= 'z'))
623                        || ((*str == '/') && (!escape_slashes)))
624        evbuffer_add_printf (out, "%c", *str);
625      else
626        evbuffer_add_printf (out, "%%%02X", (unsigned)(*str&0xFF));
627    }
628}
629
630char *
631tr_http_unescape (const char * str, int len)
632{
633  char * tmp = curl_unescape (str, len);
634  char * ret = tr_strdup (tmp);
635  curl_free (tmp);
636  return ret;
637}
638
639static int
640is_rfc2396_alnum (uint8_t ch)
641{
642  return ('0' <= ch && ch <= '9')
643      || ('A' <= ch && ch <= 'Z')
644      || ('a' <= ch && ch <= 'z')
645      || ch == '.'
646      || ch == '-'
647      || ch == '_'
648      || ch == '~';
649}
650
651void
652tr_http_escape_sha1 (char * out, const uint8_t * sha1_digest)
653{
654  const uint8_t * in = sha1_digest;
655  const uint8_t * end = in + SHA_DIGEST_LENGTH;
656
657  while (in != end)
658    if (is_rfc2396_alnum (*in))
659      *out++ = (char) *in++;
660    else
661      out += tr_snprintf (out, 4, "%%%02x", (unsigned int)*in++);
662
663  *out = '\0';
664}
Note: See TracBrowser for help on using the repository browser.