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

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

(rpc) serve the clutch static files faster

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