source: branches/1.3x/libtransmission/rpc-server.c @ 6535

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

backport r6533 to 1.3x: "fix shttpd issue reported by Gimp_"

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