source: branches/1.5x/libtransmission/rpc-server.c @ 8200

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

(1.5x libT) better filtering of maliciously-crafted URLs when serving web interface files

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