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

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

run libT, cli, daemon, gtk through the source-code formatter "uncrustify" as promised/threatened

  • Property svn:keywords set to Date Rev Author Id
File size: 22.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 6795 2008-09-23 19:11:04Z 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 "rpcimpl.h"
30#include "rpc-server.h"
31#include "utils.h"
32
33#define MY_NAME "RPC Server"
34#define MY_REALM "Transmission"
35
36#define ACTIVE_INTERVAL_MSEC 40
37#define INACTIVE_INTERVAL_MSEC 200
38
39struct tr_rpc_server
40{
41    unsigned int         isEnabled         : 1;
42    unsigned int         isPasswordEnabled : 1;
43    int                  port;
44    time_t               lastRequestTime;
45    struct shttpd_ctx *  ctx;
46    tr_handle *          session;
47    struct event         timer;
48    char *               username;
49    char *               password;
50    char *               acl;
51    tr_list *            connections;
52};
53
54#define dbgmsg( fmt... ) tr_deepLog( __FILE__, __LINE__, MY_NAME, ## fmt )
55
56static const char*
57tr_memmem( const char * s1,
58           size_t       l1,
59           const char * s2,
60           size_t       l2 )
61{
62    if( !l2 ) return s1;
63    while( l1 >= l2 )
64    {
65        l1--;
66        if( !memcmp( s1, s2, l2 ) )
67            return s1;
68        s1++;
69    }
70
71    return NULL;
72}
73
74/**
75***
76**/
77
78struct ConnBuf
79{
80    char *             key;
81    time_t             lastActivity;
82    struct evbuffer *  in;
83    struct evbuffer *  out;
84};
85
86static char*
87buildKey( struct shttpd_arg * arg )
88{
89    return tr_strdup_printf( "%s %s",
90                            shttpd_get_env( arg, "REMOTE_ADDR" ),
91                            shttpd_get_env( arg, "REQUEST_URI" ) );
92}
93
94static struct ConnBuf*
95getBuffer(
96                        tr_rpc_server * server,
97    struct shttpd_arg * arg )
98{
99    tr_list *        l;
100    char *           key = buildKey( arg );
101    struct ConnBuf * found = NULL;
102
103    for( l = server->connections; l && !found; l = l->next )
104    {
105        struct ConnBuf * buf = l->data;
106        if( !strcmp( key, buf->key ) )
107            found = buf;
108    }
109
110    if( found == NULL )
111    {
112        found = tr_new0( struct ConnBuf, 1 );
113        found->lastActivity = time( NULL );
114        found->key = tr_strdup( key );
115        found->in = evbuffer_new( );
116        found->out = evbuffer_new( );
117        tr_list_append( &server->connections, found );
118    }
119
120    tr_free( key );
121    return found;
122}
123
124static void
125pruneBuf( tr_rpc_server *  server,
126          struct ConnBuf * buf )
127{
128    tr_list_remove_data( &server->connections, buf );
129
130    evbuffer_free( buf->in );
131    evbuffer_free( buf->out );
132    tr_free( buf->key );
133    tr_free( buf );
134}
135
136/**
137***
138**/
139
140static void
141handle_upload( struct shttpd_arg * arg )
142{
143    struct tr_rpc_server * s;
144    struct ConnBuf *       cbuf;
145
146    s = arg->user_data;
147    s->lastRequestTime = time( NULL );
148    cbuf = getBuffer( s, arg );
149
150    /* if we haven't parsed the POST, do that now */
151    if( !EVBUFFER_LENGTH( cbuf->out ) )
152    {
153        const char * query_string;
154        const char * content_type;
155        const char * delim;
156        const char * in;
157        size_t       inlen;
158        char *       boundary;
159        size_t       boundary_len;
160        char         buf[64];
161        int          paused;
162
163        /* if we haven't finished reading the POST, read more now */
164        evbuffer_add( cbuf->in, arg->in.buf, arg->in.len );
165        arg->in.num_bytes = arg->in.len;
166        if( arg->flags & SHTTPD_MORE_POST_DATA )
167            return;
168
169        query_string = shttpd_get_env( arg, "QUERY_STRING" );
170        content_type = shttpd_get_header( arg, "Content-Type" );
171        in = (const char *) EVBUFFER_DATA( cbuf->in );
172        inlen = EVBUFFER_LENGTH( cbuf->in );
173        boundary =
174            tr_strdup_printf( "--%s", strstr( content_type,
175                                              "boundary=" ) +
176                             strlen( "boundary=" ) );
177        boundary_len = strlen( boundary );
178        paused = ( query_string != NULL )
179                 && ( shttpd_get_var( "paused", query_string,
180                                     strlen( query_string ), buf,
181                                     sizeof( buf ) ) == 4 )
182                 && ( !strcmp( buf, "true" ) );
183
184        delim = tr_memmem( in, inlen, boundary, boundary_len );
185        if( delim ) do
186            {
187                size_t       part_len;
188                const char * part = delim + boundary_len;
189                inlen -= ( part - in );
190                in = part;
191                delim = tr_memmem( in, inlen, boundary, boundary_len );
192                part_len = delim ? (size_t)( delim - part ) : inlen;
193
194                if( part_len )
195                {
196                    char * text = tr_strndup( part, part_len );
197                    if( strstr( text, "filename=\"" ) )
198                    {
199                        const char * body = strstr( text, "\r\n\r\n" );
200                        if( body )
201                        {
202                            char *  b64, *json, *freeme;
203                            int     json_len;
204                            size_t  body_len;
205                            tr_benc top, *args;
206
207                            body += 4;
208                            body_len = part_len - ( body - text );
209                            if( body_len >= 2
210                              && !memcmp( &body[body_len - 2], "\r\n", 2 ) )
211                                body_len -= 2;
212
213                            tr_bencInitDict( &top, 2 );
214                            args = tr_bencDictAddDict( &top, "arguments", 2 );
215                            tr_bencDictAddStr( &top, "method",
216                                               "torrent-add" );
217                            b64 = tr_base64_encode( body, body_len, NULL );
218                            tr_bencDictAddStr( args, "metainfo", b64 );
219                            tr_bencDictAddInt( args, "paused", paused );
220                            json = tr_bencSaveAsJSON( &top, &json_len );
221                            freeme =
222                                tr_rpc_request_exec_json( s->session, json,
223                                                          json_len,
224                                                          NULL );
225
226                            tr_free( freeme );
227                            tr_free( json );
228                            tr_free( b64 );
229                            tr_bencFree( &top );
230                        }
231                    }
232                    tr_free( text );
233                }
234            }
235            while( delim );
236
237        evbuffer_drain( cbuf->in, EVBUFFER_LENGTH( cbuf->in ) );
238        tr_free( boundary );
239
240        {
241            /* use xml here because json responses to file uploads is trouble.
242             * see http://www.malsup.com/jquery/form/#sample7 for details */
243            const char * response = "<result>success</result>";
244            const int    len = strlen( response );
245            evbuffer_add_printf(
246                cbuf->out, "HTTP/1.1 200 OK\r\n"
247                           "Content-Type: text/xml; charset=UTF-8\r\n"
248                           "Content-Length: %d\r\n"
249                           "\r\n"
250                           "%s\r\n", len, response );
251        }
252    }
253
254    if( EVBUFFER_LENGTH( cbuf->out ) )
255    {
256        const int n = MIN( ( int )EVBUFFER_LENGTH(
257                              cbuf->out ), arg->out.len );
258        memcpy( arg->out.buf, EVBUFFER_DATA( cbuf->out ), n );
259        evbuffer_drain( cbuf->out, n );
260        arg->out.num_bytes = n;
261    }
262
263    if( !EVBUFFER_LENGTH( cbuf->out ) )
264    {
265        arg->flags |= SHTTPD_END_OF_OUTPUT;
266        pruneBuf( s, cbuf );
267    }
268}
269
270static void
271handle_root( struct shttpd_arg * arg )
272{
273    const char * redirect = "HTTP/1.1 200 OK" "\r\n"
274                                              "Content-Type: text/html; charset=UTF-8"
275                                              "\r\n"
276                                              "\r\n"
277                                              "<html><head>"
278                                              "\r\n"
279                                              "  <meta http-equiv=\"Refresh\" content=\"2; url=/transmission/web/\">"
280                                              "\r\n"
281                                              "</head><body>"
282                                              "\r\n"
283                                              "  <p>redirecting to <a href=\"/transmission/web\">/transmission/web/</a></p>"
284                                              "\r\n"
285                                              "</body></html>"
286                                              "\r\n";
287    const size_t n = strlen( redirect );
288
289    memcpy( arg->out.buf, redirect, n );
290    arg->in.num_bytes = arg->in.len;
291    arg->out.num_bytes = n;
292    arg->flags |= SHTTPD_END_OF_OUTPUT;
293}
294
295static void
296handle_rpc( struct shttpd_arg * arg )
297{
298    struct tr_rpc_server * s;
299    struct ConnBuf *       cbuf;
300
301    s = arg->user_data;
302    s->lastRequestTime = time( NULL );
303    cbuf = getBuffer( s, arg );
304
305    if( !EVBUFFER_LENGTH( cbuf->out ) )
306    {
307        int          len = 0;
308        char *       response = NULL;
309        const char * request_method = shttpd_get_env( arg, "REQUEST_METHOD" );
310        const char * query_string = shttpd_get_env( arg, "QUERY_STRING" );
311
312        if( query_string && *query_string )
313            response = tr_rpc_request_exec_uri( s->session,
314                                                query_string,
315                                                strlen( query_string ),
316                                                &len );
317        else if( !strcmp( request_method, "POST" ) )
318        {
319            evbuffer_add( cbuf->in, arg->in.buf, arg->in.len );
320            arg->in.num_bytes = arg->in.len;
321            if( arg->flags & SHTTPD_MORE_POST_DATA )
322                return;
323            response = tr_rpc_request_exec_json( s->session,
324                                                 EVBUFFER_DATA( cbuf->in ),
325                                                 EVBUFFER_LENGTH( cbuf->in ),
326                                                 &len );
327            evbuffer_drain( cbuf->in, EVBUFFER_LENGTH( cbuf->in ) );
328        }
329
330        evbuffer_add_printf(
331            cbuf->out, "HTTP/1.1 200 OK\r\n"
332                       "Content-Type: application/json; charset=UTF-8\r\n"
333                       "Content-Length: %d\r\n"
334                       "\r\n"
335                       "%*.*s", len, len, len, response );
336        tr_free( response );
337    }
338
339    if( EVBUFFER_LENGTH( cbuf->out ) )
340    {
341        const int n = MIN( ( int )EVBUFFER_LENGTH(
342                              cbuf->out ), arg->out.len );
343        memcpy( arg->out.buf, EVBUFFER_DATA( cbuf->out ), n );
344        evbuffer_drain( cbuf->out, n );
345        arg->out.num_bytes = n;
346    }
347
348    if( !EVBUFFER_LENGTH( cbuf->out ) )
349    {
350        arg->flags |= SHTTPD_END_OF_OUTPUT;
351        pruneBuf( s, cbuf );
352    }
353}
354
355static void
356rpcPulse( int socket   UNUSED,
357          short action UNUSED,
358          void *       vserver )
359{
360    int             interval;
361    struct timeval  tv;
362    tr_rpc_server * server = vserver;
363    const time_t    now = time( NULL );
364
365    assert( server );
366
367    if( server->ctx )
368        shttpd_poll( server->ctx, 1 );
369
370    /* set a timer for the next pulse */
371    if( now - server->lastRequestTime < 300 )
372        interval = ACTIVE_INTERVAL_MSEC;
373    else
374        interval = INACTIVE_INTERVAL_MSEC;
375    tv = tr_timevalMsec( interval );
376    evtimer_add( &server->timer, &tv );
377}
378
379static void
380getPasswordFile( tr_rpc_server * server,
381                 char *          buf,
382                 int             buflen )
383{
384    tr_buildPath( buf, buflen, tr_sessionGetConfigDir( server->session ),
385                  "htpasswd",
386                  NULL );
387}
388
389static void
390startServer( tr_rpc_server * server )
391{
392    dbgmsg( "in startServer; current context is %p", server->ctx );
393
394    if( !server->ctx )
395    {
396        int            i;
397        int            argc = 0;
398        char *         argv[100];
399        char           passwd[MAX_PATH_LENGTH];
400        const char *   clutchDir = tr_getClutchDir( server->session );
401        struct timeval tv = tr_timevalMsec( INACTIVE_INTERVAL_MSEC );
402
403        getPasswordFile( server, passwd, sizeof( passwd ) );
404        if( !server->isPasswordEnabled )
405            unlink( passwd );
406        else
407            shttpd_edit_passwords( passwd, MY_REALM, server->username,
408                                   server->password );
409
410        argv[argc++] = tr_strdup( "appname-unused" );
411
412        argv[argc++] = tr_strdup( "-ports" );
413        argv[argc++] = tr_strdup_printf( "%d", server->port );
414
415        argv[argc++] = tr_strdup( "-dir_list" );
416        argv[argc++] = tr_strdup( "0" );
417
418        argv[argc++] = tr_strdup( "-auth_realm" );
419        argv[argc++] = tr_strdup( MY_REALM );
420
421        argv[argc++] = tr_strdup( "-root" );
422        argv[argc++] = tr_strdup( "/dev/null" );
423
424        if( server->acl )
425        {
426            argv[argc++] = tr_strdup( "-acl" );
427            argv[argc++] = tr_strdup( server->acl );
428        }
429        if( server->isPasswordEnabled )
430        {
431            argv[argc++] = tr_strdup( "-protect" );
432            argv[argc++] = tr_strdup_printf( "/transmission=%s", passwd );
433        }
434        if( clutchDir && *clutchDir )
435        {
436            tr_inf( _(
437                        "Serving the web interface files from \"%s\"" ),
438                    clutchDir );
439            argv[argc++] = tr_strdup( "-aliases" );
440            argv[argc++] = tr_strdup_printf( "%s=%s,%s=%s",
441                                             "/transmission/clutch",
442                                             clutchDir,
443                                             "/transmission/web",
444                                             clutchDir );
445        }
446
447        argv[argc] = NULL; /* shttpd_init() wants it null-terminated */
448
449        if( ( server->ctx = shttpd_init( argc, argv ) ) )
450        {
451            shttpd_register_uri( server->ctx, "/transmission/rpc",
452                                 handle_rpc,
453                                 server );
454            shttpd_register_uri( server->ctx, "/transmission/upload",
455                                 handle_upload,
456                                 server );
457            shttpd_register_uri( server->ctx, "/", handle_root, server );
458
459            evtimer_set( &server->timer, rpcPulse, server );
460            evtimer_add( &server->timer, &tv );
461        }
462
463        for( i = 0; i < argc; ++i )
464            tr_free( argv[i] );
465    }
466}
467
468static void
469stopServer( tr_rpc_server * server )
470{
471    if( server->ctx )
472    {
473        char passwd[MAX_PATH_LENGTH];
474        getPasswordFile( server, passwd, sizeof( passwd ) );
475        unlink( passwd );
476
477        evtimer_del( &server->timer );
478        shttpd_fini( server->ctx );
479        server->ctx = NULL;
480    }
481}
482
483void
484tr_rpcSetEnabled( tr_rpc_server * server,
485                  int             isEnabled )
486{
487    server->isEnabled = isEnabled != 0;
488
489    if( !isEnabled )
490        stopServer( server );
491    else
492        startServer( server );
493}
494
495int
496tr_rpcIsEnabled( const tr_rpc_server * server )
497{
498    return server->ctx != NULL;
499}
500
501void
502tr_rpcSetPort( tr_rpc_server * server,
503               int             port )
504{
505    if( server->port != port )
506    {
507        server->port = port;
508
509        if( server->isEnabled )
510        {
511            stopServer( server );
512            startServer( server );
513        }
514    }
515}
516
517int
518tr_rpcGetPort( const tr_rpc_server * server )
519{
520    return server->port;
521}
522
523/****
524*****  ACL
525****/
526
527/*
528 * FOR_EACH_WORD_IN_LIST, isbyte, and testACL are from, or modified from,
529 * shttpd, written by Sergey Lyubka under this license:
530 * "THE BEER-WARE LICENSE" (Revision 42):
531 * Sergey Lyubka wrote this file.  As long as you retain this notice you
532 * can do whatever you want with this stuff. If we meet some day, and you think
533 * this stuff is worth it, you can buy me a beer in return.
534 */
535
536#define FOR_EACH_WORD_IN_LIST( s, len )                                    \
537    for( ; s != NULL && ( len = strcspn( s, DELIM_CHARS ) ) != 0;       \
538                         s += len, s += strspn( s, DELIM_CHARS ) )
539
540static int
541isbyte( int n ) { return n >= 0 && n <= 255;  }
542
543static char*
544testACL( const char * s )
545{
546    int len;
547
548    FOR_EACH_WORD_IN_LIST( s, len )
549    {
550        char flag;
551        int  a, b, c, d, n, mask;
552
553        if( sscanf( s, "%c%d.%d.%d.%d%n", &flag, &a, &b, &c, &d, &n ) != 5 )
554            return tr_strdup_printf( _(
555                                         "[%s]: subnet must be [+|-]x.x.x.x[/x]" ),
556                                     s );
557        if( flag != '+' && flag != '-' )
558            return tr_strdup_printf( _( "[%s]: flag must be + or -" ), s );
559        if( !isbyte( a ) || !isbyte( b ) || !isbyte( c ) || !isbyte( d ) )
560            return tr_strdup_printf( _( "[%s]: bad ip address" ), s );
561        if( sscanf( s + n, "/%d", &mask ) == 1 && ( mask < 0 || mask > 32 ) )
562            return tr_strdup_printf( _( "[%s]: bad subnet mask %d" ), s, n );
563    }
564
565    return NULL;
566}
567
568/* 192.*.*.* --> 192.0.0.0/8
569   192.64.*.* --> 192.64.0.0/16
570   192.64.1.* --> 192.64.1.0/24
571   192.64.1.2 --> 192.64.1.2/32 */
572static void
573cidrizeOne( const char *      in,
574            int               len,
575            struct evbuffer * out )
576{
577    int          stars = 0;
578    const char * pch;
579    const char * end;
580    char         zero = '0';
581    char         huh = '?';
582
583    for( pch = in, end = pch + len; pch != end; ++pch )
584    {
585        if( stars && isdigit( *pch ) )
586            evbuffer_add( out, &huh, 1 );
587        else if( *pch != '*' )
588            evbuffer_add( out, pch, 1 );
589        else
590        {
591            evbuffer_add( out, &zero, 1 );
592            ++stars;
593        }
594    }
595
596    evbuffer_add_printf( out, "/%d", ( 32 - ( stars * 8 ) ) );
597}
598
599char*
600cidrize( const char * acl )
601{
602    int               len;
603    const char *      walk = acl;
604    char *            ret;
605    struct evbuffer * out = evbuffer_new( );
606
607    FOR_EACH_WORD_IN_LIST( walk, len )
608    {
609        cidrizeOne( walk, len, out );
610        evbuffer_add_printf( out, "," );
611    }
612
613    /* the -1 is to eat the final ", " */
614    ret = tr_strndup( EVBUFFER_DATA( out ), EVBUFFER_LENGTH( out ) - 1 );
615    evbuffer_free( out );
616    return ret;
617}
618
619int
620tr_rpcTestACL( const tr_rpc_server  * server UNUSED,
621               const char *                  acl,
622               char **                       setme_errmsg )
623{
624    int    err = 0;
625    char * cidr = cidrize( acl );
626    char * errmsg = testACL( cidr );
627
628    if( errmsg )
629    {
630        if( setme_errmsg )
631            *setme_errmsg = errmsg;
632        else
633            tr_free( errmsg );
634        err = -1;
635    }
636    tr_free( cidr );
637    return err;
638}
639
640int
641tr_rpcSetACL( tr_rpc_server * server,
642              const char *    acl,
643              char **         setme_errmsg )
644{
645    char *    cidr = cidrize( acl );
646    const int err = tr_rpcTestACL( server, cidr, setme_errmsg );
647
648    if( !err )
649    {
650        const int isEnabled = server->isEnabled;
651
652        if( isEnabled )
653            stopServer( server );
654
655        tr_free( server->acl );
656        server->acl = tr_strdup( cidr );
657        dbgmsg( "setting our ACL to [%s]", server->acl );
658
659        if( isEnabled )
660            startServer( server );
661    }
662    tr_free( cidr );
663
664    return err;
665}
666
667char*
668tr_rpcGetACL( const tr_rpc_server * server )
669{
670    return tr_strdup( server->acl ? server->acl : "" );
671}
672
673/****
674*****  PASSWORD
675****/
676
677void
678tr_rpcSetUsername( tr_rpc_server * server,
679                   const char *    username )
680{
681    const int isEnabled = server->isEnabled;
682
683    if( isEnabled )
684        stopServer( server );
685
686    tr_free( server->username );
687    server->username = tr_strdup( username );
688    dbgmsg( "setting our Username to [%s]", server->username );
689
690    if( isEnabled )
691        startServer( server );
692}
693
694char*
695tr_rpcGetUsername( const tr_rpc_server * server )
696{
697    return tr_strdup( server->username ? server->username : "" );
698}
699
700void
701tr_rpcSetPassword( tr_rpc_server * server,
702                   const char *    password )
703{
704    const int isEnabled = server->isEnabled;
705
706    if( isEnabled )
707        stopServer( server );
708
709    tr_free( server->password );
710    server->password = tr_strdup( password );
711    dbgmsg( "setting our Password to [%s]", server->password );
712
713    if( isEnabled )
714        startServer( server );
715}
716
717char*
718tr_rpcGetPassword( const tr_rpc_server * server )
719{
720    return tr_strdup( server->password ? server->password : "" );
721}
722
723void
724tr_rpcSetPasswordEnabled( tr_rpc_server * server,
725                          int             isEnabled )
726{
727    const int wasEnabled = server->isEnabled;
728
729    if( wasEnabled )
730        stopServer( server );
731
732    server->isPasswordEnabled = isEnabled;
733    dbgmsg( "setting 'password enabled' to %d", isEnabled );
734
735    if( isEnabled )
736        startServer( server );
737}
738
739int
740tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
741{
742    return server->isPasswordEnabled;
743}
744
745/****
746*****  LIFE CYCLE
747****/
748
749void
750tr_rpcClose( tr_rpc_server ** ps )
751{
752    tr_rpc_server * s = *ps;
753
754    *ps = NULL;
755
756    stopServer( s );
757    tr_free( s->username );
758    tr_free( s->password );
759    tr_free( s->acl );
760    tr_free( s );
761}
762
763tr_rpc_server *
764tr_rpcInit( tr_handle *  session,
765            int          isEnabled,
766            int          port,
767            const char * acl,
768            int          isPasswordEnabled,
769            const char * username,
770            const char * password )
771{
772    char *          errmsg;
773    tr_rpc_server * s;
774
775    if( ( errmsg = testACL ( acl ) ) )
776    {
777        tr_nerr( MY_NAME, errmsg );
778        tr_free( errmsg );
779        acl = TR_DEFAULT_RPC_ACL;
780        tr_nerr( MY_NAME, "using fallback ACL \"%s\"", acl );
781    }
782
783    s = tr_new0( tr_rpc_server, 1 );
784    s->session = session;
785    s->port = port;
786    s->acl = tr_strdup( acl );
787    s->username = tr_strdup( username );
788    s->password = tr_strdup( password );
789    s->isPasswordEnabled = isPasswordEnabled != 0;
790    s->isEnabled = isEnabled != 0;
791
792    if( isEnabled )
793        startServer( s );
794    return s;
795}
796
Note: See TracBrowser for help on using the repository browser.