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

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

#1309: Web/RPC interface ACL ignored

  • Property svn:keywords set to Date Rev Author Id
File size: 18.0 KB
Line 
1/*
2 * This file Copyright (C) 2008 Charles Kerr <charles@rebelbase.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 6823 2008-10-01 20:23:57Z charles $
11 */
12
13#include <assert.h>
14#include <string.h> /* memcpy */
15#include <limits.h> /* INT_MAX */
16
17#include <sys/types.h> /* open */
18#include <sys/stat.h>  /* open */
19#include <fcntl.h>     /* open */
20#include <unistd.h>    /* close */
21
22#ifdef HAVE_LIBZ
23 #include <zlib.h>
24#endif
25
26#include <libevent/event.h>
27#include <libevent/evhttp.h>
28
29#include "transmission.h"
30#include "bencode.h"
31#include "list.h"
32#include "platform.h"
33#include "rpcimpl.h"
34#include "rpc-server.h"
35#include "trevent.h"
36#include "utils.h"
37#include "web.h"
38
39#define MY_NAME "RPC Server"
40#define MY_REALM "Transmission"
41#define TR_N_ELEMENTS( ary ) ( sizeof( ary ) / sizeof( *ary ) )
42
43struct tr_rpc_server
44{
45    unsigned int       isEnabled         : 1;
46    unsigned int       isPasswordEnabled : 1;
47    uint16_t           port;
48    struct evhttp *    httpd;
49    tr_handle *        session;
50    char *             username;
51    char *             password;
52    char *             whitelist;
53};
54
55#define dbgmsg( fmt ... ) tr_deepLog( __FILE__, __LINE__, MY_NAME, ## fmt )
56
57/**
58***
59**/
60
61static void
62send_simple_response( struct evhttp_request * req,
63                      int                     code,
64                      const char *            text )
65{
66    const char *      code_text = tr_webGetResponseStr( code );
67    struct evbuffer * body = evbuffer_new( );
68
69    evbuffer_add_printf( body, "<h1>%s</h1>", code_text );
70    if( text )
71        evbuffer_add_printf( body, "<h2>%s</h2>", text );
72    evhttp_send_reply( req, code, code_text, body );
73    evbuffer_free( body );
74}
75
76static const char*
77tr_memmem( const char * s1,
78           size_t       l1,
79           const char * s2,
80           size_t       l2 )
81{
82    if( !l2 ) return s1;
83    while( l1 >= l2 )
84    {
85        l1--;
86        if( !memcmp( s1, s2, l2 ) )
87            return s1;
88        s1++;
89    }
90
91    return NULL;
92}
93
94static void
95handle_upload( struct evhttp_request * req,
96               struct tr_rpc_server *  server )
97{
98    if( req->type != EVHTTP_REQ_POST )
99    {
100        send_simple_response( req, 405, NULL );
101    }
102    else
103    {
104        const char * content_type = evhttp_find_header( req->input_headers,
105                                                        "Content-Type" );
106
107        const char * query = strchr( req->uri, '?' );
108        const int    paused = query && strstr( query + 1, "paused=true" );
109
110        const char * in = (const char *) EVBUFFER_DATA( req->input_buffer );
111        size_t       inlen = EVBUFFER_LENGTH( req->input_buffer );
112
113        const char * boundary_key = "boundary=";
114        const char * boundary_key_begin = strstr( content_type,
115                                                  boundary_key );
116        const char * boundary_val =
117            boundary_key_begin ? boundary_key_begin +
118            strlen( boundary_key ) : "arglebargle";
119
120        char *       boundary = tr_strdup_printf( "--%s", boundary_val );
121        const size_t boundary_len = strlen( boundary );
122
123        const char * delim = tr_memmem( in, inlen, boundary, boundary_len );
124        while( delim )
125        {
126            size_t       part_len;
127            const char * part = delim + boundary_len;
128            inlen -= ( part - in );
129            in = part;
130            delim = tr_memmem( in, inlen, boundary, boundary_len );
131            part_len = delim ? (size_t)( delim - part ) : inlen;
132
133            if( part_len )
134            {
135                char * text = tr_strndup( part, part_len );
136                if( strstr( text, "filename=\"" ) )
137                {
138                    const char * body = strstr( text, "\r\n\r\n" );
139                    if( body )
140                    {
141                        char *  b64, *json, *freeme;
142                        int     json_len;
143                        size_t  body_len;
144                        tr_benc top, *args;
145
146                        body += 4;
147                        body_len = part_len - ( body - text );
148                        if( body_len >= 2
149                          && !memcmp( &body[body_len - 2], "\r\n", 2 ) )
150                            body_len -= 2;
151
152                        tr_bencInitDict( &top, 2 );
153                        args = tr_bencDictAddDict( &top, "arguments", 2 );
154                        tr_bencDictAddStr( &top, "method", "torrent-add" );
155                        b64 = tr_base64_encode( body, body_len, NULL );
156                        tr_bencDictAddStr( args, "metainfo", b64 );
157                        tr_bencDictAddInt( args, "paused", paused );
158                        json = tr_bencSaveAsJSON( &top, &json_len );
159                        freeme = tr_rpc_request_exec_json( server->session,
160                                                           json, json_len,
161                                                           NULL );
162
163                        tr_free( freeme );
164                        tr_free( json );
165                        tr_free( b64 );
166                        tr_bencFree( &top );
167                    }
168                }
169                tr_free( text );
170            }
171        }
172
173        tr_free( boundary );
174
175        /* use xml here because json responses to file uploads is trouble.
176         * see http://www.malsup.com/jquery/form/#sample7 for details */
177        evhttp_add_header( req->output_headers, "Content-Type",
178                           "text/xml; charset=UTF-8" );
179        send_simple_response( req, HTTP_OK, NULL );
180    }
181}
182
183static const char*
184mimetype_guess( const char * path )
185{
186    unsigned int i;
187
188    const struct
189    {
190        const char *    suffix;
191        const char *    mime_type;
192    } types[] = {
193        /* these are just the ones we need for serving clutch... */
194        { "css",  "text/css"                       },
195        { "gif",  "image/gif"                      },
196        { "html", "text/html"                      },
197        { "ico",  "image/vnd.microsoft.icon"       },
198        { "js",   "application/javascript"         },
199        { "png",  "image/png"                      }
200    };
201    const char * dot = strrchr( path, '.' );
202
203    for( i = 0; dot && i < TR_N_ELEMENTS( types ); ++i )
204        if( !strcmp( dot + 1, types[i].suffix ) )
205            return types[i].mime_type;
206
207    return "application/octet-stream";
208}
209
210#ifdef HAVE_LIBZ
211static void
212compress_evbuf( struct evbuffer * evbuf )
213{
214    static struct evbuffer * tmp;
215    static z_stream          stream;
216    static unsigned char     buffer[2048];
217
218    if( !tmp )
219    {
220        tmp = evbuffer_new( );
221        deflateInit( &stream, Z_BEST_COMPRESSION );
222    }
223
224    deflateReset( &stream );
225    stream.next_in = EVBUFFER_DATA( evbuf );
226    stream.avail_in = EVBUFFER_LENGTH( evbuf );
227
228    do
229    {
230        stream.next_out = buffer;
231        stream.avail_out = sizeof( buffer );
232        if( deflate( &stream, Z_FULL_FLUSH ) == Z_OK )
233            evbuffer_add( tmp, buffer, sizeof( buffer ) - stream.avail_out );
234        else
235            break;
236    }
237    while( stream.avail_out == 0 );
238
239/*fprintf( stderr, "deflated response from %zu to %zu bytes\n", EVBUFFER_LENGTH(
240  evbuf ), EVBUFFER_LENGTH( tmp ) );*/
241    evbuffer_drain( evbuf, EVBUFFER_LENGTH( evbuf ) );
242    evbuffer_add_buffer( evbuf, tmp );
243}
244
245#endif
246
247static void
248maybe_deflate_response( struct evhttp_request * req,
249                        struct evbuffer *       response )
250{
251#ifdef HAVE_LIBZ
252    const char * accept_encoding = evhttp_find_header( req->input_headers,
253                                                       "Accept-Encoding" );
254    const int    do_deflate = accept_encoding && strstr( accept_encoding,
255                                                         "deflate" );
256    if( do_deflate )
257    {
258        evhttp_add_header( req->output_headers, "Content-Encoding",
259                           "deflate" );
260        compress_evbuf( response );
261    }
262#endif
263}
264
265static void
266serve_file( struct evhttp_request * req,
267            const char *            path )
268{
269    if( req->type != EVHTTP_REQ_GET )
270    {
271        evhttp_add_header( req->output_headers, "Allow", "GET" );
272        send_simple_response( req, 405, NULL );
273    }
274    else
275    {
276        const int fd = open( path, O_RDONLY, 0 );
277        if( fd == -1 )
278        {
279            send_simple_response( req, HTTP_NOTFOUND, NULL );
280        }
281        else
282        {
283            struct evbuffer * buf = evbuffer_new( );
284            evbuffer_read( buf, fd, INT_MAX );
285            evhttp_add_header( req->output_headers, "Content-Type",
286                              mimetype_guess(
287                                  path ) );
288            maybe_deflate_response( req, buf );
289            evhttp_send_reply( req, HTTP_OK, "OK", buf );
290            evbuffer_free( buf );
291            close( fd );
292        }
293    }
294}
295
296static void
297handle_clutch( struct evhttp_request * req,
298               struct tr_rpc_server *  server )
299{
300    const char *      uri;
301    struct evbuffer * buf = evbuffer_new( );
302
303    assert( !strncmp( req->uri, "/transmission/web/", 18 ) );
304
305    evbuffer_add_printf( buf, "%s%s", tr_getClutchDir(
306                             server->session ), TR_PATH_DELIMITER_STR );
307    uri = req->uri + 18;
308    if( ( *uri == '?' ) || ( *uri == '\0' ) )
309        evbuffer_add_printf( buf, "index.html" );
310    else
311    {
312        const char * pch = strchr( uri, '?' );
313        if( pch )
314            evbuffer_add_printf( buf, "%*.*s", (int)( pch - uri ),
315                                 (int)( pch - uri ), uri );
316        else
317            evbuffer_add_printf( buf, "%s", uri );
318    }
319
320    if( strstr( (const char *)EVBUFFER_DATA( buf ), ".." ) )
321        send_simple_response( req, 401, NULL );
322    else
323        serve_file( req, (const char *)EVBUFFER_DATA( buf ) );
324
325    evbuffer_free( buf );
326}
327
328static void
329handle_rpc( struct evhttp_request * req,
330            struct tr_rpc_server *  server )
331{
332    int               len = 0;
333    char *            response = NULL;
334    struct evbuffer * buf;
335
336    if( req->type == EVHTTP_REQ_GET )
337    {
338        const char * q;
339        if( ( q = strchr( req->uri, '?' ) ) )
340            response = tr_rpc_request_exec_uri( server->session,
341                                                q + 1,
342                                                strlen( q + 1 ),
343                                                &len );
344    }
345    else if( req->type == EVHTTP_REQ_POST )
346    {
347        response = tr_rpc_request_exec_json( server->session,
348                                             EVBUFFER_DATA( req->
349                                                            input_buffer ),
350                                             EVBUFFER_LENGTH( req->
351                                                              input_buffer ),
352                                             &len );
353    }
354
355    buf = evbuffer_new( );
356    evbuffer_add( buf, response, len );
357    maybe_deflate_response( req, buf );
358    evhttp_add_header( req->output_headers, "Content-Type",
359                       "application/json; charset=UTF-8" );
360    evhttp_send_reply( req, HTTP_OK, "OK", buf );
361    evbuffer_free( buf );
362}
363
364static int
365isAddressAllowed( const tr_rpc_server * server,
366                  const char *          address )
367{
368    const char * str;
369
370    for( str = server->whitelist; str && *str; )
371    {
372        const char * delimiter = strchr( str, ',' );
373        const int    len = delimiter ? delimiter - str : (int)strlen( str );
374        char *       token = tr_strndup( str, len );
375        const int    match = tr_wildmat( address, token );
376        tr_free( token );
377        if( match )
378            return 1;
379        if( !delimiter )
380            break;
381        str = delimiter + 1;
382    }
383
384    return 0;
385}
386
387static void
388handle_request( struct evhttp_request * req,
389                void *                  arg )
390{
391    struct tr_rpc_server * server = arg;
392
393    if( req && req->evcon )
394    {
395        const char * auth;
396        char *       user = NULL;
397        char *       pass = NULL;
398
399        evhttp_add_header( req->output_headers, "Server", MY_REALM );
400
401        auth = evhttp_find_header( req->input_headers, "Authorization" );
402
403        if( auth && !strncasecmp( auth, "basic ", 6 ) )
404        {
405            int    plen;
406            char * p = tr_base64_decode( auth + 6, 0, &plen );
407            if( p && plen && ( ( pass = strchr( p, ':' ) ) ) )
408            {
409                user = p;
410                *pass++ = '\0';
411            }
412        }
413
414        if( server->whitelist && !isAddressAllowed( server, req->remote_host ) )
415        {
416            send_simple_response( req, 401, "Unauthorized IP Address" );
417        }
418        else if( server->isPasswordEnabled && ( !pass
419                                              || !user
420                                              || strcmp( server->username,
421                                                         user )
422                                              || strcmp( server->password,
423                                                         pass ) ) )
424        {
425            evhttp_add_header( req->output_headers,
426                               "WWW-Authenticate",
427                               "Basic realm=\"" MY_REALM "\"" );
428            send_simple_response( req, 401, "Unauthorized User" );
429        }
430        else if( !strcmp( req->uri, "/transmission/web" )
431               || !strcmp( req->uri, "/transmission/clutch" )
432               || !strcmp( req->uri, "/" ) )
433        {
434            evhttp_add_header( req->output_headers, "Location",
435                               "/transmission/web/" );
436            send_simple_response( req, HTTP_MOVEPERM, NULL );
437        }
438        else if( !strncmp( req->uri, "/transmission/web/", 18 ) )
439        {
440            handle_clutch( req, server );
441        }
442        else if( !strncmp( req->uri, "/transmission/rpc", 17 ) )
443        {
444            handle_rpc( req, server );
445        }
446        else if( !strncmp( req->uri, "/transmission/upload", 20 ) )
447        {
448            handle_upload( req, server );
449        }
450        else
451        {
452            send_simple_response( req, HTTP_NOTFOUND, NULL );
453        }
454
455        tr_free( user );
456    }
457}
458
459static void
460startServer( void * vserver )
461{
462    tr_rpc_server * server  = vserver;
463
464    if( !server->httpd )
465    {
466        server->httpd = evhttp_new( tr_eventGetBase( server->session ) );
467        evhttp_bind_socket( server->httpd, "0.0.0.0", server->port );
468        evhttp_set_gencb( server->httpd, handle_request, server );
469    }
470}
471
472static void
473stopServer( tr_rpc_server * server )
474{
475    if( server->httpd )
476    {
477        evhttp_free( server->httpd );
478        server->httpd = NULL;
479    }
480}
481
482static void
483onEnabledChanged( void * vserver )
484{
485    tr_rpc_server * server = vserver;
486
487    if( !server->isEnabled )
488        stopServer( server );
489    else
490        startServer( server );
491}
492
493void
494tr_rpcSetEnabled( tr_rpc_server * server,
495                  int             isEnabled )
496{
497    server->isEnabled = isEnabled != 0;
498
499    tr_runInEventThread( server->session, onEnabledChanged, server );
500}
501
502int
503tr_rpcIsEnabled( const tr_rpc_server * server )
504{
505    return server->isEnabled;
506}
507
508static void
509restartServer( void * vserver )
510{
511    tr_rpc_server * server = vserver;
512
513    if( server->isEnabled )
514    {
515        stopServer( server );
516        startServer( server );
517    }
518}
519
520void
521tr_rpcSetPort( tr_rpc_server * server,
522               uint16_t        port )
523{
524    if( server->port != port )
525    {
526        server->port = port;
527
528        if( server->isEnabled )
529            tr_runInEventThread( server->session, restartServer, server );
530    }
531}
532
533uint16_t
534tr_rpcGetPort( const tr_rpc_server * server )
535{
536    return server->port;
537}
538
539void
540tr_rpcSetWhitelist( tr_rpc_server * server,
541                    const char *    whitelist )
542{
543    tr_free( server->whitelist );
544    server->whitelist = tr_strdup( whitelist );
545}
546
547char*
548tr_rpcGetWhitelist( const tr_rpc_server * server )
549{
550    return tr_strdup( server->whitelist ? server->whitelist : "" );
551}
552
553/****
554*****  PASSWORD
555****/
556
557void
558tr_rpcSetUsername( tr_rpc_server * server,
559                   const char *    username )
560{
561    tr_free( server->username );
562    server->username = tr_strdup( username );
563    dbgmsg( "setting our Username to [%s]", server->username );
564}
565
566char*
567tr_rpcGetUsername( const tr_rpc_server * server )
568{
569    return tr_strdup( server->username ? server->username : "" );
570}
571
572void
573tr_rpcSetPassword( tr_rpc_server * server,
574                   const char *    password )
575{
576    tr_free( server->password );
577    server->password = tr_strdup( password );
578    dbgmsg( "setting our Password to [%s]", server->password );
579}
580
581char*
582tr_rpcGetPassword( const tr_rpc_server * server )
583{
584    return tr_strdup( server->password ? server->password : "" );
585}
586
587void
588tr_rpcSetPasswordEnabled( tr_rpc_server * server,
589                          int             isEnabled )
590{
591    server->isPasswordEnabled = isEnabled != 0;
592    dbgmsg( "setting 'password enabled' to %d", isEnabled );
593}
594
595int
596tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
597{
598    return server->isPasswordEnabled;
599}
600
601/****
602*****  LIFE CYCLE
603****/
604
605static void
606closeServer( void * vserver )
607{
608    tr_rpc_server * s = vserver;
609
610    stopServer( s );
611    tr_free( s->whitelist );
612    tr_free( s->username );
613    tr_free( s->password );
614    tr_free( s );
615}
616
617void
618tr_rpcClose( tr_rpc_server ** ps )
619{
620    tr_runInEventThread( ( *ps )->session, closeServer, *ps );
621    *ps = NULL;
622}
623
624tr_rpc_server *
625tr_rpcInit( tr_handle *  session,
626            int          isEnabled,
627            uint16_t     port,
628            const char * whitelist,
629            int          isPasswordEnabled,
630            const char * username,
631            const char * password )
632{
633    tr_rpc_server * s;
634
635    s = tr_new0( tr_rpc_server, 1 );
636    s->session = session;
637    s->port = port;
638    s->whitelist = tr_strdup( whitelist && *whitelist ? whitelist : TR_DEFAULT_RPC_WHITELIST );
639    s->username = tr_strdup( username );
640    s->password = tr_strdup( password );
641    s->isPasswordEnabled = isPasswordEnabled != 0;
642    s->isEnabled = isEnabled != 0;
643    if( isEnabled )
644        tr_runInEventThread( session, startServer, s );
645    return s;
646}
647
Note: See TracBrowser for help on using the repository browser.