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

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

(trunk libT) patch from wereHamster to recycle the zlib deflate stream s.t. we don't have to keep reallocating it

  • Property svn:keywords set to Date Rev Author Id
File size: 21.1 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 7550 2008-12-30 20:40:48Z 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#ifdef HAVE_ZLIB
62    z_stream           stream;
63#endif
64};
65
66#define dbgmsg( ... ) \
67    do { \
68        if( tr_deepLoggingIsActive( ) ) \
69            tr_deepLog( __FILE__, __LINE__, MY_NAME, __VA_ARGS__ ); \
70    } while( 0 )
71
72
73/**
74***
75**/
76
77static void
78send_simple_response( struct evhttp_request * req,
79                      int                     code,
80                      const char *            text )
81{
82    const char *      code_text = tr_webGetResponseStr( code );
83    struct evbuffer * body = tr_getBuffer( );
84
85    evbuffer_add_printf( body, "<h1>%d: %s</h1>", code, code_text );
86    if( text )
87        evbuffer_add_printf( body, "%s", text );
88    evhttp_send_reply( req, code, code_text, body );
89
90    tr_releaseBuffer( body );
91}
92
93static const char*
94tr_memmem( const char * s1,
95           size_t       l1,
96           const char * s2,
97           size_t       l2 )
98{
99    if( !l2 ) return s1;
100    while( l1 >= l2 )
101    {
102        l1--;
103        if( !memcmp( s1, s2, l2 ) )
104            return s1;
105        s1++;
106    }
107
108    return NULL;
109}
110
111static void
112handle_upload( struct evhttp_request * req,
113               struct tr_rpc_server *  server )
114{
115    if( req->type != EVHTTP_REQ_POST )
116    {
117        send_simple_response( req, 405, NULL );
118    }
119    else
120    {
121        const char * content_type = evhttp_find_header( req->input_headers,
122                                                        "Content-Type" );
123
124        const char * query = strchr( req->uri, '?' );
125        const int    paused = query && strstr( query + 1, "paused=true" );
126
127        const char * in = (const char *) EVBUFFER_DATA( req->input_buffer );
128        size_t       inlen = EVBUFFER_LENGTH( req->input_buffer );
129
130        const char * boundary_key = "boundary=";
131        const char * boundary_key_begin = strstr( content_type,
132                                                  boundary_key );
133        const char * boundary_val =
134            boundary_key_begin ? boundary_key_begin +
135            strlen( boundary_key ) : "arglebargle";
136
137        char *       boundary = tr_strdup_printf( "--%s", boundary_val );
138        const size_t boundary_len = strlen( boundary );
139
140        const char * delim = tr_memmem( in, inlen, boundary, boundary_len );
141        while( delim )
142        {
143            size_t       part_len;
144            const char * part = delim + boundary_len;
145            inlen -= ( part - in );
146            in = part;
147            delim = tr_memmem( in, inlen, boundary, boundary_len );
148            part_len = delim ? (size_t)( delim - part ) : inlen;
149
150            if( part_len )
151            {
152                char * text = tr_strndup( part, part_len );
153                if( strstr( text, "filename=\"" ) )
154                {
155                    const char * body = strstr( text, "\r\n\r\n" );
156                    if( body )
157                    {
158                        char *  b64, *json, *freeme;
159                        int     json_len;
160                        size_t  body_len;
161                        tr_benc top, *args;
162
163                        body += 4; /* walk past the \r\n\r\n */
164                        body_len = part_len - ( body - text );
165                        if( body_len >= 2
166                          && !memcmp( &body[body_len - 2], "\r\n", 2 ) )
167                            body_len -= 2;
168
169                        tr_bencInitDict( &top, 2 );
170                        args = tr_bencDictAddDict( &top, "arguments", 2 );
171                        tr_bencDictAddStr( &top, "method", "torrent-add" );
172                        b64 = tr_base64_encode( body, body_len, NULL );
173                        tr_bencDictAddStr( args, "metainfo", b64 );
174                        tr_bencDictAddInt( args, "paused", paused );
175                        json = tr_bencSaveAsJSON( &top, &json_len );
176                        freeme = tr_rpc_request_exec_json( server->session,
177                                                           json, json_len,
178                                                           NULL );
179
180                        tr_free( freeme );
181                        tr_free( json );
182                        tr_free( b64 );
183                        tr_bencFree( &top );
184                    }
185                }
186                tr_free( text );
187            }
188        }
189
190        tr_free( boundary );
191
192        /* use xml here because json responses to file uploads is trouble.
193         * see http://www.malsup.com/jquery/form/#sample7 for details */
194        evhttp_add_header( req->output_headers, "Content-Type",
195                           "text/xml; charset=UTF-8" );
196        send_simple_response( req, HTTP_OK, NULL );
197    }
198}
199
200static const char*
201mimetype_guess( const char * path )
202{
203    unsigned int i;
204
205    const struct
206    {
207        const char *    suffix;
208        const char *    mime_type;
209    } types[] = {
210        /* these are just the ones we need for serving clutch... */
211        { "css",  "text/css"                  },
212        { "gif",  "image/gif"                 },
213        { "html", "text/html"                 },
214        { "ico",  "image/vnd.microsoft.icon"  },
215        { "js",   "application/javascript"    },
216        { "png",  "image/png"                 }
217    };
218    const char * dot = strrchr( path, '.' );
219
220    for( i = 0; dot && i < TR_N_ELEMENTS( types ); ++i )
221        if( !strcmp( dot + 1, types[i].suffix ) )
222            return types[i].mime_type;
223
224    return "application/octet-stream";
225}
226
227static void
228add_response( struct evhttp_request * req,
229              struct tr_rpc_server *  server,
230              struct evbuffer *       out,
231              const void *            content,
232              size_t                  content_len )
233{
234#ifndef HAVE_ZLIB
235    evbuffer_add( out, content, content_len );
236#else
237    const char * key = "Accept-Encoding";
238    const char * encoding = evhttp_find_header( req->input_headers, key );
239    const int do_deflate = encoding && strstr( encoding, "deflate" );
240
241    if( !do_deflate )
242    {
243        evbuffer_add( out, content, content_len );
244    }
245    else
246    {
247        int state;
248
249        server->stream.next_in = (Bytef*) content;
250        server->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        server->stream.next_out = EVBUFFER_DATA( out );
257        server->stream.avail_out = content_len;
258
259        state = deflate( &server->stream, Z_FINISH );
260
261        if( state == Z_STREAM_END )
262        {
263            EVBUFFER_LENGTH( out ) = content_len - server->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        deflateReset( &server->stream );
290    }
291#endif
292}
293
294static void
295serve_file( struct evhttp_request * req,
296            struct tr_rpc_server *  server,
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, server, 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, server, 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, server, 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#ifdef HAVE_ZLIB
695    deflateEnd( &s->stream );
696#endif
697    tr_free( s->whitelistStr );
698    tr_free( s->username );
699    tr_free( s->password );
700    tr_free( s );
701}
702
703void
704tr_rpcClose( tr_rpc_server ** ps )
705{
706    tr_runInEventThread( ( *ps )->session, closeServer, *ps );
707    *ps = NULL;
708}
709
710tr_rpc_server *
711tr_rpcInit( tr_session  * session,
712            tr_bool       isEnabled,
713            tr_port       port,
714            tr_bool       isWhitelistEnabled,
715            const char  * whitelist,
716            tr_bool       isPasswordEnabled,
717            const char  * username,
718            const char  * password )
719{
720    tr_rpc_server * s;
721
722    s = tr_new0( tr_rpc_server, 1 );
723    s->session = session;
724    s->port = port;
725    s->username = tr_strdup( username );
726    s->password = tr_strdup( password );
727    s->isWhitelistEnabled = isWhitelistEnabled;
728    s->isPasswordEnabled = isPasswordEnabled;
729    s->isEnabled = isEnabled != 0;
730    tr_rpcSetWhitelist( s, whitelist ? whitelist : "127.0.0.1" );
731
732#ifdef HAVE_ZLIB
733    s->stream.zalloc = (alloc_func) Z_NULL;
734    s->stream.zfree = (free_func) Z_NULL;
735    s->stream.opaque = (voidpf) Z_NULL;
736    deflateInit( &s->stream, Z_BEST_COMPRESSION );
737#endif
738
739    if( isEnabled )
740        tr_runInEventThread( session, startServer, s );
741
742    if( isEnabled )
743    {
744        tr_ninf( MY_NAME, _( "Serving RPC and Web requests on port %d" ), (int)port );
745
746        if( isWhitelistEnabled )
747            tr_ninf( MY_NAME, _( "Whitelist enabled" ) );
748
749        if( isPasswordEnabled )
750            tr_ninf( MY_NAME, _( "Password required" ) );
751    }
752
753    return s;
754}
Note: See TracBrowser for help on using the repository browser.