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

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

(rpc) fix rpc server memory leak that snuck in over the past few days' switch from shttpd to evhttp

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