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

Last change on this file since 7449 was 7449, checked in by charles, 12 years ago

(trunk libT) add html and console hints about whitelist status & configuration

  • Property svn:keywords set to Date Rev Author Id
File size: 20.4 KB
Line 
1/*
2 * This file Copyright (C) 2008 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 7449 2008-12-21 19:23:41Z 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
62#define dbgmsg( ... ) \
63    do { \
64        if( tr_deepLoggingIsActive( ) ) \
65            tr_deepLog( __FILE__, __LINE__, MY_NAME, __VA_ARGS__ ); \
66    } while( 0 )
67
68
69/**
70***
71**/
72
73static void
74send_simple_response( struct evhttp_request * req,
75                      int                     code,
76                      const char *            text )
77{
78    const char *      code_text = tr_webGetResponseStr( code );
79    struct evbuffer * body = evbuffer_new( );
80
81    evbuffer_add_printf( body, "<h1>%d: %s</h1>", code, code_text );
82    if( text )
83        evbuffer_add_printf( body, "%s", text );
84    evhttp_send_reply( req, code, code_text, body );
85    evbuffer_free( body );
86}
87
88static const char*
89tr_memmem( const char * s1,
90           size_t       l1,
91           const char * s2,
92           size_t       l2 )
93{
94    if( !l2 ) return s1;
95    while( l1 >= l2 )
96    {
97        l1--;
98        if( !memcmp( s1, s2, l2 ) )
99            return s1;
100        s1++;
101    }
102
103    return NULL;
104}
105
106static void
107handle_upload( struct evhttp_request * req,
108               struct tr_rpc_server *  server )
109{
110    if( req->type != EVHTTP_REQ_POST )
111    {
112        send_simple_response( req, 405, NULL );
113    }
114    else
115    {
116        const char * content_type = evhttp_find_header( req->input_headers,
117                                                        "Content-Type" );
118
119        const char * query = strchr( req->uri, '?' );
120        const int    paused = query && strstr( query + 1, "paused=true" );
121
122        const char * in = (const char *) EVBUFFER_DATA( req->input_buffer );
123        size_t       inlen = EVBUFFER_LENGTH( req->input_buffer );
124
125        const char * boundary_key = "boundary=";
126        const char * boundary_key_begin = strstr( content_type,
127                                                  boundary_key );
128        const char * boundary_val =
129            boundary_key_begin ? boundary_key_begin +
130            strlen( boundary_key ) : "arglebargle";
131
132        char *       boundary = tr_strdup_printf( "--%s", boundary_val );
133        const size_t boundary_len = strlen( boundary );
134
135        const char * delim = tr_memmem( in, inlen, boundary, boundary_len );
136        while( delim )
137        {
138            size_t       part_len;
139            const char * part = delim + boundary_len;
140            inlen -= ( part - in );
141            in = part;
142            delim = tr_memmem( in, inlen, boundary, boundary_len );
143            part_len = delim ? (size_t)( delim - part ) : inlen;
144
145            if( part_len )
146            {
147                char * text = tr_strndup( part, part_len );
148                if( strstr( text, "filename=\"" ) )
149                {
150                    const char * body = strstr( text, "\r\n\r\n" );
151                    if( body )
152                    {
153                        char *  b64, *json, *freeme;
154                        int     json_len;
155                        size_t  body_len;
156                        tr_benc top, *args;
157
158                        body += 4; /* walk past the \r\n\r\n */
159                        body_len = part_len - ( body - text );
160                        if( body_len >= 2
161                          && !memcmp( &body[body_len - 2], "\r\n", 2 ) )
162                            body_len -= 2;
163
164                        tr_bencInitDict( &top, 2 );
165                        args = tr_bencDictAddDict( &top, "arguments", 2 );
166                        tr_bencDictAddStr( &top, "method", "torrent-add" );
167                        b64 = tr_base64_encode( body, body_len, NULL );
168                        tr_bencDictAddStr( args, "metainfo", b64 );
169                        tr_bencDictAddInt( args, "paused", paused );
170                        json = tr_bencSaveAsJSON( &top, &json_len );
171                        freeme = tr_rpc_request_exec_json( server->session,
172                                                           json, json_len,
173                                                           NULL );
174
175                        tr_free( freeme );
176                        tr_free( json );
177                        tr_free( b64 );
178                        tr_bencFree( &top );
179                    }
180                }
181                tr_free( text );
182            }
183        }
184
185        tr_free( boundary );
186
187        /* use xml here because json responses to file uploads is trouble.
188         * see http://www.malsup.com/jquery/form/#sample7 for details */
189        evhttp_add_header( req->output_headers, "Content-Type",
190                           "text/xml; charset=UTF-8" );
191        send_simple_response( req, HTTP_OK, NULL );
192    }
193}
194
195static const char*
196mimetype_guess( const char * path )
197{
198    unsigned int i;
199
200    const struct
201    {
202        const char *    suffix;
203        const char *    mime_type;
204    } types[] = {
205        /* these are just the ones we need for serving clutch... */
206        { "css",  "text/css"                  },
207        { "gif",  "image/gif"                 },
208        { "html", "text/html"                 },
209        { "ico",  "image/vnd.microsoft.icon"  },
210        { "js",   "application/javascript"    },
211        { "png",  "image/png"                 }
212    };
213    const char * dot = strrchr( path, '.' );
214
215    for( i = 0; dot && i < TR_N_ELEMENTS( types ); ++i )
216        if( !strcmp( dot + 1, types[i].suffix ) )
217            return types[i].mime_type;
218
219    return "application/octet-stream";
220}
221
222static void
223add_response( struct evhttp_request * req,
224              struct evbuffer *       out,
225              const void *            content,
226              size_t                  content_len )
227{
228#ifndef HAVE_ZLIB
229    evbuffer_add( out, content, content_len );
230#else
231    const char * key = "Accept-Encoding";
232    const char * encoding = evhttp_find_header( req->input_headers, key );
233    const int do_deflate = encoding && strstr( encoding, "deflate" );
234
235    if( !do_deflate )
236    {
237        evbuffer_add( out, content, content_len );
238    }
239    else
240    {
241        int state;
242        z_stream stream;
243
244        stream.zalloc = (alloc_func) Z_NULL;
245        stream.zfree = (free_func) Z_NULL;
246        stream.opaque = (voidpf) Z_NULL;
247        deflateInit( &stream, Z_BEST_COMPRESSION );
248
249        stream.next_in = (Bytef*) content;
250        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        stream.next_out = EVBUFFER_DATA( out );
257        stream.avail_out = content_len;
258
259        state = deflate( &stream, Z_FINISH );
260
261        if( state == Z_STREAM_END )
262        {
263            EVBUFFER_LENGTH( out ) = content_len - 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        deflateEnd( &stream );
290    }
291#endif
292}
293
294static void
295serve_file( struct evhttp_request * req,
296            const char *            filename )
297{
298    if( req->type != EVHTTP_REQ_GET )
299    {
300        evhttp_add_header( req->output_headers, "Allow", "GET" );
301        send_simple_response( req, 405, NULL );
302    }
303    else
304    {
305        size_t content_len;
306        uint8_t * content;
307        const int error = errno;
308
309        errno = 0;
310        content_len = 0;
311        content = tr_loadFile( filename, &content_len );
312
313        if( errno )
314        {
315            send_simple_response( req, HTTP_NOTFOUND, NULL );
316        }
317        else
318        {
319            struct evbuffer * out;
320
321            errno = error;
322            out = evbuffer_new( );
323            evhttp_add_header( req->output_headers, "Content-Type",
324                               mimetype_guess( filename ) );
325            add_response( req, out, content, content_len );
326            evhttp_send_reply( req, HTTP_OK, "OK", out );
327
328            evbuffer_free( out );
329            tr_free( content );
330        }
331    }
332}
333
334static void
335handle_clutch( struct evhttp_request * req,
336               struct tr_rpc_server *  server )
337{
338    const char * clutchDir = tr_getClutchDir( server->session );
339
340    assert( !strncmp( req->uri, "/transmission/web/", 18 ) );
341
342    if( !clutchDir || !*clutchDir )
343    {
344        send_simple_response( req, HTTP_NOTFOUND,
345            "<p>Couldn't find Transmission's web interface files!</p>"
346            "<p>Users: to tell Transmission where to look, "
347            "set the TRANSMISSION_WEB_HOME environmental "
348            "variable to the folder where the web interface's "
349            "index.html is located.</p>"
350            "<p>Package Builders: to set a custom default at compile time, "
351            "#define PACKAGE_DATA_DIR in libtransmission/platform.c "
352            "or tweak tr_getClutchDir() by hand.</p>" );
353    }
354    else
355    {
356        char * pch;
357        char * subpath;
358        char * filename;
359
360        subpath = tr_strdup( req->uri + 18 );
361        if(( pch = strchr( subpath, '?' )))
362            *pch = '\0';
363
364        filename = tr_strdup_printf( "%s%s%s",
365                       clutchDir,
366                       TR_PATH_DELIMITER_STR,
367                       subpath && *subpath ? subpath : "index.html" );
368
369        serve_file( req, filename );
370
371        tr_free( filename );
372        tr_free( subpath );
373    }
374}
375
376static void
377handle_rpc( struct evhttp_request * req,
378            struct tr_rpc_server *  server )
379{
380    int               len = 0;
381    char *            out = NULL;
382    struct evbuffer * buf;
383
384    if( req->type == EVHTTP_REQ_GET )
385    {
386        const char * q;
387        if( ( q = strchr( req->uri, '?' ) ) )
388            out = tr_rpc_request_exec_uri( server->session,
389                                           q + 1,
390                                           strlen( q + 1 ),
391                                           &len );
392    }
393    else if( req->type == EVHTTP_REQ_POST )
394    {
395        out = tr_rpc_request_exec_json( server->session,
396                                        EVBUFFER_DATA( req->input_buffer ),
397                                        EVBUFFER_LENGTH( req->input_buffer ),
398                                        &len );
399    }
400
401    buf = evbuffer_new( );
402    add_response( req, buf, out, len );
403    evhttp_add_header( req->output_headers, "Content-Type",
404                       "application/json; charset=UTF-8" );
405    evhttp_send_reply( req, HTTP_OK, "OK", buf );
406
407    /* cleanup */
408    evbuffer_free( buf );
409    tr_free( out );
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        }
462        else if( server->isPasswordEnabled
463                 && ( !pass || !user || strcmp( server->username, user )
464                                     || strcmp( server->password, pass ) ) )
465        {
466            evhttp_add_header( req->output_headers,
467                               "WWW-Authenticate",
468                               "Basic realm=\"" MY_REALM "\"" );
469            send_simple_response( req, 401, "Unauthorized User" );
470        }
471        else if( !strcmp( req->uri, "/transmission/web" )
472               || !strcmp( req->uri, "/transmission/clutch" )
473               || !strcmp( req->uri, "/" ) )
474        {
475            evhttp_add_header( req->output_headers, "Location",
476                               "/transmission/web/" );
477            send_simple_response( req, HTTP_MOVEPERM, NULL );
478        }
479        else if( !strncmp( req->uri, "/transmission/web/", 18 ) )
480        {
481            handle_clutch( req, server );
482        }
483        else if( !strncmp( req->uri, "/transmission/rpc", 17 ) )
484        {
485            handle_rpc( req, server );
486        }
487        else if( !strncmp( req->uri, "/transmission/upload", 20 ) )
488        {
489            handle_upload( req, server );
490        }
491        else
492        {
493            send_simple_response( req, HTTP_NOTFOUND, NULL );
494        }
495
496        tr_free( user );
497    }
498}
499
500static void
501startServer( void * vserver )
502{
503    tr_rpc_server * server  = vserver;
504
505    if( !server->httpd )
506    {
507        server->httpd = evhttp_new( tr_eventGetBase( server->session ) );
508        evhttp_bind_socket( server->httpd, "0.0.0.0", server->port );
509        evhttp_set_gencb( server->httpd, handle_request, server );
510    }
511}
512
513static void
514stopServer( tr_rpc_server * server )
515{
516    if( server->httpd )
517    {
518        evhttp_free( server->httpd );
519        server->httpd = NULL;
520    }
521}
522
523static void
524onEnabledChanged( void * vserver )
525{
526    tr_rpc_server * server = vserver;
527
528    if( !server->isEnabled )
529        stopServer( server );
530    else
531        startServer( server );
532}
533
534void
535tr_rpcSetEnabled( tr_rpc_server * server,
536                  tr_bool         isEnabled )
537{
538    server->isEnabled = isEnabled;
539
540    tr_runInEventThread( server->session, onEnabledChanged, server );
541}
542
543tr_bool
544tr_rpcIsEnabled( const tr_rpc_server * server )
545{
546    return server->isEnabled;
547}
548
549static void
550restartServer( void * vserver )
551{
552    tr_rpc_server * server = vserver;
553
554    if( server->isEnabled )
555    {
556        stopServer( server );
557        startServer( server );
558    }
559}
560
561void
562tr_rpcSetPort( tr_rpc_server * server,
563               tr_port         port )
564{
565    if( server->port != port )
566    {
567        server->port = port;
568
569        if( server->isEnabled )
570            tr_runInEventThread( server->session, restartServer, server );
571    }
572}
573
574tr_port
575tr_rpcGetPort( const tr_rpc_server * server )
576{
577    return server->port;
578}
579
580void
581tr_rpcSetWhitelist( tr_rpc_server * server,
582                    const char    * whitelistStr )
583{
584    void * tmp;
585    const char * walk;
586
587    /* keep the string */
588    tr_free( server->whitelistStr );
589    server->whitelistStr = tr_strdup( whitelistStr );
590
591    /* clear out the old whitelist entries */
592    while(( tmp = tr_list_pop_front( &server->whitelist )))
593        tr_free( tmp );
594
595    /* build the new whitelist entries */
596    for( walk=whitelistStr; walk && *walk; ) {
597        const char * delimiters = " ,;";
598        const size_t len = strcspn( walk, delimiters );
599        char * token = tr_strndup( walk, len );
600        tr_list_append( &server->whitelist, token );
601        tr_ninf( MY_NAME, "Adding address to whitelist: [%s]", token );
602        if( walk[len]=='\0' )
603            break;
604        walk += len + 1;
605    }
606}
607
608char*
609tr_rpcGetWhitelist( const tr_rpc_server * server )
610{
611    return tr_strdup( server->whitelistStr ? server->whitelistStr : "" );
612}
613
614void
615tr_rpcSetWhitelistEnabled( tr_rpc_server  * server,
616                           tr_bool          isEnabled )
617{
618    server->isWhitelistEnabled = isEnabled != 0;
619}
620
621tr_bool
622tr_rpcGetWhitelistEnabled( const tr_rpc_server * server )
623{
624    return server->isWhitelistEnabled;
625}
626
627/****
628*****  PASSWORD
629****/
630
631void
632tr_rpcSetUsername( tr_rpc_server * server,
633                   const char *    username )
634{
635    tr_free( server->username );
636    server->username = tr_strdup( username );
637    dbgmsg( "setting our Username to [%s]", server->username );
638}
639
640char*
641tr_rpcGetUsername( const tr_rpc_server * server )
642{
643    return tr_strdup( server->username ? server->username : "" );
644}
645
646void
647tr_rpcSetPassword( tr_rpc_server * server,
648                   const char *    password )
649{
650    tr_free( server->password );
651    server->password = tr_strdup( password );
652    dbgmsg( "setting our Password to [%s]", server->password );
653}
654
655char*
656tr_rpcGetPassword( const tr_rpc_server * server )
657{
658    return tr_strdup( server->password ? server->password : "" );
659}
660
661void
662tr_rpcSetPasswordEnabled( tr_rpc_server * server,
663                          tr_bool          isEnabled )
664{
665    server->isPasswordEnabled = isEnabled;
666    dbgmsg( "setting 'password enabled' to %d", (int)isEnabled );
667}
668
669tr_bool
670tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
671{
672    return server->isPasswordEnabled;
673}
674
675/****
676*****  LIFE CYCLE
677****/
678
679static void
680closeServer( void * vserver )
681{
682    void * tmp;
683    tr_rpc_server * s = vserver;
684
685    stopServer( s );
686    while(( tmp = tr_list_pop_front( &s->whitelist )))
687        tr_free( tmp );
688    tr_free( s->whitelistStr );
689    tr_free( s->username );
690    tr_free( s->password );
691    tr_free( s );
692}
693
694void
695tr_rpcClose( tr_rpc_server ** ps )
696{
697    tr_runInEventThread( ( *ps )->session, closeServer, *ps );
698    *ps = NULL;
699}
700
701tr_rpc_server *
702tr_rpcInit( tr_session  * session,
703            tr_bool       isEnabled,
704            tr_port       port,
705            tr_bool       isWhitelistEnabled,
706            const char  * whitelist,
707            tr_bool       isPasswordEnabled,
708            const char  * username,
709            const char  * password )
710{
711    tr_rpc_server * s;
712
713    s = tr_new0( tr_rpc_server, 1 );
714    s->session = session;
715    s->port = port;
716    s->username = tr_strdup( username );
717    s->password = tr_strdup( password );
718    s->isWhitelistEnabled = isWhitelistEnabled;
719    s->isPasswordEnabled = isPasswordEnabled;
720    s->isEnabled = isEnabled != 0;
721    tr_rpcSetWhitelist( s, whitelist ? whitelist : "127.0.0.1" );
722    if( isEnabled )
723        tr_runInEventThread( session, startServer, s );
724
725    if( isEnabled )
726    {
727        tr_ninf( MY_NAME, _( "Serving RPC and Web requests on port %d" ), (int)port );
728
729        if( isWhitelistEnabled )
730            tr_ninf( MY_NAME, _( "Whitelist enabled" ) );
731
732        if( isPasswordEnabled )
733            tr_ninf( MY_NAME, _( "Password required" ) );
734    }
735
736    return s;
737}
Note: See TracBrowser for help on using the repository browser.