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

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

1129: You must add "/transmission/web/" to your IP to view web interface

  • Property svn:keywords set to Date Rev Author Id
File size: 18.8 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 6428 2008-08-05 16:41:09Z 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 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 = 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
346        argv[argc++] = tr_strdup( "-ports" );
347        argv[argc++] = tr_strdup_printf( "%d", server->port );
348
349        argv[argc++] = tr_strdup( "-dir_list" );
350        argv[argc++] = tr_strdup( "0" );
351
352        argv[argc++] = tr_strdup( "-auth_realm" );
353        argv[argc++] = tr_strdup( MY_REALM );
354
355        if( server->acl )
356        {
357            argv[argc++] = tr_strdup( "-acl" );
358            argv[argc++] = tr_strdup( server->acl );
359        }
360        if( server->isPasswordEnabled )
361        {
362            argv[argc++] = tr_strdup( "-protect" );
363            argv[argc++] = tr_strdup_printf( "/transmission=%s", passwd );
364        }
365        if( clutchDir && *clutchDir )
366        {
367            tr_inf( _( "Serving the web interface files from \"%s\"" ), clutchDir );
368            argv[argc++] = tr_strdup( "-root" );
369            argv[argc++] = tr_strdup( clutchDir );
370        }
371
372        argv[argc] = NULL; /* shttpd_init() wants it null-terminated */
373
374        server->ctx = shttpd_init( argc, argv );
375        shttpd_register_uri( server->ctx, "/transmission/rpc", handle_rpc, server );
376        shttpd_register_uri( server->ctx, "/transmission/upload", handle_upload, server );
377
378        evtimer_set( &server->timer, rpcPulse, server );
379        evtimer_add( &server->timer, &tv );
380
381        for( i=0; i<argc; ++i )
382            tr_free( argv[i] );
383    }
384}
385
386static void
387stopServer( tr_rpc_server * server )
388{
389    if( server->ctx )
390    {
391        char passwd[MAX_PATH_LENGTH];
392        getPasswordFile( server, passwd, sizeof( passwd ) );
393        unlink( passwd );
394
395        evtimer_del( &server->timer );
396        shttpd_fini( server->ctx );
397        server->ctx = NULL;
398    }
399}
400
401void
402tr_rpcSetEnabled( tr_rpc_server * server, int isEnabled )
403{
404    if( !isEnabled && server->ctx )
405        stopServer( server );
406
407    if( isEnabled && !server->ctx )
408        startServer( server );
409}
410
411int
412tr_rpcIsEnabled( const tr_rpc_server * server )
413{
414    return server->ctx != NULL;
415}
416
417void
418tr_rpcSetPort( tr_rpc_server * server, int port )
419{
420    if( server->port != port )
421    {
422        server->port = port;
423
424        if( server->ctx )
425        {
426            stopServer( server );
427            startServer( server );
428        }
429    }
430}
431
432int
433tr_rpcGetPort( const tr_rpc_server * server )
434{
435    return server->port;
436}
437
438/****
439*****  ACL
440****/
441
442/*
443 * FOR_EACH_WORD_IN_LIST, isbyte, and testACL are from, or modified from,
444 * shttpd, written by Sergey Lyubka under this license:
445 * "THE BEER-WARE LICENSE" (Revision 42):
446 * Sergey Lyubka wrote this file.  As long as you retain this notice you
447 * can do whatever you want with this stuff. If we meet some day, and you think
448 * this stuff is worth it, you can buy me a beer in return.
449 */
450
451#define FOR_EACH_WORD_IN_LIST(s,len)                                    \
452        for (; s != NULL && (len = strcspn(s, DELIM_CHARS)) != 0;       \
453                        s += len, s+= strspn(s, DELIM_CHARS))
454
455static int isbyte(int n) { return (n >= 0 && n <= 255); }
456
457static char*
458testACL( const char * s )
459{
460    int len;
461
462    FOR_EACH_WORD_IN_LIST(s, len)
463    {
464
465        char flag;
466        int  a, b, c, d, n, mask;
467
468        if( sscanf(s, "%c%d.%d.%d.%d%n",&flag,&a,&b,&c,&d,&n) != 5 )
469            return tr_strdup_printf( _( "[%s]: subnet must be [+|-]x.x.x.x[/x]" ), s );
470        if( flag != '+' && flag != '-')
471            return tr_strdup_printf( _( "[%s]: flag must be + or -" ), s );
472        if( !isbyte(a) || !isbyte(b) || !isbyte(c) || !isbyte(d) )
473            return tr_strdup_printf( _( "[%s]: bad ip address" ), s );
474        if( sscanf(s + n, "/%d", &mask) == 1 && ( mask<0 || mask>32 ) )
475            return tr_strdup_printf( _( "[%s]: bad subnet mask %d" ), s, n );
476    }
477
478    return NULL;
479}
480
481/* 192.*.*.* --> 192.0.0.0/8
482   192.64.*.* --> 192.64.0.0/16
483   192.64.1.* --> 192.64.1.0/24
484   192.64.1.2 --> 192.64.1.2/32 */
485static void
486cidrizeOne( const char * in, int len, struct evbuffer * out )
487{
488    int stars = 0;
489    const char * pch;
490    const char * end;
491    char zero = '0';
492    char huh = '?';
493
494    for( pch=in, end=pch+len; pch!=end; ++pch ) {
495        if( stars && isdigit(*pch) )
496            evbuffer_add( out, &huh, 1 ); 
497        else if( *pch!='*' )
498            evbuffer_add( out, pch, 1 );
499        else {
500            evbuffer_add( out, &zero, 1 );
501            ++stars;
502        }
503    }
504
505    evbuffer_add_printf( out, "/%d", (32-(stars*8)));
506}
507
508char*
509cidrize( const char * acl )
510{
511    int len;
512    const char * walk = acl;
513    char * ret;
514    struct evbuffer * out = evbuffer_new( );
515
516    FOR_EACH_WORD_IN_LIST( walk, len )
517    {
518        cidrizeOne( walk, len, out );
519        evbuffer_add_printf( out, "," );
520    }
521
522    /* the -1 is to eat the final ", " */
523    ret = tr_strndup( (char*) EVBUFFER_DATA(out), EVBUFFER_LENGTH(out)-1 );
524    evbuffer_free( out );
525    return ret;
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    char * cidr = cidrize( acl );
535    char * errmsg = testACL( cidr );
536    if( errmsg )
537    {
538        if( setme_errmsg )
539            *setme_errmsg = errmsg;
540        else
541            tr_free( errmsg );
542        err = -1;
543    }
544    tr_free( cidr );
545    return err;
546}
547
548int
549tr_rpcSetACL( tr_rpc_server   * server,
550              const char      * acl,
551              char           ** setme_errmsg )
552{
553    char * cidr = cidrize( acl );
554    const int err = tr_rpcTestACL( server, cidr, setme_errmsg );
555
556    if( !err )
557    {
558        const int isRunning = server->ctx != NULL;
559
560        if( isRunning )
561            stopServer( server );
562
563        tr_free( server->acl );
564        server->acl = tr_strdup( cidr );
565        dbgmsg( "setting our ACL to [%s]", server->acl );
566
567        if( isRunning )
568            startServer( server );
569    }
570    tr_free( cidr );
571
572    return err;
573}
574
575char*
576tr_rpcGetACL( const tr_rpc_server * server )
577{
578    return tr_strdup( server->acl ? server->acl : "" );
579}
580
581/****
582*****  PASSWORD
583****/
584
585void
586tr_rpcSetUsername( tr_rpc_server        * server,
587                   const char           * username )
588{
589    const int isRunning = server->ctx != NULL;
590
591    if( isRunning )
592        stopServer( server );
593
594    tr_free( server->username );
595    server->username = tr_strdup( username );
596    dbgmsg( "setting our Username to [%s]", server->username );
597
598    if( isRunning )
599        startServer( server );
600}
601
602char*
603tr_rpcGetUsername( const tr_rpc_server  * server )
604{
605    return tr_strdup( server->username ? server->username : "" );
606}
607
608void
609tr_rpcSetPassword( tr_rpc_server        * server,
610                   const char           * password )
611{
612    const int isRunning = server->ctx != NULL;
613
614    if( isRunning )
615        stopServer( server );
616
617    tr_free( server->password );
618    server->password = tr_strdup( password );
619    dbgmsg( "setting our Password to [%s]", server->password );
620
621    if( isRunning )
622        startServer( server );
623}
624
625char*
626tr_rpcGetPassword( const tr_rpc_server  * server )
627{
628    return tr_strdup( server->password ? server->password : "" );
629}
630
631void
632tr_rpcSetPasswordEnabled( tr_rpc_server  * server,
633                          int              isEnabled )
634{
635    const int isRunning = server->ctx != NULL;
636
637    if( isRunning )
638        stopServer( server );
639
640    server->isPasswordEnabled = isEnabled;
641    dbgmsg( "setting 'password enabled' to %d", isEnabled );
642
643    if( isRunning )
644        startServer( server );
645}
646
647int
648tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
649{
650    return server->isPasswordEnabled;
651}
652
653/****
654*****  LIFE CYCLE
655****/
656
657void
658tr_rpcClose( tr_rpc_server ** ps )
659{
660    tr_rpc_server * s = *ps;
661    *ps = NULL;
662
663    stopServer( s );
664    tr_free( s->username );
665    tr_free( s->password );
666    tr_free( s->acl );
667    tr_free( s );
668}
669
670tr_rpc_server *
671tr_rpcInit( tr_handle   * session,
672            int           isEnabled,
673            int           port,
674            const char  * acl,
675            int           isPasswordEnabled,
676            const char  * username,
677            const char  * password )
678{
679    char * errmsg;
680    tr_rpc_server * s;
681
682    if(( errmsg = testACL ( acl )))
683    {
684        tr_nerr( MY_NAME, errmsg );
685        tr_free( errmsg );
686        acl = TR_DEFAULT_RPC_ACL;
687        tr_nerr( MY_NAME, "using fallback ACL \"%s\"", acl );
688    }
689
690    s = tr_new0( tr_rpc_server, 1 );
691    s->session = session;
692    s->port = port;
693    s->acl = tr_strdup( acl );
694    s->username = tr_strdup( username );
695    s->password = tr_strdup( password );
696    s->isPasswordEnabled = isPasswordEnabled;
697   
698    if( isEnabled )
699        startServer( s );
700    return s;   
701}
Note: See TracBrowser for help on using the repository browser.