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

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

(trunk libT) a little more work on #1829

  • Property svn:keywords set to Date Rev Author Id
File size: 21.9 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 7912 2009-02-18 21:27:44Z 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    {
419fprintf( stderr, "%s\n", (char*)EVBUFFER_DATA(req->input_buffer) );
420        tr_rpc_request_exec_json( server->session,
421                                  EVBUFFER_DATA( req->input_buffer ),
422                                  EVBUFFER_LENGTH( req->input_buffer ),
423                                  rpc_response_func, data );
424    }
425
426}
427
428static tr_bool
429isAddressAllowed( const tr_rpc_server * server,
430                  const char *          address )
431{
432    tr_list * l;
433
434    if( !server->isWhitelistEnabled )
435        return TRUE;
436
437    for( l=server->whitelist; l!=NULL; l=l->next )
438        if( tr_wildmat( address, l->data ) )
439            return TRUE;
440
441    return FALSE;
442}
443
444static void
445handle_request( struct evhttp_request * req,
446                void *                  arg )
447{
448    struct tr_rpc_server * server = arg;
449
450    if( req && req->evcon )
451    {
452        const char * auth;
453        char *       user = NULL;
454        char *       pass = NULL;
455
456        evhttp_add_header( req->output_headers, "Server", MY_REALM );
457
458        auth = evhttp_find_header( req->input_headers, "Authorization" );
459
460        if( auth && !strncasecmp( auth, "basic ", 6 ) )
461        {
462            int    plen;
463            char * p = tr_base64_decode( auth + 6, 0, &plen );
464            if( p && plen && ( ( pass = strchr( p, ':' ) ) ) )
465            {
466                user = p;
467                *pass++ = '\0';
468            }
469        }
470
471        if( !isAddressAllowed( server, req->remote_host ) )
472        {
473            send_simple_response( req, 401,
474                "<p>Unauthorized IP Address.</p>"
475                "<p>Either disable the IP address whitelist or add your address to it.</p>"
476                "<p>If you're editing settings.json, see the 'rpc-whitelist' and 'rpc-whitelist-enabled' entries.</p>"
477                "<p>If you're still using ACLs, use a whitelist instead.  See the transmission-daemon manpage for details.</p>" );
478        }
479        else if( server->isPasswordEnabled
480                 && ( !pass || !user || strcmp( server->username, user )
481                                     || strcmp( server->password, pass ) ) )
482        {
483            evhttp_add_header( req->output_headers,
484                               "WWW-Authenticate",
485                               "Basic realm=\"" MY_REALM "\"" );
486            send_simple_response( req, 401, "Unauthorized User" );
487        }
488        else if( !strcmp( req->uri, "/transmission/web" )
489               || !strcmp( req->uri, "/transmission/clutch" )
490               || !strcmp( req->uri, "/" ) )
491        {
492            evhttp_add_header( req->output_headers, "Location",
493                               "/transmission/web/" );
494            send_simple_response( req, HTTP_MOVEPERM, NULL );
495        }
496        else if( !strncmp( req->uri, "/transmission/web/", 18 ) )
497        {
498            handle_clutch( req, server );
499        }
500        else if( !strncmp( req->uri, "/transmission/rpc", 17 ) )
501        {
502            handle_rpc( req, server );
503        }
504        else if( !strncmp( req->uri, "/transmission/upload", 20 ) )
505        {
506            handle_upload( req, server );
507        }
508        else
509        {
510            send_simple_response( req, HTTP_NOTFOUND, NULL );
511        }
512
513        tr_free( user );
514    }
515}
516
517static void
518startServer( void * vserver )
519{
520    tr_rpc_server * server  = vserver;
521
522    if( !server->httpd )
523    {
524        server->httpd = evhttp_new( tr_eventGetBase( server->session ) );
525        evhttp_bind_socket( server->httpd, "0.0.0.0", server->port );
526        evhttp_set_gencb( server->httpd, handle_request, server );
527    }
528}
529
530static void
531stopServer( tr_rpc_server * server )
532{
533    if( server->httpd )
534    {
535        evhttp_free( server->httpd );
536        server->httpd = NULL;
537    }
538}
539
540static void
541onEnabledChanged( void * vserver )
542{
543    tr_rpc_server * server = vserver;
544
545    if( !server->isEnabled )
546        stopServer( server );
547    else
548        startServer( server );
549}
550
551void
552tr_rpcSetEnabled( tr_rpc_server * server,
553                  tr_bool         isEnabled )
554{
555    server->isEnabled = isEnabled;
556
557    tr_runInEventThread( server->session, onEnabledChanged, server );
558}
559
560tr_bool
561tr_rpcIsEnabled( const tr_rpc_server * server )
562{
563    return server->isEnabled;
564}
565
566static void
567restartServer( void * vserver )
568{
569    tr_rpc_server * server = vserver;
570
571    if( server->isEnabled )
572    {
573        stopServer( server );
574        startServer( server );
575    }
576}
577
578void
579tr_rpcSetPort( tr_rpc_server * server,
580               tr_port         port )
581{
582    if( server->port != port )
583    {
584        server->port = port;
585
586        if( server->isEnabled )
587            tr_runInEventThread( server->session, restartServer, server );
588    }
589}
590
591tr_port
592tr_rpcGetPort( const tr_rpc_server * server )
593{
594    return server->port;
595}
596
597void
598tr_rpcSetWhitelist( tr_rpc_server * server,
599                    const char    * whitelistStr )
600{
601    void * tmp;
602    const char * walk;
603
604    /* keep the string */
605    tr_free( server->whitelistStr );
606    server->whitelistStr = tr_strdup( whitelistStr );
607
608    /* clear out the old whitelist entries */
609    while(( tmp = tr_list_pop_front( &server->whitelist )))
610        tr_free( tmp );
611
612    /* build the new whitelist entries */
613    for( walk=whitelistStr; walk && *walk; ) {
614        const char * delimiters = " ,;";
615        const size_t len = strcspn( walk, delimiters );
616        char * token = tr_strndup( walk, len );
617        tr_list_append( &server->whitelist, token );
618        if( strcspn( token, "+-" ) < len )
619            tr_ninf( MY_NAME, "Adding address to whitelist: %s (And it has a '+' or '-'!  Are you using an old ACL by mistake?)", token );
620        else
621            tr_ninf( MY_NAME, "Adding address to whitelist: %s", token );
622       
623        if( walk[len]=='\0' )
624            break;
625        walk += len + 1;
626    }
627}
628
629char*
630tr_rpcGetWhitelist( const tr_rpc_server * server )
631{
632    return tr_strdup( server->whitelistStr ? server->whitelistStr : "" );
633}
634
635void
636tr_rpcSetWhitelistEnabled( tr_rpc_server  * server,
637                           tr_bool          isEnabled )
638{
639    server->isWhitelistEnabled = isEnabled != 0;
640}
641
642tr_bool
643tr_rpcGetWhitelistEnabled( const tr_rpc_server * server )
644{
645    return server->isWhitelistEnabled;
646}
647
648/****
649*****  PASSWORD
650****/
651
652void
653tr_rpcSetUsername( tr_rpc_server * server,
654                   const char *    username )
655{
656    tr_free( server->username );
657    server->username = tr_strdup( username );
658    dbgmsg( "setting our Username to [%s]", server->username );
659}
660
661char*
662tr_rpcGetUsername( const tr_rpc_server * server )
663{
664    return tr_strdup( server->username ? server->username : "" );
665}
666
667void
668tr_rpcSetPassword( tr_rpc_server * server,
669                   const char *    password )
670{
671    tr_free( server->password );
672    server->password = tr_strdup( password );
673    dbgmsg( "setting our Password to [%s]", server->password );
674}
675
676char*
677tr_rpcGetPassword( const tr_rpc_server * server )
678{
679    return tr_strdup( server->password ? server->password : "" );
680}
681
682void
683tr_rpcSetPasswordEnabled( tr_rpc_server * server,
684                          tr_bool          isEnabled )
685{
686    server->isPasswordEnabled = isEnabled;
687    dbgmsg( "setting 'password enabled' to %d", (int)isEnabled );
688}
689
690tr_bool
691tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
692{
693    return server->isPasswordEnabled;
694}
695
696/****
697*****  LIFE CYCLE
698****/
699
700static void
701closeServer( void * vserver )
702{
703    void * tmp;
704    tr_rpc_server * s = vserver;
705
706    stopServer( s );
707    while(( tmp = tr_list_pop_front( &s->whitelist )))
708        tr_free( tmp );
709#ifdef HAVE_ZLIB
710    deflateEnd( &s->stream );
711#endif
712    tr_free( s->whitelistStr );
713    tr_free( s->username );
714    tr_free( s->password );
715    tr_free( s );
716}
717
718void
719tr_rpcClose( tr_rpc_server ** ps )
720{
721    tr_runInEventThread( ( *ps )->session, closeServer, *ps );
722    *ps = NULL;
723}
724
725tr_rpc_server *
726tr_rpcInit( tr_session  * session,
727            tr_benc * settings )
728{
729    tr_rpc_server * s;
730    tr_bool found;
731    int64_t i;
732    const char *str;
733
734    s = tr_new0( tr_rpc_server, 1 );
735    s->session = session;
736
737    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_ENABLED, &i );
738    assert( found );
739    s->isEnabled = i != 0;
740
741    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_PORT, &i );
742    assert( found );
743    s->port = i;
744
745    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_WHITELIST_ENABLED, &i );
746    assert( found );
747    s->isWhitelistEnabled = i != 0;
748
749    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_AUTH_REQUIRED, &i );
750    assert( found );
751    s->isPasswordEnabled = i != 0;
752
753    found = tr_bencDictFindStr( settings, TR_PREFS_KEY_RPC_WHITELIST, &str );
754    assert( found );
755    tr_rpcSetWhitelist( s, str ? str : "127.0.0.1" );
756
757    found = tr_bencDictFindStr( settings, TR_PREFS_KEY_RPC_USERNAME, &str );
758    assert( found );
759    s->username = tr_strdup( str );
760
761    found = tr_bencDictFindStr( settings, TR_PREFS_KEY_RPC_PASSWORD, &str );
762    assert( found );
763    s->password = tr_strdup( str );
764
765#ifdef HAVE_ZLIB
766    s->stream.zalloc = (alloc_func) Z_NULL;
767    s->stream.zfree = (free_func) Z_NULL;
768    s->stream.opaque = (voidpf) Z_NULL;
769    deflateInit( &s->stream, Z_BEST_COMPRESSION );
770#endif
771
772    if( s->isEnabled )
773    {
774        tr_ninf( MY_NAME, _( "Serving RPC and Web requests on port %d" ), (int) s->port );
775        tr_runInEventThread( session, startServer, s );
776
777        if( s->isWhitelistEnabled )
778            tr_ninf( MY_NAME, _( "Whitelist enabled" ) );
779
780        if( s->isPasswordEnabled )
781            tr_ninf( MY_NAME, _( "Password required" ) );
782    }
783
784    return s;
785}
Note: See TracBrowser for help on using the repository browser.