source: trunk/libtransmission/web.c @ 14070

Last change on this file since 14070 was 14070, checked in by jordan, 9 years ago

(trunk) update web.h's API s.t. there's an explicit function to use when downloading webseed content.

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