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

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

(1) add clutch to the tarball.
(2) on autoconf-based installs, install the clutch files in $(datadir)/transmission/web
(3) new function tr_getClutchDir(). Default implementation follows the XDG spec.

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