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

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

#1129 alternate fix: / should redirect to, not replace, /transmission/web

  • Property svn:keywords set to Date Rev Author Id
File size: 19.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 6430 2008-08-06 00:24:05Z 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_root( struct shttpd_arg * arg )
241{
242    const char * redirect = "HTTP/1.1 200 OK""\r\n"
243                            "Content-Type: text/html" "\r\n"
244                            "\r\n"
245                            "<html><head>" "\r\n"
246                            "  <meta http-equiv=\"Refresh\" content=\"2; url=/transmission/web\">" "\r\n"
247                            "</head><body>" "\r\n"
248                            "  <p>redirecting to <a href=\"/transmission/web\">/transmission/web</a></p>" "\r\n"
249                            "</body></html>" "\r\n";
250    const size_t n = strlen( redirect );
251    memcpy( arg->out.buf, redirect, n );
252    arg->in.num_bytes = arg->in.len;
253    arg->out.num_bytes = n;
254    arg->flags |= SHTTPD_END_OF_OUTPUT;
255}
256
257static void
258handle_rpc( struct shttpd_arg * arg )
259{
260    struct tr_rpc_server * s = arg->user_data;
261    s->lastRequestTime = time( NULL );
262    struct ConnBuf * cbuf = getBuffer( s, arg );
263
264    if( !EVBUFFER_LENGTH( cbuf->out ) )
265    {
266        int len = 0;
267        char * response = NULL;
268        const char * request_method = shttpd_get_env( arg, "REQUEST_METHOD" );
269        const char * query_string = shttpd_get_env( arg, "QUERY_STRING" );
270
271        if( query_string && *query_string )
272            response = tr_rpc_request_exec_uri( s->session,
273                                                query_string,
274                                                strlen( query_string ),
275                                                &len );
276        else if( !strcmp( request_method, "POST" ) )
277        {
278            evbuffer_add( cbuf->in, arg->in.buf, arg->in.len );
279            arg->in.num_bytes = arg->in.len;
280            if( arg->flags & SHTTPD_MORE_POST_DATA )
281                return;
282            response = tr_rpc_request_exec_json( s->session,
283                                                 EVBUFFER_DATA( cbuf->in ),
284                                                 EVBUFFER_LENGTH( cbuf->in ),
285                                                 &len );
286            evbuffer_drain( cbuf->in, EVBUFFER_LENGTH( cbuf->in ) );
287        }
288
289        evbuffer_add_printf( cbuf->out, "HTTP/1.1 200 OK\r\n"
290                                        "Content-Type: application/json\r\n"
291                                        "Content-Length: %d\r\n"
292                                        "\r\n"
293                                        "%*.*s", len, len, len, response );
294        tr_free( response );
295    }
296
297    if( EVBUFFER_LENGTH( cbuf->out ) )
298    {
299        const int n = MIN( ( int )EVBUFFER_LENGTH( cbuf->out ), arg->out.len );
300        memcpy( arg->out.buf, EVBUFFER_DATA( cbuf->out ), n );
301        evbuffer_drain( cbuf->out, n );
302        arg->out.num_bytes = n;
303    }
304
305    if( !EVBUFFER_LENGTH( cbuf->out ) )
306    {
307        arg->flags |= SHTTPD_END_OF_OUTPUT;
308        pruneBuf( s, cbuf );
309    }
310}
311
312static void
313rpcPulse( int socket UNUSED, short action UNUSED, void * vserver )
314{
315    int interval;
316    struct timeval tv;
317    tr_rpc_server * server = vserver;
318    const time_t now = time( NULL );
319
320    assert( server );
321
322    if( server->ctx )
323        shttpd_poll( server->ctx, 1 );
324
325    /* set a timer for the next pulse */
326    if( now - server->lastRequestTime < 300 )
327        interval = ACTIVE_INTERVAL_MSEC;
328    else
329        interval = INACTIVE_INTERVAL_MSEC;
330    tv = tr_timevalMsec( interval );
331    evtimer_add( &server->timer, &tv );
332}
333
334static void
335getPasswordFile( tr_rpc_server * server, char * buf, int buflen )
336{
337    tr_buildPath( buf, buflen, tr_sessionGetConfigDir( server->session ),
338                               "htpasswd",
339                               NULL );
340}
341
342static void
343startServer( tr_rpc_server * server )
344{
345    dbgmsg( "in startServer; current context is %p", server->ctx );
346
347    if( !server->ctx )
348    {
349        int i;
350        int argc = 0;
351        char * argv[100];
352        char passwd[MAX_PATH_LENGTH];
353        const char * clutchDir = tr_getClutchDir( server->session );
354        struct timeval tv = tr_timevalMsec( INACTIVE_INTERVAL_MSEC );
355
356        getPasswordFile( server, passwd, sizeof( passwd ) );
357        if( !server->isPasswordEnabled )
358            unlink( passwd );
359        else
360            edit_passwords( passwd, MY_REALM, server->username, server->password );
361
362        argv[argc++] = tr_strdup( "appname-unused" );
363
364        argv[argc++] = tr_strdup( "-ports" );
365        argv[argc++] = tr_strdup_printf( "%d", server->port );
366
367        argv[argc++] = tr_strdup( "-dir_list" );
368        argv[argc++] = tr_strdup( "0" );
369
370        argv[argc++] = tr_strdup( "-auth_realm" );
371        argv[argc++] = tr_strdup( MY_REALM );
372
373        if( server->acl )
374        {
375            argv[argc++] = tr_strdup( "-acl" );
376            argv[argc++] = tr_strdup( server->acl );
377        }
378        if( server->isPasswordEnabled )
379        {
380            argv[argc++] = tr_strdup( "-protect" );
381            argv[argc++] = tr_strdup_printf( "/transmission=%s", passwd );
382        }
383        if( clutchDir && *clutchDir )
384        {
385            tr_inf( _( "Serving the web interface files from \"%s\"" ), clutchDir );
386            argv[argc++] = tr_strdup( "-aliases" );
387            argv[argc++] = tr_strdup_printf( "%s=%s,%s=%s",
388                                             "/transmission/clutch", clutchDir,
389                                             "/transmission/web", clutchDir );
390        }
391
392        argv[argc] = NULL; /* shttpd_init() wants it null-terminated */
393
394        server->ctx = shttpd_init( argc, argv );
395        shttpd_register_uri( server->ctx, "/transmission/rpc", handle_rpc, server );
396        shttpd_register_uri( server->ctx, "/transmission/upload", handle_upload, server );
397        shttpd_register_uri( server->ctx, "/", handle_root, server );
398
399        evtimer_set( &server->timer, rpcPulse, server );
400        evtimer_add( &server->timer, &tv );
401
402        for( i=0; i<argc; ++i )
403            tr_free( argv[i] );
404    }
405}
406
407static void
408stopServer( tr_rpc_server * server )
409{
410    if( server->ctx )
411    {
412        char passwd[MAX_PATH_LENGTH];
413        getPasswordFile( server, passwd, sizeof( passwd ) );
414        unlink( passwd );
415
416        evtimer_del( &server->timer );
417        shttpd_fini( server->ctx );
418        server->ctx = NULL;
419    }
420}
421
422void
423tr_rpcSetEnabled( tr_rpc_server * server, int isEnabled )
424{
425    if( !isEnabled && server->ctx )
426        stopServer( server );
427
428    if( isEnabled && !server->ctx )
429        startServer( server );
430}
431
432int
433tr_rpcIsEnabled( const tr_rpc_server * server )
434{
435    return server->ctx != NULL;
436}
437
438void
439tr_rpcSetPort( tr_rpc_server * server, int port )
440{
441    if( server->port != port )
442    {
443        server->port = port;
444
445        if( server->ctx )
446        {
447            stopServer( server );
448            startServer( server );
449        }
450    }
451}
452
453int
454tr_rpcGetPort( const tr_rpc_server * server )
455{
456    return server->port;
457}
458
459/****
460*****  ACL
461****/
462
463/*
464 * FOR_EACH_WORD_IN_LIST, isbyte, and testACL are from, or modified from,
465 * shttpd, written by Sergey Lyubka under this license:
466 * "THE BEER-WARE LICENSE" (Revision 42):
467 * Sergey Lyubka wrote this file.  As long as you retain this notice you
468 * can do whatever you want with this stuff. If we meet some day, and you think
469 * this stuff is worth it, you can buy me a beer in return.
470 */
471
472#define FOR_EACH_WORD_IN_LIST(s,len)                                    \
473        for (; s != NULL && (len = strcspn(s, DELIM_CHARS)) != 0;       \
474                        s += len, s+= strspn(s, DELIM_CHARS))
475
476static int isbyte(int n) { return (n >= 0 && n <= 255); }
477
478static char*
479testACL( const char * s )
480{
481    int len;
482
483    FOR_EACH_WORD_IN_LIST(s, len)
484    {
485
486        char flag;
487        int  a, b, c, d, n, mask;
488
489        if( sscanf(s, "%c%d.%d.%d.%d%n",&flag,&a,&b,&c,&d,&n) != 5 )
490            return tr_strdup_printf( _( "[%s]: subnet must be [+|-]x.x.x.x[/x]" ), s );
491        if( flag != '+' && flag != '-')
492            return tr_strdup_printf( _( "[%s]: flag must be + or -" ), s );
493        if( !isbyte(a) || !isbyte(b) || !isbyte(c) || !isbyte(d) )
494            return tr_strdup_printf( _( "[%s]: bad ip address" ), s );
495        if( sscanf(s + n, "/%d", &mask) == 1 && ( mask<0 || mask>32 ) )
496            return tr_strdup_printf( _( "[%s]: bad subnet mask %d" ), s, n );
497    }
498
499    return NULL;
500}
501
502/* 192.*.*.* --> 192.0.0.0/8
503   192.64.*.* --> 192.64.0.0/16
504   192.64.1.* --> 192.64.1.0/24
505   192.64.1.2 --> 192.64.1.2/32 */
506static void
507cidrizeOne( const char * in, int len, struct evbuffer * out )
508{
509    int stars = 0;
510    const char * pch;
511    const char * end;
512    char zero = '0';
513    char huh = '?';
514
515    for( pch=in, end=pch+len; pch!=end; ++pch ) {
516        if( stars && isdigit(*pch) )
517            evbuffer_add( out, &huh, 1 ); 
518        else if( *pch!='*' )
519            evbuffer_add( out, pch, 1 );
520        else {
521            evbuffer_add( out, &zero, 1 );
522            ++stars;
523        }
524    }
525
526    evbuffer_add_printf( out, "/%d", (32-(stars*8)));
527}
528
529char*
530cidrize( const char * acl )
531{
532    int len;
533    const char * walk = acl;
534    char * ret;
535    struct evbuffer * out = evbuffer_new( );
536
537    FOR_EACH_WORD_IN_LIST( walk, len )
538    {
539        cidrizeOne( walk, len, out );
540        evbuffer_add_printf( out, "," );
541    }
542
543    /* the -1 is to eat the final ", " */
544    ret = tr_strndup( (char*) EVBUFFER_DATA(out), EVBUFFER_LENGTH(out)-1 );
545    evbuffer_free( out );
546    return ret;
547}
548
549int
550tr_rpcTestACL( const tr_rpc_server  * server UNUSED,
551               const char           * acl,
552               char                ** setme_errmsg )
553{
554    int err = 0;
555    char * cidr = cidrize( acl );
556    char * errmsg = testACL( cidr );
557    if( errmsg )
558    {
559        if( setme_errmsg )
560            *setme_errmsg = errmsg;
561        else
562            tr_free( errmsg );
563        err = -1;
564    }
565    tr_free( cidr );
566    return err;
567}
568
569int
570tr_rpcSetACL( tr_rpc_server   * server,
571              const char      * acl,
572              char           ** setme_errmsg )
573{
574    char * cidr = cidrize( acl );
575    const int err = tr_rpcTestACL( server, cidr, setme_errmsg );
576
577    if( !err )
578    {
579        const int isRunning = server->ctx != NULL;
580
581        if( isRunning )
582            stopServer( server );
583
584        tr_free( server->acl );
585        server->acl = tr_strdup( cidr );
586        dbgmsg( "setting our ACL to [%s]", server->acl );
587
588        if( isRunning )
589            startServer( server );
590    }
591    tr_free( cidr );
592
593    return err;
594}
595
596char*
597tr_rpcGetACL( const tr_rpc_server * server )
598{
599    return tr_strdup( server->acl ? server->acl : "" );
600}
601
602/****
603*****  PASSWORD
604****/
605
606void
607tr_rpcSetUsername( tr_rpc_server        * server,
608                   const char           * username )
609{
610    const int isRunning = server->ctx != NULL;
611
612    if( isRunning )
613        stopServer( server );
614
615    tr_free( server->username );
616    server->username = tr_strdup( username );
617    dbgmsg( "setting our Username to [%s]", server->username );
618
619    if( isRunning )
620        startServer( server );
621}
622
623char*
624tr_rpcGetUsername( const tr_rpc_server  * server )
625{
626    return tr_strdup( server->username ? server->username : "" );
627}
628
629void
630tr_rpcSetPassword( tr_rpc_server        * server,
631                   const char           * password )
632{
633    const int isRunning = server->ctx != NULL;
634
635    if( isRunning )
636        stopServer( server );
637
638    tr_free( server->password );
639    server->password = tr_strdup( password );
640    dbgmsg( "setting our Password to [%s]", server->password );
641
642    if( isRunning )
643        startServer( server );
644}
645
646char*
647tr_rpcGetPassword( const tr_rpc_server  * server )
648{
649    return tr_strdup( server->password ? server->password : "" );
650}
651
652void
653tr_rpcSetPasswordEnabled( tr_rpc_server  * server,
654                          int              isEnabled )
655{
656    const int isRunning = server->ctx != NULL;
657
658    if( isRunning )
659        stopServer( server );
660
661    server->isPasswordEnabled = isEnabled;
662    dbgmsg( "setting 'password enabled' to %d", isEnabled );
663
664    if( isRunning )
665        startServer( server );
666}
667
668int
669tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
670{
671    return server->isPasswordEnabled;
672}
673
674/****
675*****  LIFE CYCLE
676****/
677
678void
679tr_rpcClose( tr_rpc_server ** ps )
680{
681    tr_rpc_server * s = *ps;
682    *ps = NULL;
683
684    stopServer( s );
685    tr_free( s->username );
686    tr_free( s->password );
687    tr_free( s->acl );
688    tr_free( s );
689}
690
691tr_rpc_server *
692tr_rpcInit( tr_handle   * session,
693            int           isEnabled,
694            int           port,
695            const char  * acl,
696            int           isPasswordEnabled,
697            const char  * username,
698            const char  * password )
699{
700    char * errmsg;
701    tr_rpc_server * s;
702
703    if(( errmsg = testACL ( acl )))
704    {
705        tr_nerr( MY_NAME, errmsg );
706        tr_free( errmsg );
707        acl = TR_DEFAULT_RPC_ACL;
708        tr_nerr( MY_NAME, "using fallback ACL \"%s\"", acl );
709    }
710
711    s = tr_new0( tr_rpc_server, 1 );
712    s->session = session;
713    s->port = port;
714    s->acl = tr_strdup( acl );
715    s->username = tr_strdup( username );
716    s->password = tr_strdup( password );
717    s->isPasswordEnabled = isPasswordEnabled;
718   
719    if( isEnabled )
720        startServer( s );
721    return s;   
722}
Note: See TracBrowser for help on using the repository browser.