(trunk) support an X-Transmission-Session-Id header in the RPC server. Yesterday's approach of including the session_id in posted forms -- which is a typical approach -- isn't sufficient for Transmission, since it also allows remote access via JSON/RPC. (part 1 of 2. part 2 is kjg's web ui patch)
This commit is contained in:
parent
872465d12e
commit
6559fbbd16
121
daemon/remote.c
121
daemon/remote.c
|
@ -10,6 +10,7 @@
|
|||
* $Id$
|
||||
*/
|
||||
|
||||
#include <ctype.h> /* isspace */
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
@ -132,6 +133,7 @@ static int reqCount = 0;
|
|||
static int debug = 0;
|
||||
static char * auth = NULL;
|
||||
static char * netrc = NULL;
|
||||
static char * sessionId = NULL;
|
||||
|
||||
static char*
|
||||
tr_getcwd( void )
|
||||
|
@ -1273,56 +1275,113 @@ processResponse( const char * host,
|
|||
}
|
||||
}
|
||||
|
||||
/* look for a session id in the header in case the server gives back a 409 */
|
||||
static size_t
|
||||
parseResponseHeader( void *ptr, size_t size, size_t nmemb, void * stream UNUSED )
|
||||
{
|
||||
const char * line = ptr;
|
||||
const size_t line_len = size * nmemb;
|
||||
const char * key = TR_RPC_SESSION_ID_HEADER ": ";
|
||||
const size_t key_len = strlen( key );
|
||||
|
||||
if( ( line_len >= key_len ) && !memcmp( line, key, key_len ) )
|
||||
{
|
||||
const char * begin = line + key_len;
|
||||
const char * end = begin;
|
||||
while( !isspace( *end ) )
|
||||
++end;
|
||||
tr_free( sessionId );
|
||||
sessionId = tr_strndup( begin, end-begin );
|
||||
}
|
||||
|
||||
return line_len;
|
||||
}
|
||||
|
||||
static CURL*
|
||||
tr_curl_easy_init( struct evbuffer * writebuf )
|
||||
{
|
||||
CURL * curl = curl_easy_init( );
|
||||
curl_easy_setopt( curl, CURLOPT_USERAGENT, MY_NAME "/" LONG_VERSION_STRING );
|
||||
curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, writeFunc );
|
||||
curl_easy_setopt( curl, CURLOPT_WRITEDATA, writebuf );
|
||||
curl_easy_setopt( curl, CURLOPT_HEADERFUNCTION, parseResponseHeader );
|
||||
curl_easy_setopt( curl, CURLOPT_POST, 1 );
|
||||
curl_easy_setopt( curl, CURLOPT_NETRC, CURL_NETRC_OPTIONAL );
|
||||
curl_easy_setopt( curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY );
|
||||
curl_easy_setopt( curl, CURLOPT_TIMEOUT, 60L );
|
||||
curl_easy_setopt( curl, CURLOPT_VERBOSE, debug );
|
||||
#ifdef HAVE_ZLIB
|
||||
curl_easy_setopt( curl, CURLOPT_ENCODING, "deflate" );
|
||||
#endif
|
||||
if( netrc )
|
||||
curl_easy_setopt( curl, CURLOPT_NETRC_FILE, netrc );
|
||||
if( auth )
|
||||
curl_easy_setopt( curl, CURLOPT_USERPWD, auth );
|
||||
if( sessionId ) {
|
||||
char * h = tr_strdup_printf( "%s: %s", TR_RPC_SESSION_ID_HEADER, sessionId );
|
||||
struct curl_slist * custom_headers = curl_slist_append( NULL, h );
|
||||
curl_easy_setopt( curl, CURLOPT_HTTPHEADER, custom_headers );
|
||||
/* fixme: leaks */
|
||||
}
|
||||
return curl;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
processRequests( const char * host,
|
||||
int port,
|
||||
const char ** reqs,
|
||||
int reqCount )
|
||||
{
|
||||
int i;
|
||||
CURL * curl;
|
||||
int i;
|
||||
CURL * curl = NULL;
|
||||
struct evbuffer * buf = evbuffer_new( );
|
||||
char * url = tr_strdup_printf(
|
||||
"http://%s:%d/transmission/rpc", host, port );
|
||||
char * url = tr_strdup_printf( "http://%s:%d/transmission/rpc", host, port );
|
||||
|
||||
curl = curl_easy_init( );
|
||||
curl_easy_setopt( curl, CURLOPT_VERBOSE, debug );
|
||||
#ifdef HAVE_ZLIB
|
||||
curl_easy_setopt( curl, CURLOPT_ENCODING, "deflate" );
|
||||
#endif
|
||||
curl_easy_setopt( curl, CURLOPT_USERAGENT, MY_NAME "/" LONG_VERSION_STRING );
|
||||
curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, writeFunc );
|
||||
curl_easy_setopt( curl, CURLOPT_WRITEDATA, buf );
|
||||
curl_easy_setopt( curl, CURLOPT_POST, 1 );
|
||||
curl_easy_setopt( curl, CURLOPT_URL, url );
|
||||
curl_easy_setopt( curl, CURLOPT_NETRC, CURL_NETRC_OPTIONAL );
|
||||
curl_easy_setopt( curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY );
|
||||
curl_easy_setopt( curl, CURLOPT_TIMEOUT, 60L );
|
||||
if( netrc )
|
||||
curl_easy_setopt( curl, CURLOPT_NETRC_FILE, netrc );
|
||||
if( auth )
|
||||
curl_easy_setopt( curl, CURLOPT_USERPWD, auth );
|
||||
|
||||
for( i = 0; i < reqCount; ++i )
|
||||
for( i=0; i<reqCount; ++i )
|
||||
{
|
||||
CURLcode res;
|
||||
evbuffer_drain( buf, EVBUFFER_LENGTH( buf ) );
|
||||
|
||||
if( curl == NULL )
|
||||
{
|
||||
curl = tr_curl_easy_init( buf );
|
||||
curl_easy_setopt( curl, CURLOPT_URL, url );
|
||||
}
|
||||
|
||||
curl_easy_setopt( curl, CURLOPT_POSTFIELDS, reqs[i] );
|
||||
|
||||
if( debug )
|
||||
fprintf( stderr, "posting:\n--------\n%s\n--------\n", reqs[i] );
|
||||
if( ( res = curl_easy_perform( curl ) ) )
|
||||
tr_nerr( MY_NAME, "(%s:%d) %s", host, port,
|
||||
curl_easy_strerror( res ) );
|
||||
else
|
||||
processResponse( host, port, EVBUFFER_DATA(
|
||||
buf ), EVBUFFER_LENGTH( buf ) );
|
||||
|
||||
evbuffer_drain( buf, EVBUFFER_LENGTH( buf ) );
|
||||
tr_nerr( MY_NAME, "(%s:%d) %s", host, port, curl_easy_strerror( res ) );
|
||||
else {
|
||||
long response;
|
||||
curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &response );
|
||||
switch( response ) {
|
||||
case 200:
|
||||
processResponse( host, port, EVBUFFER_DATA(buf), EVBUFFER_LENGTH(buf) );
|
||||
break;
|
||||
case 409:
|
||||
/* session id failed. our curl header func has already
|
||||
* pulled the new session id from this response's headers,
|
||||
* build a new CURL* and try again */
|
||||
curl_easy_cleanup( curl );
|
||||
curl = NULL;
|
||||
--i;
|
||||
break;
|
||||
default:
|
||||
fprintf( stderr, "Unexpected response: %s\n", (char*)EVBUFFER_DATA(buf) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* cleanup */
|
||||
tr_free( url );
|
||||
evbuffer_free( buf );
|
||||
curl_easy_cleanup( curl );
|
||||
if( curl != NULL )
|
||||
curl_easy_cleanup( curl );
|
||||
}
|
||||
|
||||
int
|
||||
|
|
|
@ -1226,7 +1226,7 @@ getMaxPeerCount( const tr_torrent * tor )
|
|||
static int
|
||||
getPeerCount( const Torrent * t )
|
||||
{
|
||||
return tr_ptrArraySize( &t->peers );// + tr_ptrArraySize( &t->outgoingHandshakes );
|
||||
return tr_ptrArraySize( &t->peers );/* + tr_ptrArraySize( &t->outgoingHandshakes ); */
|
||||
}
|
||||
|
||||
/* FIXME: this is kind of a mess. */
|
||||
|
|
|
@ -39,6 +39,13 @@
|
|||
#include "web.h"
|
||||
#include "net.h"
|
||||
|
||||
/* session-id is used to make cross-site request forgery attacks difficult.
|
||||
* Don't disable this feature unless you really know what you're doing!
|
||||
* http://en.wikipedia.org/wiki/Cross-site_request_forgery
|
||||
* http://shiflett.org/articles/cross-site-request-forgeries
|
||||
* http://www.webappsec.org/lists/websecurity/archive/2008-04/msg00037.html */
|
||||
#define REQUIRE_SESSION_ID
|
||||
|
||||
#define MY_NAME "RPC Server"
|
||||
#define MY_REALM "Transmission"
|
||||
#define TR_N_ELEMENTS( ary ) ( sizeof( ary ) / sizeof( *ary ) )
|
||||
|
@ -61,6 +68,9 @@ struct tr_rpc_server
|
|||
char * whitelistStr;
|
||||
tr_list * whitelist;
|
||||
|
||||
char * sessionId;
|
||||
time_t sessionIdExpiresAt;
|
||||
|
||||
#ifdef HAVE_ZLIB
|
||||
z_stream stream;
|
||||
#endif
|
||||
|
@ -450,22 +460,55 @@ isAddressAllowed( const tr_rpc_server * server,
|
|||
return FALSE;
|
||||
}
|
||||
|
||||
static char*
|
||||
get_current_session_id( struct tr_rpc_server * server )
|
||||
{
|
||||
const time_t now = time( NULL );
|
||||
|
||||
if( !server->sessionId || ( now >= server->sessionIdExpiresAt ) )
|
||||
{
|
||||
int i;
|
||||
const int n = 48;
|
||||
const char * pool = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const size_t pool_size = strlen( pool );
|
||||
char * buf = tr_new( char, n+1 );
|
||||
|
||||
for( i=0; i<n; ++i )
|
||||
buf[i] = pool[ tr_cryptoRandInt( pool_size ) ];
|
||||
buf[n] = '\0';
|
||||
|
||||
tr_free( server->sessionId );
|
||||
server->sessionId = buf;
|
||||
server->sessionIdExpiresAt = now + (60*60); /* expire in an hour */
|
||||
}
|
||||
|
||||
return server->sessionId;
|
||||
}
|
||||
|
||||
|
||||
static tr_bool
|
||||
test_session_id( struct tr_rpc_server * server, struct evhttp_request * req )
|
||||
{
|
||||
const char * ours = get_current_session_id( server );
|
||||
const char * theirs = evhttp_find_header( req->input_headers, TR_RPC_SESSION_ID_HEADER );
|
||||
const tr_bool success = theirs && !strcmp( theirs, ours );
|
||||
return success;
|
||||
}
|
||||
|
||||
static void
|
||||
handle_request( struct evhttp_request * req,
|
||||
void * arg )
|
||||
handle_request( struct evhttp_request * req, void * arg )
|
||||
{
|
||||
struct tr_rpc_server * server = arg;
|
||||
|
||||
if( req && req->evcon )
|
||||
{
|
||||
const char * auth;
|
||||
char * user = NULL;
|
||||
char * pass = NULL;
|
||||
char * user = NULL;
|
||||
char * pass = NULL;
|
||||
|
||||
evhttp_add_header( req->output_headers, "Server", MY_REALM );
|
||||
|
||||
auth = evhttp_find_header( req->input_headers, "Authorization" );
|
||||
|
||||
if( auth && !strncasecmp( auth, "basic ", 6 ) )
|
||||
{
|
||||
int plen;
|
||||
|
@ -479,7 +522,7 @@ handle_request( struct evhttp_request * req,
|
|||
|
||||
if( !isAddressAllowed( server, req->remote_host ) )
|
||||
{
|
||||
send_simple_response( req, 401,
|
||||
send_simple_response( req, 403,
|
||||
"<p>Unauthorized IP Address.</p>"
|
||||
"<p>Either disable the IP address whitelist or add your address to it.</p>"
|
||||
"<p>If you're editing settings.json, see the 'rpc-whitelist' and 'rpc-whitelist-enabled' entries.</p>"
|
||||
|
@ -511,6 +554,22 @@ handle_request( struct evhttp_request * req,
|
|||
{
|
||||
handle_clutch( req, server );
|
||||
}
|
||||
#ifdef REQUIRE_SESSION_ID
|
||||
else if( !test_session_id( server, req ) )
|
||||
{
|
||||
const char * sessionId = get_current_session_id( server );
|
||||
char * tmp = tr_strdup_printf(
|
||||
"<p>Please add this header to your requests:</p>"
|
||||
"<p><code>%s: %s</code></p>"
|
||||
"<p>This requirement is to make "
|
||||
"<a href=\"http://en.wikipedia.org/wiki/Cross-site_request_forgery\">CSRF</a>"
|
||||
" attacks more difficult.</p>",
|
||||
TR_RPC_SESSION_ID_HEADER, sessionId );
|
||||
evhttp_add_header( req->output_headers, TR_RPC_SESSION_ID_HEADER, sessionId );
|
||||
send_simple_response( req, 409, tmp );
|
||||
tr_free( tmp );
|
||||
}
|
||||
#endif
|
||||
else if( !strncmp( req->uri, "/transmission/rpc", 17 ) )
|
||||
{
|
||||
handle_rpc( req, server );
|
||||
|
|
|
@ -57,6 +57,8 @@ extern "C" {
|
|||
#define SHA_DIGEST_LENGTH 20
|
||||
#define TR_INET6_ADDRSTRLEN 46
|
||||
|
||||
#define TR_RPC_SESSION_ID_HEADER "X-Transmission-Session-Id"
|
||||
|
||||
typedef uint32_t tr_file_index_t;
|
||||
typedef uint32_t tr_piece_index_t;
|
||||
typedef uint64_t tr_block_index_t;
|
||||
|
|
|
@ -541,7 +541,11 @@ Session :: exec( const char * request )
|
|||
QHttpRequestHeader header( "POST", path );
|
||||
header.setValue( "User-Agent", QCoreApplication::instance()->applicationName() + "/" + LONG_VERSION_STRING );
|
||||
header.setValue( "Content-Type", "application/json; charset=UTF-8" );
|
||||
myHttp.request( header, data, &myBuffer );
|
||||
if( !mySessionId.isEmpty( ) )
|
||||
header.setValue( TR_RPC_SESSION_ID_HEADER, mySessionId );
|
||||
QBuffer * buf = new QBuffer;
|
||||
buf->setData( data );
|
||||
myHttp.request( header, buf, &myBuffer );
|
||||
#ifdef DEBUG_HTTP
|
||||
std::cerr << "sending " << qPrintable(header.toString()) << "\nBody:\n" << request << std::endl;
|
||||
#endif
|
||||
|
@ -561,6 +565,9 @@ Session :: onRequestFinished( int id, bool error )
|
|||
{
|
||||
Q_UNUSED( id );
|
||||
|
||||
QHttpResponseHeader response = myHttp.lastResponse();
|
||||
QIODevice * sourceDevice = myHttp.currentSourceDevice( );
|
||||
|
||||
#ifdef DEBUG_HTTP
|
||||
std::cerr << "http request " << id << " ended.. response header: "
|
||||
<< qPrintable( myHttp.lastResponse().toString() )
|
||||
|
@ -569,17 +576,27 @@ Session :: onRequestFinished( int id, bool error )
|
|||
<< std::endl;
|
||||
#endif
|
||||
|
||||
if( error )
|
||||
if( ( response.statusCode() == 409 ) && response.hasKey( TR_RPC_SESSION_ID_HEADER ) )
|
||||
{
|
||||
// we got a 409 telling us our session id has expired.
|
||||
// update it and resubmit the request.
|
||||
mySessionId = response.value( TR_RPC_SESSION_ID_HEADER );
|
||||
exec( qobject_cast<QBuffer*>(sourceDevice)->buffer().constData( ) );
|
||||
}
|
||||
else if( error )
|
||||
{
|
||||
std::cerr << "http error: " << qPrintable(myHttp.errorString()) << std::endl;
|
||||
else {
|
||||
}
|
||||
else
|
||||
{
|
||||
const QByteArray& response( myBuffer.buffer( ) );
|
||||
const char * json( response.constData( ) );
|
||||
int jsonLength( response.size( ) );
|
||||
if( jsonLength>0 && json[jsonLength-1] == '\n' ) --jsonLength;
|
||||
|
||||
parseResponse( json, jsonLength );
|
||||
}
|
||||
|
||||
delete sourceDevice;
|
||||
myBuffer.buffer( ).clear( );
|
||||
myBuffer.reset( );
|
||||
assert( myBuffer.bytesAvailable( ) < 1 );
|
||||
|
|
|
@ -132,6 +132,7 @@ class Session: public QObject
|
|||
Prefs& myPrefs;
|
||||
tr_session * mySession;
|
||||
QString myConfigDir;
|
||||
QString mySessionId;
|
||||
QUrl myUrl;
|
||||
QBuffer myBuffer;
|
||||
QHttp myHttp;
|
||||
|
|
Loading…
Reference in New Issue