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

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

(daemon) when serving deflated output, strip off the first 2 and last 4 bytes from deflate()'s output, for reasons described at http://carsten.codimi.de/gzip.yaws/

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