Last active
January 28, 2020 21:11
-
-
Save noonien/3334386 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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