2008-05-18 16:44:30 +00:00
|
|
|
/*
|
|
|
|
* This file Copyright (C) 2008 Charles Kerr <charles@rebelbase.com>
|
|
|
|
*
|
|
|
|
* This file is licensed by the GPL version 2. Works owned by the
|
|
|
|
* Transmission project are granted a special exemption to clause 2(b)
|
2008-09-23 19:11:04 +00:00
|
|
|
* so that the bulk of its code can remain under the MIT license.
|
2008-05-18 16:44:30 +00:00
|
|
|
* This exemption does not extend to derived works not owned by
|
|
|
|
* the Transmission project.
|
|
|
|
*
|
2008-05-28 17:17:12 +00:00
|
|
|
* $Id$
|
2008-05-18 16:44:30 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include <assert.h>
|
2008-06-04 17:14:58 +00:00
|
|
|
#include <ctype.h> /* isdigit */
|
|
|
|
#include <errno.h>
|
|
|
|
#include <stdlib.h> /* strtol */
|
2008-05-18 16:44:30 +00:00
|
|
|
#include <string.h>
|
|
|
|
|
2008-06-05 16:23:03 +00:00
|
|
|
#include <unistd.h> /* unlink */
|
|
|
|
|
2008-05-19 00:22:04 +00:00
|
|
|
#include <libevent/event.h>
|
2008-06-05 16:23:03 +00:00
|
|
|
#include <shttpd/defs.h> /* edit_passwords */
|
2008-05-19 00:22:04 +00:00
|
|
|
#include <shttpd/shttpd.h>
|
2008-05-18 16:44:30 +00:00
|
|
|
|
|
|
|
#include "transmission.h"
|
2008-06-30 21:11:53 +00:00
|
|
|
#include "bencode.h"
|
2008-07-16 17:47:20 +00:00
|
|
|
#include "list.h"
|
2008-07-11 04:07:14 +00:00
|
|
|
#include "platform.h"
|
2008-09-05 14:31:58 +00:00
|
|
|
#include "rpcimpl.h"
|
2008-05-18 16:44:30 +00:00
|
|
|
#include "rpc-server.h"
|
|
|
|
#include "utils.h"
|
|
|
|
|
2008-06-02 19:57:16 +00:00
|
|
|
#define MY_NAME "RPC Server"
|
2008-08-22 23:04:16 +00:00
|
|
|
#define MY_REALM "Transmission"
|
2008-06-02 19:57:16 +00:00
|
|
|
|
2008-07-16 17:47:20 +00:00
|
|
|
#define ACTIVE_INTERVAL_MSEC 40
|
2008-07-17 14:45:31 +00:00
|
|
|
#define INACTIVE_INTERVAL_MSEC 200
|
2008-05-18 16:44:30 +00:00
|
|
|
|
|
|
|
struct tr_rpc_server
|
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
unsigned int isEnabled : 1;
|
|
|
|
unsigned int isPasswordEnabled : 1;
|
|
|
|
int port;
|
|
|
|
time_t lastRequestTime;
|
|
|
|
struct shttpd_ctx * ctx;
|
|
|
|
tr_handle * session;
|
|
|
|
struct event timer;
|
|
|
|
char * username;
|
|
|
|
char * password;
|
|
|
|
char * acl;
|
|
|
|
tr_list * connections;
|
2008-05-18 16:44:30 +00:00
|
|
|
};
|
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
#define dbgmsg( fmt... ) tr_deepLog( __FILE__, __LINE__, MY_NAME, ## fmt )
|
2008-06-05 04:02:46 +00:00
|
|
|
|
2008-06-30 21:11:53 +00:00
|
|
|
static const char*
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_memmem( const char * s1,
|
|
|
|
size_t l1,
|
|
|
|
const char * s2,
|
|
|
|
size_t l2 )
|
2008-06-30 21:11:53 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
if( !l2 ) return s1;
|
|
|
|
while( l1 >= l2 )
|
|
|
|
{
|
2008-07-16 17:47:20 +00:00
|
|
|
l1--;
|
2008-09-23 19:11:04 +00:00
|
|
|
if( !memcmp( s1, s2, l2 ) )
|
2008-07-16 17:47:20 +00:00
|
|
|
return s1;
|
|
|
|
s1++;
|
|
|
|
}
|
2008-09-23 19:11:04 +00:00
|
|
|
|
2008-07-16 17:47:20 +00:00
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
***
|
|
|
|
**/
|
|
|
|
|
|
|
|
struct ConnBuf
|
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
char * key;
|
|
|
|
time_t lastActivity;
|
|
|
|
struct evbuffer * in;
|
|
|
|
struct evbuffer * out;
|
2008-07-16 17:47:20 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
static char*
|
|
|
|
buildKey( struct shttpd_arg * arg )
|
|
|
|
{
|
|
|
|
return tr_strdup_printf( "%s %s",
|
2008-09-23 19:11:04 +00:00
|
|
|
shttpd_get_env( arg, "REMOTE_ADDR" ),
|
|
|
|
shttpd_get_env( arg, "REQUEST_URI" ) );
|
2008-07-16 17:47:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static struct ConnBuf*
|
2008-09-23 19:11:04 +00:00
|
|
|
getBuffer(
|
|
|
|
tr_rpc_server * server,
|
|
|
|
struct shttpd_arg * arg )
|
2008-07-16 17:47:20 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_list * l;
|
|
|
|
char * key = buildKey( arg );
|
2008-07-16 17:47:20 +00:00
|
|
|
struct ConnBuf * found = NULL;
|
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
for( l = server->connections; l && !found; l = l->next )
|
2008-07-16 17:47:20 +00:00
|
|
|
{
|
|
|
|
struct ConnBuf * buf = l->data;
|
|
|
|
if( !strcmp( key, buf->key ) )
|
|
|
|
found = buf;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( found == NULL )
|
|
|
|
{
|
|
|
|
found = tr_new0( struct ConnBuf, 1 );
|
|
|
|
found->lastActivity = time( NULL );
|
|
|
|
found->key = tr_strdup( key );
|
|
|
|
found->in = evbuffer_new( );
|
|
|
|
found->out = evbuffer_new( );
|
|
|
|
tr_list_append( &server->connections, found );
|
|
|
|
}
|
|
|
|
|
|
|
|
tr_free( key );
|
|
|
|
return found;
|
2008-06-30 21:11:53 +00:00
|
|
|
}
|
|
|
|
|
2008-07-16 17:47:20 +00:00
|
|
|
static void
|
2008-09-23 19:11:04 +00:00
|
|
|
pruneBuf( tr_rpc_server * server,
|
|
|
|
struct ConnBuf * buf )
|
2008-07-16 17:47:20 +00:00
|
|
|
{
|
|
|
|
tr_list_remove_data( &server->connections, buf );
|
|
|
|
|
|
|
|
evbuffer_free( buf->in );
|
|
|
|
evbuffer_free( buf->out );
|
|
|
|
tr_free( buf->key );
|
|
|
|
tr_free( buf );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
***
|
|
|
|
**/
|
|
|
|
|
2008-06-30 21:11:53 +00:00
|
|
|
static void
|
|
|
|
handle_upload( struct shttpd_arg * arg )
|
|
|
|
{
|
2008-08-15 20:28:43 +00:00
|
|
|
struct tr_rpc_server * s;
|
2008-09-23 19:11:04 +00:00
|
|
|
struct ConnBuf * cbuf;
|
2008-08-15 20:28:43 +00:00
|
|
|
|
|
|
|
s = arg->user_data;
|
2008-06-30 21:11:53 +00:00
|
|
|
s->lastRequestTime = time( NULL );
|
2008-08-15 20:28:43 +00:00
|
|
|
cbuf = getBuffer( s, arg );
|
2008-06-30 21:11:53 +00:00
|
|
|
|
|
|
|
/* if we haven't parsed the POST, do that now */
|
2008-07-16 17:47:20 +00:00
|
|
|
if( !EVBUFFER_LENGTH( cbuf->out ) )
|
2008-06-30 21:11:53 +00:00
|
|
|
{
|
2008-08-15 20:28:43 +00:00
|
|
|
const char * query_string;
|
|
|
|
const char * content_type;
|
|
|
|
const char * delim;
|
|
|
|
const char * in;
|
2008-09-23 19:11:04 +00:00
|
|
|
size_t inlen;
|
|
|
|
char * boundary;
|
|
|
|
size_t boundary_len;
|
|
|
|
char buf[64];
|
|
|
|
int paused;
|
2008-08-15 20:28:43 +00:00
|
|
|
|
2008-06-30 21:11:53 +00:00
|
|
|
/* if we haven't finished reading the POST, read more now */
|
2008-07-16 17:47:20 +00:00
|
|
|
evbuffer_add( cbuf->in, arg->in.buf, arg->in.len );
|
2008-07-01 02:33:31 +00:00
|
|
|
arg->in.num_bytes = arg->in.len;
|
2008-06-30 21:11:53 +00:00
|
|
|
if( arg->flags & SHTTPD_MORE_POST_DATA )
|
|
|
|
return;
|
|
|
|
|
2008-08-15 20:28:43 +00:00
|
|
|
query_string = shttpd_get_env( arg, "QUERY_STRING" );
|
|
|
|
content_type = shttpd_get_header( arg, "Content-Type" );
|
|
|
|
in = (const char *) EVBUFFER_DATA( cbuf->in );
|
|
|
|
inlen = EVBUFFER_LENGTH( cbuf->in );
|
2008-09-23 19:11:04 +00:00
|
|
|
boundary =
|
|
|
|
tr_strdup_printf( "--%s", strstr( content_type,
|
|
|
|
"boundary=" ) +
|
|
|
|
strlen( "boundary=" ) );
|
2008-08-15 20:28:43 +00:00
|
|
|
boundary_len = strlen( boundary );
|
|
|
|
paused = ( query_string != NULL )
|
2008-09-23 19:11:04 +00:00
|
|
|
&& ( shttpd_get_var( "paused", query_string,
|
|
|
|
strlen( query_string ), buf,
|
|
|
|
sizeof( buf ) ) == 4 )
|
|
|
|
&& ( !strcmp( buf, "true" ) );
|
2008-06-30 21:11:53 +00:00
|
|
|
|
|
|
|
delim = tr_memmem( in, inlen, boundary, boundary_len );
|
|
|
|
if( delim ) do
|
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
size_t part_len;
|
|
|
|
const char * part = delim + boundary_len;
|
|
|
|
inlen -= ( part - in );
|
|
|
|
in = part;
|
|
|
|
delim = tr_memmem( in, inlen, boundary, boundary_len );
|
|
|
|
part_len = delim ? (size_t)( delim - part ) : inlen;
|
|
|
|
|
|
|
|
if( part_len )
|
2008-06-30 21:11:53 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
char * text = tr_strndup( part, part_len );
|
|
|
|
if( strstr( text, "filename=\"" ) )
|
2008-06-30 21:11:53 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
const char * body = strstr( text, "\r\n\r\n" );
|
|
|
|
if( body )
|
|
|
|
{
|
|
|
|
char * b64, *json, *freeme;
|
|
|
|
int json_len;
|
|
|
|
size_t body_len;
|
|
|
|
tr_benc top, *args;
|
|
|
|
|
|
|
|
body += 4;
|
|
|
|
body_len = part_len - ( body - text );
|
|
|
|
if( body_len >= 2
|
|
|
|
&& !memcmp( &body[body_len - 2], "\r\n", 2 ) )
|
|
|
|
body_len -= 2;
|
|
|
|
|
|
|
|
tr_bencInitDict( &top, 2 );
|
|
|
|
args = tr_bencDictAddDict( &top, "arguments", 2 );
|
|
|
|
tr_bencDictAddStr( &top, "method",
|
|
|
|
"torrent-add" );
|
|
|
|
b64 = tr_base64_encode( body, body_len, NULL );
|
|
|
|
tr_bencDictAddStr( args, "metainfo", b64 );
|
|
|
|
tr_bencDictAddInt( args, "paused", paused );
|
|
|
|
json = tr_bencSaveAsJSON( &top, &json_len );
|
|
|
|
freeme =
|
|
|
|
tr_rpc_request_exec_json( s->session, json,
|
|
|
|
json_len,
|
|
|
|
NULL );
|
|
|
|
|
|
|
|
tr_free( freeme );
|
|
|
|
tr_free( json );
|
|
|
|
tr_free( b64 );
|
|
|
|
tr_bencFree( &top );
|
|
|
|
}
|
2008-06-30 21:11:53 +00:00
|
|
|
}
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_free( text );
|
2008-06-30 21:11:53 +00:00
|
|
|
}
|
|
|
|
}
|
2008-09-23 19:11:04 +00:00
|
|
|
while( delim );
|
2008-06-30 21:11:53 +00:00
|
|
|
|
2008-07-16 17:47:20 +00:00
|
|
|
evbuffer_drain( cbuf->in, EVBUFFER_LENGTH( cbuf->in ) );
|
2008-06-30 21:11:53 +00:00
|
|
|
tr_free( boundary );
|
|
|
|
|
|
|
|
{
|
2008-07-01 02:33:31 +00:00
|
|
|
/* use xml here because json responses to file uploads is trouble.
|
|
|
|
* see http://www.malsup.com/jquery/form/#sample7 for details */
|
|
|
|
const char * response = "<result>success</result>";
|
2008-09-23 19:11:04 +00:00
|
|
|
const int len = strlen( response );
|
|
|
|
evbuffer_add_printf(
|
|
|
|
cbuf->out, "HTTP/1.1 200 OK\r\n"
|
|
|
|
"Content-Type: text/xml; charset=UTF-8\r\n"
|
|
|
|
"Content-Length: %d\r\n"
|
|
|
|
"\r\n"
|
|
|
|
"%s\r\n", len, response );
|
2008-06-30 21:11:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-07-16 17:47:20 +00:00
|
|
|
if( EVBUFFER_LENGTH( cbuf->out ) )
|
2008-06-30 21:11:53 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
const int n = MIN( ( int )EVBUFFER_LENGTH(
|
|
|
|
cbuf->out ), arg->out.len );
|
2008-07-16 17:47:20 +00:00
|
|
|
memcpy( arg->out.buf, EVBUFFER_DATA( cbuf->out ), n );
|
|
|
|
evbuffer_drain( cbuf->out, n );
|
2008-06-30 21:11:53 +00:00
|
|
|
arg->out.num_bytes = n;
|
|
|
|
}
|
|
|
|
|
2008-07-16 17:47:20 +00:00
|
|
|
if( !EVBUFFER_LENGTH( cbuf->out ) )
|
|
|
|
{
|
2008-06-30 21:11:53 +00:00
|
|
|
arg->flags |= SHTTPD_END_OF_OUTPUT;
|
2008-07-16 17:47:20 +00:00
|
|
|
pruneBuf( s, cbuf );
|
|
|
|
}
|
2008-06-30 21:11:53 +00:00
|
|
|
}
|
|
|
|
|
2008-08-06 00:24:05 +00:00
|
|
|
static void
|
|
|
|
handle_root( struct shttpd_arg * arg )
|
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
const char * redirect = "HTTP/1.1 200 OK" "\r\n"
|
|
|
|
"Content-Type: text/html; charset=UTF-8"
|
|
|
|
"\r\n"
|
|
|
|
"\r\n"
|
|
|
|
"<html><head>"
|
|
|
|
"\r\n"
|
|
|
|
" <meta http-equiv=\"Refresh\" content=\"2; url=/transmission/web/\">"
|
|
|
|
"\r\n"
|
|
|
|
"</head><body>"
|
|
|
|
"\r\n"
|
|
|
|
" <p>redirecting to <a href=\"/transmission/web\">/transmission/web/</a></p>"
|
|
|
|
"\r\n"
|
|
|
|
"</body></html>"
|
|
|
|
"\r\n";
|
2008-08-06 00:24:05 +00:00
|
|
|
const size_t n = strlen( redirect );
|
2008-09-23 19:11:04 +00:00
|
|
|
|
2008-08-06 00:24:05 +00:00
|
|
|
memcpy( arg->out.buf, redirect, n );
|
|
|
|
arg->in.num_bytes = arg->in.len;
|
|
|
|
arg->out.num_bytes = n;
|
|
|
|
arg->flags |= SHTTPD_END_OF_OUTPUT;
|
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
static void
|
|
|
|
handle_rpc( struct shttpd_arg * arg )
|
|
|
|
{
|
2008-08-15 20:28:43 +00:00
|
|
|
struct tr_rpc_server * s;
|
2008-09-23 19:11:04 +00:00
|
|
|
struct ConnBuf * cbuf;
|
2008-08-15 20:28:43 +00:00
|
|
|
|
|
|
|
s = arg->user_data;
|
2008-06-10 17:13:56 +00:00
|
|
|
s->lastRequestTime = time( NULL );
|
2008-08-15 20:28:43 +00:00
|
|
|
cbuf = getBuffer( s, arg );
|
2008-05-18 16:44:30 +00:00
|
|
|
|
2008-07-16 17:47:20 +00:00
|
|
|
if( !EVBUFFER_LENGTH( cbuf->out ) )
|
2008-05-18 16:44:30 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
int len = 0;
|
|
|
|
char * response = NULL;
|
2008-05-18 16:44:30 +00:00
|
|
|
const char * request_method = shttpd_get_env( arg, "REQUEST_METHOD" );
|
|
|
|
const char * query_string = shttpd_get_env( arg, "QUERY_STRING" );
|
|
|
|
|
|
|
|
if( query_string && *query_string )
|
|
|
|
response = tr_rpc_request_exec_uri( s->session,
|
|
|
|
query_string,
|
|
|
|
strlen( query_string ),
|
|
|
|
&len );
|
|
|
|
else if( !strcmp( request_method, "POST" ) )
|
|
|
|
{
|
2008-07-16 17:47:20 +00:00
|
|
|
evbuffer_add( cbuf->in, arg->in.buf, arg->in.len );
|
2008-05-18 16:44:30 +00:00
|
|
|
arg->in.num_bytes = arg->in.len;
|
2008-05-19 18:16:58 +00:00
|
|
|
if( arg->flags & SHTTPD_MORE_POST_DATA )
|
2008-05-18 16:44:30 +00:00
|
|
|
return;
|
2008-05-19 18:16:58 +00:00
|
|
|
response = tr_rpc_request_exec_json( s->session,
|
2008-07-16 17:47:20 +00:00
|
|
|
EVBUFFER_DATA( cbuf->in ),
|
|
|
|
EVBUFFER_LENGTH( cbuf->in ),
|
2008-05-19 18:16:58 +00:00
|
|
|
&len );
|
2008-07-16 17:47:20 +00:00
|
|
|
evbuffer_drain( cbuf->in, EVBUFFER_LENGTH( cbuf->in ) );
|
2008-05-18 16:44:30 +00:00
|
|
|
}
|
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
evbuffer_add_printf(
|
|
|
|
cbuf->out, "HTTP/1.1 200 OK\r\n"
|
|
|
|
"Content-Type: application/json; charset=UTF-8\r\n"
|
|
|
|
"Content-Length: %d\r\n"
|
|
|
|
"\r\n"
|
|
|
|
"%*.*s", len, len, len, response );
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_free( response );
|
|
|
|
}
|
|
|
|
|
2008-07-16 17:47:20 +00:00
|
|
|
if( EVBUFFER_LENGTH( cbuf->out ) )
|
2008-05-18 16:44:30 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
const int n = MIN( ( int )EVBUFFER_LENGTH(
|
|
|
|
cbuf->out ), arg->out.len );
|
2008-07-16 17:47:20 +00:00
|
|
|
memcpy( arg->out.buf, EVBUFFER_DATA( cbuf->out ), n );
|
|
|
|
evbuffer_drain( cbuf->out, n );
|
2008-05-18 16:44:30 +00:00
|
|
|
arg->out.num_bytes = n;
|
|
|
|
}
|
|
|
|
|
2008-07-16 17:47:20 +00:00
|
|
|
if( !EVBUFFER_LENGTH( cbuf->out ) )
|
|
|
|
{
|
2008-05-18 16:44:30 +00:00
|
|
|
arg->flags |= SHTTPD_END_OF_OUTPUT;
|
2008-07-16 17:47:20 +00:00
|
|
|
pruneBuf( s, cbuf );
|
|
|
|
}
|
2008-05-18 16:44:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2008-09-23 19:11:04 +00:00
|
|
|
rpcPulse( int socket UNUSED,
|
|
|
|
short action UNUSED,
|
|
|
|
void * vserver )
|
2008-05-18 16:44:30 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
int interval;
|
|
|
|
struct timeval tv;
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_rpc_server * server = vserver;
|
2008-09-23 19:11:04 +00:00
|
|
|
const time_t now = time( NULL );
|
2008-05-18 16:44:30 +00:00
|
|
|
|
|
|
|
assert( server );
|
|
|
|
|
2008-06-25 11:34:35 +00:00
|
|
|
if( server->ctx )
|
|
|
|
shttpd_poll( server->ctx, 1 );
|
2008-05-18 16:44:30 +00:00
|
|
|
|
|
|
|
/* set a timer for the next pulse */
|
2008-07-16 17:47:20 +00:00
|
|
|
if( now - server->lastRequestTime < 300 )
|
|
|
|
interval = ACTIVE_INTERVAL_MSEC;
|
2008-06-10 19:25:18 +00:00
|
|
|
else
|
2008-07-16 17:47:20 +00:00
|
|
|
interval = INACTIVE_INTERVAL_MSEC;
|
2008-05-18 16:44:30 +00:00
|
|
|
tv = tr_timevalMsec( interval );
|
|
|
|
evtimer_add( &server->timer, &tv );
|
|
|
|
}
|
|
|
|
|
2008-06-05 16:23:03 +00:00
|
|
|
static void
|
2008-09-23 19:11:04 +00:00
|
|
|
getPasswordFile( tr_rpc_server * server,
|
|
|
|
char * buf,
|
|
|
|
int buflen )
|
2008-06-05 16:23:03 +00:00
|
|
|
{
|
|
|
|
tr_buildPath( buf, buflen, tr_sessionGetConfigDir( server->session ),
|
2008-09-23 19:11:04 +00:00
|
|
|
"htpasswd",
|
|
|
|
NULL );
|
2008-06-05 16:23:03 +00:00
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
static void
|
|
|
|
startServer( tr_rpc_server * server )
|
|
|
|
{
|
2008-06-05 04:02:46 +00:00
|
|
|
dbgmsg( "in startServer; current context is %p", server->ctx );
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
if( !server->ctx )
|
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
int i;
|
|
|
|
int argc = 0;
|
|
|
|
char * argv[100];
|
|
|
|
char passwd[MAX_PATH_LENGTH];
|
|
|
|
const char * clutchDir = tr_getClutchDir( server->session );
|
2008-07-16 17:47:20 +00:00
|
|
|
struct timeval tv = tr_timevalMsec( INACTIVE_INTERVAL_MSEC );
|
2008-05-18 16:44:30 +00:00
|
|
|
|
2008-06-05 16:23:03 +00:00
|
|
|
getPasswordFile( server, passwd, sizeof( passwd ) );
|
|
|
|
if( !server->isPasswordEnabled )
|
|
|
|
unlink( passwd );
|
|
|
|
else
|
2008-09-23 19:11:04 +00:00
|
|
|
shttpd_edit_passwords( passwd, MY_REALM, server->username,
|
|
|
|
server->password );
|
2008-06-05 16:23:03 +00:00
|
|
|
|
2008-07-16 23:55:49 +00:00
|
|
|
argv[argc++] = tr_strdup( "appname-unused" );
|
2008-07-17 14:45:31 +00:00
|
|
|
|
2008-07-16 23:55:49 +00:00
|
|
|
argv[argc++] = tr_strdup( "-ports" );
|
|
|
|
argv[argc++] = tr_strdup_printf( "%d", server->port );
|
2008-07-17 14:45:31 +00:00
|
|
|
|
2008-07-16 23:55:49 +00:00
|
|
|
argv[argc++] = tr_strdup( "-dir_list" );
|
|
|
|
argv[argc++] = tr_strdup( "0" );
|
2008-07-17 14:45:31 +00:00
|
|
|
|
2008-07-16 23:55:49 +00:00
|
|
|
argv[argc++] = tr_strdup( "-auth_realm" );
|
|
|
|
argv[argc++] = tr_strdup( MY_REALM );
|
2008-07-17 14:45:31 +00:00
|
|
|
|
2008-08-15 19:45:46 +00:00
|
|
|
argv[argc++] = tr_strdup( "-root" );
|
|
|
|
argv[argc++] = tr_strdup( "/dev/null" );
|
|
|
|
|
2008-07-16 23:55:49 +00:00
|
|
|
if( server->acl )
|
|
|
|
{
|
|
|
|
argv[argc++] = tr_strdup( "-acl" );
|
|
|
|
argv[argc++] = tr_strdup( server->acl );
|
2008-07-11 17:09:53 +00:00
|
|
|
}
|
2008-07-16 23:55:49 +00:00
|
|
|
if( server->isPasswordEnabled )
|
|
|
|
{
|
|
|
|
argv[argc++] = tr_strdup( "-protect" );
|
|
|
|
argv[argc++] = tr_strdup_printf( "/transmission=%s", passwd );
|
2008-06-05 04:02:46 +00:00
|
|
|
}
|
2008-07-16 23:55:49 +00:00
|
|
|
if( clutchDir && *clutchDir )
|
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_inf( _(
|
|
|
|
"Serving the web interface files from \"%s\"" ),
|
|
|
|
clutchDir );
|
2008-08-06 00:24:05 +00:00
|
|
|
argv[argc++] = tr_strdup( "-aliases" );
|
|
|
|
argv[argc++] = tr_strdup_printf( "%s=%s,%s=%s",
|
2008-09-23 19:11:04 +00:00
|
|
|
"/transmission/clutch",
|
|
|
|
clutchDir,
|
|
|
|
"/transmission/web",
|
|
|
|
clutchDir );
|
2008-06-05 16:23:03 +00:00
|
|
|
}
|
2008-05-18 16:44:30 +00:00
|
|
|
|
2008-07-17 14:45:31 +00:00
|
|
|
argv[argc] = NULL; /* shttpd_init() wants it null-terminated */
|
2008-07-17 04:27:03 +00:00
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
if( ( server->ctx = shttpd_init( argc, argv ) ) )
|
2008-09-03 19:59:09 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
shttpd_register_uri( server->ctx, "/transmission/rpc",
|
|
|
|
handle_rpc,
|
|
|
|
server );
|
|
|
|
shttpd_register_uri( server->ctx, "/transmission/upload",
|
|
|
|
handle_upload,
|
|
|
|
server );
|
2008-09-03 19:59:09 +00:00
|
|
|
shttpd_register_uri( server->ctx, "/", handle_root, server );
|
2008-07-16 23:55:49 +00:00
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
evtimer_set( &server->timer, rpcPulse, server );
|
|
|
|
evtimer_add( &server->timer, &tv );
|
|
|
|
}
|
2008-07-16 23:55:49 +00:00
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
for( i = 0; i < argc; ++i )
|
2008-07-16 23:55:49 +00:00
|
|
|
tr_free( argv[i] );
|
2008-05-18 16:44:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
stopServer( tr_rpc_server * server )
|
|
|
|
{
|
|
|
|
if( server->ctx )
|
|
|
|
{
|
2008-06-05 16:23:03 +00:00
|
|
|
char passwd[MAX_PATH_LENGTH];
|
|
|
|
getPasswordFile( server, passwd, sizeof( passwd ) );
|
|
|
|
unlink( passwd );
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
evtimer_del( &server->timer );
|
|
|
|
shttpd_fini( server->ctx );
|
|
|
|
server->ctx = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_rpcSetEnabled( tr_rpc_server * server,
|
|
|
|
int isEnabled )
|
2008-05-18 16:44:30 +00:00
|
|
|
{
|
2008-09-03 19:59:09 +00:00
|
|
|
server->isEnabled = isEnabled != 0;
|
2008-05-18 16:44:30 +00:00
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( !isEnabled )
|
|
|
|
stopServer( server );
|
|
|
|
else
|
2008-05-18 16:44:30 +00:00
|
|
|
startServer( server );
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
tr_rpcIsEnabled( const tr_rpc_server * server )
|
|
|
|
{
|
|
|
|
return server->ctx != NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_rpcSetPort( tr_rpc_server * server,
|
|
|
|
int port )
|
2008-05-18 16:44:30 +00:00
|
|
|
{
|
|
|
|
if( server->port != port )
|
|
|
|
{
|
|
|
|
server->port = port;
|
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( server->isEnabled )
|
2008-05-18 16:44:30 +00:00
|
|
|
{
|
|
|
|
stopServer( server );
|
|
|
|
startServer( server );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
tr_rpcGetPort( const tr_rpc_server * server )
|
|
|
|
{
|
|
|
|
return server->port;
|
|
|
|
}
|
|
|
|
|
2008-06-05 16:23:03 +00:00
|
|
|
/****
|
|
|
|
***** ACL
|
|
|
|
****/
|
|
|
|
|
2008-06-02 19:44:19 +00:00
|
|
|
/*
|
2008-07-16 23:55:49 +00:00
|
|
|
* FOR_EACH_WORD_IN_LIST, isbyte, and testACL are from, or modified from,
|
2008-06-02 19:44:19 +00:00
|
|
|
* shttpd, written by Sergey Lyubka under this license:
|
|
|
|
* "THE BEER-WARE LICENSE" (Revision 42):
|
|
|
|
* Sergey Lyubka wrote this file. As long as you retain this notice you
|
|
|
|
* can do whatever you want with this stuff. If we meet some day, and you think
|
|
|
|
* this stuff is worth it, you can buy me a beer in return.
|
|
|
|
*/
|
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
#define FOR_EACH_WORD_IN_LIST( s, len ) \
|
|
|
|
for( ; s != NULL && ( len = strcspn( s, DELIM_CHARS ) ) != 0; \
|
|
|
|
s += len, s += strspn( s, DELIM_CHARS ) )
|
2008-06-02 19:44:19 +00:00
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
static int
|
|
|
|
isbyte( int n ) { return n >= 0 && n <= 255; }
|
2008-06-02 19:44:19 +00:00
|
|
|
|
|
|
|
static char*
|
|
|
|
testACL( const char * s )
|
|
|
|
{
|
|
|
|
int len;
|
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
FOR_EACH_WORD_IN_LIST( s, len )
|
2008-06-02 19:44:19 +00:00
|
|
|
{
|
|
|
|
char flag;
|
|
|
|
int a, b, c, d, n, mask;
|
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
if( sscanf( s, "%c%d.%d.%d.%d%n", &flag, &a, &b, &c, &d, &n ) != 5 )
|
|
|
|
return tr_strdup_printf( _(
|
|
|
|
"[%s]: subnet must be [+|-]x.x.x.x[/x]" ),
|
|
|
|
s );
|
|
|
|
if( flag != '+' && flag != '-' )
|
2008-06-02 19:44:19 +00:00
|
|
|
return tr_strdup_printf( _( "[%s]: flag must be + or -" ), s );
|
2008-09-23 19:11:04 +00:00
|
|
|
if( !isbyte( a ) || !isbyte( b ) || !isbyte( c ) || !isbyte( d ) )
|
2008-06-02 19:44:19 +00:00
|
|
|
return tr_strdup_printf( _( "[%s]: bad ip address" ), s );
|
2008-09-23 19:11:04 +00:00
|
|
|
if( sscanf( s + n, "/%d", &mask ) == 1 && ( mask < 0 || mask > 32 ) )
|
2008-06-02 19:44:19 +00:00
|
|
|
return tr_strdup_printf( _( "[%s]: bad subnet mask %d" ), s, n );
|
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2008-06-04 17:14:58 +00:00
|
|
|
/* 192.*.*.* --> 192.0.0.0/8
|
|
|
|
192.64.*.* --> 192.64.0.0/16
|
|
|
|
192.64.1.* --> 192.64.1.0/24
|
|
|
|
192.64.1.2 --> 192.64.1.2/32 */
|
|
|
|
static void
|
2008-09-23 19:11:04 +00:00
|
|
|
cidrizeOne( const char * in,
|
|
|
|
int len,
|
|
|
|
struct evbuffer * out )
|
2008-06-04 17:14:58 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
int stars = 0;
|
2008-06-04 17:14:58 +00:00
|
|
|
const char * pch;
|
|
|
|
const char * end;
|
2008-09-23 19:11:04 +00:00
|
|
|
char zero = '0';
|
|
|
|
char huh = '?';
|
2008-06-04 17:14:58 +00:00
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
for( pch = in, end = pch + len; pch != end; ++pch )
|
|
|
|
{
|
|
|
|
if( stars && isdigit( *pch ) )
|
|
|
|
evbuffer_add( out, &huh, 1 );
|
|
|
|
else if( *pch != '*' )
|
2008-06-04 17:14:58 +00:00
|
|
|
evbuffer_add( out, pch, 1 );
|
2008-09-23 19:11:04 +00:00
|
|
|
else
|
|
|
|
{
|
2008-06-04 17:14:58 +00:00
|
|
|
evbuffer_add( out, &zero, 1 );
|
|
|
|
++stars;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
evbuffer_add_printf( out, "/%d", ( 32 - ( stars * 8 ) ) );
|
2008-06-04 17:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
char*
|
|
|
|
cidrize( const char * acl )
|
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
int len;
|
|
|
|
const char * walk = acl;
|
|
|
|
char * ret;
|
2008-06-04 17:14:58 +00:00
|
|
|
struct evbuffer * out = evbuffer_new( );
|
|
|
|
|
|
|
|
FOR_EACH_WORD_IN_LIST( walk, len )
|
|
|
|
{
|
|
|
|
cidrizeOne( walk, len, out );
|
2008-06-09 23:28:14 +00:00
|
|
|
evbuffer_add_printf( out, "," );
|
2008-06-04 17:14:58 +00:00
|
|
|
}
|
|
|
|
|
2008-06-11 15:17:59 +00:00
|
|
|
/* the -1 is to eat the final ", " */
|
2008-08-21 16:12:17 +00:00
|
|
|
ret = tr_strndup( EVBUFFER_DATA( out ), EVBUFFER_LENGTH( out ) - 1 );
|
2008-06-04 17:14:58 +00:00
|
|
|
evbuffer_free( out );
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2008-06-02 19:44:19 +00:00
|
|
|
int
|
2008-06-04 17:14:58 +00:00
|
|
|
tr_rpcTestACL( const tr_rpc_server * server UNUSED,
|
2008-09-23 19:11:04 +00:00
|
|
|
const char * acl,
|
|
|
|
char ** setme_errmsg )
|
2008-05-18 16:44:30 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
int err = 0;
|
2008-06-04 17:14:58 +00:00
|
|
|
char * cidr = cidrize( acl );
|
|
|
|
char * errmsg = testACL( cidr );
|
2008-09-23 19:11:04 +00:00
|
|
|
|
2008-06-02 19:44:19 +00:00
|
|
|
if( errmsg )
|
|
|
|
{
|
2008-06-04 04:49:45 +00:00
|
|
|
if( setme_errmsg )
|
|
|
|
*setme_errmsg = errmsg;
|
2008-06-04 06:36:50 +00:00
|
|
|
else
|
|
|
|
tr_free( errmsg );
|
2008-06-04 19:46:37 +00:00
|
|
|
err = -1;
|
2008-06-02 19:44:19 +00:00
|
|
|
}
|
2008-06-04 17:14:58 +00:00
|
|
|
tr_free( cidr );
|
2008-06-04 19:46:37 +00:00
|
|
|
return err;
|
2008-06-04 17:14:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
int
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_rpcSetACL( tr_rpc_server * server,
|
|
|
|
const char * acl,
|
|
|
|
char ** setme_errmsg )
|
2008-06-04 17:14:58 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
char * cidr = cidrize( acl );
|
2008-06-04 19:46:37 +00:00
|
|
|
const int err = tr_rpcTestACL( server, cidr, setme_errmsg );
|
2008-06-04 17:14:58 +00:00
|
|
|
|
2008-06-05 04:02:46 +00:00
|
|
|
if( !err )
|
2008-06-02 19:44:19 +00:00
|
|
|
{
|
2008-09-03 19:59:09 +00:00
|
|
|
const int isEnabled = server->isEnabled;
|
2008-06-04 17:14:58 +00:00
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( isEnabled )
|
2008-06-02 19:44:19 +00:00
|
|
|
stopServer( server );
|
2008-05-18 16:44:30 +00:00
|
|
|
|
2008-06-02 19:44:19 +00:00
|
|
|
tr_free( server->acl );
|
2008-06-04 17:14:58 +00:00
|
|
|
server->acl = tr_strdup( cidr );
|
2008-06-05 04:02:46 +00:00
|
|
|
dbgmsg( "setting our ACL to [%s]", server->acl );
|
2008-05-18 16:44:30 +00:00
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( isEnabled )
|
2008-06-02 19:44:19 +00:00
|
|
|
startServer( server );
|
|
|
|
}
|
2008-06-08 04:23:56 +00:00
|
|
|
tr_free( cidr );
|
2008-06-02 19:44:19 +00:00
|
|
|
|
2008-06-04 19:46:37 +00:00
|
|
|
return err;
|
2008-05-18 16:44:30 +00:00
|
|
|
}
|
|
|
|
|
2008-06-05 16:23:03 +00:00
|
|
|
char*
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_rpcGetACL( const tr_rpc_server * server )
|
|
|
|
{
|
2008-06-05 16:23:03 +00:00
|
|
|
return tr_strdup( server->acl ? server->acl : "" );
|
|
|
|
}
|
|
|
|
|
|
|
|
/****
|
|
|
|
***** PASSWORD
|
|
|
|
****/
|
|
|
|
|
|
|
|
void
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_rpcSetUsername( tr_rpc_server * server,
|
|
|
|
const char * username )
|
2008-06-05 16:23:03 +00:00
|
|
|
{
|
2008-09-03 19:59:09 +00:00
|
|
|
const int isEnabled = server->isEnabled;
|
2008-06-05 16:23:03 +00:00
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( isEnabled )
|
2008-06-05 16:23:03 +00:00
|
|
|
stopServer( server );
|
|
|
|
|
|
|
|
tr_free( server->username );
|
|
|
|
server->username = tr_strdup( username );
|
|
|
|
dbgmsg( "setting our Username to [%s]", server->username );
|
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( isEnabled )
|
2008-06-05 16:23:03 +00:00
|
|
|
startServer( server );
|
|
|
|
}
|
|
|
|
|
|
|
|
char*
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_rpcGetUsername( const tr_rpc_server * server )
|
2008-06-05 16:23:03 +00:00
|
|
|
{
|
|
|
|
return tr_strdup( server->username ? server->username : "" );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_rpcSetPassword( tr_rpc_server * server,
|
|
|
|
const char * password )
|
2008-06-05 16:23:03 +00:00
|
|
|
{
|
2008-09-03 19:59:09 +00:00
|
|
|
const int isEnabled = server->isEnabled;
|
2008-06-05 16:23:03 +00:00
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( isEnabled )
|
2008-06-05 16:23:03 +00:00
|
|
|
stopServer( server );
|
|
|
|
|
|
|
|
tr_free( server->password );
|
|
|
|
server->password = tr_strdup( password );
|
|
|
|
dbgmsg( "setting our Password to [%s]", server->password );
|
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( isEnabled )
|
2008-06-05 16:23:03 +00:00
|
|
|
startServer( server );
|
|
|
|
}
|
|
|
|
|
|
|
|
char*
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_rpcGetPassword( const tr_rpc_server * server )
|
2008-06-05 16:23:03 +00:00
|
|
|
{
|
|
|
|
return tr_strdup( server->password ? server->password : "" );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_rpcSetPasswordEnabled( tr_rpc_server * server,
|
|
|
|
int isEnabled )
|
2008-06-05 16:23:03 +00:00
|
|
|
{
|
2008-09-03 19:59:09 +00:00
|
|
|
const int wasEnabled = server->isEnabled;
|
2008-06-05 16:23:03 +00:00
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( wasEnabled )
|
2008-06-05 16:23:03 +00:00
|
|
|
stopServer( server );
|
|
|
|
|
|
|
|
server->isPasswordEnabled = isEnabled;
|
|
|
|
dbgmsg( "setting 'password enabled' to %d", isEnabled );
|
|
|
|
|
2008-09-03 19:59:09 +00:00
|
|
|
if( isEnabled )
|
2008-06-05 16:23:03 +00:00
|
|
|
startServer( server );
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
tr_rpcIsPasswordEnabled( const tr_rpc_server * server )
|
|
|
|
{
|
|
|
|
return server->isPasswordEnabled;
|
2008-05-18 16:44:30 +00:00
|
|
|
}
|
|
|
|
|
2008-06-05 16:23:03 +00:00
|
|
|
/****
|
|
|
|
***** LIFE CYCLE
|
|
|
|
****/
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
void
|
|
|
|
tr_rpcClose( tr_rpc_server ** ps )
|
|
|
|
{
|
|
|
|
tr_rpc_server * s = *ps;
|
2008-09-23 19:11:04 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
*ps = NULL;
|
|
|
|
|
|
|
|
stopServer( s );
|
2008-07-16 19:36:46 +00:00
|
|
|
tr_free( s->username );
|
|
|
|
tr_free( s->password );
|
2008-05-19 18:16:58 +00:00
|
|
|
tr_free( s->acl );
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_free( s );
|
|
|
|
}
|
|
|
|
|
|
|
|
tr_rpc_server *
|
2008-09-23 19:11:04 +00:00
|
|
|
tr_rpcInit( tr_handle * session,
|
|
|
|
int isEnabled,
|
|
|
|
int port,
|
|
|
|
const char * acl,
|
|
|
|
int isPasswordEnabled,
|
|
|
|
const char * username,
|
|
|
|
const char * password )
|
2008-05-18 16:44:30 +00:00
|
|
|
{
|
2008-09-23 19:11:04 +00:00
|
|
|
char * errmsg;
|
2008-06-02 19:57:16 +00:00
|
|
|
tr_rpc_server * s;
|
|
|
|
|
2008-09-23 19:11:04 +00:00
|
|
|
if( ( errmsg = testACL ( acl ) ) )
|
2008-06-02 19:57:16 +00:00
|
|
|
{
|
|
|
|
tr_nerr( MY_NAME, errmsg );
|
|
|
|
tr_free( errmsg );
|
|
|
|
acl = TR_DEFAULT_RPC_ACL;
|
|
|
|
tr_nerr( MY_NAME, "using fallback ACL \"%s\"", acl );
|
|
|
|
}
|
|
|
|
|
|
|
|
s = tr_new0( tr_rpc_server, 1 );
|
2008-05-18 16:44:30 +00:00
|
|
|
s->session = session;
|
|
|
|
s->port = port;
|
|
|
|
s->acl = tr_strdup( acl );
|
2008-06-05 16:23:03 +00:00
|
|
|
s->username = tr_strdup( username );
|
|
|
|
s->password = tr_strdup( password );
|
2008-09-03 19:59:09 +00:00
|
|
|
s->isPasswordEnabled = isPasswordEnabled != 0;
|
|
|
|
s->isEnabled = isEnabled != 0;
|
2008-09-23 19:11:04 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
if( isEnabled )
|
|
|
|
startServer( s );
|
2008-09-23 19:11:04 +00:00
|
|
|
return s;
|
2008-05-18 16:44:30 +00:00
|
|
|
}
|
2008-09-23 19:11:04 +00:00
|
|
|
|