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

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

rpc-server cleanups. add true wildmat control. break the Mac build a little harder.

  • Property svn:keywords set to Date Rev Author Id
File size: 16.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 6798 2008-09-25 18:48:09Z charles $
11 */
12
13#include <assert.h>
14#include <string.h> /* memcpy */
15#include <limits.h> /* INT_MAX */
16
17#include <sys/types.h>
18#include <sys/stat.h>
19#include <fcntl.h> /* open */
20#include <unistd.h> /* close */
21
22#include <libevent/event.h>
23#include <libevent/evhttp.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#include "web.h"
33
34#define MY_NAME "RPC Server"
35#define MY_REALM "Transmission"
36#define MAX_TOKEN_LEN 16
37#define TR_N_ELEMENTS( ary ) ( sizeof( ary ) / sizeof( *ary ) )
38
39struct acl_addr
40{
41    char flag; /* '+' to allow, '-' to deny */
42    char quads[4][MAX_TOKEN_LEN];
43};
44
45struct tr_rpc_server
46{
47    unsigned int         isEnabled         : 1;
48    unsigned int         isPasswordEnabled : 1;
49    int                  port;
50    time_t               lastRequestTime;
51    struct evhttp *      httpd;
52    tr_handle *          session;
53    char *               username;
54    char *               password;
55    char *               acl;
56};
57
58#define dbgmsg( fmt... ) tr_deepLog( __FILE__, __LINE__, MY_NAME, ## fmt )
59
60static const char*
61tr_memmem( const char * s1,
62           size_t       l1,
63           const char * s2,
64           size_t       l2 )
65{
66    if( !l2 ) return s1;
67    while( l1 >= l2 )
68    {
69        l1--;
70        if( !memcmp( s1, s2, l2 ) )
71            return s1;
72        s1++;
73    }
74
75    return NULL;
76}
77
78
79/****
80*****  ACL UTILITIES
81****/
82
83static int
84parseAddress( const char * addr, struct acl_addr * setme, const char ** end )
85{
86    const char * pch;
87
88    memset( setme, 0, sizeof( struct acl_addr ) );
89
90    if( !addr ) return 0;
91
92    if(!((pch = strchr( addr, '.' )))) return 0;
93    if( pch-addr > MAX_TOKEN_LEN ) return 0;
94    memcpy( setme->quads[0], addr, pch-addr );
95    addr = pch + 1;
96
97    if(!((pch = strchr( addr, '.' )))) return 0;
98    if( pch-addr > MAX_TOKEN_LEN ) return 0;
99    memcpy( setme->quads[1], addr, pch-addr );
100    addr = pch + 1;
101
102    if(!((pch = strchr( addr, '.' )))) return 0;
103    if( pch-addr > MAX_TOKEN_LEN ) return 0;
104    memcpy( setme->quads[2], addr, pch-addr );
105    addr = pch + 1;
106
107    while( *pch && *pch!=',' ) ++pch;
108    if( pch-addr > MAX_TOKEN_LEN ) return 0;
109    memcpy( setme->quads[3], addr, pch-addr );
110
111    *end = pch;
112    return 1;
113}
114
115static int
116isAddressAllowed( const tr_rpc_server * server,
117                  const char * address )
118{
119    const char * acl;
120    struct acl_addr tmp;
121    const char * end;
122    const int parsed = parseAddress( address, &tmp, &end );
123
124    if( !parsed )
125        return 0;
126
127    for( acl=server->acl; acl && *acl; )
128    {
129        const char * delimiter = strchr( acl, ',' );
130        const int len = delimiter ? delimiter-acl : (int)strlen( acl );
131        char * token = tr_strndup( acl, len );
132        const int match = tr_wildmat( address, token+1 );
133        tr_free( token );
134        if( match )
135            return *acl == '+';
136        if( !delimiter )
137            break;
138        acl = delimiter + 1;
139    }
140
141    return 0;
142}
143
144/**
145***
146**/
147
148static void
149send_simple_response( struct evhttp_request * req, int code, const char * text )
150{
151    const char * code_text = tr_webGetResponseStr( code );
152    struct evbuffer * body = evbuffer_new( );
153    evbuffer_add_printf( body, "<h1>%s</h1>", text ? text : code_text );
154    evhttp_send_reply( req, code, code_text, body );
155    evbuffer_free( body );
156}
157
158static void
159handle_upload( struct evhttp_request * req, struct tr_rpc_server * server )
160{
161    if( req->type != EVHTTP_REQ_POST )
162    {
163        send_simple_response( req, HTTP_BADREQUEST, NULL );
164    }
165    else
166    {
167
168        const char * content_type = evhttp_find_header( req->input_headers, "Content-Type" );
169
170        const char * query = strchr( req->uri, '?' );
171        const int paused = query && strstr( query+1, "paused=true" );
172
173        const char * in = (const char *) EVBUFFER_DATA( req->input_buffer );
174        size_t inlen = EVBUFFER_LENGTH( req->input_buffer );
175
176        char * boundary =
177            tr_strdup_printf( "--%s", strstr( content_type,
178                                              "boundary=" ) +
179                             strlen( "boundary=" ) );
180        const size_t boundary_len = strlen( boundary );
181
182        const char * delim = tr_memmem( in, inlen, boundary, boundary_len );
183        while( delim )
184            {
185                size_t       part_len;
186                const char * part = delim + boundary_len;
187                inlen -= ( part - in );
188                in = part;
189                delim = tr_memmem( in, inlen, boundary, boundary_len );
190                part_len = delim ? (size_t)( delim - part ) : inlen;
191
192                if( part_len )
193                {
194                    char * text = tr_strndup( part, part_len );
195                    if( strstr( text, "filename=\"" ) )
196                    {
197                        const char * body = strstr( text, "\r\n\r\n" );
198                        if( body )
199                        {
200                            char *  b64, *json, *freeme;
201                            int     json_len;
202                            size_t  body_len;
203                            tr_benc top, *args;
204
205                            body += 4;
206                            body_len = part_len - ( body - text );
207                            if( body_len >= 2
208                              && !memcmp( &body[body_len - 2], "\r\n", 2 ) )
209                                body_len -= 2;
210
211                            tr_bencInitDict( &top, 2 );
212                            args = tr_bencDictAddDict( &top, "arguments", 2 );
213                            tr_bencDictAddStr( &top, "method",
214                                               "torrent-add" );
215                            b64 = tr_base64_encode( body, body_len, NULL );
216                            tr_bencDictAddStr( args, "metainfo", b64 );
217                            tr_bencDictAddInt( args, "paused", paused );
218                            json = tr_bencSaveAsJSON( &top, &json_len );
219                            freeme =
220                                tr_rpc_request_exec_json( server->session,
221                                                          json, json_len,
222                                                          NULL );
223
224                            tr_free( freeme );
225                            tr_free( json );
226                            tr_free( b64 );
227                            tr_bencFree( &top );
228                        }
229                    }
230                    tr_free( text );
231                }
232            }
233
234        tr_free( boundary );
235
236        /* use xml here because json responses to file uploads is trouble.
237           * see http://www.malsup.com/jquery/form/#sample7 for details */
238        evhttp_add_header(req->output_headers, "Content-Type", "text/xml; charset=UTF-8" );
239        send_simple_response( req, HTTP_OK, NULL );
240    }
241}
242
243static const char*
244mimetype_guess( const char * path )
245{
246    unsigned int i;
247    const struct {
248        const char * suffix;
249        const char * mime_type;
250    } types[] = {
251        /* these are just the ones we need for serving clutch... */
252        { "css", "text/css" },
253        { "gif", "image/gif" },
254        { "html", "text/html" },
255        { "ico", "image/vnd.microsoft.icon" },
256        { "js", "application/javascript" },
257        { "png", "image/png" }
258    };
259    const char * dot = strrchr( path, '.' );
260
261    for( i=0; dot && i<TR_N_ELEMENTS(types); ++i )
262        if( !strcmp( dot+1, types[i].suffix ) )
263            return types[i].mime_type;
264
265    return "application/octet-stream";
266}
267
268static void
269serve_file( struct evhttp_request * req, const char * path )
270{
271    if( req->type != EVHTTP_REQ_GET )
272    {
273        evhttp_add_header(req->output_headers, "Allow", "GET");
274        send_simple_response( req, 405, NULL );
275    }
276    else
277    {
278        const int fd = open( path, O_RDONLY, 0 );
279        if( fd == -1 )
280        {
281            send_simple_response( req, HTTP_NOTFOUND, NULL );
282        }
283        else
284        {
285            char size[12];
286            struct evbuffer * buf = evbuffer_new();
287
288            evbuffer_read(buf, fd, INT_MAX );
289            evhttp_add_header(req->output_headers, "Content-Type", mimetype_guess( path ) );
290            snprintf(size, sizeof(size), "%zu", EVBUFFER_LENGTH( buf ) );
291            evhttp_add_header(req->output_headers, "Content-Length", size );
292            evhttp_send_reply( req, HTTP_OK, "OK", buf );
293
294            evbuffer_free(buf);
295            close( fd );
296        }
297    }
298}
299
300static void
301handle_clutch( struct evhttp_request * req, struct tr_rpc_server * server )
302{
303    const char * uri;
304    struct evbuffer * buf = evbuffer_new( );
305
306    assert( !strncmp( req->uri, "/transmission/web/", 18 ) );
307
308    evbuffer_add_printf( buf, "%s%s", tr_getClutchDir( server->session ), TR_PATH_DELIMITER_STR );
309    uri = req->uri + 18;
310    if( (*uri=='?') || (*uri=='\0') )
311        evbuffer_add_printf( buf, "index.html" );
312    else {
313        const char * pch;
314        if(( pch = strchr( uri, '?' )))
315            evbuffer_add_printf( buf, "%*.*s", (int)(pch-uri), (int)(pch-uri), uri );
316        else
317            evbuffer_add_printf( buf, "%s", uri );
318    }
319                             
320    if( strstr( (const char *)EVBUFFER_DATA( buf ), ".." ) )
321        serve_file( req, "/dev/null" );
322    else
323        serve_file( req, (const char *)EVBUFFER_DATA( buf ) );
324
325    evbuffer_free( buf );
326}
327
328static void
329handle_rpc( struct evhttp_request * req, struct tr_rpc_server * server )
330{
331    int len = 0;
332    char * response;
333    struct evbuffer * buf;
334
335    if( req->type == EVHTTP_REQ_GET )
336    {
337        const char * q;
338        if(( q = strchr( req->uri, '?' )))
339            response = tr_rpc_request_exec_uri( server->session,
340                                                q + 1,
341                                                strlen( q + 1 ),
342                                                &len );
343    }
344    else if( req->type == EVHTTP_REQ_POST )
345    {
346        response = tr_rpc_request_exec_json( server->session,
347                                             EVBUFFER_DATA( req->input_buffer ),
348                                             EVBUFFER_LENGTH( req->input_buffer ),
349                                             &len );
350    }
351
352    buf = evbuffer_new( );
353    evbuffer_add( buf, response, len );
354    evhttp_add_header( req->output_headers, "Content-Type", "application/json; charset=UTF-8" );
355    evhttp_send_reply( req, HTTP_OK, "OK", buf );
356    evbuffer_free( buf );
357}
358
359static void
360handle_request( struct evhttp_request * req, void * arg )
361{
362    struct tr_rpc_server * server = arg;
363
364    if (req && req->evcon )
365    {
366        const char * auth;
367        char * user = NULL;
368        char * pass = NULL;
369
370        evhttp_add_header( req->output_headers, "Server", MY_REALM );
371       
372        auth = evhttp_find_header( req->input_headers, "Authorization" );
373
374        if( auth && !strncasecmp( auth, "basic ", 6 ) )
375        {
376            int plen;
377            char * p = tr_base64_decode( auth + 6, 0, &plen );
378            if( p && plen && (( pass = strchr( p, ':' )))) {
379                user = p;
380                *pass++ = '\0';
381            }
382        }
383
384        if( server->acl && !isAddressAllowed( server, req->remote_host ) )
385        {
386            send_simple_response( req, 401, "Unauthorized IP Address" );
387        }
388        else if( server->isPasswordEnabled && (    !pass
389                                                || !user
390                                                || strcmp( server->username, user )
391                                                || strcmp( server->password, pass ) ) )
392        {
393            evhttp_add_header( req->output_headers,
394                "WWW-Authenticate", "Basic realm=\"" MY_REALM "\"" );
395            send_simple_response( req, 401, "Unauthorized User" );
396        }
397        else if( !strcmp( req->uri, "/transmission/web" ) ||
398                 !strcmp( req->uri, "/transmission/clutch" ) ||
399                 !strcmp( req->uri, "/" ) )
400        {
401            evhttp_add_header( req->output_headers, "Location", "/transmission/web/" );
402            send_simple_response( req, HTTP_MOVEPERM, NULL );
403        }
404        else if( !strncmp( req->uri, "/transmission/web/", 18 ) )
405        {
406            handle_clutch( req, server );
407        }
408        else if( !strncmp( req->uri, "/transmission/rpc", 17 ) )
409        {
410            handle_rpc( req, server );
411        }
412        else if( !strncmp( req->uri, "/transmission/upload", 20 ) )
413        {
414            handle_upload( req, server );
415        }
416        else
417        {
418            send_simple_response( req, HTTP_NOTFOUND, NULL );
419        }
420
421        tr_free( user );
422    }
423}
424
425static void
426startServer( tr_rpc_server * server )
427{
428    dbgmsg( "in startServer; current context is %p", server->httpd );
429
430    if( !server->httpd )
431        if( ( server->httpd = evhttp_start( "0.0.0.0", server->port ) ) )
432            evhttp_set_gencb( server->httpd, handle_request, server );
433}
434
435static void
436stopServer( tr_rpc_server * server )
437{
438    if( server->httpd )
439    {
440        evhttp_free( server->httpd );
441        server->httpd = NULL;
442    }
443}
444
445void
446tr_rpcSetEnabled( tr_rpc_server * server,
447                  int             isEnabled )
448{
449    server->isEnabled = isEnabled != 0;
450
451    if( !isEnabled )
452        stopServer( server );
453    else
454        startServer( server );
455}
456
457int
458tr_rpcIsEnabled( const tr_rpc_server * server )
459{
460    return server->isEnabled;
461}
462
463void
464tr_rpcSetPort( tr_rpc_server * server,
465               int             port )
466{
467    if( server->port != port )
468    {
469        server->port = port;
470
471        if( server->isEnabled )
472        {
473            stopServer( server );
474            startServer( server );
475        }
476    }
477}
478
479int
480tr_rpcGetPort( const tr_rpc_server * server )
481{
482    return server->port;
483}
484
485void
486tr_rpcSetACL( tr_rpc_server * server,
487              const char *    acl )
488{
489    tr_free( server->acl );
490    server->acl = tr_strdup( acl );
491}
492
493char*
494tr_rpcGetACL( const tr_rpc_server * server )
495{
496    return tr_strdup( server->acl ? server->acl : "" );
497}
498
499/****
500*****  PASSWORD
501****/
502
503void
504tr_rpcSetUsername( tr_rpc_server * server,
505                   const char *    username )
506{
507    tr_free( server->username );
508    server->username = tr_strdup( username );
509    dbgmsg( "setting our Username to [%s]", server->username );
510}
511
512char*
513tr_rpcGetUsername( const tr_rpc_server * server )
514{
515    return tr_strdup( server->username ? server->username : "" );
516}
517
518void
519tr_rpcSetPassword( tr_rpc_server * server,
520                   const char *    password )
521{
522    tr_free( server->password );
523    server->password = tr_strdup( password );
524    dbgmsg( "setting our Password to [%s]", server->password );
525}
526
527char*
528tr_rpcGetPassword( const tr_rpc_server * server )
529{
530    return tr_strdup( server->password ? server->password : "" );
531}
532
533void
534tr_rpcSetPasswordEnabled( tr_rpc_server * server,
535                          int             isEnabled )
536{
537    server->isPasswordEnabled = isEnabled != 0;
538    dbgmsg( "setting 'password enabled' to %d", isEnabled );
539}
540
541int
542tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
543{
544    return server->isPasswordEnabled;
545}
546
547/****
548*****  LIFE CYCLE
549****/
550
551void
552tr_rpcClose( tr_rpc_server ** ps )
553{
554    tr_rpc_server * s = *ps;
555
556    *ps = NULL;
557
558    stopServer( s );
559    tr_free( s->acl );
560    tr_free( s->username );
561    tr_free( s->password );
562    tr_free( s );
563}
564
565tr_rpc_server *
566tr_rpcInit( tr_handle *  session,
567            int          isEnabled,
568            int          port,
569            const char * acl,
570            int          isPasswordEnabled,
571            const char * username,
572            const char * password )
573{
574    tr_rpc_server * s;
575
576    s = tr_new0( tr_rpc_server, 1 );
577    s->session = session;
578    s->port = port;
579    s->acl = tr_strdup( acl && *acl ? acl : TR_DEFAULT_RPC_ACL );
580    s->username = tr_strdup( username );
581    s->password = tr_strdup( password );
582    s->isPasswordEnabled = isPasswordEnabled != 0;
583    s->isEnabled = isEnabled != 0;
584
585    if( isEnabled )
586        startServer( s );
587    return s;
588}
Note: See TracBrowser for help on using the repository browser.