1101 lines
32 KiB
C
1101 lines
32 KiB
C
/*
|
|
* This file Copyright (C) 2007 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)
|
|
* so that the bulk of its code can remain under the MIT license.
|
|
* This exemption does not extend to derived works not owned by
|
|
* the Transmission project.
|
|
*
|
|
* $Id$
|
|
*/
|
|
|
|
#include <assert.h>
|
|
#include <ctype.h> /* isalnum */
|
|
#include <limits.h> /* INT_MAX */
|
|
#include <stdio.h> /* snprintf */
|
|
#include <stdlib.h>
|
|
#include <string.h> /* strcmp, strchr */
|
|
#include <sys/queue.h> /* for evhttp */
|
|
#include <sys/types.h> /* for evhttp */
|
|
|
|
#include <event.h>
|
|
#include <evhttp.h>
|
|
|
|
#include "transmission.h"
|
|
#include "bencode.h"
|
|
#include "completion.h"
|
|
#include "net.h"
|
|
#include "ptrarray.h"
|
|
#include "publish.h"
|
|
#include "timer.h"
|
|
#include "tracker.h"
|
|
#include "trevent.h"
|
|
#include "utils.h"
|
|
|
|
#define MINUTES_TO_MSEC(N) ((N) * 60 * 1000)
|
|
|
|
/* manual announces via "update tracker" are allowed this frequently */
|
|
#define MANUAL_ANNOUNCE_INTERVAL_MSEC (MINUTES_TO_MSEC(10))
|
|
|
|
/* unless the tracker tells us otherwise, rescrape this frequently */
|
|
#define DEFAULT_SCRAPE_INTERVAL_MSEC (MINUTES_TO_MSEC(15))
|
|
|
|
/* unless the tracker tells us otherwise, reannounce this frequently */
|
|
#define DEFAULT_ANNOUNCE_INTERVAL_MSEC (MINUTES_TO_MSEC(20))
|
|
|
|
/* this is how long we'll leave a scrape request hanging before timeout */
|
|
#define SCRAPE_TIMEOUT_INTERVAL_SEC 60
|
|
|
|
/* this is how long we'll leave a tracker request hanging before timeout */
|
|
#define REQ_TIMEOUT_INTERVAL_SEC 60
|
|
|
|
/* the value of the 'numwant' argument passed in tracker requests */
|
|
#define NUMWANT 75
|
|
|
|
/* the length of the 'key' argument passed in tracker requests */
|
|
#define TR_KEY_LEN 10
|
|
|
|
|
|
/**
|
|
***
|
|
**/
|
|
|
|
typedef struct
|
|
{
|
|
tr_handle_t * handle;
|
|
|
|
tr_ptrArray_t * torrents;
|
|
tr_ptrArray_t * scraping;
|
|
tr_ptrArray_t * scrapeQueue;
|
|
|
|
/* these are set from the latest scrape or tracker response */
|
|
int announceIntervalMsec;
|
|
int minAnnounceIntervalMsec;
|
|
int scrapeIntervalMsec;
|
|
|
|
/* calculated when we get fewer scrapes
|
|
back than we asked for */
|
|
int multiscrapeMax;
|
|
|
|
tr_tracker_info_t * redirect;
|
|
tr_tracker_info_t * addresses;
|
|
int addressIndex;
|
|
int addressCount;
|
|
int * tierFronts;
|
|
|
|
char * primaryAddress;
|
|
|
|
/* sent as the "key" argument in tracker requests
|
|
to verify us if our IP address changes.
|
|
This is immutable for the life of the tracker object. */
|
|
char key_param[TR_KEY_LEN+1];
|
|
|
|
tr_timer_tag scrapeTag;
|
|
}
|
|
Tracker;
|
|
|
|
/* this is the Torrent struct, but since it's the pointer
|
|
passed around in the public API of this tracker module,
|
|
its *public* name is tr_tracker_s... wheee */
|
|
typedef struct tr_tracker_s
|
|
{
|
|
tr_publisher_t * publisher;
|
|
|
|
/* torrent hash string */
|
|
uint8_t hash[SHA_DIGEST_LENGTH];
|
|
char escaped[SHA_DIGEST_LENGTH * 3 + 1];
|
|
|
|
/* corresponds to the peer_id sent as a tracker request parameter.
|
|
OiNK's op TooMuchTime says: "When the same torrent is opened and
|
|
closed and opened again without quitting Transmission ...
|
|
change the peerid. It would help sometimes if a stopped event
|
|
was missed to ensure that we didn't think someone was cheating. */
|
|
char peer_id[TR_ID_LEN + 1];
|
|
|
|
/* these are set from the latest scrape or tracker response...
|
|
-1 means 'unknown' */
|
|
int timesDownloaded;
|
|
int seeders;
|
|
int leechers;
|
|
char * trackerID;
|
|
|
|
/* the last tracker request we sent. (started, stopped, etc.)
|
|
automatic announces are an empty string;
|
|
NULL means no message has ever been sent */
|
|
char * lastRequest;
|
|
|
|
uint64_t manualAnnounceAllowedAt;
|
|
|
|
Tracker * tracker;
|
|
|
|
tr_timer_tag scrapeTag;
|
|
tr_timer_tag reannounceTag;
|
|
|
|
struct evhttp_request * httpReq;
|
|
|
|
tr_torrent_t * torrent;
|
|
}
|
|
Torrent;
|
|
|
|
static int
|
|
trackerCompare( const void * va, const void * vb )
|
|
{
|
|
const Tracker * a = ( const Tracker * ) va;
|
|
const Tracker * b = ( const Tracker * ) vb;
|
|
return strcmp( a->primaryAddress, b->primaryAddress );
|
|
}
|
|
|
|
static int
|
|
torrentCompare( const void * va, const void * vb )
|
|
{
|
|
const Torrent * a = (const Torrent*) va;
|
|
const Torrent * b = (const Torrent*) vb;
|
|
return memcmp( a->hash, b->hash, SHA_DIGEST_LENGTH );
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
typedef struct
|
|
{
|
|
char * address;
|
|
int port;
|
|
struct evhttp_connection * evconn;
|
|
}
|
|
connection_key_t;
|
|
|
|
static int
|
|
connectionCompare( const void * va, const void * vb )
|
|
{
|
|
const connection_key_t * a = (const connection_key_t *) va;
|
|
const connection_key_t * b = (const connection_key_t *) vb;
|
|
int ret = strcmp( a->address, b->address );
|
|
if( ret ) return ret;
|
|
return a->port - b->port;
|
|
}
|
|
|
|
static struct evhttp_connection*
|
|
getConnection( const char * address, int port )
|
|
{
|
|
connection_key_t *val, tmp;
|
|
|
|
static tr_ptrArray_t * connections = NULL;
|
|
if( !connections )
|
|
connections = tr_ptrArrayNew( );
|
|
|
|
tmp.address = (char*) address;
|
|
tmp.port = port;
|
|
val = tr_ptrArrayFindSorted( connections, &tmp, connectionCompare );
|
|
|
|
if( !val )
|
|
{
|
|
val = tr_new( connection_key_t, 1 );
|
|
val->address = tr_strdup( address );
|
|
val->port = port;
|
|
val->evconn = evhttp_connection_new( address, port );
|
|
tr_ptrArrayInsertSorted( connections, val, connectionCompare );
|
|
}
|
|
|
|
return val->evconn;
|
|
}
|
|
|
|
/***
|
|
**** PUBLISH
|
|
***/
|
|
|
|
static const tr_tracker_event_t emptyEvent = { 0, NULL, NULL, NULL, 0 };
|
|
|
|
static void
|
|
publishMessage( Torrent * tor, const char * msg, int type )
|
|
{
|
|
tr_tracker_event_t event = emptyEvent;
|
|
event.hash = tor->hash;
|
|
event.messageType = type;
|
|
event.text = msg;
|
|
tr_publisherPublish( tor->publisher, tor, &event );
|
|
}
|
|
static void
|
|
publishErrorMessage( Torrent * tor, const char * msg )
|
|
{
|
|
publishMessage( tor, msg, TR_TRACKER_ERROR );
|
|
}
|
|
static void
|
|
publishWarningMessage( Torrent * tor, const char * msg )
|
|
{
|
|
publishMessage( tor, msg, TR_TRACKER_WARNING );
|
|
}
|
|
|
|
static void
|
|
publishNewPeers( Torrent * tor, int count, uint8_t * peers )
|
|
{
|
|
tr_tracker_event_t event = emptyEvent;
|
|
event.hash = tor->hash;
|
|
event.messageType = TR_TRACKER_PEERS;
|
|
event.peerCount = count;
|
|
event.peerCompact = peers;
|
|
tr_inf( "Torrent \"%s\" got %d new peers", tor->torrent->info.name, count );
|
|
tr_publisherPublish( tor->publisher, tor, &event );
|
|
}
|
|
|
|
static void
|
|
publishStopped( Torrent * tor )
|
|
{
|
|
tr_tracker_event_t event = emptyEvent;
|
|
event.hash = tor->hash;
|
|
event.messageType = TR_TRACKER_STOPPED;
|
|
tr_publisherPublish( tor->publisher, tor, &event );
|
|
}
|
|
|
|
/***
|
|
**** LIFE CYCLE
|
|
***/
|
|
|
|
static tr_ptrArray_t *
|
|
getTrackerLookupTable( void )
|
|
{
|
|
static tr_ptrArray_t * myTrackers = NULL;
|
|
if( !myTrackers )
|
|
myTrackers = tr_ptrArrayNew( );
|
|
return myTrackers;
|
|
}
|
|
|
|
static void
|
|
generateKeyParam( char * msg, int len )
|
|
{
|
|
int i;
|
|
const char * pool = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
for( i=0; i<len; ++i )
|
|
*msg++ = pool[tr_rand(36)];
|
|
*msg = '\0';
|
|
}
|
|
|
|
static int onTrackerScrapeNow( void* );
|
|
|
|
static void
|
|
tr_trackerScrapeSoon( Tracker * t )
|
|
{
|
|
/* don't start more than one scrape at once for the same tracker... */
|
|
if( !tr_ptrArrayEmpty( t->scraping ) )
|
|
return;
|
|
|
|
if( !t->scrapeTag )
|
|
t->scrapeTag = tr_timerNew( t->handle,
|
|
onTrackerScrapeNow, t,
|
|
NULL, 1000 );
|
|
}
|
|
|
|
static Tracker*
|
|
tr_trackerGet( const tr_torrent_t * tor )
|
|
{
|
|
const tr_info_t * info = &tor->info;
|
|
tr_ptrArray_t * trackers = getTrackerLookupTable( );
|
|
Tracker *t, tmp;
|
|
assert( info != NULL );
|
|
assert( info->primaryAddress && *info->primaryAddress );
|
|
tmp.primaryAddress = info->primaryAddress;
|
|
t = tr_ptrArrayFindSorted( trackers, &tmp, trackerCompare );
|
|
|
|
assert( t==NULL || !strcmp(t->primaryAddress,info->primaryAddress) );
|
|
|
|
if( t == NULL ) /* no such tracker.... create one */
|
|
{
|
|
int i, j, sum, *iwalk;
|
|
tr_tracker_info_t * nwalk;
|
|
tr_dbg( "making a new tracker for \"%s\"", info->primaryAddress );
|
|
|
|
t = tr_new0( Tracker, 1 );
|
|
t->handle = tor->handle;
|
|
t->primaryAddress = tr_strdup( info->primaryAddress );
|
|
t->scrapeIntervalMsec = DEFAULT_SCRAPE_INTERVAL_MSEC;
|
|
t->announceIntervalMsec = DEFAULT_ANNOUNCE_INTERVAL_MSEC;
|
|
t->minAnnounceIntervalMsec = DEFAULT_ANNOUNCE_INTERVAL_MSEC;
|
|
t->multiscrapeMax = INT_MAX;
|
|
t->torrents = tr_ptrArrayNew( );
|
|
t->scraping = tr_ptrArrayNew( );
|
|
t->scrapeQueue = tr_ptrArrayNew( );
|
|
generateKeyParam( t->key_param, TR_KEY_LEN );
|
|
|
|
for( sum=i=0; i<info->trackerTiers; ++i )
|
|
sum += info->trackerList[i].count;
|
|
t->addresses = nwalk = tr_new0( tr_tracker_info_t, sum );
|
|
t->addressIndex = 0;
|
|
t->addressCount = sum;
|
|
t->tierFronts = iwalk = tr_new0( int, sum );
|
|
|
|
for( i=0; i<info->trackerTiers; ++i )
|
|
{
|
|
const int tierFront = nwalk - t->addresses;
|
|
|
|
for( j=0; j<info->trackerList[i].count; ++j )
|
|
{
|
|
const tr_tracker_info_t * src = &info->trackerList[i].list[j];
|
|
nwalk->address = tr_strdup( src->address );
|
|
nwalk->port = src->port;
|
|
nwalk->announce = tr_strdup( src->announce );
|
|
nwalk->scrape = tr_strdup( src->scrape );
|
|
++nwalk;
|
|
|
|
*iwalk++ = tierFront;
|
|
}
|
|
}
|
|
|
|
assert( nwalk - t->addresses == sum );
|
|
assert( iwalk - t->tierFronts == sum );
|
|
|
|
tr_ptrArrayInsertSorted( trackers, t, trackerCompare );
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
static Torrent *
|
|
getExistingTorrent( Tracker * t, const uint8_t hash[SHA_DIGEST_LENGTH] )
|
|
{
|
|
Torrent tmp;
|
|
memcpy( tmp.hash, hash, SHA_DIGEST_LENGTH );
|
|
return tr_ptrArrayFindSorted( t->torrents, &tmp, torrentCompare );
|
|
}
|
|
|
|
static void
|
|
escape( char * out, const uint8_t * in, int in_len ) /* rfc2396 */
|
|
{
|
|
const uint8_t *end = in + in_len;
|
|
while( in != end )
|
|
if( isalnum(*in) )
|
|
*out++ = (char) *in++;
|
|
else
|
|
out += snprintf( out, 4, "%%%02X", (unsigned int)*in++ );
|
|
*out = '\0';
|
|
}
|
|
|
|
static int
|
|
onTorrentFreeNow( void * vtor )
|
|
{
|
|
Torrent * tor = (Torrent *) vtor;
|
|
Tracker * t = tor->tracker;
|
|
|
|
tr_ptrArrayRemoveSorted( t->torrents, tor, torrentCompare );
|
|
tr_ptrArrayRemoveSorted( t->scrapeQueue, tor, torrentCompare );
|
|
tr_ptrArrayRemoveSorted( t->scraping, tor, torrentCompare );
|
|
|
|
tr_timerFree( &tor->scrapeTag );
|
|
tr_timerFree( &tor->reannounceTag );
|
|
tr_publisherFree( tor->publisher );
|
|
tr_free( tor->trackerID );
|
|
tr_free( tor->lastRequest );
|
|
tr_free( tor );
|
|
|
|
if( tr_ptrArrayEmpty( t->torrents ) ) /* last one.. free the tracker too */
|
|
{
|
|
int i;
|
|
tr_ptrArrayRemoveSorted( getTrackerLookupTable( ), t, trackerCompare );
|
|
|
|
tr_ptrArrayFree( t->torrents );
|
|
tr_ptrArrayFree( t->scrapeQueue );
|
|
tr_ptrArrayFree( t->scraping );
|
|
|
|
for( i=0; i<t->addressCount; ++i )
|
|
tr_trackerInfoClear( &t->addresses[i] );
|
|
|
|
if( t->redirect ) {
|
|
tr_trackerInfoClear( t->redirect );
|
|
tr_free( t->redirect );
|
|
}
|
|
|
|
tr_timerFree( &t->scrapeTag );
|
|
|
|
tr_free( t->primaryAddress );
|
|
tr_free( t->addresses );
|
|
tr_free( t->tierFronts );
|
|
tr_free( t );
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
void
|
|
tr_trackerFree( Torrent * tor )
|
|
{
|
|
tr_timerNew( tor->tracker->handle,
|
|
onTorrentFreeNow, tor, NULL, 0 );
|
|
}
|
|
|
|
Torrent*
|
|
tr_trackerNew( tr_torrent_t * torrent )
|
|
{
|
|
Torrent * tor;
|
|
Tracker * t = tr_trackerGet( torrent );
|
|
assert( getExistingTorrent( t, torrent->info.hash ) == NULL );
|
|
|
|
/* create a new Torrent and queue it for scraping */
|
|
tor = tr_new0( Torrent, 1 );
|
|
tor->publisher = tr_publisherNew( );
|
|
tor->tracker = t;
|
|
tor->torrent = torrent;
|
|
tor->timesDownloaded = -1;
|
|
tor->seeders = -1;
|
|
tor->leechers = -1;
|
|
tor->manualAnnounceAllowedAt = ~0;
|
|
memcpy( tor->hash, torrent->info.hash, SHA_DIGEST_LENGTH );
|
|
escape( tor->escaped, torrent->info.hash, SHA_DIGEST_LENGTH );
|
|
tr_ptrArrayInsertSorted( t->torrents, tor, torrentCompare );
|
|
tr_ptrArrayInsertSorted( t->scrapeQueue, tor, torrentCompare );
|
|
tr_trackerScrapeSoon( t );
|
|
return tor;
|
|
}
|
|
|
|
/***
|
|
**** UTIL
|
|
***/
|
|
|
|
static int
|
|
parseBencResponse( struct evhttp_request * req, benc_val_t * setme )
|
|
{
|
|
const unsigned char * body = EVBUFFER_DATA( req->input_buffer );
|
|
const int bodylen = EVBUFFER_LENGTH( req->input_buffer );
|
|
int ret = 1;
|
|
int i;
|
|
|
|
for( i=0; ret && i<bodylen; ++i )
|
|
if( !tr_bencLoad( body+i, bodylen-1, setme, NULL ) )
|
|
ret = 0;
|
|
return ret;
|
|
}
|
|
|
|
static char*
|
|
updateAddresses( Tracker * t, const struct evhttp_request * req )
|
|
{
|
|
char * ret = NULL;
|
|
int used = 0, max = 0;
|
|
int moveToNextAddress = FALSE;
|
|
|
|
if( !req )
|
|
{
|
|
tr_sprintf( &ret, &used, &max,
|
|
"Connecting to %s got a NULL response",
|
|
t->addresses[t->addressIndex].announce );
|
|
tr_inf( ret );
|
|
|
|
moveToNextAddress = TRUE;
|
|
}
|
|
else if( req->response_code == HTTP_OK )
|
|
{
|
|
if( t->redirect != NULL )
|
|
{
|
|
/* multitracker spec: "if a connection with a tracker is
|
|
successful, it will be moved to the front of the tier." */
|
|
const int i = t->addressIndex;
|
|
const int j = t->tierFronts[i];
|
|
const tr_tracker_info_t swap = t->addresses[i];
|
|
t->addresses[i] = t->addresses[j];
|
|
t->addresses[j] = swap;
|
|
}
|
|
}
|
|
else if( ( req->response_code == HTTP_MOVEPERM )
|
|
|| ( req->response_code == HTTP_MOVETEMP ) )
|
|
{
|
|
const char * loc = evhttp_find_header( req->input_headers, "Location" );
|
|
tr_tracker_info_t tmp;
|
|
if( tr_trackerInfoInit( &tmp, loc, -1 ) ) /* a bad redirect? */
|
|
{
|
|
moveToNextAddress = TRUE;
|
|
}
|
|
else if( req->response_code == HTTP_MOVEPERM )
|
|
{
|
|
tr_tracker_info_t * cur = &t->addresses[t->addressIndex];
|
|
tr_trackerInfoClear( cur );
|
|
*cur = tmp;
|
|
}
|
|
else if( req->response_code == HTTP_MOVETEMP )
|
|
{
|
|
if( t->redirect == NULL )
|
|
t->redirect = tr_new0( tr_tracker_info_t, 1 );
|
|
else
|
|
tr_trackerInfoClear( t->redirect );
|
|
*t->redirect = tmp;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
tr_sprintf( &ret, &used, &max,
|
|
"Error connecting: %s",
|
|
t->addresses[t->addressIndex].announce,
|
|
req->response_code_line );
|
|
|
|
moveToNextAddress = TRUE;
|
|
}
|
|
|
|
if( moveToNextAddress )
|
|
if ( ++t->addressIndex >= t->addressCount )
|
|
t->addressIndex = 0;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static tr_tracker_info_t *
|
|
getCurrentAddress( const Tracker * t )
|
|
{
|
|
assert( t->addresses != NULL );
|
|
assert( t->addressIndex >= 0 );
|
|
assert( t->addressIndex < t->addressCount );
|
|
|
|
return &t->addresses[t->addressIndex];
|
|
}
|
|
static int
|
|
trackerSupportsScrape( const Tracker * t )
|
|
{
|
|
const tr_tracker_info_t * info = getCurrentAddress( t );
|
|
|
|
return ( info != NULL )
|
|
&& ( info->scrape != NULL )
|
|
&& ( info->scrape[0] != '\0' );
|
|
}
|
|
|
|
|
|
static void
|
|
addCommonHeaders( const Tracker * t,
|
|
struct evhttp_request * req )
|
|
{
|
|
char buf[1024];
|
|
tr_tracker_info_t * address = getCurrentAddress( t );
|
|
snprintf( buf, sizeof(buf), "%s:%d", address->address, address->port );
|
|
evhttp_add_header( req->output_headers, "Host", buf );
|
|
evhttp_add_header( req->output_headers, "Connection", "close" );
|
|
evhttp_add_header( req->output_headers, "Content-Length", "0" );
|
|
evhttp_add_header( req->output_headers, "User-Agent",
|
|
TR_NAME "/" LONG_VERSION_STRING );
|
|
}
|
|
|
|
/***
|
|
****
|
|
**** SCRAPE
|
|
****
|
|
***/
|
|
|
|
static int
|
|
onTorrentScrapeNow( void * vtor )
|
|
{
|
|
Torrent * tor = (Torrent *) vtor;
|
|
if( trackerSupportsScrape( tor->tracker ) ) {
|
|
tr_ptrArrayInsertSorted( tor->tracker->scrapeQueue, tor, torrentCompare );
|
|
tr_trackerScrapeSoon( tor->tracker );
|
|
}
|
|
tor->scrapeTag = NULL;
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
onScrapeResponse( struct evhttp_request * req, void * vt )
|
|
{
|
|
char * errmsg;
|
|
Tracker * t = (Tracker*) vt;
|
|
|
|
tr_inf( "Got scrape response from '%s': %s",
|
|
t->primaryAddress,
|
|
(req ? req->response_code_line : "(null)") );
|
|
|
|
if( req && ( req->response_code == HTTP_OK ) )
|
|
{
|
|
int numResponses = 0;
|
|
benc_val_t benc, *files;
|
|
const int n_scraping = tr_ptrArraySize( t->scraping );
|
|
const int bencLoaded = !parseBencResponse( req, &benc );
|
|
|
|
if( bencLoaded
|
|
&& (( files = tr_bencDictFind( &benc, "files" ) ))
|
|
&& ( files->type == TYPE_DICT ) )
|
|
{
|
|
int i;
|
|
for( i=0; i<files->val.l.count; i+=2 )
|
|
{
|
|
const uint8_t* hash =
|
|
(const uint8_t*) files->val.l.vals[i].val.s.s;
|
|
benc_val_t *tmp, *flags;
|
|
benc_val_t *tordict = &files->val.l.vals[i+1];
|
|
Torrent * tor = getExistingTorrent( t, hash );
|
|
++numResponses;
|
|
|
|
if( !tor ) {
|
|
tr_err( "Got an unrequested scrape response!" );
|
|
continue;
|
|
}
|
|
|
|
if(( tmp = tr_bencDictFind( tordict, "complete" )))
|
|
tor->seeders = tmp->val.i;
|
|
|
|
if(( tmp = tr_bencDictFind( tordict, "incomplete" )))
|
|
tor->leechers = tmp->val.i;
|
|
|
|
if(( tmp = tr_bencDictFind( tordict, "downloaded" )))
|
|
tor->timesDownloaded = tmp->val.i;
|
|
|
|
if(( flags = tr_bencDictFind( tordict, "flags" )))
|
|
if(( tmp = tr_bencDictFind( flags, "min_request_interval")))
|
|
t->scrapeIntervalMsec = tmp->val.i * 1000;
|
|
|
|
assert( tr_ptrArrayFindSorted(t->scraping,tor,torrentCompare) );
|
|
tr_ptrArrayRemoveSorted( t->scraping, tor, torrentCompare );
|
|
|
|
assert( !tor->scrapeTag );
|
|
tor->scrapeTag = tr_timerNew( t->handle,
|
|
onTorrentScrapeNow,
|
|
tor, NULL,
|
|
t->scrapeIntervalMsec );
|
|
tr_dbg( "Torrent '%s' scrape successful."
|
|
" Rescraping in %d seconds",
|
|
tor->torrent->info.name, t->scrapeIntervalMsec/1000 );
|
|
}
|
|
|
|
if( !files->val.l.count )
|
|
{
|
|
/* got an empty files dictionary! This probably means the
|
|
torrents we're scraping have expired from the tracker,
|
|
so make sure they're stopped. It also means any previous
|
|
changes to multiscrapeMax are suspect, so reset that. */
|
|
|
|
int n;
|
|
Torrent ** torrents = (Torrent**)
|
|
tr_ptrArrayPeek( t->scraping, &n );
|
|
for( i=0; i<n; ++i )
|
|
tr_trackerStop( torrents[i] );
|
|
tr_ptrArrayClear( t->scraping );
|
|
|
|
t->multiscrapeMax = INT_MAX;
|
|
}
|
|
}
|
|
|
|
if( bencLoaded )
|
|
tr_bencFree( &benc );
|
|
|
|
/* if the tracker gave us back fewer torrents than we
|
|
thought we should get, maybe our multiscrape string
|
|
is too big... limit it based on how many we got back */
|
|
if( ( 0 < numResponses ) && ( numResponses < n_scraping ) )
|
|
t->multiscrapeMax = numResponses;
|
|
}
|
|
|
|
if (( errmsg = updateAddresses( t, req ) )) {
|
|
tr_err( errmsg );
|
|
tr_free( errmsg );
|
|
}
|
|
|
|
if( !tr_ptrArrayEmpty( t->scraping ) )
|
|
{
|
|
int i, n;
|
|
Torrent ** torrents =
|
|
(Torrent**) tr_ptrArrayPeek( t->scraping, &n );
|
|
for( i=0; i<n; ++i )
|
|
onTorrentScrapeNow( torrents[i] );
|
|
tr_ptrArrayClear( t->scraping );
|
|
}
|
|
|
|
if( !tr_ptrArrayEmpty( t->scrapeQueue ) )
|
|
tr_trackerScrapeSoon( t );
|
|
}
|
|
|
|
static int
|
|
onTrackerScrapeNow( void * vt )
|
|
{
|
|
Tracker * t = (Tracker*) vt;
|
|
const tr_tracker_info_t * address = getCurrentAddress( t );
|
|
|
|
assert( tr_ptrArrayEmpty( t->scraping ) );
|
|
|
|
if( trackerSupportsScrape( t ) && !tr_ptrArrayEmpty( t->scrapeQueue ) )
|
|
{
|
|
int i, n, len, addr_len, ask_n;
|
|
char *march, *uri;
|
|
Torrent ** torrents =
|
|
(Torrent**) tr_ptrArrayPeek( t->scrapeQueue, &n );
|
|
struct evhttp_connection *evcon = NULL;
|
|
struct evhttp_request *req = NULL;
|
|
|
|
ask_n = n;
|
|
if( ask_n > t->multiscrapeMax )
|
|
ask_n = t->multiscrapeMax;
|
|
|
|
/**
|
|
*** Build the scrape request
|
|
**/
|
|
|
|
len = addr_len = strlen( address->scrape );
|
|
for( i=0; i<ask_n; ++i )
|
|
len += strlen("&info_hash=") + strlen(torrents[i]->escaped);
|
|
++len; /* for nul */
|
|
uri = march = tr_new( char, len );
|
|
memcpy( march, address->scrape, addr_len ); march += addr_len;
|
|
for( i=0; i<ask_n; ++i ) {
|
|
const int elen = strlen( torrents[i]->escaped );
|
|
*march++ = i?'&':'?';
|
|
memcpy( march, "info_hash=", 10); march += 10;
|
|
memcpy( march, torrents[i]->escaped, elen ); march += elen;
|
|
}
|
|
*march++ = '\0';
|
|
assert( march - uri == len );
|
|
|
|
/* move the first n_ask torrents from scrapeQueue to scraping */
|
|
for( i=0; i<ask_n; ++i )
|
|
tr_ptrArrayInsertSorted( t->scraping, torrents[i], torrentCompare );
|
|
tr_ptrArrayErase( t->scrapeQueue, 0, ask_n );
|
|
|
|
/* ping the tracker */
|
|
tr_inf( "Sending scrape to tracker %s:%d: %s",
|
|
address->address, address->port, uri );
|
|
evcon = getConnection( address->address, address->port );
|
|
evhttp_connection_set_timeout( evcon, SCRAPE_TIMEOUT_INTERVAL_SEC );
|
|
req = evhttp_request_new( onScrapeResponse, t );
|
|
assert( req );
|
|
addCommonHeaders( t, req );
|
|
tr_evhttp_make_request( t->handle, evcon, req, EVHTTP_REQ_GET, uri );
|
|
}
|
|
|
|
t->scrapeTag = NULL;
|
|
return FALSE;
|
|
}
|
|
|
|
/***
|
|
****
|
|
**** TRACKER REQUESTS
|
|
****
|
|
***/
|
|
|
|
static int
|
|
torrentIsRunning( const Torrent * tor )
|
|
{
|
|
return ( tor != NULL )
|
|
&& ( tor->lastRequest != NULL )
|
|
&& ( strcmp( tor->lastRequest, "stopped" ) );
|
|
}
|
|
|
|
static char*
|
|
buildTrackerRequestURI( const Torrent * tor, const char * eventName )
|
|
{
|
|
const tr_torrent_t * torrent = tor->torrent;
|
|
const int stopping = !strcmp( eventName, "stopped" );
|
|
const int numwant = stopping ? 0 : NUMWANT;
|
|
char buf[4096];
|
|
|
|
snprintf( buf, sizeof(buf), "%s"
|
|
"?info_hash=%s"
|
|
"&peer_id=%s"
|
|
"&port=%d"
|
|
"&uploaded=%"PRIu64
|
|
"&downloaded=%"PRIu64
|
|
"&corrupt=%"PRIu64
|
|
"&left=%"PRIu64
|
|
"&compact=1"
|
|
"&numwant=%d"
|
|
"&key=%s"
|
|
"%s%s"
|
|
"%s%s",
|
|
getCurrentAddress(tor->tracker)->announce,
|
|
tor->escaped,
|
|
tor->peer_id,
|
|
torrent->publicPort,
|
|
torrent->uploadedCur,
|
|
torrent->downloadedCur,
|
|
torrent->corruptCur,
|
|
tr_cpLeftUntilComplete( torrent->completion ),
|
|
numwant,
|
|
tor->tracker->key_param,
|
|
( ( eventName && *eventName ) ? "&event=" : "" ),
|
|
( ( eventName && *eventName ) ? eventName : "" ),
|
|
( ( tor->trackerID && *tor->trackerID ) ? "&trackerid=" : "" ),
|
|
( ( tor->trackerID && *tor->trackerID ) ? tor->trackerID : "" ) );
|
|
|
|
return tr_strdup( buf );
|
|
}
|
|
|
|
/* Convert to compact form */
|
|
static uint8_t *
|
|
parseOldPeers( benc_val_t * bePeers, int * peerCount )
|
|
{
|
|
int i, count;
|
|
uint8_t * compact;
|
|
|
|
assert( bePeers->type == TYPE_LIST );
|
|
|
|
compact = tr_new( uint8_t, 6 * bePeers->val.l.count );
|
|
|
|
for( i=count=0; i<bePeers->val.l.count; ++i )
|
|
{
|
|
struct in_addr addr;
|
|
tr_port_t port;
|
|
benc_val_t * val;
|
|
benc_val_t * peer = &bePeers->val.l.vals[i];
|
|
|
|
val = tr_bencDictFind( peer, "ip" );
|
|
if( !val || val->type!=TYPE_STR || tr_netResolve(val->val.s.s, &addr) )
|
|
continue;
|
|
|
|
memcpy( &compact[6 * count], &addr, 4 );
|
|
|
|
val = tr_bencDictFind( peer, "port" );
|
|
if( !val || val->type!=TYPE_INT || val->val.i<0 || val->val.i>0xffff )
|
|
continue;
|
|
|
|
port = htons( val->val.i );
|
|
memcpy( &compact[6 * count + 4], &port, 2 );
|
|
++count;
|
|
}
|
|
|
|
*peerCount = count;
|
|
return compact;
|
|
}
|
|
|
|
/* handle braindead trackers whose minimums is higher
|
|
than the interval. */
|
|
static void
|
|
setAnnounceInterval( Tracker * t,
|
|
int minimum,
|
|
int interval )
|
|
{
|
|
assert( t != NULL );
|
|
|
|
if( minimum > 0 )
|
|
t->minAnnounceIntervalMsec = minimum;
|
|
|
|
if( interval > 0 )
|
|
t->announceIntervalMsec = interval;
|
|
|
|
if( t->announceIntervalMsec < t->minAnnounceIntervalMsec )
|
|
t->announceIntervalMsec = t->minAnnounceIntervalMsec;
|
|
}
|
|
|
|
static int onReannounceNow( void * vtor );
|
|
|
|
static void
|
|
onTrackerResponse( struct evhttp_request * req, void * vtor )
|
|
{
|
|
char * errmsg;
|
|
Torrent * tor = (Torrent *) vtor;
|
|
const int isStopped = !torrentIsRunning( tor );
|
|
int reannounceInterval;
|
|
|
|
tr_inf( "Torrent \"%s\" tracker response: %s",
|
|
tor->torrent->info.name,
|
|
( req ? req->response_code_line : "(null)") );
|
|
|
|
if( req && ( req->response_code == HTTP_OK ) )
|
|
{
|
|
benc_val_t benc;
|
|
const int bencLoaded = !parseBencResponse( req, &benc );
|
|
|
|
if( bencLoaded && benc.type==TYPE_DICT )
|
|
{
|
|
benc_val_t * tmp;
|
|
|
|
if(( tmp = tr_bencDictFind( &benc, "failure reason" )))
|
|
publishErrorMessage( tor, tmp->val.s.s );
|
|
|
|
if(( tmp = tr_bencDictFind( &benc, "warning message" )))
|
|
publishWarningMessage( tor, tmp->val.s.s );
|
|
|
|
if(( tmp = tr_bencDictFind( &benc, "interval" )))
|
|
setAnnounceInterval( tor->tracker, -1, tmp->val.i * 1000 );
|
|
|
|
if(( tmp = tr_bencDictFind( &benc, "min interval" )))
|
|
setAnnounceInterval( tor->tracker, tmp->val.i * 1000, -1 );
|
|
|
|
if(( tmp = tr_bencDictFind( &benc, "tracker id" )))
|
|
tor->trackerID = tr_strndup( tmp->val.s.s, tmp->val.s.i );
|
|
|
|
if(( tmp = tr_bencDictFind( &benc, "complete" )))
|
|
tor->seeders = tmp->val.i;
|
|
|
|
if(( tmp = tr_bencDictFind( &benc, "incomplete" )))
|
|
tor->leechers = tmp->val.i;
|
|
|
|
if(( tmp = tr_bencDictFind( &benc, "peers" )))
|
|
{
|
|
int peerCount = 0;
|
|
uint8_t * peerCompact = NULL;
|
|
|
|
if( tmp->type == TYPE_LIST ) /* original protocol */
|
|
{
|
|
if( tmp->val.l.count > 0 )
|
|
peerCompact = parseOldPeers( tmp, &peerCount );
|
|
}
|
|
else if( tmp->type == TYPE_STR ) /* "compact" extension */
|
|
{
|
|
if( tmp->val.s.i >= 6 )
|
|
{
|
|
peerCount = tmp->val.s.i / 6;
|
|
peerCompact = tr_new( uint8_t, tmp->val.s.i );
|
|
memcpy( peerCompact, tmp->val.s.s, tmp->val.s.i );
|
|
}
|
|
}
|
|
|
|
publishNewPeers( tor, peerCount, peerCompact );
|
|
tr_free( peerCompact );
|
|
}
|
|
}
|
|
|
|
reannounceInterval = isStopped
|
|
? -1
|
|
: tor->tracker->announceIntervalMsec;
|
|
|
|
if( bencLoaded )
|
|
tr_bencFree( &benc );
|
|
}
|
|
else
|
|
{
|
|
tr_inf( "Bad response from tracker '%s' on request '%s' "
|
|
"for torrent '%s'... trying again in 30 seconds",
|
|
tor->tracker->primaryAddress,
|
|
tor->lastRequest,
|
|
tor->torrent->info.name );
|
|
|
|
reannounceInterval = 30 * 1000;
|
|
}
|
|
|
|
if (( errmsg = updateAddresses( tor->tracker, req ) )) {
|
|
publishErrorMessage( tor, errmsg );
|
|
tr_err( errmsg );
|
|
tr_free( errmsg );
|
|
}
|
|
|
|
tor->httpReq = NULL;
|
|
|
|
if( isStopped )
|
|
publishStopped( tor );
|
|
else if( reannounceInterval > 0 ) {
|
|
tr_dbg( "torrent '%s' reannouncing in %d seconds",
|
|
tor->torrent->info.name, (reannounceInterval/1000) );
|
|
tor->reannounceTag = tr_timerNew( tor->tracker->handle,
|
|
onReannounceNow, tor, NULL,
|
|
reannounceInterval );
|
|
tor->manualAnnounceAllowedAt
|
|
= tr_date() + MANUAL_ANNOUNCE_INTERVAL_MSEC;
|
|
}
|
|
}
|
|
|
|
static int
|
|
sendTrackerRequest( void * vtor, const char * eventName )
|
|
{
|
|
Torrent * tor = (Torrent *) vtor;
|
|
const tr_tracker_info_t * address = getCurrentAddress( tor->tracker );
|
|
char * uri = buildTrackerRequestURI( tor, eventName );
|
|
struct evhttp_connection * evcon = NULL;
|
|
|
|
tr_inf( "Torrent \"%s\" sending '%s' to tracker %s:%d: %s",
|
|
tor->torrent->info.name,
|
|
(eventName ? eventName : "periodic announce"),
|
|
address->address, address->port,
|
|
uri );
|
|
|
|
/* kill any pending requests */
|
|
tr_timerFree( &tor->reannounceTag );
|
|
|
|
evcon = getConnection( address->address, address->port );
|
|
if ( !evcon ) {
|
|
tr_err( "Can't make a connection to %s:%d", address->address, address->port );
|
|
tr_free( uri );
|
|
} else {
|
|
tr_free( tor->lastRequest );
|
|
tor->lastRequest = tr_strdup( eventName );
|
|
evhttp_connection_set_timeout( evcon, REQ_TIMEOUT_INTERVAL_SEC );
|
|
tor->httpReq = evhttp_request_new( onTrackerResponse, tor );
|
|
addCommonHeaders( tor->tracker, tor->httpReq );
|
|
tr_evhttp_make_request( tor->tracker->handle, evcon,
|
|
tor->httpReq, EVHTTP_REQ_GET, uri );
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static int
|
|
onReannounceNow( void * vtor )
|
|
{
|
|
Torrent * tor = (Torrent *) vtor;
|
|
sendTrackerRequest( tor, "" );
|
|
tor->reannounceTag = NULL;
|
|
return FALSE;
|
|
}
|
|
|
|
/***
|
|
**** PUBLIC
|
|
***/
|
|
|
|
tr_publisher_tag
|
|
tr_trackerSubscribe( Torrent * tor,
|
|
tr_delivery_func func,
|
|
void * user_data )
|
|
{
|
|
return tr_publisherSubscribe( tor->publisher, func, user_data );
|
|
}
|
|
|
|
void
|
|
tr_trackerUnsubscribe( Torrent * tor,
|
|
tr_publisher_tag tag )
|
|
{
|
|
tr_publisherUnsubscribe( tor->publisher, tag );
|
|
}
|
|
|
|
const tr_tracker_info_t *
|
|
tr_trackerGetAddress( const Torrent * tor )
|
|
{
|
|
return getCurrentAddress( tor->tracker );
|
|
}
|
|
|
|
int
|
|
tr_trackerCanManualAnnounce ( const Torrent * tor )
|
|
{
|
|
/* return true if this torrent's currently running
|
|
and it's been long enough since the last announce */
|
|
return ( torrentIsRunning( tor ) )
|
|
&& ( tr_date() >= tor->manualAnnounceAllowedAt );
|
|
}
|
|
|
|
void
|
|
tr_trackerGetCounts( const Torrent * tor,
|
|
int * setme_completedCount,
|
|
int * setme_leecherCount,
|
|
int * setme_seederCount )
|
|
{
|
|
if( setme_completedCount )
|
|
*setme_completedCount = tor->timesDownloaded;
|
|
|
|
if( setme_leecherCount )
|
|
*setme_leecherCount = tor->leechers;
|
|
|
|
if( setme_seederCount )
|
|
*setme_seederCount = tor->seeders;
|
|
}
|
|
|
|
void
|
|
tr_trackerStart( Torrent * tor )
|
|
{
|
|
tr_peerIdNew( tor->peer_id, sizeof(tor->peer_id) );
|
|
|
|
if( !tor->reannounceTag && !tor->httpReq )
|
|
sendTrackerRequest( tor, "started" );
|
|
}
|
|
|
|
void
|
|
tr_trackerReannounce( Torrent * tor )
|
|
{
|
|
sendTrackerRequest( tor, "started" );
|
|
}
|
|
|
|
void
|
|
tr_trackerCompleted( Torrent * tor )
|
|
{
|
|
sendTrackerRequest( tor, "completed" );
|
|
}
|
|
|
|
void
|
|
tr_trackerStop( Torrent * tor )
|
|
{
|
|
sendTrackerRequest( tor, "stopped" );
|
|
}
|
|
|
|
void
|
|
tr_trackerChangeMyPort( Torrent * tor )
|
|
{
|
|
if( torrentIsRunning( tor ) )
|
|
tr_trackerReannounce( tor );
|
|
}
|