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

Last change on this file since 6351 was 6351, checked in by charles, 14 years ago

(rpc) possible fix for the new breakage caused by the shttpd upgrade in r6349

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