Add week 7 materials
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user