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

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

(trunk libT) add yet another ACL diagnostic message

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