source: trunk/libtransmission/rpc-server.c @ 7744

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

(trunk libT) In RPC, add general support for nonblocking methods, and specific support for adding a torrent via its URL and fetching it via curl without blocking.

  • Property svn:keywords set to Date Rev Author Id
File size: 21.8 KB
Line 
1/*
2 * This file Copyright (C) 2008-2009 Charles Kerr <charles@transmissionbt.com>
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: rpc-server.c 7744 2009-01-18 15:24:26Z charles $
11 */
12
13#include <assert.h>
14#include <errno.h>
15#include <string.h> /* memcpy */
16#include <limits.h> /* INT_MAX */
17
18#include <sys/types.h> /* open */
19#include <sys/stat.h>  /* open */
20#include <fcntl.h>     /* open */
21#include <unistd.h>    /* close */
22
23#ifdef HAVE_ZLIB
24 #include <zlib.h>
25#endif
26
27#include <libevent/event.h>
28#include <libevent/evhttp.h>
29
30#include "transmission.h"
31#include "bencode.h"
32#include "list.h"
33#include "platform.h"
34#include "rpcimpl.h"
35#include "rpc-server.h"
36#include "trevent.h"
37#include "utils.h"
38#include "web.h"
39
40#define MY_NAME "RPC Server"
41#define MY_REALM "Transmission"
42#define TR_N_ELEMENTS( ary ) ( sizeof( ary ) / sizeof( *ary ) )
43
44#ifdef WIN32
45#define strncasecmp _strnicmp
46#endif
47
48struct tr_rpc_server
49{
50    tr_bool            isEnabled;
51    tr_bool            isPasswordEnabled;
52    tr_bool            isWhitelistEnabled;
53    tr_port            port;
54    struct evhttp *    httpd;
55    tr_session *       session;
56    char *             username;
57    char *             password;
58    char *             whitelistStr;
59    tr_list *          whitelist;
60
61#ifdef HAVE_ZLIB
62    z_stream           stream;
63#endif
64};
65
66#define dbgmsg( ... ) \
67    do { \
68        if( tr_deepLoggingIsActive( ) ) \
69            tr_deepLog( __FILE__, __LINE__, MY_NAME, __VA_ARGS__ ); \
70    } while( 0 )
71
72
73/**
74***
75**/
76
77static void
78send_simple_response( struct evhttp_request * req,
79                      int                     code,
80                      const char *            text )
81{
82    const char *      code_text = tr_webGetResponseStr( code );
83    struct evbuffer * body = tr_getBuffer( );
84
85    evbuffer_add_printf( body, "<h1>%d: %s</h1>", code, code_text );
86    if( text )
87        evbuffer_add_printf( body, "%s", text );
88    evhttp_send_reply( req, code, code_text, body );
89
90    tr_releaseBuffer( body );
91}
92
93static const char*
94tr_memmem( const char * s1,
95           size_t       l1,
96           const char * s2,
97           size_t       l2 )
98{
99    if( !l2 ) return s1;
100    while( l1 >= l2 )
101    {
102        l1--;
103        if( !memcmp( s1, s2, l2 ) )
104            return s1;
105        s1++;
106    }
107
108    return NULL;
109}
110
111static void
112handle_upload( struct evhttp_request * req,
113               struct tr_rpc_server *  server )
114{
115    if( req->type != EVHTTP_REQ_POST )
116    {
117        send_simple_response( req, 405, NULL );
118    }
119    else
120    {
121        const char * content_type = evhttp_find_header( req->input_headers,
122                                                        "Content-Type" );
123
124        const char * query = strchr( req->uri, '?' );
125        const int    paused = query && strstr( query + 1, "paused=true" );
126
127        const char * in = (const char *) EVBUFFER_DATA( req->input_buffer );
128        size_t       inlen = EVBUFFER_LENGTH( req->input_buffer );
129
130        const char * boundary_key = "boundary=";
131        const char * boundary_key_begin = strstr( content_type,
132                                                  boundary_key );
133        const char * boundary_val =
134            boundary_key_begin ? boundary_key_begin +
135            strlen( boundary_key ) : "arglebargle";
136
137        char *       boundary = tr_strdup_printf( "--%s", boundary_val );
138        const size_t boundary_len = strlen( boundary );
139
140        const char * delim = tr_memmem( in, inlen, boundary, boundary_len );
141        while( delim )
142        {
143            size_t       part_len;
144            const char * part = delim + boundary_len;
145            inlen -= ( part - in );
146            in = part;
147            delim = tr_memmem( in, inlen, boundary, boundary_len );
148            part_len = delim ? (size_t)( delim - part ) : inlen;
149
150            if( part_len )
151            {
152                char * text = tr_strndup( part, part_len );
153                if( strstr( text, "filename=\"" ) )
154                {
155                    const char * body = strstr( text, "\r\n\r\n" );
156                    if( body )
157                    {
158                        char * b64;
159                        size_t  body_len;
160                        tr_benc top, *args;
161                        struct evbuffer * json = tr_getBuffer( );
162
163                        body += 4; /* walk past the \r\n\r\n */
164                        body_len = part_len - ( body - text );
165                        if( body_len >= 2
166                          && !memcmp( &body[body_len - 2], "\r\n", 2 ) )
167                            body_len -= 2;
168
169                        tr_bencInitDict( &top, 2 );
170                        args = tr_bencDictAddDict( &top, "arguments", 2 );
171                        tr_bencDictAddStr( &top, "method", "torrent-add" );
172                        b64 = tr_base64_encode( body, body_len, NULL );
173                        tr_bencDictAddStr( args, "metainfo", b64 );
174                        tr_bencDictAddInt( args, "paused", paused );
175                        tr_bencSaveAsJSON( &top, json );
176                        tr_rpc_request_exec_json( server->session,
177                                                  EVBUFFER_DATA( json ),
178                                                  EVBUFFER_LENGTH( json ),
179                                                  NULL, NULL );
180
181                        tr_releaseBuffer( json );
182                        tr_free( b64 );
183                        tr_bencFree( &top );
184                    }
185                }
186                tr_free( text );
187            }
188        }
189
190        tr_free( boundary );
191
192        /* use xml here because json responses to file uploads is trouble.
193         * see http://www.malsup.com/jquery/form/#sample7 for details */
194        evhttp_add_header( req->output_headers, "Content-Type",
195                           "text/xml; charset=UTF-8" );
196        send_simple_response( req, HTTP_OK, NULL );
197    }
198}
199
200static const char*
201mimetype_guess( const char * path )
202{
203    unsigned int i;
204
205    const struct
206    {
207        const char *    suffix;
208        const char *    mime_type;
209    } types[] = {
210        /* these are just the ones we need for serving clutch... */
211        { "css",  "text/css"                  },
212        { "gif",  "image/gif"                 },
213        { "html", "text/html"                 },
214        { "ico",  "image/vnd.microsoft.icon"  },
215        { "js",   "application/javascript"    },
216        { "png",  "image/png"                 }
217    };
218    const char * dot = strrchr( path, '.' );
219
220    for( i = 0; dot && i < TR_N_ELEMENTS( types ); ++i )
221        if( !strcmp( dot + 1, types[i].suffix ) )
222            return types[i].mime_type;
223
224    return "application/octet-stream";
225}
226
227static void
228add_response( struct evhttp_request * req,
229              struct tr_rpc_server *  server,
230              struct evbuffer *       out,
231              const void *            content,
232              size_t                  content_len )
233{
234#ifndef HAVE_ZLIB
235    evbuffer_add( out, content, content_len );
236#else
237    const char * key = "Accept-Encoding";
238    const char * encoding = evhttp_find_header( req->input_headers, key );
239    const int do_deflate = encoding && strstr( encoding, "deflate" );
240
241    if( !do_deflate )
242    {
243        evbuffer_add( out, content, content_len );
244    }
245    else
246    {
247        int state;
248
249        server->stream.next_in = (Bytef*) content;
250        server->stream.avail_in = content_len;
251
252        /* allocate space for the raw data and call deflate() just once --
253         * we won't use the deflated data if it's longer than the raw data,
254         * so it's okay to let deflate() run out of output buffer space */
255        evbuffer_expand( out, content_len );
256        server->stream.next_out = EVBUFFER_DATA( out );
257        server->stream.avail_out = content_len;
258
259        state = deflate( &server->stream, Z_FINISH );
260
261        if( state == Z_STREAM_END )
262        {
263            EVBUFFER_LENGTH( out ) = content_len - server->stream.avail_out;
264
265            /* http://carsten.codimi.de/gzip.yaws/
266               It turns out that some browsers expect deflated data without
267               the first two bytes (a kind of header) and and the last four
268               bytes (an ADLER32 checksum). This format can of course
269               be produced by simply stripping these off. */
270            if( EVBUFFER_LENGTH( out ) >= 6 ) {
271                EVBUFFER_LENGTH( out ) -= 4;
272                evbuffer_drain( out, 2 );
273            }
274
275#if 0
276            tr_ninf( MY_NAME, _( "Deflated response from %zu bytes to %zu" ),
277                              content_len,
278                              EVBUFFER_LENGTH( out ) );
279#endif
280            evhttp_add_header( req->output_headers,
281                               "Content-Encoding", "deflate" );
282        }
283        else
284        {
285            evbuffer_drain( out, EVBUFFER_LENGTH( out ) );
286            evbuffer_add( out, content, content_len );
287        }
288
289        deflateReset( &server->stream );
290    }
291#endif
292}
293
294static void
295serve_file( struct evhttp_request * req,
296            struct tr_rpc_server *  server,
297            const char *            filename )
298{
299    if( req->type != EVHTTP_REQ_GET )
300    {
301        evhttp_add_header( req->output_headers, "Allow", "GET" );
302        send_simple_response( req, 405, NULL );
303    }
304    else
305    {
306        size_t content_len;
307        uint8_t * content;
308        const int error = errno;
309
310        errno = 0;
311        content_len = 0;
312        content = tr_loadFile( filename, &content_len );
313
314        if( errno )
315        {
316            send_simple_response( req, HTTP_NOTFOUND, NULL );
317        }
318        else
319        {
320            struct evbuffer * out;
321
322            errno = error;
323            out = tr_getBuffer( );
324            evhttp_add_header( req->output_headers, "Content-Type",
325                               mimetype_guess( filename ) );
326            add_response( req, server, out, content, content_len );
327            evhttp_send_reply( req, HTTP_OK, "OK", out );
328
329            tr_releaseBuffer( out );
330            tr_free( content );
331        }
332    }
333}
334
335static void
336handle_clutch( struct evhttp_request * req,
337               struct tr_rpc_server *  server )
338{
339    const char * clutchDir = tr_getClutchDir( server->session );
340
341    assert( !strncmp( req->uri, "/transmission/web/", 18 ) );
342
343    if( !clutchDir || !*clutchDir )
344    {
345        send_simple_response( req, HTTP_NOTFOUND,
346            "<p>Couldn't find Transmission's web interface files!</p>"
347            "<p>Users: to tell Transmission where to look, "
348            "set the TRANSMISSION_WEB_HOME environmental "
349            "variable to the folder where the web interface's "
350            "index.html is located.</p>"
351            "<p>Package Builders: to set a custom default at compile time, "
352            "#define PACKAGE_DATA_DIR in libtransmission/platform.c "
353            "or tweak tr_getClutchDir() by hand.</p>" );
354    }
355    else
356    {
357        char * pch;
358        char * subpath;
359        char * filename;
360
361        subpath = tr_strdup( req->uri + 18 );
362        if(( pch = strchr( subpath, '?' )))
363            *pch = '\0';
364
365        filename = tr_strdup_printf( "%s%s%s",
366                       clutchDir,
367                       TR_PATH_DELIMITER_STR,
368                       subpath && *subpath ? subpath : "index.html" );
369
370        serve_file( req, server, filename );
371
372        tr_free( filename );
373        tr_free( subpath );
374    }
375}
376
377struct rpc_response_data
378{
379    struct evhttp_request * req;
380    struct tr_rpc_server  * server;
381};
382
383static void
384rpc_response_func( tr_session      * session UNUSED,
385                   const char      * response,
386                   size_t            response_len,
387                   void            * user_data )
388{
389    struct rpc_response_data * data = user_data;
390    struct evbuffer * buf = tr_getBuffer( );
391
392    add_response( data->req, data->server, buf, response, response_len );
393    evhttp_add_header( data->req->output_headers,
394                           "Content-Type", "application/json; charset=UTF-8" );
395    evhttp_send_reply( data->req, HTTP_OK, "OK", buf );
396
397    tr_releaseBuffer( buf );
398    tr_free( data );
399}
400
401
402static void
403handle_rpc( struct evhttp_request * req,
404            struct tr_rpc_server  * server )
405{
406    struct rpc_response_data * data = tr_new0( struct rpc_response_data, 1 );
407
408    data->req = req;
409    data->server = server;
410   
411    if( req->type == EVHTTP_REQ_GET )
412    {
413        const char * q;
414        if( ( q = strchr( req->uri, '?' ) ) )
415            tr_rpc_request_exec_uri( server->session, q+1, -1, rpc_response_func, data );
416    }
417    else if( req->type == EVHTTP_REQ_POST )
418    {
419        tr_rpc_request_exec_json( server->session,
420                                  EVBUFFER_DATA( req->input_buffer ),
421                                  EVBUFFER_LENGTH( req->input_buffer ),
422                                  rpc_response_func, data );
423    }
424
425}
426
427static tr_bool
428isAddressAllowed( const tr_rpc_server * server,
429                  const char *          address )
430{
431    tr_list * l;
432
433    if( !server->isWhitelistEnabled )
434        return TRUE;
435
436    for( l=server->whitelist; l!=NULL; l=l->next )
437        if( tr_wildmat( address, l->data ) )
438            return TRUE;
439
440    return FALSE;
441}
442
443static void
444handle_request( struct evhttp_request * req,
445                void *                  arg )
446{
447    struct tr_rpc_server * server = arg;
448
449    if( req && req->evcon )
450    {
451        const char * auth;
452        char *       user = NULL;
453        char *       pass = NULL;
454
455        evhttp_add_header( req->output_headers, "Server", MY_REALM );
456
457        auth = evhttp_find_header( req->input_headers, "Authorization" );
458
459        if( auth && !strncasecmp( auth, "basic ", 6 ) )
460        {
461            int    plen;
462            char * p = tr_base64_decode( auth + 6, 0, &plen );
463            if( p && plen && ( ( pass = strchr( p, ':' ) ) ) )
464            {
465                user = p;
466                *pass++ = '\0';
467            }
468        }
469
470        if( !isAddressAllowed( server, req->remote_host ) )
471        {
472            send_simple_response( req, 401,
473                "<p>Unauthorized IP Address.</p>"
474                "<p>Either disable the IP address whitelist or add your address to it.</p>"
475                "<p>If you're editing settings.json, see the 'rpc-whitelist' and 'rpc-whitelist-enabled' entries.</p>"
476                "<p>If you're still using ACLs, use a whitelist instead.  See the transmission-daemon manpage for details.</p>" );
477        }
478        else if( server->isPasswordEnabled
479                 && ( !pass || !user || strcmp( server->username, user )
480                                     || strcmp( server->password, pass ) ) )
481        {
482            evhttp_add_header( req->output_headers,
483                               "WWW-Authenticate",
484                               "Basic realm=\"" MY_REALM "\"" );
485            send_simple_response( req, 401, "Unauthorized User" );
486        }
487        else if( !strcmp( req->uri, "/transmission/web" )
488               || !strcmp( req->uri, "/transmission/clutch" )
489               || !strcmp( req->uri, "/" ) )
490        {
491            evhttp_add_header( req->output_headers, "Location",
492                               "/transmission/web/" );
493            send_simple_response( req, HTTP_MOVEPERM, NULL );
494        }
495        else if( !strncmp( req->uri, "/transmission/web/", 18 ) )
496        {
497            handle_clutch( req, server );
498        }
499        else if( !strncmp( req->uri, "/transmission/rpc", 17 ) )
500        {
501            handle_rpc( req, server );
502        }
503        else if( !strncmp( req->uri, "/transmission/upload", 20 ) )
504        {
505            handle_upload( req, server );
506        }
507        else
508        {
509            send_simple_response( req, HTTP_NOTFOUND, NULL );
510        }
511
512        tr_free( user );
513    }
514}
515
516static void
517startServer( void * vserver )
518{
519    tr_rpc_server * server  = vserver;
520
521    if( !server->httpd )
522    {
523        server->httpd = evhttp_new( tr_eventGetBase( server->session ) );
524        evhttp_bind_socket( server->httpd, "0.0.0.0", server->port );
525        evhttp_set_gencb( server->httpd, handle_request, server );
526    }
527}
528
529static void
530stopServer( tr_rpc_server * server )
531{
532    if( server->httpd )
533    {
534        evhttp_free( server->httpd );
535        server->httpd = NULL;
536    }
537}
538
539static void
540onEnabledChanged( void * vserver )
541{
542    tr_rpc_server * server = vserver;
543
544    if( !server->isEnabled )
545        stopServer( server );
546    else
547        startServer( server );
548}
549
550void
551tr_rpcSetEnabled( tr_rpc_server * server,
552                  tr_bool         isEnabled )
553{
554    server->isEnabled = isEnabled;
555
556    tr_runInEventThread( server->session, onEnabledChanged, server );
557}
558
559tr_bool
560tr_rpcIsEnabled( const tr_rpc_server * server )
561{
562    return server->isEnabled;
563}
564
565static void
566restartServer( void * vserver )
567{
568    tr_rpc_server * server = vserver;
569
570    if( server->isEnabled )
571    {
572        stopServer( server );
573        startServer( server );
574    }
575}
576
577void
578tr_rpcSetPort( tr_rpc_server * server,
579               tr_port         port )
580{
581    if( server->port != port )
582    {
583        server->port = port;
584
585        if( server->isEnabled )
586            tr_runInEventThread( server->session, restartServer, server );
587    }
588}
589
590tr_port
591tr_rpcGetPort( const tr_rpc_server * server )
592{
593    return server->port;
594}
595
596void
597tr_rpcSetWhitelist( tr_rpc_server * server,
598                    const char    * whitelistStr )
599{
600    void * tmp;
601    const char * walk;
602
603    /* keep the string */
604    tr_free( server->whitelistStr );
605    server->whitelistStr = tr_strdup( whitelistStr );
606
607    /* clear out the old whitelist entries */
608    while(( tmp = tr_list_pop_front( &server->whitelist )))
609        tr_free( tmp );
610
611    /* build the new whitelist entries */
612    for( walk=whitelistStr; walk && *walk; ) {
613        const char * delimiters = " ,;";
614        const size_t len = strcspn( walk, delimiters );
615        char * token = tr_strndup( walk, len );
616        tr_list_append( &server->whitelist, token );
617        if( strcspn( token, "+-" ) < len )
618            tr_ninf( MY_NAME, "Adding address to whitelist: %s (And it has a '+' or '-'!  Are you using an old ACL by mistake?)", token );
619        else
620            tr_ninf( MY_NAME, "Adding address to whitelist: %s", token );
621       
622        if( walk[len]=='\0' )
623            break;
624        walk += len + 1;
625    }
626}
627
628char*
629tr_rpcGetWhitelist( const tr_rpc_server * server )
630{
631    return tr_strdup( server->whitelistStr ? server->whitelistStr : "" );
632}
633
634void
635tr_rpcSetWhitelistEnabled( tr_rpc_server  * server,
636                           tr_bool          isEnabled )
637{
638    server->isWhitelistEnabled = isEnabled != 0;
639}
640
641tr_bool
642tr_rpcGetWhitelistEnabled( const tr_rpc_server * server )
643{
644    return server->isWhitelistEnabled;
645}
646
647/****
648*****  PASSWORD
649****/
650
651void
652tr_rpcSetUsername( tr_rpc_server * server,
653                   const char *    username )
654{
655    tr_free( server->username );
656    server->username = tr_strdup( username );
657    dbgmsg( "setting our Username to [%s]", server->username );
658}
659
660char*
661tr_rpcGetUsername( const tr_rpc_server * server )
662{
663    return tr_strdup( server->username ? server->username : "" );
664}
665
666void
667tr_rpcSetPassword( tr_rpc_server * server,
668                   const char *    password )
669{
670    tr_free( server->password );
671    server->password = tr_strdup( password );
672    dbgmsg( "setting our Password to [%s]", server->password );
673}
674
675char*
676tr_rpcGetPassword( const tr_rpc_server * server )
677{
678    return tr_strdup( server->password ? server->password : "" );
679}
680
681void
682tr_rpcSetPasswordEnabled( tr_rpc_server * server,
683                          tr_bool          isEnabled )
684{
685    server->isPasswordEnabled = isEnabled;
686    dbgmsg( "setting 'password enabled' to %d", (int)isEnabled );
687}
688
689tr_bool
690tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
691{
692    return server->isPasswordEnabled;
693}
694
695/****
696*****  LIFE CYCLE
697****/
698
699static void
700closeServer( void * vserver )
701{
702    void * tmp;
703    tr_rpc_server * s = vserver;
704
705    stopServer( s );
706    while(( tmp = tr_list_pop_front( &s->whitelist )))
707        tr_free( tmp );
708#ifdef HAVE_ZLIB
709    deflateEnd( &s->stream );
710#endif
711    tr_free( s->whitelistStr );
712    tr_free( s->username );
713    tr_free( s->password );
714    tr_free( s );
715}
716
717void
718tr_rpcClose( tr_rpc_server ** ps )
719{
720    tr_runInEventThread( ( *ps )->session, closeServer, *ps );
721    *ps = NULL;
722}
723
724tr_rpc_server *
725tr_rpcInit( tr_session  * session,
726            tr_benc * settings )
727{
728    tr_rpc_server * s;
729    tr_bool found;
730    int64_t i;
731    const char *str;
732
733    s = tr_new0( tr_rpc_server, 1 );
734    s->session = session;
735
736    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_ENABLED, &i );
737    assert( found );
738    s->isEnabled = i != 0;
739
740    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_PORT, &i );
741    assert( found );
742    s->port = i;
743
744    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_WHITELIST_ENABLED, &i );
745    assert( found );
746    s->isWhitelistEnabled = i != 0;
747
748    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_AUTH_REQUIRED, &i );
749    assert( found );
750    s->isPasswordEnabled = i != 0;
751
752    found = tr_bencDictFindStr( settings, TR_PREFS_KEY_RPC_WHITELIST, &str );
753    assert( found );
754    tr_rpcSetWhitelist( s, str ? str : "127.0.0.1" );
755
756    found = tr_bencDictFindStr( settings, TR_PREFS_KEY_RPC_USERNAME, &str );
757    assert( found );
758    s->username = tr_strdup( str );
759
760    found = tr_bencDictFindStr( settings, TR_PREFS_KEY_RPC_PASSWORD, &str );
761    assert( found );
762    s->password = tr_strdup( str );
763
764#ifdef HAVE_ZLIB
765    s->stream.zalloc = (alloc_func) Z_NULL;
766    s->stream.zfree = (free_func) Z_NULL;
767    s->stream.opaque = (voidpf) Z_NULL;
768    deflateInit( &s->stream, Z_BEST_COMPRESSION );
769#endif
770
771    if( s->isEnabled )
772    {
773        tr_ninf( MY_NAME, _( "Serving RPC and Web requests on port %d" ), (int) s->port );
774        tr_runInEventThread( session, startServer, s );
775
776        if( s->isWhitelistEnabled )
777            tr_ninf( MY_NAME, _( "Whitelist enabled" ) );
778
779        if( s->isPasswordEnabled )
780            tr_ninf( MY_NAME, _( "Password required" ) );
781    }
782
783    return s;
784}
Note: See TracBrowser for help on using the repository browser.