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

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

(trunk libT) Clean up RPC server initialization (wereHamster)

  • Property svn:keywords set to Date Rev Author Id
File size: 21.5 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 7706 2009-01-13 16:35:06Z 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 );
180
181                        tr_releaseBuffer( json );
182                        tr_free( b64 );
183                        tr_bencFree( &top );
184                    }
185                }
186                tr_free( text );
187            }
188        }
189
190        tr_free( boundary );
191
192        /* use xml here because json responses to file uploads is trouble.
193         * see http://www.malsup.com/jquery/form/#sample7 for details */
194        evhttp_add_header( req->output_headers, "Content-Type",
195                           "text/xml; charset=UTF-8" );
196        send_simple_response( req, HTTP_OK, NULL );
197    }
198}
199
200static const char*
201mimetype_guess( const char * path )
202{
203    unsigned int i;
204
205    const struct
206    {
207        const char *    suffix;
208        const char *    mime_type;
209    } types[] = {
210        /* these are just the ones we need for serving clutch... */
211        { "css",  "text/css"                  },
212        { "gif",  "image/gif"                 },
213        { "html", "text/html"                 },
214        { "ico",  "image/vnd.microsoft.icon"  },
215        { "js",   "application/javascript"    },
216        { "png",  "image/png"                 }
217    };
218    const char * dot = strrchr( path, '.' );
219
220    for( i = 0; dot && i < TR_N_ELEMENTS( types ); ++i )
221        if( !strcmp( dot + 1, types[i].suffix ) )
222            return types[i].mime_type;
223
224    return "application/octet-stream";
225}
226
227static void
228add_response( struct evhttp_request * req,
229              struct tr_rpc_server *  server,
230              struct evbuffer *       out,
231              const void *            content,
232              size_t                  content_len )
233{
234#ifndef HAVE_ZLIB
235    evbuffer_add( out, content, content_len );
236#else
237    const char * key = "Accept-Encoding";
238    const char * encoding = evhttp_find_header( req->input_headers, key );
239    const int do_deflate = encoding && strstr( encoding, "deflate" );
240
241    if( !do_deflate )
242    {
243        evbuffer_add( out, content, content_len );
244    }
245    else
246    {
247        int state;
248
249        server->stream.next_in = (Bytef*) content;
250        server->stream.avail_in = content_len;
251
252        /* allocate space for the raw data and call deflate() just once --
253         * we won't use the deflated data if it's longer than the raw data,
254         * so it's okay to let deflate() run out of output buffer space */
255        evbuffer_expand( out, content_len );
256        server->stream.next_out = EVBUFFER_DATA( out );
257        server->stream.avail_out = content_len;
258
259        state = deflate( &server->stream, Z_FINISH );
260
261        if( state == Z_STREAM_END )
262        {
263            EVBUFFER_LENGTH( out ) = content_len - server->stream.avail_out;
264
265            /* http://carsten.codimi.de/gzip.yaws/
266               It turns out that some browsers expect deflated data without
267               the first two bytes (a kind of header) and and the last four
268               bytes (an ADLER32 checksum). This format can of course
269               be produced by simply stripping these off. */
270            if( EVBUFFER_LENGTH( out ) >= 6 ) {
271                EVBUFFER_LENGTH( out ) -= 4;
272                evbuffer_drain( out, 2 );
273            }
274
275#if 0
276            tr_ninf( MY_NAME, _( "Deflated response from %zu bytes to %zu" ),
277                              content_len,
278                              EVBUFFER_LENGTH( out ) );
279#endif
280            evhttp_add_header( req->output_headers,
281                               "Content-Encoding", "deflate" );
282        }
283        else
284        {
285            evbuffer_drain( out, EVBUFFER_LENGTH( out ) );
286            evbuffer_add( out, content, content_len );
287        }
288
289        deflateReset( &server->stream );
290    }
291#endif
292}
293
294static void
295serve_file( struct evhttp_request * req,
296            struct tr_rpc_server *  server,
297            const char *            filename )
298{
299    if( req->type != EVHTTP_REQ_GET )
300    {
301        evhttp_add_header( req->output_headers, "Allow", "GET" );
302        send_simple_response( req, 405, NULL );
303    }
304    else
305    {
306        size_t content_len;
307        uint8_t * content;
308        const int error = errno;
309
310        errno = 0;
311        content_len = 0;
312        content = tr_loadFile( filename, &content_len );
313
314        if( errno )
315        {
316            send_simple_response( req, HTTP_NOTFOUND, NULL );
317        }
318        else
319        {
320            struct evbuffer * out;
321
322            errno = error;
323            out = tr_getBuffer( );
324            evhttp_add_header( req->output_headers, "Content-Type",
325                               mimetype_guess( filename ) );
326            add_response( req, server, out, content, content_len );
327            evhttp_send_reply( req, HTTP_OK, "OK", out );
328
329            tr_releaseBuffer( out );
330            tr_free( content );
331        }
332    }
333}
334
335static void
336handle_clutch( struct evhttp_request * req,
337               struct tr_rpc_server *  server )
338{
339    const char * clutchDir = tr_getClutchDir( server->session );
340
341    assert( !strncmp( req->uri, "/transmission/web/", 18 ) );
342
343    if( !clutchDir || !*clutchDir )
344    {
345        send_simple_response( req, HTTP_NOTFOUND,
346            "<p>Couldn't find Transmission's web interface files!</p>"
347            "<p>Users: to tell Transmission where to look, "
348            "set the TRANSMISSION_WEB_HOME environmental "
349            "variable to the folder where the web interface's "
350            "index.html is located.</p>"
351            "<p>Package Builders: to set a custom default at compile time, "
352            "#define PACKAGE_DATA_DIR in libtransmission/platform.c "
353            "or tweak tr_getClutchDir() by hand.</p>" );
354    }
355    else
356    {
357        char * pch;
358        char * subpath;
359        char * filename;
360
361        subpath = tr_strdup( req->uri + 18 );
362        if(( pch = strchr( subpath, '?' )))
363            *pch = '\0';
364
365        filename = tr_strdup_printf( "%s%s%s",
366                       clutchDir,
367                       TR_PATH_DELIMITER_STR,
368                       subpath && *subpath ? subpath : "index.html" );
369
370        serve_file( req, server, filename );
371
372        tr_free( filename );
373        tr_free( subpath );
374    }
375}
376
377static void
378handle_rpc( struct evhttp_request * req,
379            struct tr_rpc_server  * server )
380{
381    struct evbuffer * response = tr_getBuffer( );
382
383    if( req->type == EVHTTP_REQ_GET )
384    {
385        const char * q;
386        if( ( q = strchr( req->uri, '?' ) ) )
387            tr_rpc_request_exec_uri( server->session, q + 1, strlen( q + 1 ), response );
388    }
389    else if( req->type == EVHTTP_REQ_POST )
390    {
391        tr_rpc_request_exec_json( server->session,
392                                  EVBUFFER_DATA( req->input_buffer ),
393                                  EVBUFFER_LENGTH( req->input_buffer ),
394                                  response );
395    }
396
397    {
398        struct evbuffer * buf = tr_getBuffer( );
399        add_response( req, server, buf,
400                      EVBUFFER_DATA( response ),
401                      EVBUFFER_LENGTH( response ) );
402        evhttp_add_header( req->output_headers, "Content-Type",
403                                                "application/json; charset=UTF-8" );
404        evhttp_send_reply( req, HTTP_OK, "OK", buf );
405        tr_releaseBuffer( buf );
406    }
407
408    /* cleanup */
409    tr_releaseBuffer( response );
410}
411
412static tr_bool
413isAddressAllowed( const tr_rpc_server * server,
414                  const char *          address )
415{
416    tr_list * l;
417
418    if( !server->isWhitelistEnabled )
419        return TRUE;
420
421    for( l=server->whitelist; l!=NULL; l=l->next )
422        if( tr_wildmat( address, l->data ) )
423            return TRUE;
424
425    return FALSE;
426}
427
428static void
429handle_request( struct evhttp_request * req,
430                void *                  arg )
431{
432    struct tr_rpc_server * server = arg;
433
434    if( req && req->evcon )
435    {
436        const char * auth;
437        char *       user = NULL;
438        char *       pass = NULL;
439
440        evhttp_add_header( req->output_headers, "Server", MY_REALM );
441
442        auth = evhttp_find_header( req->input_headers, "Authorization" );
443
444        if( auth && !strncasecmp( auth, "basic ", 6 ) )
445        {
446            int    plen;
447            char * p = tr_base64_decode( auth + 6, 0, &plen );
448            if( p && plen && ( ( pass = strchr( p, ':' ) ) ) )
449            {
450                user = p;
451                *pass++ = '\0';
452            }
453        }
454
455        if( !isAddressAllowed( server, req->remote_host ) )
456        {
457            send_simple_response( req, 401,
458                "<p>Unauthorized IP Address.</p>"
459                "<p>Either disable the IP address whitelist or add your address to it.</p>"
460                "<p>If you're editing settings.json, see the 'rpc-whitelist' and 'rpc-whitelist-enabled' entries.</p>"
461                "<p>If you're still using ACLs, use a whitelist instead.  See the transmission-daemon manpage for details.</p>" );
462        }
463        else if( server->isPasswordEnabled
464                 && ( !pass || !user || strcmp( server->username, user )
465                                     || strcmp( server->password, pass ) ) )
466        {
467            evhttp_add_header( req->output_headers,
468                               "WWW-Authenticate",
469                               "Basic realm=\"" MY_REALM "\"" );
470            send_simple_response( req, 401, "Unauthorized User" );
471        }
472        else if( !strcmp( req->uri, "/transmission/web" )
473               || !strcmp( req->uri, "/transmission/clutch" )
474               || !strcmp( req->uri, "/" ) )
475        {
476            evhttp_add_header( req->output_headers, "Location",
477                               "/transmission/web/" );
478            send_simple_response( req, HTTP_MOVEPERM, NULL );
479        }
480        else if( !strncmp( req->uri, "/transmission/web/", 18 ) )
481        {
482            handle_clutch( req, server );
483        }
484        else if( !strncmp( req->uri, "/transmission/rpc", 17 ) )
485        {
486            handle_rpc( req, server );
487        }
488        else if( !strncmp( req->uri, "/transmission/upload", 20 ) )
489        {
490            handle_upload( req, server );
491        }
492        else
493        {
494            send_simple_response( req, HTTP_NOTFOUND, NULL );
495        }
496
497        tr_free( user );
498    }
499}
500
501static void
502startServer( void * vserver )
503{
504    tr_rpc_server * server  = vserver;
505
506    if( !server->httpd )
507    {
508        server->httpd = evhttp_new( tr_eventGetBase( server->session ) );
509        evhttp_bind_socket( server->httpd, "0.0.0.0", server->port );
510        evhttp_set_gencb( server->httpd, handle_request, server );
511    }
512}
513
514static void
515stopServer( tr_rpc_server * server )
516{
517    if( server->httpd )
518    {
519        evhttp_free( server->httpd );
520        server->httpd = NULL;
521    }
522}
523
524static void
525onEnabledChanged( void * vserver )
526{
527    tr_rpc_server * server = vserver;
528
529    if( !server->isEnabled )
530        stopServer( server );
531    else
532        startServer( server );
533}
534
535void
536tr_rpcSetEnabled( tr_rpc_server * server,
537                  tr_bool         isEnabled )
538{
539    server->isEnabled = isEnabled;
540
541    tr_runInEventThread( server->session, onEnabledChanged, server );
542}
543
544tr_bool
545tr_rpcIsEnabled( const tr_rpc_server * server )
546{
547    return server->isEnabled;
548}
549
550static void
551restartServer( void * vserver )
552{
553    tr_rpc_server * server = vserver;
554
555    if( server->isEnabled )
556    {
557        stopServer( server );
558        startServer( server );
559    }
560}
561
562void
563tr_rpcSetPort( tr_rpc_server * server,
564               tr_port         port )
565{
566    if( server->port != port )
567    {
568        server->port = port;
569
570        if( server->isEnabled )
571            tr_runInEventThread( server->session, restartServer, server );
572    }
573}
574
575tr_port
576tr_rpcGetPort( const tr_rpc_server * server )
577{
578    return server->port;
579}
580
581void
582tr_rpcSetWhitelist( tr_rpc_server * server,
583                    const char    * whitelistStr )
584{
585    void * tmp;
586    const char * walk;
587
588    /* keep the string */
589    tr_free( server->whitelistStr );
590    server->whitelistStr = tr_strdup( whitelistStr );
591
592    /* clear out the old whitelist entries */
593    while(( tmp = tr_list_pop_front( &server->whitelist )))
594        tr_free( tmp );
595
596    /* build the new whitelist entries */
597    for( walk=whitelistStr; walk && *walk; ) {
598        const char * delimiters = " ,;";
599        const size_t len = strcspn( walk, delimiters );
600        char * token = tr_strndup( walk, len );
601        tr_list_append( &server->whitelist, token );
602        if( strcspn( token, "+-" ) < len )
603            tr_ninf( MY_NAME, "Adding address to whitelist: %s (And it has a '+' or '-'!  Are you using an old ACL by mistake?)", token );
604        else
605            tr_ninf( MY_NAME, "Adding address to whitelist: %s", token );
606       
607        if( walk[len]=='\0' )
608            break;
609        walk += len + 1;
610    }
611}
612
613char*
614tr_rpcGetWhitelist( const tr_rpc_server * server )
615{
616    return tr_strdup( server->whitelistStr ? server->whitelistStr : "" );
617}
618
619void
620tr_rpcSetWhitelistEnabled( tr_rpc_server  * server,
621                           tr_bool          isEnabled )
622{
623    server->isWhitelistEnabled = isEnabled != 0;
624}
625
626tr_bool
627tr_rpcGetWhitelistEnabled( const tr_rpc_server * server )
628{
629    return server->isWhitelistEnabled;
630}
631
632/****
633*****  PASSWORD
634****/
635
636void
637tr_rpcSetUsername( tr_rpc_server * server,
638                   const char *    username )
639{
640    tr_free( server->username );
641    server->username = tr_strdup( username );
642    dbgmsg( "setting our Username to [%s]", server->username );
643}
644
645char*
646tr_rpcGetUsername( const tr_rpc_server * server )
647{
648    return tr_strdup( server->username ? server->username : "" );
649}
650
651void
652tr_rpcSetPassword( tr_rpc_server * server,
653                   const char *    password )
654{
655    tr_free( server->password );
656    server->password = tr_strdup( password );
657    dbgmsg( "setting our Password to [%s]", server->password );
658}
659
660char*
661tr_rpcGetPassword( const tr_rpc_server * server )
662{
663    return tr_strdup( server->password ? server->password : "" );
664}
665
666void
667tr_rpcSetPasswordEnabled( tr_rpc_server * server,
668                          tr_bool          isEnabled )
669{
670    server->isPasswordEnabled = isEnabled;
671    dbgmsg( "setting 'password enabled' to %d", (int)isEnabled );
672}
673
674tr_bool
675tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
676{
677    return server->isPasswordEnabled;
678}
679
680/****
681*****  LIFE CYCLE
682****/
683
684static void
685closeServer( void * vserver )
686{
687    void * tmp;
688    tr_rpc_server * s = vserver;
689
690    stopServer( s );
691    while(( tmp = tr_list_pop_front( &s->whitelist )))
692        tr_free( tmp );
693#ifdef HAVE_ZLIB
694    deflateEnd( &s->stream );
695#endif
696    tr_free( s->whitelistStr );
697    tr_free( s->username );
698    tr_free( s->password );
699    tr_free( s );
700}
701
702void
703tr_rpcClose( tr_rpc_server ** ps )
704{
705    tr_runInEventThread( ( *ps )->session, closeServer, *ps );
706    *ps = NULL;
707}
708
709tr_rpc_server *
710tr_rpcInit( tr_session  * session,
711            tr_benc * settings )
712{
713    tr_rpc_server * s;
714    tr_bool found;
715    int64_t i;
716    const char *str;
717
718    s = tr_new0( tr_rpc_server, 1 );
719    s->session = session;
720
721    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_ENABLED, &i );
722    assert( found );
723    s->isEnabled = i != 0;
724
725    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_PORT, &i );
726    assert( found );
727    s->port = i;
728
729    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_WHITELIST_ENABLED, &i );
730    assert( found );
731    s->isWhitelistEnabled = i != 0;
732
733    found = tr_bencDictFindInt( settings, TR_PREFS_KEY_RPC_AUTH_REQUIRED, &i );
734    assert( found );
735    s->isPasswordEnabled = i != 0;
736
737    found = tr_bencDictFindStr( settings, TR_PREFS_KEY_RPC_WHITELIST, &str );
738    assert( found );
739    tr_rpcSetWhitelist( s, str ? str : "127.0.0.1" );
740
741    found = tr_bencDictFindStr( settings, TR_PREFS_KEY_RPC_USERNAME, &str );
742    assert( found );
743    s->username = tr_strdup( str );
744
745    found = tr_bencDictFindStr( settings, TR_PREFS_KEY_RPC_PASSWORD, &str );
746    assert( found );
747    s->password = tr_strdup( str );
748
749#ifdef HAVE_ZLIB
750    s->stream.zalloc = (alloc_func) Z_NULL;
751    s->stream.zfree = (free_func) Z_NULL;
752    s->stream.opaque = (voidpf) Z_NULL;
753    deflateInit( &s->stream, Z_BEST_COMPRESSION );
754#endif
755
756    if( s->isEnabled )
757    {
758        tr_ninf( MY_NAME, _( "Serving RPC and Web requests on port %d" ), (int) s->port );
759        tr_runInEventThread( session, startServer, s );
760
761        if( s->isWhitelistEnabled )
762            tr_ninf( MY_NAME, _( "Whitelist enabled" ) );
763
764        if( s->isPasswordEnabled )
765            tr_ninf( MY_NAME, _( "Password required" ) );
766    }
767
768    return s;
769}
Note: See TracBrowser for help on using the repository browser.