Files
hse-2026/materials/07-naming-lookup/seminar/bittorrent/live.py
T
2026-10-21 23:03:28 +03:00

171 lines
7.2 KiB
Python

"""Real local BitTorrent transfer using libtorrent; no public trackers or peers."""
import hashlib
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import socket
import struct
import tempfile
import threading
import time
from urllib.parse import parse_qs, urlsplit
import libtorrent as lt
from metainfo import encode, inspect, make_metainfo
class Tracker(ThreadingHTTPServer):
daemon_threads = True
def __init__(self):
super().__init__(("127.0.0.1", 6969), Announce)
self.enabled = True
self.peers = {}
self.failed_announces = 0
self.infohash = b""
self.lock = threading.Lock()
class Announce(BaseHTTPRequestHandler):
def do_GET(self):
tracker = self.server
params = parse_qs(urlsplit(self.path).query, encoding="latin-1")
if urlsplit(self.path).path != "/announce":
self.send_error(404)
return
with tracker.lock:
if not tracker.enabled:
tracker.failed_announces += 1
self.send_error(503, "Tracker unavailable in this experiment")
print("TRACKER 503: rejected announce", flush=True)
return
try:
infohash = params["info_hash"][0].encode("latin-1")
port = int(params["port"][0])
if infohash != tracker.infohash or not 1 <= port <= 65535:
raise ValueError("Unexpected torrent or port")
except (KeyError, ValueError):
self.send_error(400)
return
if params.get("event") == ["stopped"]:
tracker.peers.pop(port, None)
else:
tracker.peers[port] = self.client_address[0]
# Compact peer list: IPv4 (4 bytes) followed by TCP port (2 bytes).
others = [(p, ip) for p, ip in tracker.peers.items() if p != port]
body = encode({b"interval": 5, b"min interval": 1,
b"peers": b"".join(socket.inet_aton(ip) + struct.pack("!H", p)
for p, ip in others)})
print(f"TRACKER announce from :{port} -> {[p for p, _ in others]}", flush=True)
self.send_response(200)
self.send_header("Content-Type", "application/x-bittorrent")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *_):
pass
def client(port, folder, torrent, rate=0):
settings = {
"listen_interfaces": f"127.0.0.1:{port}",
"outgoing_interfaces": "127.0.0.1",
"enable_dht": False, "enable_lsd": False,
"enable_upnp": False, "enable_natpmp": False,
"enable_outgoing_utp": False, "enable_incoming_utp": False,
"allow_multiple_connections_per_ip": True,
"seeding_outgoing_connections": False,
"min_announce_interval": 1,
"download_rate_limit": rate,
"ignore_limits_on_local_network": False,
}
# flags=0 avoids installing the default extensions, including ut_pex.
session = lt.session(settings, flags=0)
params = lt.add_torrent_params()
params.ti = lt.torrent_info(str(torrent))
params.save_path = str(folder)
params.flags &= ~lt.torrent_flags.paused
params.flags &= ~lt.torrent_flags.auto_managed
handle = session.add_torrent(params)
return session, handle
def wait_for(label, predicate, handles, timeout=60):
deadline, next_print = time.monotonic() + timeout, 0
while time.monotonic() < deadline:
if predicate():
return
if time.monotonic() >= next_print:
for name, handle in handles:
status = handle.status()
peers = [str(peer.ip) for peer in handle.get_peer_info()]
print(f"{label}: {name} {100 * status.progress:.1f}% "
f"bytes_done={status.total_done} peers={peers}", flush=True)
if status.errc.value():
raise RuntimeError(str(status.errc))
next_print = time.monotonic() + 2
time.sleep(0.2)
raise RuntimeError("Timed out: " + label)
def run():
print("libtorrent", lt.version, "| DHT/LSD/UPnP/NAT-PMP off; no default extensions (PEX)")
tracker = Tracker()
thread = threading.Thread(target=tracker.serve_forever, daemon=True)
thread.start()
sessions = []
handles = []
try:
with tempfile.TemporaryDirectory(prefix="seminar7-bt-") as temporary:
root = Path(temporary)
seed_dir, old_dir, new_dir = (root / x for x in ("seed", "known", "fresh"))
for directory in (seed_dir, old_dir, new_dir):
directory.mkdir()
data = bytes(range(256)) * (4 * 1024 * 1024 // 256)
(seed_dir / "lesson.bin").write_bytes(data)
raw = encode(make_metainfo("lesson.bin", data, private=True))
torrent = root / "lesson.torrent"
torrent.write_bytes(raw)
tracker.infohash = bytes.fromhex(inspect(raw)[1])
session, seed = client(6881, seed_dir, torrent)
sessions.append(session)
handles.append(seed)
wait_for("seed ready", lambda: seed.status().is_seeding and 6881 in tracker.peers,
[("seed", seed)])
session, known = client(6882, old_dir, torrent, rate=128 * 1024)
sessions.append(session)
handles.append(known)
wait_for("discover and connect", lambda: 0 < known.status().total_done < len(data),
[("known", known)])
before = known.status().total_done
tracker.enabled = False
print("\nTRACKER UNAVAILABLE (HTTP 503). Starting a fresh client.", flush=True)
session, fresh = client(6883, new_dir, torrent, rate=0)
sessions.append(session)
handles.append(fresh)
wait_for("observe outage",
lambda: tracker.failed_announces > 0 and known.status().total_done > before + 65536,
[("known", known), ("fresh", fresh)], timeout=30)
assert fresh.status().total_done == 0 and fresh.status().num_peers == 0
print("PASS: known client made progress; fresh client has zero peers/data", flush=True)
sessions[1].apply_settings({"download_rate_limit": 0})
wait_for("finish without tracker", lambda: known.status().is_seeding,
[("known", known), ("fresh", fresh)])
assert hashlib.sha256((old_dir / "lesson.bin").read_bytes()).digest() == hashlib.sha256(data).digest()
assert fresh.status().total_done == 0
print("PASS: known client finished and SHA-256 matches while tracker is unavailable", flush=True)
tracker.enabled = True
fresh.force_reannounce()
wait_for("tracker restored", lambda: fresh.status().is_seeding, [("fresh", fresh)])
assert (new_dir / "lesson.bin").read_bytes() == data
print("PASS: fresh client discovered peers after tracker recovery and received exact data", flush=True)
for session in sessions:
session.pause()
finally:
tracker.shutdown()
tracker.server_close()
if __name__ == "__main__":
run()