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

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

(trunk) back out the avahi patch

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