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

Last change on this file since 6566 was 6566, checked in by muks, 13 years ago

Add UTF-8 charset to Content-Type HTTP headers

  • Property svn:keywords set to Date Rev Author Id
File size: 20.1 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 6566 2008-08-17 21:16:22Z muks $
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
19#include <unistd.h> /* unlink */
20
21#include <libevent/event.h>
22#include <shttpd/defs.h> /* edit_passwords */
23#include <shttpd/shttpd.h>
24
25#include "transmission.h"
26#include "bencode.h"
27#include "list.h"
28#include "platform.h"
29#include "rpc.h"
30#include "rpc-server.h"
31#include "utils.h"
32
33#define MY_NAME "RPC Server"
34#define MY_REALM "Transmission RPC Server"
35
36#define ACTIVE_INTERVAL_MSEC 40
37#define INACTIVE_INTERVAL_MSEC 200
38
39struct tr_rpc_server
40{
41    int port;
42    time_t lastRequestTime;
43    struct shttpd_ctx * ctx;
44    tr_handle * session;
45    struct event timer;
46    int isPasswordEnabled;
47    char * username;
48    char * password;
49    char * acl;
50    tr_list * connections;
51};
52
53#define dbgmsg(fmt...) tr_deepLog(__FILE__, __LINE__, MY_NAME, ##fmt )
54
55static const char*
56tr_memmem( const char * s1, size_t l1,
57           const char * s2, size_t l2 )
58{
59    if (!l2) return s1;
60    while (l1 >= l2) {
61        l1--;
62        if (!memcmp(s1,s2,l2))
63            return s1;
64        s1++;
65    }
66    return NULL;
67}
68
69/**
70***
71**/
72
73struct ConnBuf
74{
75    char * key;
76    time_t lastActivity;
77    struct evbuffer * in;
78    struct evbuffer * out;
79};
80
81static char*
82buildKey( struct shttpd_arg * arg )
83{
84    return tr_strdup_printf( "%s %s",
85                             shttpd_get_env( arg, "REMOTE_ADDR" ),
86                             shttpd_get_env( arg, "REQUEST_URI" ) );
87}
88
89static struct ConnBuf*
90getBuffer( tr_rpc_server * server, struct shttpd_arg * arg )
91{
92    tr_list * l;
93    char * key = buildKey( arg );
94    struct ConnBuf * found = NULL;
95
96    for( l=server->connections; l && !found; l=l->next )
97    {
98        struct ConnBuf * buf = l->data;
99        if( !strcmp( key, buf->key ) )
100            found = buf;
101    }
102
103    if( found == NULL )
104    {
105        found = tr_new0( struct ConnBuf, 1 );
106        found->lastActivity = time( NULL );
107        found->key = tr_strdup( key );
108        found->in = evbuffer_new( );
109        found->out = evbuffer_new( );
110        tr_list_append( &server->connections, found );
111    }
112
113    tr_free( key );
114    return found;
115}
116
117static void
118pruneBuf( tr_rpc_server * server, struct ConnBuf * buf )
119{
120    tr_list_remove_data( &server->connections, buf );
121
122    evbuffer_free( buf->in );
123    evbuffer_free( buf->out );
124    tr_free( buf->key );
125    tr_free( buf );
126}
127
128/**
129***
130**/
131
132static void
133handle_upload( struct shttpd_arg * arg )
134{
135    struct tr_rpc_server * s;
136    struct ConnBuf * cbuf;
137
138    s = arg->user_data;
139    s->lastRequestTime = time( NULL );
140    cbuf = getBuffer( s, arg );
141
142    /* if we haven't parsed the POST, do that now */
143    if( !EVBUFFER_LENGTH( cbuf->out ) )
144    {
145        const char * query_string;
146        const char * content_type;
147        const char * delim;
148        const char * in;
149        size_t inlen;
150        char * boundary;
151        size_t boundary_len;
152        char buf[64];
153        int paused;
154
155        /* if we haven't finished reading the POST, read more now */
156        evbuffer_add( cbuf->in, arg->in.buf, arg->in.len );
157        arg->in.num_bytes = arg->in.len;
158        if( arg->flags & SHTTPD_MORE_POST_DATA )
159            return;
160
161        query_string = shttpd_get_env( arg, "QUERY_STRING" );
162        content_type = shttpd_get_header( arg, "Content-Type" );
163        in = (const char *) EVBUFFER_DATA( cbuf->in );
164        inlen = EVBUFFER_LENGTH( cbuf->in );
165        boundary = tr_strdup_printf( "--%s", strstr( content_type, "boundary=" ) + strlen( "boundary=" ) );
166        boundary_len = strlen( boundary );
167        paused = ( query_string != NULL )
168              && ( shttpd_get_var( "paused", query_string, strlen( query_string ), buf, sizeof( buf ) ) == 4 )
169              && ( !strcmp( buf, "true" ) );
170
171        delim = tr_memmem( in, inlen, boundary, boundary_len );
172        if( delim ) do
173        {
174            size_t part_len;
175            const char * part = delim + boundary_len;
176            inlen -= ( part - in );
177            in = part;
178            delim = tr_memmem( in, inlen, boundary, boundary_len );
179            part_len = delim ? (size_t)(delim-part) : inlen;
180
181            if( part_len )
182            {
183                char * text = tr_strndup( part, part_len );
184                if( strstr( text, "filename=\"" ) )
185                {
186                    const char * body = strstr( text, "\r\n\r\n" );
187                    if( body )
188                    {
189                        char * b64, *json, *freeme;
190                        int json_len;
191                        size_t body_len;
192                        tr_benc top, *args;
193
194                        body += 4;
195                        body_len = part_len - ( body - text );
196                        if( body_len >= 2 && !memcmp(&body[body_len-2],"\r\n",2) )
197                            body_len -= 2;
198
199                        tr_bencInitDict( &top, 2 );
200                        args = tr_bencDictAddDict( &top, "arguments", 2 );
201                        tr_bencDictAddStr( &top, "method", "torrent-add" );
202                        b64 = tr_base64_encode( body, body_len, NULL );
203                        tr_bencDictAddStr( args, "metainfo", b64 );
204                        tr_bencDictAddInt( args, "paused", paused );
205                        json = tr_bencSaveAsJSON( &top, &json_len );
206                        freeme = tr_rpc_request_exec_json( s->session, json, json_len, NULL );
207
208                        tr_free( freeme );
209                        tr_free( json );
210                        tr_free( b64 );
211                        tr_bencFree( &top );
212                    }
213                }
214                tr_free( text );
215            }
216        }
217        while( delim );
218
219        evbuffer_drain( cbuf->in, EVBUFFER_LENGTH( cbuf->in ) );
220        tr_free( boundary );
221
222        {
223            /* use xml here because json responses to file uploads is trouble.
224             * see http://www.malsup.com/jquery/form/#sample7 for details */
225            const char * response = "<result>success</result>";
226            const int len = strlen( response );
227            evbuffer_add_printf( cbuf->out, "HTTP/1.1 200 OK\r\n"
228                                            "Content-Type: text/xml; charset=UTF-8\r\n"
229                                            "Content-Length: %d\r\n"
230                                            "\r\n"
231                                           "%s\r\n", len, response );
232        }
233    }
234
235    if( EVBUFFER_LENGTH( cbuf->out ) )
236    {
237        const int n = MIN( ( int )EVBUFFER_LENGTH( cbuf->out ), arg->out.len );
238        memcpy( arg->out.buf, EVBUFFER_DATA( cbuf->out ), n );
239        evbuffer_drain( cbuf->out, n );
240        arg->out.num_bytes = n;
241    }
242
243    if( !EVBUFFER_LENGTH( cbuf->out ) )
244    {
245        arg->flags |= SHTTPD_END_OF_OUTPUT;
246        pruneBuf( s, cbuf );
247    }
248}
249
250static void
251handle_root( struct shttpd_arg * arg )
252{
253    const char * redirect = "HTTP/1.1 200 OK""\r\n"
254                            "Content-Type: text/html; charset=UTF-8" "\r\n"
255                            "\r\n"
256                            "<html><head>" "\r\n"
257                            "  <meta http-equiv=\"Refresh\" content=\"2; url=/transmission/web/\">" "\r\n"
258                            "</head><body>" "\r\n"
259                            "  <p>redirecting to <a href=\"/transmission/web\">/transmission/web/</a></p>" "\r\n"
260                            "</body></html>" "\r\n";
261    const size_t n = strlen( redirect );
262    memcpy( arg->out.buf, redirect, n );
263    arg->in.num_bytes = arg->in.len;
264    arg->out.num_bytes = n;
265    arg->flags |= SHTTPD_END_OF_OUTPUT;
266}
267
268static void
269handle_rpc( struct shttpd_arg * arg )
270{
271    struct tr_rpc_server * s;
272    struct ConnBuf * cbuf;
273
274    s = arg->user_data;
275    s->lastRequestTime = time( NULL );
276    cbuf = getBuffer( s, arg );
277
278    if( !EVBUFFER_LENGTH( cbuf->out ) )
279    {
280        int len = 0;
281        char * response = NULL;
282        const char * request_method = shttpd_get_env( arg, "REQUEST_METHOD" );
283        const char * query_string = shttpd_get_env( arg, "QUERY_STRING" );
284
285        if( query_string && *query_string )
286            response = tr_rpc_request_exec_uri( s->session,
287                                                query_string,
288                                                strlen( query_string ),
289                                                &len );
290        else if( !strcmp( request_method, "POST" ) )
291        {
292            evbuffer_add( cbuf->in, arg->in.buf, arg->in.len );
293            arg->in.num_bytes = arg->in.len;
294            if( arg->flags & SHTTPD_MORE_POST_DATA )
295                return;
296            response = tr_rpc_request_exec_json( s->session,
297                                                 EVBUFFER_DATA( cbuf->in ),
298                                                 EVBUFFER_LENGTH( cbuf->in ),
299                                                 &len );
300            evbuffer_drain( cbuf->in, EVBUFFER_LENGTH( cbuf->in ) );
301        }
302
303        evbuffer_add_printf( cbuf->out, "HTTP/1.1 200 OK\r\n"
304                                        "Content-Type: application/json; charset=UTF-8\r\n"
305                                        "Content-Length: %d\r\n"
306                                        "\r\n"
307                                        "%*.*s", len, len, len, response );
308        tr_free( response );
309    }
310
311    if( EVBUFFER_LENGTH( cbuf->out ) )
312    {
313        const int n = MIN( ( int )EVBUFFER_LENGTH( cbuf->out ), arg->out.len );
314        memcpy( arg->out.buf, EVBUFFER_DATA( cbuf->out ), n );
315        evbuffer_drain( cbuf->out, n );
316        arg->out.num_bytes = n;
317    }
318
319    if( !EVBUFFER_LENGTH( cbuf->out ) )
320    {
321        arg->flags |= SHTTPD_END_OF_OUTPUT;
322        pruneBuf( s, cbuf );
323    }
324}
325
326static void
327rpcPulse( int socket UNUSED, short action UNUSED, void * vserver )
328{
329    int interval;
330    struct timeval tv;
331    tr_rpc_server * server = vserver;
332    const time_t now = time( NULL );
333
334    assert( server );
335
336    if( server->ctx )
337        shttpd_poll( server->ctx, 1 );
338
339    /* set a timer for the next pulse */
340    if( now - server->lastRequestTime < 300 )
341        interval = ACTIVE_INTERVAL_MSEC;
342    else
343        interval = INACTIVE_INTERVAL_MSEC;
344    tv = tr_timevalMsec( interval );
345    evtimer_add( &server->timer, &tv );
346}
347
348static void
349getPasswordFile( tr_rpc_server * server, char * buf, int buflen )
350{
351    tr_buildPath( buf, buflen, tr_sessionGetConfigDir( server->session ),
352                               "htpasswd",
353                               NULL );
354}
355
356static void
357startServer( tr_rpc_server * server )
358{
359    dbgmsg( "in startServer; current context is %p", server->ctx );
360
361    if( !server->ctx )
362    {
363        int i;
364        int argc = 0;
365        char * argv[100];
366        char passwd[MAX_PATH_LENGTH];
367        const char * clutchDir = tr_getClutchDir( server->session );
368        struct timeval tv = tr_timevalMsec( INACTIVE_INTERVAL_MSEC );
369
370        getPasswordFile( server, passwd, sizeof( passwd ) );
371        if( !server->isPasswordEnabled )
372            unlink( passwd );
373        else
374            edit_passwords( passwd, MY_REALM, server->username, server->password );
375
376        argv[argc++] = tr_strdup( "appname-unused" );
377
378        argv[argc++] = tr_strdup( "-ports" );
379        argv[argc++] = tr_strdup_printf( "%d", server->port );
380
381        argv[argc++] = tr_strdup( "-dir_list" );
382        argv[argc++] = tr_strdup( "0" );
383
384        argv[argc++] = tr_strdup( "-auth_realm" );
385        argv[argc++] = tr_strdup( MY_REALM );
386
387        argv[argc++] = tr_strdup( "-root" );
388        argv[argc++] = tr_strdup( "/dev/null" );
389
390        if( server->acl )
391        {
392            argv[argc++] = tr_strdup( "-acl" );
393            argv[argc++] = tr_strdup( server->acl );
394        }
395        if( server->isPasswordEnabled )
396        {
397            argv[argc++] = tr_strdup( "-protect" );
398            argv[argc++] = tr_strdup_printf( "/transmission=%s", passwd );
399        }
400        if( clutchDir && *clutchDir )
401        {
402            tr_inf( _( "Serving the web interface files from \"%s\"" ), clutchDir );
403            argv[argc++] = tr_strdup( "-aliases" );
404            argv[argc++] = tr_strdup_printf( "%s=%s,%s=%s",
405                                             "/transmission/clutch", clutchDir,
406                                             "/transmission/web", clutchDir );
407        }
408
409        argv[argc] = NULL; /* shttpd_init() wants it null-terminated */
410
411        server->ctx = shttpd_init( argc, argv );
412        shttpd_register_uri( server->ctx, "/transmission/rpc", handle_rpc, server );
413        shttpd_register_uri( server->ctx, "/transmission/upload", handle_upload, server );
414        shttpd_register_uri( server->ctx, "/", handle_root, server );
415
416        evtimer_set( &server->timer, rpcPulse, server );
417        evtimer_add( &server->timer, &tv );
418
419        for( i=0; i<argc; ++i )
420            tr_free( argv[i] );
421    }
422}
423
424static void
425stopServer( tr_rpc_server * server )
426{
427    if( server->ctx )
428    {
429        char passwd[MAX_PATH_LENGTH];
430        getPasswordFile( server, passwd, sizeof( passwd ) );
431        unlink( passwd );
432
433        evtimer_del( &server->timer );
434        shttpd_fini( server->ctx );
435        server->ctx = NULL;
436    }
437}
438
439void
440tr_rpcSetEnabled( tr_rpc_server * server, int isEnabled )
441{
442    if( !isEnabled && server->ctx )
443        stopServer( server );
444
445    if( isEnabled && !server->ctx )
446        startServer( server );
447}
448
449int
450tr_rpcIsEnabled( const tr_rpc_server * server )
451{
452    return server->ctx != NULL;
453}
454
455void
456tr_rpcSetPort( tr_rpc_server * server, int port )
457{
458    if( server->port != port )
459    {
460        server->port = port;
461
462        if( server->ctx )
463        {
464            stopServer( server );
465            startServer( server );
466        }
467    }
468}
469
470int
471tr_rpcGetPort( const tr_rpc_server * server )
472{
473    return server->port;
474}
475
476/****
477*****  ACL
478****/
479
480/*
481 * FOR_EACH_WORD_IN_LIST, isbyte, and testACL are from, or modified from,
482 * shttpd, written by Sergey Lyubka under this license:
483 * "THE BEER-WARE LICENSE" (Revision 42):
484 * Sergey Lyubka wrote this file.  As long as you retain this notice you
485 * can do whatever you want with this stuff. If we meet some day, and you think
486 * this stuff is worth it, you can buy me a beer in return.
487 */
488
489#define FOR_EACH_WORD_IN_LIST(s,len)                                    \
490        for (; s != NULL && (len = strcspn(s, DELIM_CHARS)) != 0;       \
491                        s += len, s+= strspn(s, DELIM_CHARS))
492
493static int isbyte(int n) { return (n >= 0 && n <= 255); }
494
495static char*
496testACL( const char * s )
497{
498    int len;
499
500    FOR_EACH_WORD_IN_LIST(s, len)
501    {
502
503        char flag;
504        int  a, b, c, d, n, mask;
505
506        if( sscanf(s, "%c%d.%d.%d.%d%n",&flag,&a,&b,&c,&d,&n) != 5 )
507            return tr_strdup_printf( _( "[%s]: subnet must be [+|-]x.x.x.x[/x]" ), s );
508        if( flag != '+' && flag != '-')
509            return tr_strdup_printf( _( "[%s]: flag must be + or -" ), s );
510        if( !isbyte(a) || !isbyte(b) || !isbyte(c) || !isbyte(d) )
511            return tr_strdup_printf( _( "[%s]: bad ip address" ), s );
512        if( sscanf(s + n, "/%d", &mask) == 1 && ( mask<0 || mask>32 ) )
513            return tr_strdup_printf( _( "[%s]: bad subnet mask %d" ), s, n );
514    }
515
516    return NULL;
517}
518
519/* 192.*.*.* --> 192.0.0.0/8
520   192.64.*.* --> 192.64.0.0/16
521   192.64.1.* --> 192.64.1.0/24
522   192.64.1.2 --> 192.64.1.2/32 */
523static void
524cidrizeOne( const char * in, int len, struct evbuffer * out )
525{
526    int stars = 0;
527    const char * pch;
528    const char * end;
529    char zero = '0';
530    char huh = '?';
531
532    for( pch=in, end=pch+len; pch!=end; ++pch ) {
533        if( stars && isdigit(*pch) )
534            evbuffer_add( out, &huh, 1 ); 
535        else if( *pch!='*' )
536            evbuffer_add( out, pch, 1 );
537        else {
538            evbuffer_add( out, &zero, 1 );
539            ++stars;
540        }
541    }
542
543    evbuffer_add_printf( out, "/%d", (32-(stars*8)));
544}
545
546char*
547cidrize( const char * acl )
548{
549    int len;
550    const char * walk = acl;
551    char * ret;
552    struct evbuffer * out = evbuffer_new( );
553
554    FOR_EACH_WORD_IN_LIST( walk, len )
555    {
556        cidrizeOne( walk, len, out );
557        evbuffer_add_printf( out, "," );
558    }
559
560    /* the -1 is to eat the final ", " */
561    ret = tr_strndup( (char*) EVBUFFER_DATA(out), EVBUFFER_LENGTH(out)-1 );
562    evbuffer_free( out );
563    return ret;
564}
565
566int
567tr_rpcTestACL( const tr_rpc_server  * server UNUSED,
568               const char           * acl,
569               char                ** setme_errmsg )
570{
571    int err = 0;
572    char * cidr = cidrize( acl );
573    char * errmsg = testACL( cidr );
574    if( errmsg )
575    {
576        if( setme_errmsg )
577            *setme_errmsg = errmsg;
578        else
579            tr_free( errmsg );
580        err = -1;
581    }
582    tr_free( cidr );
583    return err;
584}
585
586int
587tr_rpcSetACL( tr_rpc_server   * server,
588              const char      * acl,
589              char           ** setme_errmsg )
590{
591    char * cidr = cidrize( acl );
592    const int err = tr_rpcTestACL( server, cidr, setme_errmsg );
593
594    if( !err )
595    {
596        const int isRunning = server->ctx != NULL;
597
598        if( isRunning )
599            stopServer( server );
600
601        tr_free( server->acl );
602        server->acl = tr_strdup( cidr );
603        dbgmsg( "setting our ACL to [%s]", server->acl );
604
605        if( isRunning )
606            startServer( server );
607    }
608    tr_free( cidr );
609
610    return err;
611}
612
613char*
614tr_rpcGetACL( const tr_rpc_server * server )
615{
616    return tr_strdup( server->acl ? server->acl : "" );
617}
618
619/****
620*****  PASSWORD
621****/
622
623void
624tr_rpcSetUsername( tr_rpc_server        * server,
625                   const char           * username )
626{
627    const int isRunning = server->ctx != NULL;
628
629    if( isRunning )
630        stopServer( server );
631
632    tr_free( server->username );
633    server->username = tr_strdup( username );
634    dbgmsg( "setting our Username to [%s]", server->username );
635
636    if( isRunning )
637        startServer( server );
638}
639
640char*
641tr_rpcGetUsername( const tr_rpc_server  * server )
642{
643    return tr_strdup( server->username ? server->username : "" );
644}
645
646void
647tr_rpcSetPassword( tr_rpc_server        * server,
648                   const char           * password )
649{
650    const int isRunning = server->ctx != NULL;
651
652    if( isRunning )
653        stopServer( server );
654
655    tr_free( server->password );
656    server->password = tr_strdup( password );
657    dbgmsg( "setting our Password to [%s]", server->password );
658
659    if( isRunning )
660        startServer( server );
661}
662
663char*
664tr_rpcGetPassword( const tr_rpc_server  * server )
665{
666    return tr_strdup( server->password ? server->password : "" );
667}
668
669void
670tr_rpcSetPasswordEnabled( tr_rpc_server  * server,
671                          int              isEnabled )
672{
673    const int isRunning = server->ctx != NULL;
674
675    if( isRunning )
676        stopServer( server );
677
678    server->isPasswordEnabled = isEnabled;
679    dbgmsg( "setting 'password enabled' to %d", isEnabled );
680
681    if( isRunning )
682        startServer( server );
683}
684
685int
686tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
687{
688    return server->isPasswordEnabled;
689}
690
691/****
692*****  LIFE CYCLE
693****/
694
695void
696tr_rpcClose( tr_rpc_server ** ps )
697{
698    tr_rpc_server * s = *ps;
699    *ps = NULL;
700
701    stopServer( s );
702    tr_free( s->username );
703    tr_free( s->password );
704    tr_free( s->acl );
705    tr_free( s );
706}
707
708tr_rpc_server *
709tr_rpcInit( tr_handle   * session,
710            int           isEnabled,
711            int           port,
712            const char  * acl,
713            int           isPasswordEnabled,
714            const char  * username,
715            const char  * password )
716{
717    char * errmsg;
718    tr_rpc_server * s;
719
720    if(( errmsg = testACL ( acl )))
721    {
722        tr_nerr( MY_NAME, errmsg );
723        tr_free( errmsg );
724        acl = TR_DEFAULT_RPC_ACL;
725        tr_nerr( MY_NAME, "using fallback ACL \"%s\"", acl );
726    }
727
728    s = tr_new0( tr_rpc_server, 1 );
729    s->session = session;
730    s->port = port;
731    s->acl = tr_strdup( acl );
732    s->username = tr_strdup( username );
733    s->password = tr_strdup( password );
734    s->isPasswordEnabled = isPasswordEnabled;
735   
736    if( isEnabled )
737        startServer( s );
738    return s;   
739}
Note: See TracBrowser for help on using the repository browser.