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

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

(rpc) if libt can't find the Clutch files, give a helpful 404 message for end-users and binary packagers about how to use CLUTCH_HOME and PACKAGE_DATA_DIR.

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