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

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

(trunk) #1559: Simplify tr_sessionInitFull

  • Property svn:keywords set to Date Rev Author Id
File size: 19.5 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 7367 2008-12-13 23:17:36Z 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_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_ZLIB
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
264            /* http://carsten.codimi.de/gzip.yaws/
265               It turns out that some browsers expect deflated data without
266               the first two bytes (a kind of header) and and the last four
267               bytes (an ADLER32 checksum). This format can of course
268               be produced by simply stripping these off. */
269            if( EVBUFFER_LENGTH( out ) >= 6 ) {
270                EVBUFFER_LENGTH( out ) -= 4;
271                evbuffer_drain( out, 2 );
272            }
273
274#if 0
275            tr_ninf( MY_NAME, _( "Deflated response from %zu bytes to %zu" ),
276                              content_len,
277                              EVBUFFER_LENGTH( out ) );
278#endif
279            evhttp_add_header( req->output_headers,
280                               "Content-Encoding", "deflate" );
281        }
282        else
283        {
284            evbuffer_drain( out, EVBUFFER_LENGTH( out ) );
285            evbuffer_add( out, content, content_len );
286        }
287
288        deflateEnd( &stream );
289    }
290#endif
291}
292
293static void
294serve_file( struct evhttp_request * req,
295            const char *            filename )
296{
297    if( req->type != EVHTTP_REQ_GET )
298    {
299        evhttp_add_header( req->output_headers, "Allow", "GET" );
300        send_simple_response( req, 405, NULL );
301    }
302    else
303    {
304        size_t content_len;
305        uint8_t * content;
306        const int error = errno;
307
308        errno = 0;
309        content_len = 0;
310        content = tr_loadFile( filename, &content_len );
311
312        if( errno )
313        {
314            send_simple_response( req, HTTP_NOTFOUND, NULL );
315        }
316        else
317        {
318            struct evbuffer * out;
319
320            errno = error;
321            out = evbuffer_new( );
322            evhttp_add_header( req->output_headers, "Content-Type",
323                               mimetype_guess( filename ) );
324            add_response( req, out, content, content_len );
325            evhttp_send_reply( req, HTTP_OK, "OK", out );
326
327            evbuffer_free( out );
328            tr_free( content );
329        }
330    }
331}
332
333static void
334handle_clutch( struct evhttp_request * req,
335               struct tr_rpc_server *  server )
336{
337    const char * clutchDir = tr_getClutchDir( server->session );
338
339    assert( !strncmp( req->uri, "/transmission/web/", 18 ) );
340
341    if( !clutchDir || !*clutchDir )
342    {
343        send_simple_response( req, HTTP_NOTFOUND,
344            "<p>Couldn't find Transmission's web interface files!</p>"
345            "<p>Users: to tell Transmission where to look, "
346            "set the TRANSMISSION_WEB_HOME environmental "
347            "variable to the folder where the web interface's "
348            "index.html is located.</p>"
349            "<p>Package Builders: to set a custom default at compile time, "
350            "#define PACKAGE_DATA_DIR in libtransmission/platform.c "
351            "or tweak tr_getClutchDir() by hand.</p>" );
352    }
353    else
354    {
355        char * pch;
356        char * subpath;
357        char * filename;
358
359        subpath = tr_strdup( req->uri + 18 );
360        if(( pch = strchr( subpath, '?' )))
361            *pch = '\0';
362
363        filename = tr_strdup_printf( "%s%s%s",
364                       clutchDir,
365                       TR_PATH_DELIMITER_STR,
366                       subpath && *subpath ? subpath : "index.html" );
367
368        serve_file( req, filename );
369
370        tr_free( filename );
371        tr_free( subpath );
372    }
373}
374
375static void
376handle_rpc( struct evhttp_request * req,
377            struct tr_rpc_server *  server )
378{
379    int               len = 0;
380    char *            out = NULL;
381    struct evbuffer * buf;
382
383    if( req->type == EVHTTP_REQ_GET )
384    {
385        const char * q;
386        if( ( q = strchr( req->uri, '?' ) ) )
387            out = tr_rpc_request_exec_uri( server->session,
388                                           q + 1,
389                                           strlen( q + 1 ),
390                                           &len );
391    }
392    else if( req->type == EVHTTP_REQ_POST )
393    {
394        out = tr_rpc_request_exec_json( server->session,
395                                        EVBUFFER_DATA( req->input_buffer ),
396                                        EVBUFFER_LENGTH( req->input_buffer ),
397                                        &len );
398    }
399
400    buf = evbuffer_new( );
401    add_response( req, buf, out, len );
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
406    /* cleanup */
407    evbuffer_free( buf );
408    tr_free( out );
409}
410
411static int
412isAddressAllowed( const tr_rpc_server * server,
413                  const char *          address )
414{
415    const char * str;
416
417    if( !server->isWhitelistEnabled )
418        return 1;
419
420    for( str = server->whitelist; str && *str; )
421    {
422        const char * delimiter = strchr( str, ',' );
423        const int    len = delimiter ? delimiter - str : (int)strlen( str );
424        char *       token = tr_strndup( str, len );
425        const int    match = tr_wildmat( address, token );
426        tr_free( token );
427        if( match )
428            return 1;
429        if( !delimiter )
430            break;
431        str = delimiter + 1;
432    }
433
434    return 0;
435}
436
437static void
438handle_request( struct evhttp_request * req,
439                void *                  arg )
440{
441    struct tr_rpc_server * server = arg;
442
443    if( req && req->evcon )
444    {
445        const char * auth;
446        char *       user = NULL;
447        char *       pass = NULL;
448
449        evhttp_add_header( req->output_headers, "Server", MY_REALM );
450
451        auth = evhttp_find_header( req->input_headers, "Authorization" );
452
453        if( auth && !strncasecmp( auth, "basic ", 6 ) )
454        {
455            int    plen;
456            char * p = tr_base64_decode( auth + 6, 0, &plen );
457            if( p && plen && ( ( pass = strchr( p, ':' ) ) ) )
458            {
459                user = p;
460                *pass++ = '\0';
461            }
462        }
463
464        if( !isAddressAllowed( server, req->remote_host ) )
465        {
466            send_simple_response( req, 401, "Unauthorized IP Address" );
467        }
468        else if( server->isPasswordEnabled
469                 && ( !pass || !user || strcmp( server->username, user )
470                                     || strcmp( server->password, pass ) ) )
471        {
472            evhttp_add_header( req->output_headers,
473                               "WWW-Authenticate",
474                               "Basic realm=\"" MY_REALM "\"" );
475            send_simple_response( req, 401, "Unauthorized User" );
476        }
477        else if( !strcmp( req->uri, "/transmission/web" )
478               || !strcmp( req->uri, "/transmission/clutch" )
479               || !strcmp( req->uri, "/" ) )
480        {
481            evhttp_add_header( req->output_headers, "Location",
482                               "/transmission/web/" );
483            send_simple_response( req, HTTP_MOVEPERM, NULL );
484        }
485        else if( !strncmp( req->uri, "/transmission/web/", 18 ) )
486        {
487            handle_clutch( req, server );
488        }
489        else if( !strncmp( req->uri, "/transmission/rpc", 17 ) )
490        {
491            handle_rpc( req, server );
492        }
493        else if( !strncmp( req->uri, "/transmission/upload", 20 ) )
494        {
495            handle_upload( req, server );
496        }
497        else
498        {
499            send_simple_response( req, HTTP_NOTFOUND, NULL );
500        }
501
502        tr_free( user );
503    }
504}
505
506static void
507startServer( void * vserver )
508{
509    tr_rpc_server * server  = vserver;
510
511    if( !server->httpd )
512    {
513        server->httpd = evhttp_new( tr_eventGetBase( server->session ) );
514        evhttp_bind_socket( server->httpd, "0.0.0.0", server->port );
515        evhttp_set_gencb( server->httpd, handle_request, server );
516    }
517}
518
519static void
520stopServer( tr_rpc_server * server )
521{
522    if( server->httpd )
523    {
524        evhttp_free( server->httpd );
525        server->httpd = NULL;
526    }
527}
528
529static void
530onEnabledChanged( void * vserver )
531{
532    tr_rpc_server * server = vserver;
533
534    if( !server->isEnabled )
535        stopServer( server );
536    else
537        startServer( server );
538}
539
540void
541tr_rpcSetEnabled( tr_rpc_server * server,
542                  int             isEnabled )
543{
544    server->isEnabled = isEnabled != 0;
545
546    tr_runInEventThread( server->session, onEnabledChanged, server );
547}
548
549int
550tr_rpcIsEnabled( const tr_rpc_server * server )
551{
552    return server->isEnabled;
553}
554
555static void
556restartServer( void * vserver )
557{
558    tr_rpc_server * server = vserver;
559
560    if( server->isEnabled )
561    {
562        stopServer( server );
563        startServer( server );
564    }
565}
566
567void
568tr_rpcSetPort( tr_rpc_server * server,
569               tr_port         port )
570{
571    if( server->port != port )
572    {
573        server->port = port;
574
575        if( server->isEnabled )
576            tr_runInEventThread( server->session, restartServer, server );
577    }
578}
579
580tr_port
581tr_rpcGetPort( const tr_rpc_server * server )
582{
583    return server->port;
584}
585
586void
587tr_rpcSetWhitelist( tr_rpc_server * server,
588                    const char *    whitelist )
589{
590    tr_free( server->whitelist );
591    server->whitelist = tr_strdup( whitelist );
592}
593
594char*
595tr_rpcGetWhitelist( const tr_rpc_server * server )
596{
597    return tr_strdup( server->whitelist ? server->whitelist : "" );
598}
599
600void
601tr_rpcSetWhitelistEnabled( tr_rpc_server  * server,
602                           int              isEnabled )
603{
604    server->isWhitelistEnabled = isEnabled != 0;
605}
606
607int
608tr_rpcGetWhitelistEnabled( const tr_rpc_server * server )
609{
610    return server->isWhitelistEnabled;
611}
612
613/****
614*****  PASSWORD
615****/
616
617void
618tr_rpcSetUsername( tr_rpc_server * server,
619                   const char *    username )
620{
621    tr_free( server->username );
622    server->username = tr_strdup( username );
623    dbgmsg( "setting our Username to [%s]", server->username );
624}
625
626char*
627tr_rpcGetUsername( const tr_rpc_server * server )
628{
629    return tr_strdup( server->username ? server->username : "" );
630}
631
632void
633tr_rpcSetPassword( tr_rpc_server * server,
634                   const char *    password )
635{
636    tr_free( server->password );
637    server->password = tr_strdup( password );
638    dbgmsg( "setting our Password to [%s]", server->password );
639}
640
641char*
642tr_rpcGetPassword( const tr_rpc_server * server )
643{
644    return tr_strdup( server->password ? server->password : "" );
645}
646
647void
648tr_rpcSetPasswordEnabled( tr_rpc_server * server,
649                          int             isEnabled )
650{
651    server->isPasswordEnabled = isEnabled != 0;
652    dbgmsg( "setting 'password enabled' to %d", isEnabled );
653}
654
655int
656tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
657{
658    return server->isPasswordEnabled;
659}
660
661/****
662*****  LIFE CYCLE
663****/
664
665static void
666closeServer( void * vserver )
667{
668    tr_rpc_server * s = vserver;
669
670    stopServer( s );
671    tr_free( s->whitelist );
672    tr_free( s->username );
673    tr_free( s->password );
674    tr_free( s );
675}
676
677void
678tr_rpcClose( tr_rpc_server ** ps )
679{
680    tr_runInEventThread( ( *ps )->session, closeServer, *ps );
681    *ps = NULL;
682}
683
684tr_rpc_server *
685tr_rpcInit( tr_handle *  session,
686            int          isEnabled,
687            tr_port      port,
688            int          isWhitelistEnabled,
689            const char * whitelist,
690            int          isPasswordEnabled,
691            const char * username,
692            const char * password )
693{
694    tr_rpc_server * s;
695
696    s = tr_new0( tr_rpc_server, 1 );
697    s->session = session;
698    s->port = port;
699    s->whitelist = tr_strdup( whitelist && *whitelist ? whitelist : "127.0.0.1" );
700    s->username = tr_strdup( username );
701    s->password = tr_strdup( password );
702    s->isWhitelistEnabled = isWhitelistEnabled != 0;
703    s->isPasswordEnabled = isPasswordEnabled != 0;
704    s->isEnabled = isEnabled != 0;
705    if( isEnabled )
706        tr_runInEventThread( session, startServer, s );
707    return s;
708}
Note: See TracBrowser for help on using the repository browser.