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

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

(libT) more win32 work copied from spry's code:
(1) add #define for {read,write,strncasecmp} -> {_read,_write,_strnicmp}
(2) fix a couple more variadic macros that didn't get fixed in the last commit
(3) use evutil_strtoll() instead of strtoll()

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