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

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

(trunk libT) one more ACL warning

  • Property svn:keywords set to Date Rev Author Id
File size: 20.7 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 7451 2008-12-21 19:35:38Z 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        if( strcspn( token, "+-" ) < len )
602            tr_ninf( MY_NAME, "Adding address to whitelist: %s (And it has a '+' or '-'!  Are you using an old ACL by mistake?)", token );
603        else
604            tr_ninf( MY_NAME, "Adding address to whitelist: %s", token );
605       
606        if( walk[len]=='\0' )
607            break;
608        walk += len + 1;
609    }
610}
611
612char*
613tr_rpcGetWhitelist( const tr_rpc_server * server )
614{
615    return tr_strdup( server->whitelistStr ? server->whitelistStr : "" );
616}
617
618void
619tr_rpcSetWhitelistEnabled( tr_rpc_server  * server,
620                           tr_bool          isEnabled )
621{
622    server->isWhitelistEnabled = isEnabled != 0;
623}
624
625tr_bool
626tr_rpcGetWhitelistEnabled( const tr_rpc_server * server )
627{
628    return server->isWhitelistEnabled;
629}
630
631/****
632*****  PASSWORD
633****/
634
635void
636tr_rpcSetUsername( tr_rpc_server * server,
637                   const char *    username )
638{
639    tr_free( server->username );
640    server->username = tr_strdup( username );
641    dbgmsg( "setting our Username to [%s]", server->username );
642}
643
644char*
645tr_rpcGetUsername( const tr_rpc_server * server )
646{
647    return tr_strdup( server->username ? server->username : "" );
648}
649
650void
651tr_rpcSetPassword( tr_rpc_server * server,
652                   const char *    password )
653{
654    tr_free( server->password );
655    server->password = tr_strdup( password );
656    dbgmsg( "setting our Password to [%s]", server->password );
657}
658
659char*
660tr_rpcGetPassword( const tr_rpc_server * server )
661{
662    return tr_strdup( server->password ? server->password : "" );
663}
664
665void
666tr_rpcSetPasswordEnabled( tr_rpc_server * server,
667                          tr_bool          isEnabled )
668{
669    server->isPasswordEnabled = isEnabled;
670    dbgmsg( "setting 'password enabled' to %d", (int)isEnabled );
671}
672
673tr_bool
674tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
675{
676    return server->isPasswordEnabled;
677}
678
679/****
680*****  LIFE CYCLE
681****/
682
683static void
684closeServer( void * vserver )
685{
686    void * tmp;
687    tr_rpc_server * s = vserver;
688
689    stopServer( s );
690    while(( tmp = tr_list_pop_front( &s->whitelist )))
691        tr_free( tmp );
692    tr_free( s->whitelistStr );
693    tr_free( s->username );
694    tr_free( s->password );
695    tr_free( s );
696}
697
698void
699tr_rpcClose( tr_rpc_server ** ps )
700{
701    tr_runInEventThread( ( *ps )->session, closeServer, *ps );
702    *ps = NULL;
703}
704
705tr_rpc_server *
706tr_rpcInit( tr_session  * session,
707            tr_bool       isEnabled,
708            tr_port       port,
709            tr_bool       isWhitelistEnabled,
710            const char  * whitelist,
711            tr_bool       isPasswordEnabled,
712            const char  * username,
713            const char  * password )
714{
715    tr_rpc_server * s;
716
717    s = tr_new0( tr_rpc_server, 1 );
718    s->session = session;
719    s->port = port;
720    s->username = tr_strdup( username );
721    s->password = tr_strdup( password );
722    s->isWhitelistEnabled = isWhitelistEnabled;
723    s->isPasswordEnabled = isPasswordEnabled;
724    s->isEnabled = isEnabled != 0;
725    tr_rpcSetWhitelist( s, whitelist ? whitelist : "127.0.0.1" );
726    if( isEnabled )
727        tr_runInEventThread( session, startServer, s );
728
729    if( isEnabled )
730    {
731        tr_ninf( MY_NAME, _( "Serving RPC and Web requests on port %d" ), (int)port );
732
733        if( isWhitelistEnabled )
734            tr_ninf( MY_NAME, _( "Whitelist enabled" ) );
735
736        if( isPasswordEnabled )
737            tr_ninf( MY_NAME, _( "Password required" ) );
738    }
739
740    return s;
741}
Note: See TracBrowser for help on using the repository browser.