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

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

(libT) low-hanging fruit discovered from softwareelves' shark profile.

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