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

Last change on this file since 8080 was 8080, checked in by jhujhiti, 13 years ago

(trunk libT) #1276 - hash rpc password with sha1

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