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

Last change on this file since 6800 was 6800, checked in by charles, 14 years ago

(libT) more rpc-server cleanup

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