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

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

(rpc) uploading files via an http form w/file inputs works now.

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