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

Last change on this file since 8072 was 8072, checked in by livings124, 13 years ago

#1276 encrypt the password to access web client interface using SHA-2

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