Skip to content
Cannot retrieve contributors at this time
# pieces - An experimental BitTorrent client
# Copyright 2016
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
import aiohttp
import random
import logging
import socket
from struct import unpack
from urllib.parse import urlencode
from . import bencoding
class TrackerResponse:
The response from the tracker after a successful connection to the
trackers announce URL.
Even though the connection was successful from a network point of view,
the tracker might have returned an error (stated in the `failure`
def __init__(self, response: dict):
self.response = response
def failure(self):
If this response was a failed response, this is the error message to
why the tracker request failed.
If no error occurred this will be None
if b'failure reason' in self.response:
return self.response[b'failure reason'].decode('utf-8')
return None
def interval(self) -> int:
Interval in seconds that the client should wait between sending
periodic requests to the tracker.
return self.response.get(b'interval', 0)
def complete(self) -> int:
Number of peers with the entire file, i.e. seeders.
return self.response.get(b'complete', 0)
def incomplete(self) -> int:
Number of non-seeder peers, aka "leechers".
return self.response.get(b'incomplete', 0)
def peers(self):
A list of tuples for each peer structured as (ip, port)
# The BitTorrent specification specifies two types of responses. One
# where the peers field is a list of dictionaries and one where all
# the peers are encoded in a single string
peers = self.response[b'peers']
if type(peers) == list:
# TODO Implement support for dictionary peer list
logging.debug('Dictionary model peers are returned by tracker')
raise NotImplementedError()
logging.debug('Binary model peers are returned by tracker')
# Split the string in pieces of length 6 bytes, where the first
# 4 characters is the IP the last 2 is the TCP port.
peers = [peers[i:i+6] for i in range(0, len(peers), 6)]
# Convert the encoded address to a list of tuples
return [(socket.inet_ntoa(p[:4]), _decode_port(p[4:]))
for p in peers]
def __str__(self):
return "incomplete: {incomplete}\n" \
"complete: {complete}\n" \
"interval: {interval}\n" \
"peers: {peers}\n".format(
peers=", ".join([x for (x, _) in self.peers]))
class Tracker:
Represents the connection to a tracker for a given Torrent that is either
under download or seeding state.
def __init__(self, torrent):
self.torrent = torrent
self.peer_id = _calculate_peer_id()
self.http_client = aiohttp.ClientSession()
async def connect(self,
first: bool = None,
uploaded: int = 0,
downloaded: int = 0):
Makes the announce call to the tracker to update with our statistics
as well as get a list of available peers to connect to.
If the call was successful, the list of peers will be updated as a
result of calling this function.
:param first: Whether or not this is the first announce call
:param uploaded: The total number of bytes uploaded
:param downloaded: The total number of bytes downloaded
params = {
'info_hash': self.torrent.info_hash,
'peer_id': self.peer_id,
'port': 6889,
'uploaded': uploaded,
'downloaded': downloaded,
'left': self.torrent.total_size - downloaded,
'compact': 1}
if first:
params['event'] = 'started'
url = self.torrent.announce + '?' + urlencode(params)'Connecting to tracker at: ' + url)
async with self.http_client.get(url) as response:
if not response.status == 200:
raise ConnectionError('Unable to connect to tracker: status code {}'.format(response.status))
data = await
return TrackerResponse(bencoding.Decoder(data).decode())
def close(self):
def raise_for_error(self, tracker_response):
A (hacky) fix to detect errors by tracker even when the response has a status code of 200
# a tracker response containing an error will have a utf-8 message only.
# see:
message = tracker_response.decode("utf-8")
if "failure" in message:
raise ConnectionError('Unable to connect to tracker: {}'.format(message))
# a successful tracker response will have non-uncicode data, so it's a safe to bet ignore this exception.
except UnicodeDecodeError:
def _construct_tracker_parameters(self):
Constructs the URL parameters used when issuing the announce call
to the tracker.
return {
'info_hash': self.torrent.info_hash,
'peer_id': self.peer_id,
'port': 6889,
# TODO Update stats when communicating with tracker
'uploaded': 0,
'downloaded': 0,
'left': 0,
'compact': 1}
def _calculate_peer_id():
Calculate and return a unique Peer ID.
The `peer id` is a 20 byte long identifier. This implementation use the
Azureus style `-PC1000-<random-characters>`.
Read more:
return '-PC0001-' + ''.join(
[str(random.randint(0, 9)) for _ in range(12)])
def _decode_port(port):
Converts a 32-bit packed binary port number to int
# Convert from C style big-endian encoded as unsigned short
return unpack(">H", port)[0]