Skip to content

Instantly share code, notes, and snippets.

@noonien
Last active January 28, 2020 21:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save noonien/3334386 to your computer and use it in GitHub Desktop.
Save noonien/3334386 to your computer and use it in GitHub Desktop.
from torrent.database import mongo
from flask import Blueprint, Response, current_app, request
from bencode import bencode
from IPy import IP
from socket import inet_pton, inet_ntop, ntohs, htons, AF_INET, AF_INET6
from struct import pack
bp = Blueprint('tracker', __name__)
class TrackerFailure(Exception):
pass
def _response(data):
response = Response()
response.data = bencode(data)
response.mimetype = 'text/plain'
return response
@bp.errorhandler(TrackerFailure)
def _fail(error):
return _response({'failure reason': str(reason)})
@bp.route('/announce')
def announce():
args = request.args
# Mandatory fields. Torrent hash and basic peer info
#
# info_hash: mandatory. 20-byte.
info_hash = args.get('info_hash', None)
if info_hash is None or len(info_hash) != 20:
raise TrackerFailure('Invalid info_hash')
# peer_id: mandatory. 20-byte.
peer_id = args.get('peer_id', None)
if peer_id is None or len(peer_id) != 20:
raise TrackerFailure('Invalid peer_id')
# port: mandatory. integer
port = args.get('port', None)
if port is None or not port.isdigit():
raise TrackerFailure('Invalid port')
port = int(port)
if port == 0 or port > 65535:
raise TrackerFailure('Invalid port')
# Data transfer statistics
#
# uploaded: optional. base ten of ascii. bytes uploaded
uploaded = args.get('uploaded', '0')
if not uploaded.isdigit():
raise TrackerFailure('Invalid value for "uploaded"')
uploaded = int(uploaded)
# downloaded: optional. bytes downloaded.
downloaded = args.get('downloaded', '0')
if not downloaded.isdigit():
raise TrackerFailure('Invalue value for "downloaded"')
downloaded = int(downloaded)
# left: optional. bytes left.
left = args.get('left', '0')
if not left.isdigit():
raise TrackerFailure('Invalid value for "left"')
left = int(left)
# Client state
#
# event: optional. once specified, must be one of
# 'started', 'completed', 'stopped'
event = args.get('event', '').lower()
if not event in ['', 'started', 'stopped', 'completed']:
raise TrackerFailure('Invalid value for "event"')
# numwant: optional: integer >= 0
numwant = args.get('numwant', '50')
if not numwant.isdigit():
raise TrackerFailure('Invalid value for "numwant"')
numwant = int(numwant)
# Client flags
#
# compact: optional. '1' means compact peer list.
compact = args.get('compact', None) == '1'
# no_peer_id: optional. '1' means we can omit peer_id in peers.
no_peer_id = args.get('no_peer_id', None) == '1'
# supportcrypto: optional. '1' means crypto support
supportcrypto = args.get('supportcrypto', None) == '1'
# requirecrypto: optional. '1' means require crypto support
requirecrypto = args.get('requirecrypto', None) == '1'
# Client and tracker identifiers
#
# ip: optional. IPv4 or IPv6 address of peer
ip = args.get('ip', None)
if not ip is None:
try:
ip = IP(ip)
except ValueError:
ip = None
# Database interaction
db = mongo.db
torrents = db.torrents
peers = db.peers
# Check if the torrent exists
torrent = torrents.find_one({'info_hash': info_hash}, fields={'_id': 1})
if torrent is None:
raise TrackerFailure('Torrent not found')
# Fetch the peer's info
peer = peers.find_one({'torrent': torrent['_id'], 'peer_id': peer_id})
if event == 'stopped':
# If the peer was previously registered, remove it and update stats
if not peer is None:
peers.remove({'_id': peer['_id']})
torrents.update({'_id': torrent['_id']},
{'$inc': {'seeders' if peer['seeder'] else 'leechers': -1}})
return _response({'interval': 10, 'peers': {}})
# Dict with stats to increment/decrement
torrent_stats_update = {}
if event == 'completed':
torrent_stats_update['downloads'] = 1
# If peer was not previously registered, set initial fields
if peer is None:
# If the peer is registering and did not provide an ip, use the
# remote_addr as ip
if ip is None:
ip = IP(request.remote_addr)
peer = {
'torrent': torrent['_id'],
'peer_id': peer_id,
'seeder': left == 0
}
# Increment peer number
torrent_stats_update['seeders' if peer['seeder'] else 'leechers'] = 1
# If an ip was specified, set the new ip to it
if not ip is None:
peer['ip'] = inet_pton(AF_INET6 if ip.version() == 6 else AF_INET, str(ip))
peer['ipv6'] = ip.version() == 6,
peer['port'] = port
peer['crypto'] = supportcrypto
peer['reqcrypto'] = requirecrypto
# Only update the stats when a change has been made, this accounts for
# continuing partial downloaded torrents
if left == 0 and peer['seeder'] == False:
peer['seeder'] = True
# Increment seeders
torrent_stats_update['seeders'] = 1
torrent_stats_update['leechers'] = -1
elif left > 0 and peer['seeder'] == True:
peer['seeder'] = False
# Decrement seeders
torrent_stats_update['seeders'] = -1
torrent_stats_update['leechers'] = 1
peers.save(peer)
# Apply torrent stats updates
if torrent_stats_update:
torrents.update({'_id': torrent['_id']}, {'$inc': torrent_stats_update})
# Build database query to search for peers
#
# Only find peers associated with this torrent and not the current peer
peer_filter = {'torrent': torrent['_id'], '$not': {'_id': peer['_id']}}
peer_sort = []
# Include peers with IPv6 support, if peer supports IPv6
if not peer['ipv6']:
peer_filter['ipv6'] = False
# Don't provider seeders to seeder peers
if peer['seeder']:
peer_filter['seeder'] = False
# Only provide peers with crypto support if
if requirecrypto:
peer_filter['crypto'] = True
# If peer supports crypto, give it crypto peers first
elif supportcrypto:
peer_sort.append({'crypto', -1})
# If peer doesn't support crypto, don't give it peers that require it
else:
peer_filter['reqcrypto'] = False
# Get peers cursor
peers_cur = peers.find(peer_filter).sort(peer_sort).limit(numwant)
# Start building a response
#
response = {}
# Refresh interavl: 30mins for seeders, 1hour for non-seeders
if peer['seeder']:
response['interval'] = current_app.config.get('ANNOUNCE_INTERVAL_SEEDER', 30 * 60)
else:
response['interval'] = current_app.config.get('ANNOUNCE_INTERVAL_LEECHER', 60 * 60)
# If the peer has an IPv6 address we will also include IPv6 peers,
# who'se addresses can't be compacted
if compact and not peer['ipv6']:
# Compact list
# Concatenate a packet byte array of ip and port in network order
peer_list = ''.join(struct.pack('s!H', p['ip'], p['port']) for p in peers_cur)
else:
# Non-compacted list
peer_list = []
for p in peers_cur:
peer_info = {'ip': inet_ntop(AF_INET6 if p['ipv6'] else AF_INET, p['ip']), 'port': p['port']}
if not no_peer_id:
peer_info['peer id'] = p['peer_id']
response['peers'] = peer_list
return _response(response)
@bp.route('/scrape')
def scrape():
info_hash = request.args.get('info_hash', None)
if info_hash is None:
raise TrackerFailure('Full scrape function is not available with this tracker')
if len(info_hash) != 20:
raise TrackerFailure('Invalid info_hash')
torrent = mongo.db.torrents.find_one({'_id': info_hash})
if torrent is None:
raise TrackerFailure('Torrent not found')
return _response({
'files': {
info_hash: {
'downloaded': torrent['downloads'],
'complete': torrent['seeders'],
'incomplete': torrent['leechers'],
'name': 'Something'
}
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment