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

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

#1209: shttpd crash when setting port to one already in use

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