source: trunk/libtransmission/web.c @ 9710

Last change on this file since 9710 was 9710, checked in by charles, 13 years ago

(trunk libT) use curl_multi_socket_action(), even on OS X.

  • Property svn:keywords set to Date Rev Author Id
File size: 19.7 KB
Line 
1/*
2 * This file Copyright (C) 2008-2009 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 9710 2009-12-10 19:05:21Z charles $
11 */
12
13#include <assert.h>
14#include <stdlib.h> /* bsearch */
15
16#include <event.h>
17
18#define CURL_DISABLE_TYPECHECK /* otherwise -Wunreachable-code goes insane */
19#include <curl/curl.h>
20
21#include "transmission.h"
22#include "list.h"
23#include "net.h" /* socklen_t */
24#include "session.h"
25#include "trevent.h" /* tr_runInEventThread() */
26#include "utils.h"
27#include "version.h"
28#include "web.h"
29
30enum
31{
32    /* arbitrary number */
33    DEFAULT_TIMER_MSEC = 1500
34};
35
36static void
37tr_multi_perform( tr_web * g, int fd );
38
39#if 0
40#define dbgmsg(...) \
41    do { \
42        fprintf( stderr, __VA_ARGS__ ); \
43        fprintf( stderr, "\n" ); \
44    } while( 0 )
45#else
46#define dbgmsg( ... ) \
47    do { \
48        if( tr_deepLoggingIsActive( ) ) \
49            tr_deepLog( __FILE__, __LINE__, "web", __VA_ARGS__ ); \
50    } while( 0 )
51#endif
52
53struct tr_web_sockinfo
54{
55    int fd;
56    tr_bool evset;
57    struct event ev;
58};
59
60struct tr_web
61{
62    tr_bool closing;
63    int prev_running;
64    int still_running;
65    long timer_msec;
66    CURLM * multi;
67    tr_session * session;
68    tr_bool haveAddr;
69    tr_address addr;
70    struct event timer_event;
71    tr_list * fds;
72};
73
74/***
75****
76***/
77
78static struct tr_web_sockinfo *
79getSockinfo( tr_web * web, int fd, tr_bool createIfMissing )
80{
81    tr_list * l;
82
83    for( l=web->fds; l!=NULL; l=l->next ) {
84        struct tr_web_sockinfo * s =  l->data;
85        if( s->fd == fd ) {
86            dbgmsg( "looked up sockinfo %p for fd %d", s, fd );
87            return s;
88        }
89    }
90
91    if( createIfMissing ) {
92        struct tr_web_sockinfo * s =  tr_new0( struct tr_web_sockinfo, 1 );
93        s->fd = fd;
94        tr_list_prepend( &web->fds, s );
95        dbgmsg( "created sockinfo %p for fd %d... we now have %d sockinfos", s, fd, tr_list_size(web->fds) );
96        return s;
97    }
98
99    return NULL;
100}
101
102static void
103clearSockinfoEvent( struct tr_web_sockinfo * s )
104{
105    if( s && s->evset )
106    {
107        dbgmsg( "clearing libevent polling for sockinfo %p, fd %d", s, s->fd );
108        event_del( &s->ev );
109        s->evset = FALSE;
110    }
111}
112
113static void
114purgeSockinfo( tr_web * web, int fd )
115{
116    struct tr_web_sockinfo * s = getSockinfo( web, fd, FALSE );
117
118    if( s != NULL )
119    {
120        tr_list_remove_data( &web->fds, s );
121        clearSockinfoEvent( s );
122        dbgmsg( "freeing sockinfo %p, fd %d", s, s->fd );
123        tr_free( s );
124    }
125}
126
127/***
128****
129***/
130
131struct tr_web_task
132{
133    unsigned long tag;
134    struct evbuffer * response;
135    char * url;
136    char * range;
137    tr_session * session;
138    tr_web_done_func * done_func;
139    void * done_func_user_data;
140};
141
142static size_t
143writeFunc( void * ptr, size_t size, size_t nmemb, void * vtask )
144{
145    const size_t byteCount = size * nmemb;
146    struct tr_web_task * task = vtask;
147    evbuffer_add( task->response, ptr, byteCount );
148    dbgmsg( "wrote %zu bytes to task %p's buffer", byteCount, task );
149    return byteCount;
150}
151
152static int
153getCurlProxyType( tr_proxy_type t )
154{
155    switch( t )
156    {
157        case TR_PROXY_SOCKS4: return CURLPROXY_SOCKS4;
158        case TR_PROXY_SOCKS5: return CURLPROXY_SOCKS5;
159        default:              return CURLPROXY_HTTP;
160    }
161}
162
163static void
164sockoptfunction( void * vtask, curl_socket_t fd, curlsocktype purpose UNUSED )
165{
166    struct tr_web_task * task = vtask;
167    const tr_bool isScrape = strstr( task->url, "scrape" ) != NULL;
168    const tr_bool isAnnounce = strstr( task->url, "announce" ) != NULL;
169
170    /* announce and scrape requests have tiny payloads...
171     * which have very small payloads */
172    if( isScrape || isAnnounce )
173    {
174        int sndbuf = 1024;
175        int rcvbuf = isScrape ? 2048 : 3072;
176
177        if( setsockopt( fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf) ) )
178            tr_inf( "Unable to set SO_SNDBUF on socket %d: %s", fd, tr_strerror( sockerrno ) );
179
180        if( setsockopt( fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf) ) )
181            tr_inf( "Unable to set SO_RCVBUF on socket %d: %s", fd, tr_strerror( sockerrno ) );
182    }
183}
184
185static void
186addTask( void * vtask )
187{
188    struct tr_web_task * task = vtask;
189    const tr_session * session = task->session;
190
191    if( session && session->web )
192    {
193        struct tr_web * web = session->web;
194        CURL * easy;
195        long timeout;
196
197        dbgmsg( "adding task #%lu [%s]", task->tag, task->url );
198
199        easy = curl_easy_init( );
200
201        if( !task->range && session->isProxyEnabled ) {
202            curl_easy_setopt( easy, CURLOPT_PROXY, session->proxy );
203            curl_easy_setopt( easy, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
204            curl_easy_setopt( easy, CURLOPT_PROXYPORT, session->proxyPort );
205            curl_easy_setopt( easy, CURLOPT_PROXYTYPE,
206                                      getCurlProxyType( session->proxyType ) );
207        }
208        if( !task->range && session->isProxyAuthEnabled ) {
209            char * str = tr_strdup_printf( "%s:%s", session->proxyUsername,
210                                                    session->proxyPassword );
211            curl_easy_setopt( easy, CURLOPT_PROXYUSERPWD, str );
212            tr_free( str );
213        }
214
215        curl_easy_setopt( easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
216
217        /* set a time limit for announces & scrapes */
218        if( strstr( task->url, "scrape" ) != NULL )
219            timeout = 20L;
220        else if( strstr( task->url, "announce" ) != NULL )
221            timeout = 30L;
222        else
223            timeout = 240L;
224        curl_easy_setopt( easy, CURLOPT_TIMEOUT, timeout );
225        curl_easy_setopt( easy, CURLOPT_CONNECTTIMEOUT, timeout-5 );
226        dbgmsg( "new task's timeout is %ld\n", timeout );
227
228        curl_easy_setopt( easy, CURLOPT_SOCKOPTFUNCTION, sockoptfunction );
229        curl_easy_setopt( easy, CURLOPT_SOCKOPTDATA, task );
230        curl_easy_setopt( easy, CURLOPT_DNS_CACHE_TIMEOUT, 1800L );
231        curl_easy_setopt( easy, CURLOPT_FOLLOWLOCATION, 1L );
232        curl_easy_setopt( easy, CURLOPT_AUTOREFERER, 1L );
233        curl_easy_setopt( easy, CURLOPT_FORBID_REUSE, 1L );
234        curl_easy_setopt( easy, CURLOPT_MAXREDIRS, -1L );
235        curl_easy_setopt( easy, CURLOPT_NOSIGNAL, 1L );
236        curl_easy_setopt( easy, CURLOPT_PRIVATE, task );
237        curl_easy_setopt( easy, CURLOPT_SSL_VERIFYHOST, 0L );
238        curl_easy_setopt( easy, CURLOPT_SSL_VERIFYPEER, 0L );
239        curl_easy_setopt( easy, CURLOPT_URL, task->url );
240        curl_easy_setopt( easy, CURLOPT_USERAGENT,
241                                           TR_NAME "/" LONG_VERSION_STRING );
242        curl_easy_setopt( easy, CURLOPT_VERBOSE,
243                                       getenv( "TR_CURL_VERBOSE" ) != NULL );
244        if( web->haveAddr )
245            curl_easy_setopt( easy, CURLOPT_INTERFACE, tr_ntop_non_ts( &web->addr ) );
246        curl_easy_setopt( easy, CURLOPT_WRITEDATA, task );
247        curl_easy_setopt( easy, CURLOPT_WRITEFUNCTION, writeFunc );
248        if( task->range )
249            curl_easy_setopt( easy, CURLOPT_RANGE, task->range );
250        else /* don't set encoding on webseeds; it messes up binary data */
251            curl_easy_setopt( easy, CURLOPT_ENCODING, "" );
252
253        {
254            const CURLMcode mcode = curl_multi_add_handle( web->multi, easy );
255            tr_assert( mcode == CURLM_OK, "curl_multi_add_handle() failed: %d (%s)", mcode, curl_multi_strerror( mcode ) );
256            if( mcode == CURLM_OK )
257                ++web->still_running;
258            else
259                tr_err( "%s", curl_multi_strerror( mcode ) );
260
261            tr_multi_perform( web, CURL_SOCKET_TIMEOUT );
262        }
263    }
264}
265
266/***
267****
268***/
269
270static void
271task_free( struct tr_web_task * task )
272{
273    evbuffer_free( task->response );
274    tr_free( task->range );
275    tr_free( task->url );
276    tr_free( task );
277}
278
279static void
280task_finish( struct tr_web_task * task, long response_code )
281{
282    dbgmsg( "finished a web task... response code is %ld", response_code );
283    dbgmsg( "===================================================" );
284
285    if( task->done_func != NULL )
286        task->done_func( task->session,
287                         response_code,
288                         EVBUFFER_DATA( task->response ),
289                         EVBUFFER_LENGTH( task->response ),
290                         task->done_func_user_data );
291    task_free( task );
292}
293
294static void
295remove_finished_tasks( tr_web * g )
296{
297    CURL * easy;
298
299    do
300    {
301        CURLMsg * msg;
302        int msgs_left;
303
304        easy = NULL;
305        while(( msg = curl_multi_info_read( g->multi, &msgs_left ))) {
306            if( msg->msg == CURLMSG_DONE ) {
307                easy = msg->easy_handle;
308                break;
309            }
310        }
311
312        if( easy ) {
313            long code;
314            long fd;
315            struct tr_web_task * task;
316            CURLcode ecode;
317            CURLMcode mcode;
318
319            ecode = curl_easy_getinfo( easy, CURLINFO_PRIVATE, (void*)&task );
320            tr_assert( ecode == CURLE_OK, "curl_easy_getinfo() failed: %d (%s)", ecode, curl_easy_strerror( ecode ) );
321
322            ecode = curl_easy_getinfo( easy, CURLINFO_RESPONSE_CODE, &code );
323            tr_assert( ecode == CURLE_OK, "curl_easy_getinfo() failed: %d (%s)", ecode, curl_easy_strerror( ecode ) );
324
325            ecode = curl_easy_getinfo( easy, CURLINFO_LASTSOCKET, &fd );
326            tr_assert( ecode == CURLE_OK, "curl_easy_getinfo() failed: %d (%s)", ecode, curl_easy_strerror( ecode ) );
327            if( fd != -1L )
328                purgeSockinfo( g, fd );
329
330            mcode = curl_multi_remove_handle( g->multi, easy );
331            tr_assert( mcode == CURLM_OK, "curl_multi_remove_handle() failed: %d (%s)", mcode, curl_multi_strerror( mcode ) );
332
333            curl_easy_cleanup( easy );
334            task_finish( task, code );
335        }
336    }
337    while ( easy );
338
339    g->prev_running = g->still_running;
340}
341
342static void
343stop_timer( tr_web* g )
344{
345    if( evtimer_pending( &g->timer_event, NULL ) )
346    {
347        dbgmsg( "deleting the pending global timer" );
348        evtimer_del( &g->timer_event );
349    }
350}
351
352static void
353restart_timer( tr_web * g )
354{
355    assert( tr_amInEventThread( g->session ) );
356    assert( g->session != NULL );
357    assert( g->session->events != NULL );
358
359    stop_timer( g );
360    dbgmsg( "adding a timeout for %.1f seconds from now", g->timer_msec/1000.0 );
361    tr_timerAddMsec( &g->timer_event, g->timer_msec );
362}
363
364static void
365web_close( tr_web * g )
366{
367    CURLMcode mcode;
368
369    stop_timer( g );
370
371    mcode = curl_multi_cleanup( g->multi );
372    tr_assert( mcode == CURLM_OK, "curl_multi_cleanup() failed: %d (%s)", mcode, curl_multi_strerror( mcode ) );
373    if( mcode != CURLM_OK )
374        tr_err( "%s", curl_multi_strerror( mcode ) );
375
376    tr_free( g );
377}
378
379/* note: this function can free the tr_web if its 'closing' flag is set
380   and no tasks remain.  callers must not reference their g pointer
381   after calling this function */
382static void
383tr_multi_perform( tr_web * g, int fd )
384{
385    int closed = FALSE;
386    CURLMcode mcode;
387
388    dbgmsg( "check_run_count: prev_running %d, still_running %d",
389            g->prev_running, g->still_running );
390
391    /* invoke libcurl's processing */
392    do {
393        dbgmsg( "calling curl_multi_socket_action..." );
394        mcode = curl_multi_socket_action( g->multi, fd, 0, &g->still_running );
395        fd = CURL_SOCKET_TIMEOUT;
396        dbgmsg( "done calling curl_multi_socket_action..." );
397    } while( mcode == CURLM_CALL_MULTI_SOCKET );
398    tr_assert( mcode == CURLM_OK, "curl_multi_perform() failed: %d (%s)", mcode, curl_multi_strerror( mcode ) );
399    if( mcode != CURLM_OK )
400        tr_err( "%s", curl_multi_strerror( mcode ) );
401
402    remove_finished_tasks( g );
403
404    if( !g->still_running ) {
405        assert( tr_list_size( g->fds ) == 0 );
406        stop_timer( g );
407        if( g->closing ) {
408            web_close( g );
409            closed = TRUE;
410        }
411    }
412
413    if( !closed )
414        restart_timer( g );
415}
416
417/* libevent says that sock is ready to be processed, so wake up libcurl */
418static void
419event_cb( int fd, short kind UNUSED, void * g )
420{
421    tr_multi_perform( g, fd );
422}
423
424/* CURLMOPT_SOCKETFUNCTION */
425static int
426sock_cb( CURL            * e UNUSED,
427         curl_socket_t     fd,
428         int               action,
429         void            * vweb,
430         void            * unused UNUSED)
431{
432    struct tr_web * web = vweb;
433    dbgmsg( "sock_cb: action is %d, fd is %d", action, (int)fd );
434
435    if( action == CURL_POLL_REMOVE )
436    {
437        purgeSockinfo( web, fd );
438    }
439    else
440    {
441        struct tr_web_sockinfo * sockinfo = getSockinfo( web, fd, TRUE );
442        const int kind = EV_PERSIST
443                       | (( action & CURL_POLL_IN ) ? EV_READ : 0 )
444                       | (( action & CURL_POLL_OUT ) ? EV_WRITE : 0 );
445        dbgmsg( "setsock: fd is %d, curl action is %d, libevent action is %d", fd, action, kind );
446        assert( tr_amInEventThread( web->session ) );
447        assert( kind != EV_PERSIST );
448
449        /* clear any old polling on this fd */
450        clearSockinfoEvent( sockinfo );
451
452        /* set the new polling on this fd */
453        dbgmsg( "enabling (libevent %d, libcurl %d) polling on sockinfo %p, fd %d", action, kind, sockinfo, fd );
454        event_set( &sockinfo->ev, fd, kind, event_cb, web );
455        event_add( &sockinfo->ev, NULL );
456        sockinfo->evset = TRUE;
457    }
458
459    return 0;
460}
461
462/* libevent says that timer_msec have passed, so wake up libcurl */
463static void
464libevent_timer_cb( int fd UNUSED, short what UNUSED, void * g )
465{
466    dbgmsg( "libevent timer is done" );
467    tr_multi_perform( g, CURL_SOCKET_TIMEOUT );
468}
469
470/* libcurl documentation: "If 0, it means you should proceed immediately
471 * without waiting for anything. If it returns -1, there's no timeout at all
472 * set ... (but) you must not wait too long (more than a few seconds perhaps)
473 * before you call curl_multi_perform() again."  */
474static void
475multi_timer_cb( CURLM * multi UNUSED, long timer_msec, void * vg )
476{
477    tr_web * g = vg;
478
479    if( timer_msec < 1 ) {
480        if( timer_msec == 0 ) /* call it immediately */
481            libevent_timer_cb( 0, 0, g );
482        timer_msec = DEFAULT_TIMER_MSEC;
483    }
484
485    g->timer_msec = timer_msec;
486    restart_timer( g );
487}
488
489/****
490*****
491****/
492
493void
494tr_webRun( tr_session         * session,
495           const char         * url,
496           const char         * range,
497           tr_web_done_func     done_func,
498           void               * done_func_user_data )
499{
500    if( session->web )
501    {
502        struct tr_web_task * task;
503        static unsigned long tag = 0;
504
505        task = tr_new0( struct tr_web_task, 1 );
506        task->session = session;
507        task->url = tr_strdup( url );
508        task->range = tr_strdup( range );
509        task->done_func = done_func;
510        task->done_func_user_data = done_func_user_data;
511        task->tag = ++tag;
512        task->response = evbuffer_new( );
513
514        tr_runInEventThread( session, addTask, task );
515    }
516}
517
518void
519tr_webSetInterface( tr_web * web, const tr_address * addr )
520{
521    if(( web->haveAddr = ( addr != NULL )))
522        web->addr = *addr;
523}
524
525tr_web*
526tr_webInit( tr_session * session )
527{
528    tr_web * web;
529    static int curlInited = FALSE;
530
531    /* call curl_global_init if we haven't done it already.
532     * try to enable ssl for https support; but if that fails,
533     * try a plain vanilla init */
534    if( curlInited == FALSE ) {
535        curlInited = TRUE;
536        if( curl_global_init( CURL_GLOBAL_SSL ) )
537            curl_global_init( 0 );
538    }
539
540    web = tr_new0( struct tr_web, 1 );
541    web->multi = curl_multi_init( );
542    web->session = session;
543    web->timer_msec = DEFAULT_TIMER_MSEC; /* overwritten by multi_timer_cb() */
544
545    evtimer_set( &web->timer_event, libevent_timer_cb, web );
546    curl_multi_setopt( web->multi, CURLMOPT_SOCKETDATA, web );
547    curl_multi_setopt( web->multi, CURLMOPT_SOCKETFUNCTION, sock_cb );
548    curl_multi_setopt( web->multi, CURLMOPT_TIMERDATA, web );
549    curl_multi_setopt( web->multi, CURLMOPT_TIMERFUNCTION, multi_timer_cb );
550
551    return web;
552}
553
554void
555tr_webClose( tr_web ** web_in )
556{
557    tr_web * web = *web_in;
558    *web_in = NULL;
559    if( web->still_running < 1 )
560        web_close( web );
561    else
562        web->closing = 1;
563}
564
565/*****
566******
567******
568*****/
569
570static struct http_msg {
571    long code;
572    const char * text;
573} http_msg[] = {
574    {   0, "No Response" },
575    { 101, "Switching Protocols" },
576    { 200, "OK" },
577    { 201, "Created" },
578    { 202, "Accepted" },
579    { 203, "Non-Authoritative Information" },
580    { 204, "No Content" },
581    { 205, "Reset Content" },
582    { 206, "Partial Content" },
583    { 300, "Multiple Choices" },
584    { 301, "Moved Permanently" },
585    { 302, "Found" },
586    { 303, "See Other" },
587    { 304, "Not Modified" },
588    { 305, "Use Proxy" },
589    { 306, "(Unused)" },
590    { 307, "Temporary Redirect" },
591    { 400, "Bad Request" },
592    { 401, "Unauthorized" },
593    { 402, "Payment Required" },
594    { 403, "Forbidden" },
595    { 404, "Not Found" },
596    { 405, "Method Not Allowed" },
597    { 406, "Not Acceptable" },
598    { 407, "Proxy Authentication Required" },
599    { 408, "Request Timeout" },
600    { 409, "Conflict" },
601    { 410, "Gone" },
602    { 411, "Length Required" },
603    { 412, "Precondition Failed" },
604    { 413, "Request Entity Too Large" },
605    { 414, "Request-URI Too Long" },
606    { 415, "Unsupported Media Type" },
607    { 416, "Requested Range Not Satisfiable" },
608    { 417, "Expectation Failed" },
609    { 500, "Internal Server Error" },
610    { 501, "Not Implemented" },
611    { 502, "Bad Gateway" },
612    { 503, "Service Unavailable" },
613    { 504, "Gateway Timeout" },
614    { 505, "HTTP Version Not Supported" }
615};
616
617static int
618compareResponseCodes( const void * va, const void * vb )
619{
620    const long a = *(const long*) va;
621    const struct http_msg * b = vb;
622    return a - b->code;
623}
624
625const char *
626tr_webGetResponseStr( long code )
627{
628    struct http_msg * msg = bsearch( &code,
629                                     http_msg,
630                                     sizeof( http_msg ) / sizeof( http_msg[0] ),
631                                     sizeof( http_msg[0] ),
632                                     compareResponseCodes );
633    return msg ? msg->text : "Unknown Error";
634}
635
636/* escapes a string to be URI-legal as per RFC 2396.
637   like curl_escape() but can optionally avoid escaping slashes. */
638void
639tr_http_escape( struct evbuffer  * out,
640                const char       * str,
641                int                len,
642                tr_bool            escape_slashes )
643{
644    int i;
645
646    if( ( len < 0 ) && ( str != NULL ) )
647        len = strlen( str );
648
649    for( i = 0; i < len; i++ ) {
650        switch( str[i] ) {
651        case ',': case '-': case '.':
652        case '0': case '1': case '2': case '3': case '4':
653        case '5': case '6': case '7': case '8': case '9':
654        case 'a': case 'b': case 'c': case 'd': case 'e':
655        case 'f': case 'g': case 'h': case 'i': case 'j':
656        case 'k': case 'l': case 'm': case 'n': case 'o':
657        case 'p': case 'q': case 'r': case 's': case 't':
658        case 'u': case 'v': case 'w': case 'x': case 'y': case 'z':
659        case 'A': case 'B': case 'C': case 'D': case 'E':
660        case 'F': case 'G': case 'H': case 'I': case 'J':
661        case 'K': case 'L': case 'M': case 'N': case 'O':
662        case 'P': case 'Q': case 'R': case 'S': case 'T':
663        case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z':
664            evbuffer_add( out, &str[i], 1 );
665            break;
666        case '/':
667            if(!escape_slashes) {
668                evbuffer_add( out, &str[i], 1 );
669                break;
670            }
671            /* Fall through. */
672        default:
673            evbuffer_add_printf( out, "%%%02X", (unsigned)(str[i]&0xFF) );
674            break;
675        }
676    }
677}
678
679char*
680tr_http_unescape( const char * str, int len )
681{
682    char * tmp = curl_unescape( str, len );
683    char * ret = tr_strdup( tmp );
684    curl_free( tmp );
685    return ret;
686}
Note: See TracBrowser for help on using the repository browser.