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

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

(libT) experimental code to serve clutch/rpc via evhttpd

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