"""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()