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

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

(trunk libT) have a pool of reusable evbuffers

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