From 4732ca67ee6da11b533afc57f4883107c91267e2 Mon Sep 17 00:00:00 2001 From: ookami125 Date: Wed, 24 Jun 2026 23:40:18 -0400 Subject: [PATCH] Initial commit: tracker and DHT protocol library --- .gitignore | 1 + CMakeLists.txt | 51 + PLAN.md | 98 ++ README.md | 79 ++ .../__pycache__/tracker_probe.cpython-314.pyc | Bin 0 -> 37518 bytes harness/tracker_probe.py | 563 +++++++++++ include/tracker.h | 338 +++++++ src/dht.c | 669 +++++++++++++ src/tracker_http.c | 897 ++++++++++++++++++ src/tracker_internal.h | 63 ++ src/tracker_store.c | 383 ++++++++ src/tracker_udp.c | 357 +++++++ tests/test_tracker.c | 667 +++++++++++++ 13 files changed, 4166 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 PLAN.md create mode 100644 README.md create mode 100644 harness/__pycache__/tracker_probe.cpython-314.pyc create mode 100644 harness/tracker_probe.py create mode 100644 include/tracker.h create mode 100644 src/dht.c create mode 100644 src/tracker_http.c create mode 100644 src/tracker_internal.h create mode 100644 src/tracker_store.c create mode 100644 src/tracker_udp.c create mode 100644 tests/test_tracker.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5963454 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.16) +project(torrent_tracker C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +option(TRACKER_NATIVE "Optimize for the build host (-march=native)" ON) +option(TRACKER_ASAN "Build with AddressSanitizer/UBSan" OFF) +option(TRACKER_TESTS "Build protocol tests" ON) + +add_library(torrenttracker SHARED + src/tracker_http.c + src/tracker_udp.c + src/tracker_store.c + src/dht.c +) +target_include_directories(torrenttracker PUBLIC include) +target_compile_options(torrenttracker PRIVATE + -O3 -Wall -Wextra -Wno-unused-parameter +) + +if(TRACKER_NATIVE AND NOT TRACKER_ASAN) + target_compile_options(torrenttracker PRIVATE -march=native) +endif() + +if(TRACKER_ASAN) + target_compile_options(torrenttracker PRIVATE -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer) + target_link_options(torrenttracker PRIVATE -fsanitize=address,undefined) +endif() + +option(TRACKER_LTO "Enable link-time optimization" ON) +if(TRACKER_LTO AND NOT TRACKER_ASAN) + include(CheckIPOSupported) + check_ipo_supported(RESULT _ipo_ok OUTPUT _ipo_msg) + if(_ipo_ok) + set_target_properties(torrenttracker PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON) + endif() +endif() + +set_target_properties(torrenttracker PROPERTIES OUTPUT_NAME torrenttracker) + +if(TRACKER_TESTS) + enable_testing() + add_executable(test_tracker tests/test_tracker.c) + target_link_libraries(test_tracker PRIVATE torrenttracker) + add_test(NAME tracker_protocols COMMAND test_tracker) +endif() diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..5d442d1 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,98 @@ +# torrent-tracker Protocol Plan + +Goal: build a tracker-side library that can serve the tracker protocols and +extensions seen in real torrents, with protocol parsing separated from swarm +storage and serving policy. + +## Implemented Foundation + +- BEP-3 HTTP announce parameters: `info_hash`, `peer_id`, `port`, `uploaded`, + `downloaded`, `left`, `compact`, `no_peer_id`, `event`, `numwant`, `key`, + `ip`, and `trackerid`. +- BEP-23 compact peer responses: IPv4 `peers` and BEP-7-style IPv6 `peers6`. +- BEP-48 HTTP scrape parsing and bencoded scrape responses. +- BEP-15 UDP connect, announce, scrape, and error packet handling. +- BEP-41 UDP announce extension parser, including concatenated URLData chunks. +- Client-side HTTP announce/scrape query builders and bencoded tracker response + parsers. +- Client-side UDP connect/announce/scrape packet builders and response parsers + with transaction-id validation. +- BEP-5 DHT/KRPC message builders and parser for `ping`, `find_node`, + `get_peers`, `announce_peer`, response, and error packets. +- BEP-32 IPv6 DHT compact node support through `nodes6` plus `want` flags. +- In-memory swarm table keyed by 20-byte tracker infohash. +- Announce handling for insert/update/stop, source-address-derived endpoints, + seed/leecher counts, completed-download scrape counts, no-self filtering, + randomized peer selection, `numwant` clamping, and stale peer pruning. + +## Tracker-Side Roadmap + +## Client-Side Roadmap + +1. DHT node/session manager: + Add a UDP event loop, node ID generation, transaction table, timeout/retry + logic, bootstrap nodes, and request dispatch for KRPC packets. + +2. DHT routing table: + Implement BEP-5 k-buckets, XOR distance sorting, good/questionable/bad node + state, bucket refresh, and persistence of known nodes between runs. + +3. DHT peer discovery: + Implement iterative `get_peers` lookup, token storage, `announce_peer`, and + integration with the tracker client result format so callers can consume + central tracker and DHT peers through one path. + +4. Tracker session manager: + Add a small client state machine that stores UDP connection IDs until expiry, + schedules announces at `interval`, handles started/completed/stopped events, + retries UDP with exponential backoff, and rotates across announce-list tiers. + +5. Transport adapters: + Wrap the protocol helpers with optional HTTP(S) and UDP socket code. Keep TLS + outside the core ABI, but provide a CLI/harness path that exercises real + network announces through the C helpers. + +6. Magnet and v2 metadata: + Keep accepting 20-byte tracker hashes: v1 SHA-1 infohash and BEP-52 truncated + SHA-256 for v2/hybrid torrents. Add metadata utilities or bindings so callers + can compute the right tracker hash from metainfo without duplicating logic. + +7. Client interop tests: + Compare C helper output against libtorrent/tracker_probe behavior for HTTP, + UDP, compact IPv4, `peers6`, scrape, failure responses, BEP-41 URLData, and + DHT KRPC packets observed from real nodes. + +## Tracker-Side Roadmap + +1. Network daemons: + Add HTTP/HTTPS and UDP listeners around the protocol core. HTTPS should live + behind a TLS terminator initially; the library API should not require a TLS + dependency. + +2. IPv6 and multi-homed behavior: + The store accepts IPv4 and IPv6 endpoints and prefers source addresses over + user-supplied `ip`. Add daemon-level tests for announcing the same peer over + multiple listen interfaces and returning family-appropriate UDP responses. + +3. Tracker policy: + Add hooks for private torrents, whitelist/auth tokens in HTTP query strings + and BEP-41 UDP URLData, per-swarm limits, rate limits, and abuse controls. + +4. Response selection: + Add stronger selection policy controls: per-family caps, seed/leecher mix, + deterministic test hooks for sampling, and configurable `tracker id` + generation/validation. + +5. Persistence and admin: + Add optional durable storage or snapshot/restore for long-running trackers, + metrics export, registered-torrent management, and scrape-cache policy. + +6. Compatibility matrix: + Build interop tests against libtorrent, Transmission, qBittorrent, aria2, + rtorrent, and opentracker for HTTP, UDP, compact, IPv6, scrape, and private + torrent behavior. + +7. Optional adjacent discovery: + DHT, PEX, and local peer discovery are peer-discovery mechanisms rather than + central tracker protocols. Keep them as separate modules if this project grows + into a full discovery stack. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fc0435 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# torrent-tracker + +A C tracker-side BitTorrent protocol library, shaped to match the neighboring +`torrent-peer` project but focused on announce/scrape handling instead of peer +wire transfer. + +Current scope: + +- HTTP(S) announce query parsing for BEP-3 tracker parameters. +- HTTP scrape query parsing and bencoded scrape responses. +- Compact HTTP tracker responses for IPv4 `peers` and IPv6 `peers6`. +- UDP tracker request parsing and response writing for BEP-15. +- UDP announce URLData extension parsing for BEP-41. +- Client-side HTTP query builders and bencoded response parsers for announce + and scrape. +- Client-side UDP connect/announce/scrape request builders and response parsers. +- BEP-5 DHT/KRPC message builders and parser for `ping`, `find_node`, + `get_peers`, `announce_peer`, responses, and errors. +- BEP-32 IPv6 DHT compact `nodes6` parsing/writing and `want` flags. +- In-memory swarm storage for announces, peer expiry, seed/leecher accounting, + completed counts, scrape data, no-self filtering, and randomized peer + selection with `numwant` clamping. +- A protocol-neutral announce/scrape model that a tracker storage engine can + use regardless of wire protocol. + +This is now the protocol core plus an embeddable in-memory tracker store, not a +complete daemon yet. The next layer should add connection listeners, request +routing, rate limiting, and auth hooks around this ABI. + +## Build + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +``` + +Options: + +- `-DTRACKER_NATIVE=OFF` for portable builds without `-march=native`. +- `-DTRACKER_ASAN=ON` for AddressSanitizer/UBSan. +- `-DTRACKER_TESTS=OFF` to skip the test executable. + +## Test + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug +cmake --build build +ctest --test-dir build --output-on-failure +``` + +## Real Tracker Probe + +Point the harness at a `.torrent` file to announce to its HTTP(S)/UDP trackers +and print returned swarm stats and peers: + +```sh +python harness/tracker_probe.py file.torrent --max-trackers 8 +``` + +Useful options: + +- `--tracker URL` probes an explicit tracker instead of the torrent's tracker + list. Repeat it to test several URLs. +- `--scrape` also tries HTTP scrape URLs derived from announce URLs. +- `--timeout SECONDS`, `--numwant N`, and `--port PORT` control announce + behavior. + +## Layout + +| Path | Role | +|------|------| +| `include/tracker.h` | public C ABI | +| `src/tracker_http.c` | HTTP(S) tracker client/server helpers and bencode handling | +| `src/tracker_udp.c` | UDP tracker client/server packet parser/writers | +| `src/tracker_store.c` | in-memory swarm table and response selection | +| `src/dht.c` | DHT/KRPC message parser and writers | +| `harness/tracker_probe.py` | real-world HTTP/UDP tracker probe for `.torrent` files | +| `tests/test_tracker.c` | focused protocol tests | +| `PLAN.md` | protocol/extension roadmap | diff --git a/harness/__pycache__/tracker_probe.cpython-314.pyc b/harness/__pycache__/tracker_probe.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b23bb134166938e8fe5e40109fcc0b9761a56f6 GIT binary patch literal 37518 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 PR9Ado%jwpz+5G Any: + value, pos = self._value(0, top=True) + if pos != len(self.data): + raise BencodeError(f"trailing data at byte {pos}") + return value + + def _value(self, pos: int, *, top: bool = False) -> tuple[Any, int]: + if pos >= len(self.data): + raise BencodeError("unexpected end of bencode") + c = self.data[pos] + if c == ord("i"): + return self._int(pos) + if c == ord("l"): + return self._list(pos) + if c == ord("d"): + return self._dict(pos, top=top) + if ord("0") <= c <= ord("9"): + return self._bytes(pos) + raise BencodeError(f"invalid bencode byte {c!r} at {pos}") + + def _int(self, pos: int) -> tuple[int, int]: + end = self.data.find(b"e", pos) + if end < 0: + raise BencodeError("unterminated integer") + raw = self.data[pos + 1:end] + if not raw: + raise BencodeError("empty integer") + return int(raw), end + 1 + + def _bytes(self, pos: int) -> tuple[bytes, int]: + colon = self.data.find(b":", pos) + if colon < 0: + raise BencodeError("unterminated byte string length") + n = int(self.data[pos:colon]) + start = colon + 1 + end = start + n + if end > len(self.data): + raise BencodeError("byte string exceeds input") + return self.data[start:end], end + + def _list(self, pos: int) -> tuple[list[Any], int]: + out: list[Any] = [] + pos += 1 + while pos < len(self.data) and self.data[pos] != ord("e"): + value, pos = self._value(pos) + out.append(value) + if pos >= len(self.data): + raise BencodeError("unterminated list") + return out, pos + 1 + + def _dict(self, pos: int, *, top: bool = False) -> tuple[dict[bytes, Any], int]: + out: dict[bytes, Any] = {} + pos += 1 + while pos < len(self.data) and self.data[pos] != ord("e"): + key, pos = self._bytes(pos) + value_start = pos + value, pos = self._value(pos) + if top and key == b"info": + self.info_span = (value_start, pos) + out[key] = value + if pos >= len(self.data): + raise BencodeError("unterminated dict") + return out, pos + 1 + + +@dataclass +class TorrentMeta: + path: str + name: str + info_hash: bytes + info_hash_kind: str + total_size: int + trackers: list[str] + + +@dataclass +class AnnounceResult: + tracker: str + ok: bool + protocol: str + interval: int | None = None + min_interval: int | None = None + complete: int | None = None + incomplete: int | None = None + downloaded: int | None = None + peers: list[tuple[str, int]] | None = None + warning: str | None = None + error: str | None = None + elapsed_ms: float = 0.0 + + +def _text(value: Any, default: str = "") -> str: + if isinstance(value, bytes): + return value.decode("utf-8", "replace") + return default + + +def _file_tree_size(node: Any) -> int: + if not isinstance(node, dict): + return 0 + total = 0 + file_marker = node.get(b"") + if isinstance(file_marker, dict): + total += int(file_marker.get(b"length", 0)) + for key, child in node.items(): + if key != b"": + total += _file_tree_size(child) + return total + + +def _total_size(info: dict[bytes, Any]) -> int: + if b"length" in info: + return int(info[b"length"]) + if b"files" in info: + return sum(int(f.get(b"length", 0)) for f in info[b"files"]) + if b"file tree" in info: + return _file_tree_size(info[b"file tree"]) + return 0 + + +def _trackers(meta: dict[bytes, Any]) -> list[str]: + urls: list[str] = [] + announce = meta.get(b"announce") + if isinstance(announce, bytes): + urls.append(_text(announce)) + tiers = meta.get(b"announce-list") + if isinstance(tiers, list): + for tier in tiers: + if not isinstance(tier, list): + continue + for item in tier: + if isinstance(item, bytes): + urls.append(_text(item)) + seen: set[str] = set() + out: list[str] = [] + for url in urls: + if url and url not in seen: + seen.add(url) + out.append(url) + return out + + +def load_torrent(path: str) -> TorrentMeta: + raw = open(path, "rb").read() + dec = BDecoder(raw) + meta = dec.parse() + if not isinstance(meta, dict) or dec.info_span is None: + raise BencodeError("metainfo does not contain a top-level info dict") + info = meta[b"info"] + info_raw = raw[dec.info_span[0]:dec.info_span[1]] + if b"pieces" in info: + info_hash = hashlib.sha1(info_raw).digest() + kind = "v1 sha1" + elif info.get(b"meta version") == 2: + info_hash = hashlib.sha256(info_raw).digest()[:20] + kind = "v2 sha256-truncated" + else: + raise BencodeError("unsupported torrent: no v1 pieces or v2 meta version") + return TorrentMeta( + path=path, + name=_text(info.get(b"name"), os.path.basename(path)), + info_hash=info_hash, + info_hash_kind=kind, + total_size=_total_size(info), + trackers=_trackers(meta), + ) + + +def make_peer_id() -> bytes: + return b"-TG0001-" + os.urandom(12) + + +def _http_announce_url(url: str, meta: TorrentMeta, peer_id: bytes, port: int, + key: int, numwant: int, event: str) -> str: + parts = urlsplit(url) + query = parts.query + extra = [ + ("info_hash", quote_from_bytes(meta.info_hash, safe="")), + ("peer_id", quote_from_bytes(peer_id, safe="")), + ("port", str(port)), + ("uploaded", "0"), + ("downloaded", "0"), + ("left", str(meta.total_size)), + ("compact", "1"), + ("numwant", str(numwant)), + ("key", str(key)), + ] + if event: + extra.append(("event", event)) + suffix = "&".join(f"{k}={v}" for k, v in extra) + query = f"{query}&{suffix}" if query else suffix + return urlunsplit((parts.scheme, parts.netloc, parts.path, query, parts.fragment)) + + +def _decode_compact(peers: bytes, family: int) -> list[tuple[str, int]]: + stride = 6 if family == socket.AF_INET else 18 + addr_len = 4 if family == socket.AF_INET else 16 + if len(peers) % stride != 0: + raise ValueError(f"compact peer string length {len(peers)} is not a multiple of {stride}") + out = [] + for off in range(0, len(peers), stride): + addr = socket.inet_ntop(family, peers[off:off + addr_len]) + port = struct.unpack("!H", peers[off + addr_len:off + stride])[0] + out.append((addr, port)) + return out + + +def _decode_peer_list(value: Any) -> list[tuple[str, int]]: + peers: list[tuple[str, int]] = [] + if isinstance(value, bytes): + peers.extend(_decode_compact(value, socket.AF_INET)) + elif isinstance(value, list): + for item in value: + if not isinstance(item, dict): + continue + ip = _text(item.get(b"ip")) + port = item.get(b"port") + if ip and isinstance(port, int): + peers.append((ip, port)) + return peers + + +def _parse_tracker_dict(raw: bytes, tracker: str, protocol: str, + elapsed_ms: float) -> AnnounceResult: + data = BDecoder(raw).parse() + if not isinstance(data, dict): + return AnnounceResult(tracker, False, protocol, error="response is not a dict", + elapsed_ms=elapsed_ms) + failure = data.get(b"failure reason") + if isinstance(failure, bytes): + return AnnounceResult(tracker, False, protocol, error=_text(failure), + elapsed_ms=elapsed_ms) + peers = _decode_peer_list(data.get(b"peers", b"")) + peers6 = data.get(b"peers6") + if isinstance(peers6, bytes): + peers.extend(_decode_compact(peers6, socket.AF_INET6)) + return AnnounceResult( + tracker=tracker, + ok=True, + protocol=protocol, + interval=data.get(b"interval") if isinstance(data.get(b"interval"), int) else None, + min_interval=data.get(b"min interval") if isinstance(data.get(b"min interval"), int) else None, + complete=data.get(b"complete") if isinstance(data.get(b"complete"), int) else None, + incomplete=data.get(b"incomplete") if isinstance(data.get(b"incomplete"), int) else None, + peers=peers, + warning=_text(data.get(b"warning message")) if b"warning message" in data else None, + elapsed_ms=elapsed_ms, + ) + + +def announce_http(url: str, meta: TorrentMeta, peer_id: bytes, port: int, + key: int, numwant: int, event: str, timeout: float) -> AnnounceResult: + start = time.monotonic() + announce_url = _http_announce_url(url, meta, peer_id, port, key, numwant, event) + try: + req = Request(announce_url, headers={"User-Agent": "torrent-tracker-harness/0.1"}) + ctx = ssl.create_default_context() + with urlopen(req, timeout=timeout, context=ctx) as resp: + raw = resp.read(2 * 1024 * 1024) + elapsed = (time.monotonic() - start) * 1000.0 + return _parse_tracker_dict(raw, url, "http", elapsed) + except Exception as exc: + elapsed = (time.monotonic() - start) * 1000.0 + return AnnounceResult(url, False, "http", error=str(exc), elapsed_ms=elapsed) + + +def _udp_url_data(url: str) -> bytes: + parts = urlsplit(url) + data = parts.path or b"" + if isinstance(data, str): + data = data.encode("utf-8") + if parts.query: + data += b"?" + parts.query.encode("utf-8") + return data + + +def _udp_options(url: str) -> bytes: + data = _udp_url_data(url) + if not data: + return b"" + out = bytearray() + for off in range(0, len(data), 255): + chunk = data[off:off + 255] + out += bytes([0x02, len(chunk)]) + chunk + return bytes(out) + + +def _udp_roundtrip(sock: socket.socket, packet: bytes, txid: int, + timeout: float) -> bytes: + deadline = time.monotonic() + timeout + delay = min(timeout, 1.0) + while True: + sock.send(packet) + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("UDP tracker timed out") + sock.settimeout(min(delay, remaining)) + try: + raw = sock.recv(65535) + except socket.timeout: + delay = min(delay * 2.0, 8.0) + continue + if len(raw) >= 8 and struct.unpack_from("!I", raw, 4)[0] == txid: + return raw + + +def _parse_udp_announce(raw: bytes, tracker: str, family: int, + elapsed_ms: float) -> AnnounceResult: + if len(raw) < 8: + return AnnounceResult(tracker, False, "udp", error="short UDP response", + elapsed_ms=elapsed_ms) + action = struct.unpack_from("!I", raw, 0)[0] + if action == UDP_ERROR: + return AnnounceResult(tracker, False, "udp", + error=raw[8:].decode("utf-8", "replace"), + elapsed_ms=elapsed_ms) + if action != UDP_ANNOUNCE or len(raw) < 20: + return AnnounceResult(tracker, False, "udp", + error=f"unexpected UDP action {action}", + elapsed_ms=elapsed_ms) + interval, incomplete, complete = struct.unpack_from("!III", raw, 8) + peers = _decode_compact(raw[20:], family) + return AnnounceResult(tracker, True, "udp", interval=interval, + complete=complete, incomplete=incomplete, + peers=peers, elapsed_ms=elapsed_ms) + + +def announce_udp(url: str, meta: TorrentMeta, peer_id: bytes, port: int, + key: int, numwant: int, event: str, timeout: float) -> AnnounceResult: + parts = urlsplit(url) + host = parts.hostname + if not host: + return AnnounceResult(url, False, "udp", error="missing UDP tracker host") + tracker_port = parts.port or 80 + event_id = {"": 0, "completed": 1, "started": 2, "stopped": 3}.get(event, 0) + start = time.monotonic() + try: + infos = socket.getaddrinfo(host, tracker_port, 0, socket.SOCK_DGRAM) + last_error: Exception | None = None + for family, socktype, proto, _canon, sockaddr in infos: + if family not in (socket.AF_INET, socket.AF_INET6): + continue + try: + with socket.socket(family, socktype, proto) as sock: + sock.connect(sockaddr) + txid = random.getrandbits(32) + connect = struct.pack("!QII", UDP_PROTOCOL_ID, UDP_CONNECT, txid) + raw = _udp_roundtrip(sock, connect, txid, timeout) + action, got_txid, conn_id = struct.unpack("!IIQ", raw[:16]) + if action != UDP_CONNECT or got_txid != txid: + raise OSError("invalid UDP connect response") + txid = random.getrandbits(32) + announce = struct.pack( + "!QII20s20sQQQIIIiH", + conn_id, + UDP_ANNOUNCE, + txid, + meta.info_hash, + peer_id, + 0, + meta.total_size, + 0, + event_id, + 0, + key, + numwant, + port, + ) + _udp_options(url) + raw = _udp_roundtrip(sock, announce, txid, timeout) + elapsed = (time.monotonic() - start) * 1000.0 + return _parse_udp_announce(raw, url, family, elapsed) + except Exception as exc: + last_error = exc + continue + elapsed = (time.monotonic() - start) * 1000.0 + return AnnounceResult(url, False, "udp", + error=str(last_error or "no usable address"), + elapsed_ms=elapsed) + except Exception as exc: + elapsed = (time.monotonic() - start) * 1000.0 + return AnnounceResult(url, False, "udp", error=str(exc), elapsed_ms=elapsed) + + +def _scrape_url(url: str) -> str | None: + parts = urlsplit(url) + idx = parts.path.rfind("announce") + if idx < 0: + return None + path = parts.path[:idx] + "scrape" + parts.path[idx + len("announce"):] + return urlunsplit((parts.scheme, parts.netloc, path, parts.query, parts.fragment)) + + +def scrape_http(url: str, meta: TorrentMeta, timeout: float) -> AnnounceResult: + scrape = _scrape_url(url) + if not scrape: + return AnnounceResult(url, False, "http-scrape", error="no scrape URL") + parts = urlsplit(scrape) + q = parts.query + suffix = "info_hash=" + quote_from_bytes(meta.info_hash, safe="") + q = f"{q}&{suffix}" if q else suffix + scrape = urlunsplit((parts.scheme, parts.netloc, parts.path, q, parts.fragment)) + start = time.monotonic() + try: + req = Request(scrape, headers={"User-Agent": "torrent-tracker-harness/0.1"}) + ctx = ssl.create_default_context() + with urlopen(req, timeout=timeout, context=ctx) as resp: + raw = resp.read(2 * 1024 * 1024) + elapsed = (time.monotonic() - start) * 1000.0 + data = BDecoder(raw).parse() + if not isinstance(data, dict): + raise ValueError("scrape response is not a dict") + failure = data.get(b"failure reason") + if isinstance(failure, bytes): + return AnnounceResult(url, False, "http-scrape", error=_text(failure), + elapsed_ms=elapsed) + files = data.get(b"files", {}) + entry = files.get(meta.info_hash) if isinstance(files, dict) else None + if not isinstance(entry, dict): + return AnnounceResult(url, False, "http-scrape", + error="info_hash missing from scrape response", + elapsed_ms=elapsed) + return AnnounceResult(url, True, "http-scrape", + complete=entry.get(b"complete"), + incomplete=entry.get(b"incomplete"), + downloaded=entry.get(b"downloaded"), + elapsed_ms=elapsed) + except Exception as exc: + elapsed = (time.monotonic() - start) * 1000.0 + return AnnounceResult(url, False, "http-scrape", error=str(exc), + elapsed_ms=elapsed) + + +def announce(url: str, meta: TorrentMeta, peer_id: bytes, port: int, key: int, + numwant: int, event: str, timeout: float) -> AnnounceResult: + scheme = urlsplit(url).scheme.lower() + if scheme in ("http", "https"): + return announce_http(url, meta, peer_id, port, key, numwant, event, timeout) + if scheme == "udp": + return announce_udp(url, meta, peer_id, port, key, numwant, event, timeout) + return AnnounceResult(url, False, scheme or "unknown", + error=f"unsupported tracker scheme {scheme!r}") + + +def print_result(result: AnnounceResult, max_peers: int) -> None: + status = "ok" if result.ok else "fail" + print(f"[{status}] {result.protocol} {result.tracker} ({result.elapsed_ms:.0f} ms)") + if result.error: + print(f" error: {result.error}") + if result.warning: + print(f" warning: {result.warning}") + if result.ok: + stats = [] + if result.interval is not None: + stats.append(f"interval={result.interval}") + if result.min_interval is not None: + stats.append(f"min_interval={result.min_interval}") + if result.complete is not None: + stats.append(f"seeders={result.complete}") + if result.incomplete is not None: + stats.append(f"leechers={result.incomplete}") + if result.downloaded is not None: + stats.append(f"downloaded={result.downloaded}") + if stats: + print(" " + " ".join(stats)) + peers = result.peers or [] + if peers: + shown = ", ".join(f"{host}:{port}" for host, port in peers[:max_peers]) + suffix = "" if len(peers) <= max_peers else f" ... +{len(peers) - max_peers}" + print(f" peers[{len(peers)}]: {shown}{suffix}") + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description="Probe real BitTorrent trackers from a .torrent file.") + ap.add_argument("torrent", help="path to .torrent file") + ap.add_argument("--tracker", action="append", + help="tracker URL to probe instead of URLs from the torrent; repeatable") + ap.add_argument("--max-trackers", type=int, default=8, + help="maximum trackers to probe from the torrent") + ap.add_argument("--timeout", type=float, default=8.0, + help="per-tracker timeout in seconds") + ap.add_argument("--numwant", type=int, default=50, + help="numwant value in announce requests") + ap.add_argument("--port", type=int, default=6881, + help="port value to announce") + ap.add_argument("--event", choices=["", "started", "completed", "stopped"], + default="started") + ap.add_argument("--scrape", action="store_true", + help="also try HTTP scrape endpoints derived from announce URLs") + ap.add_argument("--show-peers", type=int, default=10, + help="number of returned peers to print per tracker") + args = ap.parse_args(argv) + + meta = load_torrent(args.torrent) + trackers = args.tracker or meta.trackers + if args.max_trackers > 0: + trackers = trackers[:args.max_trackers] + + print(f"torrent: {meta.name}") + print(f"size: {meta.total_size} bytes") + print(f"info_hash: {meta.info_hash.hex()} ({meta.info_hash_kind})") + print(f"trackers: {len(trackers)}") + if not trackers: + print("no trackers found") + return 1 + + peer_id = make_peer_id() + key = random.getrandbits(32) + successes = 0 + for url in trackers: + result = announce(url, meta, peer_id, args.port, key, args.numwant, + args.event, args.timeout) + if result.ok: + successes += 1 + print_result(result, args.show_peers) + if args.scrape and urlsplit(url).scheme.lower() in ("http", "https"): + print_result(scrape_http(url, meta, args.timeout), args.show_peers) + + return 0 if successes else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/include/tracker.h b/include/tracker.h new file mode 100644 index 0000000..9f34a96 --- /dev/null +++ b/include/tracker.h @@ -0,0 +1,338 @@ +/* + * tracker.h - Protocol core for BitTorrent trackers. + * + * This library focuses on tracker-side parsing and response formatting. The + * ABI deliberately stays protocol-neutral: HTTP(S) and UDP announce/scrape + * inputs normalize into the same request structs, and servers can format + * compact IPv4/IPv6 peer responses from one endpoint list. + */ +#ifndef TORRENT_TRACKER_H +#define TORRENT_TRACKER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define TRACKER_INFO_HASH_LEN 20u +#define TRACKER_PEER_ID_LEN 20u +#define TRACKER_MAX_PEERS 256u +#define TRACKER_MAX_SCRAPE 74u +#define TRACKER_MAX_URL_DATA 512u +#define DHT_NODE_ID_LEN 20u +#define DHT_MAX_TRANSACTION 16u +#define DHT_MAX_TOKEN 64u +#define DHT_MAX_NODES 256u +#define DHT_MAX_ERROR 128u + +typedef enum { + TRACKER_OK = 0, + TRACKER_EINVAL = -1, + TRACKER_ETOOSMALL= -2, + TRACKER_ETOOBIG = -3, + TRACKER_ENOSPC = -4, + TRACKER_EPARSE = -5 +} tracker_result; + +typedef enum { + TRACKER_EVENT_NONE = 0, + TRACKER_EVENT_COMPLETED = 1, + TRACKER_EVENT_STARTED = 2, + TRACKER_EVENT_STOPPED = 3 +} tracker_event; + +typedef enum { + TRACKER_ADDR_IPV4 = 4, + TRACKER_ADDR_IPV6 = 6 +} tracker_addr_family; + +typedef struct { + uint8_t family; /* TRACKER_ADDR_IPV4 or TRACKER_ADDR_IPV6 */ + uint8_t addr[16]; /* first 4 bytes used for IPv4 */ + uint16_t port; /* host byte order */ + uint8_t peer_id[20]; + uint8_t has_peer_id; +} tracker_peer; + +typedef struct { + uint8_t info_hash[20]; + uint8_t peer_id[20]; + uint16_t port; + uint64_t uploaded; + uint64_t downloaded; + uint64_t left; + int32_t numwant; /* -1 means default */ + uint32_t key; + uint32_t ip4; /* host byte order, 0 means use source IP */ + tracker_event event; + uint8_t compact; + uint8_t no_peer_id; + uint8_t has_key; + uint8_t has_ip4; + char ip[64]; /* HTTP ip= value, if supplied */ + char tracker_id[128]; + char url_data[TRACKER_MAX_URL_DATA]; /* BEP-41 UDP URLData */ +} tracker_announce_request; + +typedef struct { + uint32_t interval; + uint32_t min_interval; + uint32_t complete; /* seeders */ + uint32_t incomplete; /* leechers */ + const char *tracker_id; + const tracker_peer *peers; + size_t peer_count; + uint8_t compact; /* compact peers/peers6 response */ +} tracker_announce_response; + +typedef struct { + uint8_t info_hash[20]; + uint32_t complete; + uint32_t downloaded; + uint32_t incomplete; +} tracker_scrape_file; + +typedef struct { + const tracker_scrape_file *files; + size_t file_count; +} tracker_scrape_response; + +typedef struct { + uint32_t interval; /* announce interval advertised to clients */ + uint32_t min_interval; /* optional minimum announce interval */ + uint32_t peer_timeout; /* expire peers older than this many seconds */ + uint32_t default_numwant; /* used when request numwant is -1 */ + uint32_t max_numwant; /* hard cap on returned peers */ + uint64_t random_seed; /* 0 => deterministic default seed */ +} tracker_store_config; + +typedef struct tracker_store tracker_store; + +tracker_store *tracker_store_create(const tracker_store_config *cfg); +void tracker_store_destroy(tracker_store *store); + +/* Apply one announce to the in-memory swarm table and prepare a response. + * source_addr is the observed remote address; source_addr->port is ignored and + * req->port is advertised. Trackers should prefer this source address over + * user-supplied ip= values to avoid reflector abuse. out_peers is caller-owned + * response storage and is referenced by resp->peers on success. */ +int tracker_store_announce(tracker_store *store, + const tracker_announce_request *req, + const tracker_peer *source_addr, + uint64_t now_sec, + tracker_peer *out_peers, + size_t out_peer_cap, + tracker_announce_response *resp); + +int tracker_store_scrape(tracker_store *store, + const uint8_t hashes[][20], + size_t hash_count, + tracker_scrape_file *out_files, + size_t out_file_cap, + tracker_scrape_response *resp); + +/* Remove peers that have not announced within cfg.peer_timeout seconds. */ +size_t tracker_store_prune(tracker_store *store, uint64_t now_sec); + +size_t tracker_store_swarm_count(const tracker_store *store); +size_t tracker_store_peer_count(const tracker_store *store); + +typedef enum { + TRACKER_UDP_CONNECT = 0, + TRACKER_UDP_ANNOUNCE = 1, + TRACKER_UDP_SCRAPE = 2, + TRACKER_UDP_ERROR = 3 +} tracker_udp_action; + +typedef struct { + tracker_udp_action action; + uint32_t transaction_id; + uint64_t connection_id; + tracker_announce_request announce; + uint8_t scrape_hashes[TRACKER_MAX_SCRAPE][20]; + size_t scrape_count; +} tracker_udp_request; + +/* HTTP(S) tracker protocol. Query may be the raw query string or a full path + * containing '?'. info_hash and peer_id are percent-decoded and must be 20 + * bytes for announce requests. */ +int tracker_http_parse_announce_query(const char *query, + tracker_announce_request *out); +int tracker_http_parse_scrape_query(const char *query, + uint8_t hashes[][20], size_t max_hashes, + size_t *hash_count); +int tracker_http_write_announce_response(const tracker_announce_response *resp, + uint8_t *buf, size_t cap, + size_t *written); +int tracker_http_write_scrape_response(const tracker_scrape_response *resp, + uint8_t *buf, size_t cap, + size_t *written); +int tracker_http_write_failure(const char *message, uint8_t *buf, size_t cap, + size_t *written); + +/* Client-side HTTP helpers. Query writers produce the path query component + * without a leading '?'. Response parsers accept a raw bencoded tracker body + * and fill caller-owned peer/file arrays. */ +int tracker_http_write_announce_query(const tracker_announce_request *req, + char *buf, size_t cap, + size_t *written); +int tracker_http_write_scrape_query(const uint8_t hashes[][20], + size_t hash_count, + char *buf, size_t cap, + size_t *written); +int tracker_http_parse_announce_response(const uint8_t *buf, size_t len, + tracker_peer *out_peers, + size_t out_peer_cap, + tracker_announce_response *resp); +int tracker_http_parse_scrape_response(const uint8_t *buf, size_t len, + tracker_scrape_file *out_files, + size_t out_file_cap, + tracker_scrape_response *resp); + +/* UDP tracker protocol (BEP-15 plus BEP-41 URLData parsing on announces). */ +int tracker_udp_parse_request(const uint8_t *packet, size_t len, + tracker_addr_family source_family, + tracker_udp_request *out); +int tracker_udp_write_connect_response(uint32_t transaction_id, + uint64_t connection_id, + uint8_t *buf, size_t cap, + size_t *written); +int tracker_udp_write_announce_response(uint32_t transaction_id, + tracker_addr_family family, + const tracker_announce_response *resp, + uint8_t *buf, size_t cap, + size_t *written); +int tracker_udp_write_scrape_response(uint32_t transaction_id, + const tracker_scrape_response *resp, + uint8_t *buf, size_t cap, + size_t *written); +int tracker_udp_write_error(uint32_t transaction_id, const char *message, + uint8_t *buf, size_t cap, size_t *written); + +/* Client-side UDP helpers. */ +int tracker_udp_write_connect_request(uint32_t transaction_id, + uint8_t *buf, size_t cap, + size_t *written); +int tracker_udp_parse_connect_response(const uint8_t *packet, size_t len, + uint32_t transaction_id, + uint64_t *connection_id); +int tracker_udp_write_announce_request(uint64_t connection_id, + uint32_t transaction_id, + const tracker_announce_request *req, + uint8_t *buf, size_t cap, + size_t *written); +int tracker_udp_parse_announce_response(const uint8_t *packet, size_t len, + uint32_t transaction_id, + tracker_addr_family family, + tracker_peer *out_peers, + size_t out_peer_cap, + tracker_announce_response *resp); +int tracker_udp_write_scrape_request(uint64_t connection_id, + uint32_t transaction_id, + const uint8_t hashes[][20], + size_t hash_count, + uint8_t *buf, size_t cap, + size_t *written); +int tracker_udp_parse_scrape_response(const uint8_t *packet, size_t len, + uint32_t transaction_id, + tracker_scrape_file *out_files, + size_t out_file_cap, + tracker_scrape_response *resp); + +typedef enum { + DHT_MSG_QUERY = 1, + DHT_MSG_RESPONSE = 2, + DHT_MSG_ERROR = 3 +} dht_message_type; + +typedef enum { + DHT_QUERY_NONE = 0, + DHT_QUERY_PING = 1, + DHT_QUERY_FIND_NODE = 2, + DHT_QUERY_GET_PEERS = 3, + DHT_QUERY_ANNOUNCE_PEER = 4 +} dht_query_type; + +typedef enum { + DHT_ERR_GENERIC = 201, + DHT_ERR_SERVER = 202, + DHT_ERR_PROTOCOL = 203, + DHT_ERR_METHOD_UNKNOWN = 204 +} dht_error_code; + +typedef struct { + uint8_t id[20]; + uint8_t family; /* TRACKER_ADDR_IPV4 or TRACKER_ADDR_IPV6 */ + uint8_t addr[16]; + uint16_t port; /* host byte order */ +} dht_node; + +typedef struct { + dht_message_type type; + dht_query_type query; + uint8_t transaction[DHT_MAX_TRANSACTION]; + size_t transaction_len; + uint8_t id[20]; + uint8_t target[20]; + uint8_t info_hash[20]; + uint16_t port; + uint8_t implied_port; + uint8_t want_ipv4; + uint8_t want_ipv6; + uint8_t token[DHT_MAX_TOKEN]; + size_t token_len; + dht_node nodes[DHT_MAX_NODES]; + size_t node_count; + tracker_peer peers[TRACKER_MAX_PEERS]; + size_t peer_count; + int error_code; + char error_message[DHT_MAX_ERROR]; +} dht_message; + +int dht_write_ping_query(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + uint8_t *buf, size_t cap, size_t *written); +int dht_write_find_node_query(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t target[20], + uint8_t want_ipv4, uint8_t want_ipv6, + uint8_t *buf, size_t cap, size_t *written); +int dht_write_get_peers_query(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t info_hash[20], + uint8_t want_ipv4, uint8_t want_ipv6, + uint8_t *buf, size_t cap, size_t *written); +int dht_write_announce_peer_query(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t info_hash[20], + uint16_t port, + const uint8_t *token, size_t token_len, + uint8_t implied_port, + uint8_t *buf, size_t cap, size_t *written); + +int dht_write_ping_response(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + uint8_t *buf, size_t cap, size_t *written); +int dht_write_nodes_response(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t *token, size_t token_len, + const dht_node *nodes, size_t node_count, + uint8_t *buf, size_t cap, size_t *written); +int dht_write_peers_response(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t *token, size_t token_len, + const tracker_peer *peers, size_t peer_count, + uint8_t *buf, size_t cap, size_t *written); +int dht_write_error(const uint8_t *tx, size_t tx_len, int code, + const char *message, + uint8_t *buf, size_t cap, size_t *written); + +int dht_parse_message(const uint8_t *packet, size_t len, dht_message *out); + +#ifdef __cplusplus +} +#endif + +#endif /* TORRENT_TRACKER_H */ diff --git a/src/dht.c b/src/dht.c new file mode 100644 index 0000000..d216e7f --- /dev/null +++ b/src/dht.c @@ -0,0 +1,669 @@ +#include "tracker.h" +#include "tracker_internal.h" + +#include +#include +#include +#include + +static int put_str(tr_writer *w, const void *s, size_t n) +{ + char tmp[32]; + int rc; + int len = snprintf(tmp, sizeof(tmp), "%zu:", n); + if (len < 0 || (size_t)len >= sizeof(tmp)) return TRACKER_EPARSE; + rc = tr_put(w, tmp, (size_t)len); + if (rc != TRACKER_OK) return rc; + return tr_put(w, s, n); +} + +static int put_lit(tr_writer *w, const char *s) +{ + return tr_put(w, s, strlen(s)); +} + +static int put_int(tr_writer *w, int64_t v) +{ + char tmp[32]; + int len = snprintf(tmp, sizeof(tmp), "i%llde", (long long)v); + if (len < 0 || (size_t)len >= sizeof(tmp)) return TRACKER_EPARSE; + return tr_put(w, tmp, (size_t)len); +} + +static int valid_tx(const uint8_t *tx, size_t tx_len) +{ + return tx && tx_len > 0 && tx_len <= DHT_MAX_TRANSACTION; +} + +static int write_query_prefix(tr_writer *w, const uint8_t *tx, size_t tx_len, + const char *query) +{ + int rc; + if (!valid_tx(tx, tx_len)) return TRACKER_EINVAL; + rc = put_lit(w, "d1:ad"); + if (rc != TRACKER_OK) return rc; + (void)query; + return TRACKER_OK; +} + +static int write_query_suffix(tr_writer *w, const uint8_t *tx, size_t tx_len, + const char *query) +{ + int rc = put_lit(w, "e1:q"); + if (rc != TRACKER_OK) return rc; + rc = put_str(w, query, strlen(query)); + if (rc != TRACKER_OK) return rc; + rc = put_lit(w, "1:t"); + if (rc != TRACKER_OK) return rc; + rc = put_str(w, tx, tx_len); + if (rc != TRACKER_OK) return rc; + return put_lit(w, "1:y1:qe"); +} + +static int put_want(tr_writer *w, uint8_t want_ipv4, uint8_t want_ipv6) +{ + int rc; + if (!want_ipv4 && !want_ipv6) return TRACKER_OK; + rc = put_lit(w, "4:wantl"); + if (rc != TRACKER_OK) return rc; + if (want_ipv4) { + rc = put_lit(w, "2:n4"); + if (rc != TRACKER_OK) return rc; + } + if (want_ipv6) { + rc = put_lit(w, "2:n6"); + if (rc != TRACKER_OK) return rc; + } + return tr_putc(w, 'e'); +} + +int dht_write_ping_query(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + uint8_t *buf, size_t cap, size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!id || !buf || !written) return TRACKER_EINVAL; + rc = write_query_prefix(&w, tx, tx_len, "ping"); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "2:id"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, id, 20); + if (rc != TRACKER_OK) return rc; + rc = write_query_suffix(&w, tx, tx_len, "ping"); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +int dht_write_find_node_query(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t target[20], + uint8_t want_ipv4, uint8_t want_ipv6, + uint8_t *buf, size_t cap, size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!id || !target || !buf || !written) return TRACKER_EINVAL; + rc = write_query_prefix(&w, tx, tx_len, "find_node"); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "2:id"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, id, 20); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "6:target"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, target, 20); + if (rc != TRACKER_OK) return rc; + rc = put_want(&w, want_ipv4, want_ipv6); + if (rc != TRACKER_OK) return rc; + rc = write_query_suffix(&w, tx, tx_len, "find_node"); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +int dht_write_get_peers_query(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t info_hash[20], + uint8_t want_ipv4, uint8_t want_ipv6, + uint8_t *buf, size_t cap, size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!id || !info_hash || !buf || !written) return TRACKER_EINVAL; + rc = write_query_prefix(&w, tx, tx_len, "get_peers"); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "2:id"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, id, 20); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "9:info_hash"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, info_hash, 20); + if (rc != TRACKER_OK) return rc; + rc = put_want(&w, want_ipv4, want_ipv6); + if (rc != TRACKER_OK) return rc; + rc = write_query_suffix(&w, tx, tx_len, "get_peers"); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +int dht_write_announce_peer_query(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t info_hash[20], + uint16_t port, + const uint8_t *token, size_t token_len, + uint8_t implied_port, + uint8_t *buf, size_t cap, size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!id || !info_hash || !token || !buf || !written) return TRACKER_EINVAL; + if (token_len > DHT_MAX_TOKEN) return TRACKER_ETOOBIG; + rc = write_query_prefix(&w, tx, tx_len, "announce_peer"); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "2:id"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, id, 20); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "12:implied_port"); + if (rc != TRACKER_OK) return rc; + rc = put_int(&w, implied_port ? 1 : 0); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "9:info_hash"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, info_hash, 20); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "4:port"); + if (rc != TRACKER_OK) return rc; + rc = put_int(&w, port); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "5:token"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, token, token_len); + if (rc != TRACKER_OK) return rc; + rc = write_query_suffix(&w, tx, tx_len, "announce_peer"); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +static int write_response_prefix(tr_writer *w, const uint8_t *tx, size_t tx_len) +{ + int rc; + if (!valid_tx(tx, tx_len)) return TRACKER_EINVAL; + rc = put_lit(w, "d1:rd"); + if (rc != TRACKER_OK) return rc; + return TRACKER_OK; +} + +static int write_response_suffix(tr_writer *w, const uint8_t *tx, size_t tx_len) +{ + int rc = put_lit(w, "e1:t"); + if (rc != TRACKER_OK) return rc; + rc = put_str(w, tx, tx_len); + if (rc != TRACKER_OK) return rc; + return put_lit(w, "1:y1:re"); +} + +int dht_write_ping_response(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + uint8_t *buf, size_t cap, size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!id || !buf || !written) return TRACKER_EINVAL; + rc = write_response_prefix(&w, tx, tx_len); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "2:id"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, id, 20); + if (rc != TRACKER_OK) return rc; + rc = write_response_suffix(&w, tx, tx_len); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +static int put_compact_nodes(tr_writer *w, const dht_node *nodes, + size_t node_count, tracker_addr_family family) +{ + uint8_t compact[DHT_MAX_NODES * 38u]; + size_t len = 0; + size_t stride = family == TRACKER_ADDR_IPV4 ? 26u : 38u; + size_t addr_len = family == TRACKER_ADDR_IPV4 ? 4u : 16u; + int rc; + for (size_t i = 0; i < node_count; i++) { + if (nodes[i].family != family) continue; + memcpy(compact + len, nodes[i].id, 20); + memcpy(compact + len + 20, nodes[i].addr, addr_len); + tr_write_u16(compact + len + 20 + addr_len, nodes[i].port); + len += stride; + } + if (len == 0) return TRACKER_OK; + rc = put_lit(w, family == TRACKER_ADDR_IPV4 ? "5:nodes" : "6:nodes6"); + if (rc != TRACKER_OK) return rc; + return put_str(w, compact, len); +} + +int dht_write_nodes_response(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t *token, size_t token_len, + const dht_node *nodes, size_t node_count, + uint8_t *buf, size_t cap, size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!id || !nodes || !buf || !written) return TRACKER_EINVAL; + if (node_count > DHT_MAX_NODES || token_len > DHT_MAX_TOKEN) return TRACKER_ETOOBIG; + rc = write_response_prefix(&w, tx, tx_len); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "2:id"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, id, 20); + if (rc != TRACKER_OK) return rc; + rc = put_compact_nodes(&w, nodes, node_count, TRACKER_ADDR_IPV4); + if (rc != TRACKER_OK) return rc; + rc = put_compact_nodes(&w, nodes, node_count, TRACKER_ADDR_IPV6); + if (rc != TRACKER_OK) return rc; + if (token) { + rc = put_lit(&w, "5:token"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, token, token_len); + if (rc != TRACKER_OK) return rc; + } + rc = write_response_suffix(&w, tx, tx_len); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +int dht_write_peers_response(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t *token, size_t token_len, + const tracker_peer *peers, size_t peer_count, + uint8_t *buf, size_t cap, size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!id || !peers || !buf || !written) return TRACKER_EINVAL; + if (peer_count > TRACKER_MAX_PEERS || token_len > DHT_MAX_TOKEN) return TRACKER_ETOOBIG; + rc = write_response_prefix(&w, tx, tx_len); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "2:id"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, id, 20); + if (rc != TRACKER_OK) return rc; + if (token) { + rc = put_lit(&w, "5:token"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, token, token_len); + if (rc != TRACKER_OK) return rc; + } + rc = put_lit(&w, "6:valuesl"); + if (rc != TRACKER_OK) return rc; + for (size_t i = 0; i < peer_count; i++) { + uint8_t compact[18]; + size_t addr_len = peers[i].family == TRACKER_ADDR_IPV4 ? 4u : 16u; + size_t stride = addr_len + 2u; + if (peers[i].family != TRACKER_ADDR_IPV4 && + peers[i].family != TRACKER_ADDR_IPV6) { + return TRACKER_EINVAL; + } + memcpy(compact, peers[i].addr, addr_len); + tr_write_u16(compact + addr_len, peers[i].port); + rc = put_str(&w, compact, stride); + if (rc != TRACKER_OK) return rc; + } + rc = tr_putc(&w, 'e'); + if (rc != TRACKER_OK) return rc; + rc = write_response_suffix(&w, tx, tx_len); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +int dht_write_error(const uint8_t *tx, size_t tx_len, int code, + const char *message, + uint8_t *buf, size_t cap, size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!valid_tx(tx, tx_len) || !message || !buf || !written) return TRACKER_EINVAL; + rc = put_lit(&w, "d1:el"); + if (rc != TRACKER_OK) return rc; + rc = put_int(&w, code); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, message, strlen(message)); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "e1:t"); + if (rc != TRACKER_OK) return rc; + rc = put_str(&w, tx, tx_len); + if (rc != TRACKER_OK) return rc; + rc = put_lit(&w, "1:y1:ee"); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +typedef struct { + const uint8_t *buf; + size_t len; +} dht_bview; + +static int bstr(dht_bview v, size_t *pos, const uint8_t **s, size_t *n) +{ + size_t p = *pos; + size_t value = 0; + if (p >= v.len || !isdigit(v.buf[p])) return TRACKER_EPARSE; + while (p < v.len && isdigit(v.buf[p])) { + value = value * 10u + (size_t)(v.buf[p] - '0'); + p++; + } + if (p >= v.len || v.buf[p] != ':') return TRACKER_EPARSE; + p++; + if (value > v.len - p) return TRACKER_ETOOSMALL; + *s = v.buf + p; + *n = value; + *pos = p + value; + return TRACKER_OK; +} + +static int bint(dht_bview v, size_t *pos, int64_t *out) +{ + char tmp[32]; + size_t p = *pos; + size_t start; + size_t n; + char *end = NULL; + if (p >= v.len || v.buf[p++] != 'i') return TRACKER_EPARSE; + start = p; + while (p < v.len && v.buf[p] != 'e') p++; + if (p >= v.len) return TRACKER_EPARSE; + n = p - start; + if (n == 0 || n >= sizeof(tmp)) return TRACKER_EPARSE; + memcpy(tmp, v.buf + start, n); + tmp[n] = '\0'; + *out = strtoll(tmp, &end, 10); + if (!end || *end != '\0') return TRACKER_EPARSE; + *pos = p + 1u; + return TRACKER_OK; +} + +static int bskip(dht_bview v, size_t *pos) +{ + if (*pos >= v.len) return TRACKER_EPARSE; + if (v.buf[*pos] == 'i') { + int64_t ignored; + return bint(v, pos, &ignored); + } + if (isdigit(v.buf[*pos])) { + const uint8_t *s; + size_t n; + return bstr(v, pos, &s, &n); + } + if (v.buf[*pos] == 'l') { + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + int rc = bskip(v, pos); + if (rc != TRACKER_OK) return rc; + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; + } + if (v.buf[*pos] == 'd') { + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + const uint8_t *key; + size_t key_len; + int rc = bstr(v, pos, &key, &key_len); + if (rc != TRACKER_OK) return rc; + rc = bskip(v, pos); + if (rc != TRACKER_OK) return rc; + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; + } + return TRACKER_EPARSE; +} + +static int parse_want(dht_bview v, size_t *pos, dht_message *out) +{ + if (*pos >= v.len || v.buf[*pos] != 'l') return TRACKER_EPARSE; + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + const uint8_t *s; + size_t n; + int rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK) return rc; + if (n == 2 && memcmp(s, "n4", 2) == 0) out->want_ipv4 = 1; + if (n == 2 && memcmp(s, "n6", 2) == 0) out->want_ipv6 = 1; + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; +} + +static int parse_nodes(const uint8_t *s, size_t n, tracker_addr_family family, + dht_message *out) +{ + size_t stride = family == TRACKER_ADDR_IPV4 ? 26u : 38u; + size_t addr_len = family == TRACKER_ADDR_IPV4 ? 4u : 16u; + if (n % stride != 0) return TRACKER_EPARSE; + for (size_t off = 0; off < n; off += stride) { + if (out->node_count >= DHT_MAX_NODES) return TRACKER_ENOSPC; + dht_node *node = &out->nodes[out->node_count++]; + memset(node, 0, sizeof(*node)); + memcpy(node->id, s + off, 20); + node->family = (uint8_t)family; + memcpy(node->addr, s + off + 20, addr_len); + node->port = tr_read_u16(s + off + 20 + addr_len); + } + return TRACKER_OK; +} + +static int parse_values(dht_bview v, size_t *pos, dht_message *out) +{ + if (*pos >= v.len || v.buf[*pos] != 'l') return TRACKER_EPARSE; + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + const uint8_t *s; + size_t n; + int rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK) return rc; + if (out->peer_count >= TRACKER_MAX_PEERS) return TRACKER_ENOSPC; + tracker_peer *peer = &out->peers[out->peer_count++]; + memset(peer, 0, sizeof(*peer)); + if (n == 6) { + peer->family = TRACKER_ADDR_IPV4; + memcpy(peer->addr, s, 4); + peer->port = tr_read_u16(s + 4); + } else if (n == 18) { + peer->family = TRACKER_ADDR_IPV6; + memcpy(peer->addr, s, 16); + peer->port = tr_read_u16(s + 16); + } else { + return TRACKER_EPARSE; + } + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; +} + +static int parse_args(dht_bview v, size_t *pos, dht_message *out) +{ + if (*pos >= v.len || v.buf[*pos] != 'd') return TRACKER_EPARSE; + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + const uint8_t *key; + const uint8_t *s; + size_t key_len; + size_t n; + int64_t iv; + int rc = bstr(v, pos, &key, &key_len); + if (rc != TRACKER_OK) return rc; + if (key_len == 2 && memcmp(key, "id", 2) == 0) { + rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK || n != 20) return TRACKER_EPARSE; + memcpy(out->id, s, 20); + } else if (key_len == 6 && memcmp(key, "target", 6) == 0) { + rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK || n != 20) return TRACKER_EPARSE; + memcpy(out->target, s, 20); + } else if (key_len == 9 && memcmp(key, "info_hash", 9) == 0) { + rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK || n != 20) return TRACKER_EPARSE; + memcpy(out->info_hash, s, 20); + } else if (key_len == 4 && memcmp(key, "port", 4) == 0) { + rc = bint(v, pos, &iv); + if (rc != TRACKER_OK || iv < 0 || iv > 65535) return TRACKER_EPARSE; + out->port = (uint16_t)iv; + } else if (key_len == 12 && memcmp(key, "implied_port", 12) == 0) { + rc = bint(v, pos, &iv); + if (rc != TRACKER_OK) return rc; + out->implied_port = iv != 0; + } else if (key_len == 5 && memcmp(key, "token", 5) == 0) { + rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK || n > DHT_MAX_TOKEN) return TRACKER_EPARSE; + memcpy(out->token, s, n); + out->token_len = n; + } else if (key_len == 4 && memcmp(key, "want", 4) == 0) { + rc = parse_want(v, pos, out); + if (rc != TRACKER_OK) return rc; + } else { + rc = bskip(v, pos); + if (rc != TRACKER_OK) return rc; + } + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; +} + +static int parse_response(dht_bview v, size_t *pos, dht_message *out) +{ + if (*pos >= v.len || v.buf[*pos] != 'd') return TRACKER_EPARSE; + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + const uint8_t *key; + const uint8_t *s; + size_t key_len; + size_t n; + int rc = bstr(v, pos, &key, &key_len); + if (rc != TRACKER_OK) return rc; + if (key_len == 2 && memcmp(key, "id", 2) == 0) { + rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK || n != 20) return TRACKER_EPARSE; + memcpy(out->id, s, 20); + } else if (key_len == 5 && memcmp(key, "nodes", 5) == 0) { + rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK) return rc; + rc = parse_nodes(s, n, TRACKER_ADDR_IPV4, out); + if (rc != TRACKER_OK) return rc; + } else if (key_len == 6 && memcmp(key, "nodes6", 6) == 0) { + rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK) return rc; + rc = parse_nodes(s, n, TRACKER_ADDR_IPV6, out); + if (rc != TRACKER_OK) return rc; + } else if (key_len == 5 && memcmp(key, "token", 5) == 0) { + rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK || n > DHT_MAX_TOKEN) return TRACKER_EPARSE; + memcpy(out->token, s, n); + out->token_len = n; + } else if (key_len == 6 && memcmp(key, "values", 6) == 0) { + rc = parse_values(v, pos, out); + if (rc != TRACKER_OK) return rc; + } else { + rc = bskip(v, pos); + if (rc != TRACKER_OK) return rc; + } + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; +} + +static int parse_error(dht_bview v, size_t *pos, dht_message *out) +{ + const uint8_t *s; + size_t n; + int64_t code; + int rc; + if (*pos >= v.len || v.buf[*pos] != 'l') return TRACKER_EPARSE; + (*pos)++; + rc = bint(v, pos, &code); + if (rc != TRACKER_OK) return rc; + rc = bstr(v, pos, &s, &n); + if (rc != TRACKER_OK) return rc; + if (n >= sizeof(out->error_message)) n = sizeof(out->error_message) - 1u; + out->error_code = (int)code; + memcpy(out->error_message, s, n); + out->error_message[n] = '\0'; + if (*pos >= v.len || v.buf[*pos] != 'e') return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; +} + +static dht_query_type query_type(const uint8_t *s, size_t n) +{ + if (n == 4 && memcmp(s, "ping", 4) == 0) return DHT_QUERY_PING; + if (n == 9 && memcmp(s, "find_node", 9) == 0) return DHT_QUERY_FIND_NODE; + if (n == 9 && memcmp(s, "get_peers", 9) == 0) return DHT_QUERY_GET_PEERS; + if (n == 13 && memcmp(s, "announce_peer", 13) == 0) return DHT_QUERY_ANNOUNCE_PEER; + return DHT_QUERY_NONE; +} + +int dht_parse_message(const uint8_t *packet, size_t len, dht_message *out) +{ + dht_bview v = {packet, len}; + size_t pos = 0; + if (!packet || !out) return TRACKER_EINVAL; + memset(out, 0, sizeof(*out)); + if (pos >= v.len || v.buf[pos++] != 'd') return TRACKER_EPARSE; + while (pos < v.len && v.buf[pos] != 'e') { + const uint8_t *key; + const uint8_t *s; + size_t key_len; + size_t n; + int rc = bstr(v, &pos, &key, &key_len); + if (rc != TRACKER_OK) return rc; + if (key_len == 1 && key[0] == 't') { + rc = bstr(v, &pos, &s, &n); + if (rc != TRACKER_OK || n > DHT_MAX_TRANSACTION) return TRACKER_EPARSE; + memcpy(out->transaction, s, n); + out->transaction_len = n; + } else if (key_len == 1 && key[0] == 'y') { + rc = bstr(v, &pos, &s, &n); + if (rc != TRACKER_OK || n != 1) return TRACKER_EPARSE; + if (s[0] == 'q') out->type = DHT_MSG_QUERY; + else if (s[0] == 'r') out->type = DHT_MSG_RESPONSE; + else if (s[0] == 'e') out->type = DHT_MSG_ERROR; + else return TRACKER_EPARSE; + } else if (key_len == 1 && key[0] == 'q') { + rc = bstr(v, &pos, &s, &n); + if (rc != TRACKER_OK) return rc; + out->query = query_type(s, n); + } else if (key_len == 1 && key[0] == 'a') { + rc = parse_args(v, &pos, out); + if (rc != TRACKER_OK) return rc; + } else if (key_len == 1 && key[0] == 'r') { + rc = parse_response(v, &pos, out); + if (rc != TRACKER_OK) return rc; + } else if (key_len == 1 && key[0] == 'e') { + rc = parse_error(v, &pos, out); + if (rc != TRACKER_OK) return rc; + } else { + rc = bskip(v, &pos); + if (rc != TRACKER_OK) return rc; + } + } + if (pos >= v.len || out->transaction_len == 0 || out->type == 0) { + return TRACKER_EPARSE; + } + return TRACKER_OK; +} diff --git a/src/tracker_http.c b/src/tracker_http.c new file mode 100644 index 0000000..3c6a028 --- /dev/null +++ b/src/tracker_http.c @@ -0,0 +1,897 @@ +#include "tracker.h" +#include "tracker_internal.h" + +#include +#include +#include +#include +#include + +static int hexval(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static int url_decode(const char *src, size_t n, uint8_t *dst, size_t cap, + size_t *out_len) +{ + size_t j = 0; + for (size_t i = 0; i < n; i++) { + uint8_t c = (uint8_t)src[i]; + if (c == '%') { + if (i + 2 >= n) return TRACKER_EPARSE; + int hi = hexval(src[i + 1]); + int lo = hexval(src[i + 2]); + if (hi < 0 || lo < 0) return TRACKER_EPARSE; + c = (uint8_t)((hi << 4) | lo); + i += 2; + } + if (j >= cap) return TRACKER_ENOSPC; + dst[j++] = c; + } + *out_len = j; + return TRACKER_OK; +} + +static int decode_text(const char *src, size_t n, char *dst, size_t cap) +{ + size_t len = 0; + int rc = url_decode(src, n, (uint8_t *)dst, cap ? cap - 1 : 0, &len); + if (rc != TRACKER_OK) return rc; + dst[len] = '\0'; + return TRACKER_OK; +} + +static int parse_u64(const char *src, size_t n, uint64_t *out) +{ + char tmp[32]; + char *end = NULL; + if (n == 0 || n >= sizeof(tmp)) return TRACKER_EPARSE; + memcpy(tmp, src, n); + tmp[n] = '\0'; + for (size_t i = 0; i < n; i++) { + if (!isdigit((unsigned char)tmp[i])) return TRACKER_EPARSE; + } + *out = strtoull(tmp, &end, 10); + return (end && *end == '\0') ? TRACKER_OK : TRACKER_EPARSE; +} + +static int parse_i32(const char *src, size_t n, int32_t *out) +{ + char tmp[24]; + char *end = NULL; + long v; + if (n == 0 || n >= sizeof(tmp)) return TRACKER_EPARSE; + memcpy(tmp, src, n); + tmp[n] = '\0'; + v = strtol(tmp, &end, 10); + if (!end || *end != '\0') return TRACKER_EPARSE; + *out = (int32_t)v; + return TRACKER_OK; +} + +static int parse_event(const char *src, size_t n, tracker_event *event) +{ + if (n == 0) { + *event = TRACKER_EVENT_NONE; + } else if (n == 7 && memcmp(src, "started", 7) == 0) { + *event = TRACKER_EVENT_STARTED; + } else if (n == 9 && memcmp(src, "completed", 9) == 0) { + *event = TRACKER_EVENT_COMPLETED; + } else if (n == 7 && memcmp(src, "stopped", 7) == 0) { + *event = TRACKER_EVENT_STOPPED; + } else { + return TRACKER_EPARSE; + } + return TRACKER_OK; +} + +static const char *query_start(const char *query) +{ + const char *q = strchr(query, '?'); + return q ? q + 1 : query; +} + +static int next_pair(const char **cursor, const char **k, size_t *kn, + const char **v, size_t *vn) +{ + const char *p = *cursor; + const char *amp; + const char *eq; + if (!p || *p == '\0') return 0; + amp = strchr(p, '&'); + if (!amp) amp = p + strlen(p); + eq = memchr(p, '=', (size_t)(amp - p)); + if (eq) { + *k = p; + *kn = (size_t)(eq - p); + *v = eq + 1; + *vn = (size_t)(amp - eq - 1); + } else { + *k = p; + *kn = (size_t)(amp - p); + *v = amp; + *vn = 0; + } + *cursor = (*amp == '&') ? amp + 1 : amp; + return 1; +} + +int tracker_http_parse_announce_query(const char *query, + tracker_announce_request *out) +{ + const char *p; + int have_info_hash = 0; + int have_peer_id = 0; + int have_port = 0; + int have_uploaded = 0; + int have_downloaded = 0; + int have_left = 0; + + if (!query || !out) return TRACKER_EINVAL; + memset(out, 0, sizeof(*out)); + out->numwant = -1; + p = query_start(query); + + while (*p) { + const char *k; + const char *v; + size_t kn; + size_t vn; + uint8_t decoded[256]; + size_t decoded_len = 0; + int rc; + if (!next_pair(&p, &k, &kn, &v, &vn)) break; + + if (kn == 9 && memcmp(k, "info_hash", 9) == 0) { + rc = url_decode(v, vn, decoded, sizeof(decoded), &decoded_len); + if (rc != TRACKER_OK || decoded_len != 20) return TRACKER_EPARSE; + memcpy(out->info_hash, decoded, 20); + have_info_hash = 1; + } else if (kn == 7 && memcmp(k, "peer_id", 7) == 0) { + rc = url_decode(v, vn, decoded, sizeof(decoded), &decoded_len); + if (rc != TRACKER_OK || decoded_len != 20) return TRACKER_EPARSE; + memcpy(out->peer_id, decoded, 20); + have_peer_id = 1; + } else if (kn == 4 && memcmp(k, "port", 4) == 0) { + uint64_t value; + rc = parse_u64(v, vn, &value); + if (rc != TRACKER_OK || value > 65535) return TRACKER_EPARSE; + out->port = (uint16_t)value; + have_port = 1; + } else if (kn == 8 && memcmp(k, "uploaded", 8) == 0) { + rc = parse_u64(v, vn, &out->uploaded); + if (rc != TRACKER_OK) return rc; + have_uploaded = 1; + } else if (kn == 10 && memcmp(k, "downloaded", 10) == 0) { + rc = parse_u64(v, vn, &out->downloaded); + if (rc != TRACKER_OK) return rc; + have_downloaded = 1; + } else if (kn == 4 && memcmp(k, "left", 4) == 0) { + rc = parse_u64(v, vn, &out->left); + if (rc != TRACKER_OK) return rc; + have_left = 1; + } else if (kn == 7 && memcmp(k, "compact", 7) == 0) { + int32_t value; + rc = parse_i32(v, vn, &value); + if (rc != TRACKER_OK) return rc; + out->compact = value != 0; + } else if (kn == 10 && memcmp(k, "no_peer_id", 10) == 0) { + int32_t value; + rc = parse_i32(v, vn, &value); + if (rc != TRACKER_OK) return rc; + out->no_peer_id = value != 0; + } else if (kn == 5 && memcmp(k, "event", 5) == 0) { + rc = parse_event(v, vn, &out->event); + if (rc != TRACKER_OK) return rc; + } else if (kn == 7 && memcmp(k, "numwant", 7) == 0) { + rc = parse_i32(v, vn, &out->numwant); + if (rc != TRACKER_OK) return rc; + } else if (kn == 3 && memcmp(k, "key", 3) == 0) { + uint64_t value; + rc = parse_u64(v, vn, &value); + if (rc != TRACKER_OK || value > UINT32_MAX) return TRACKER_EPARSE; + out->key = (uint32_t)value; + out->has_key = 1; + } else if (kn == 2 && memcmp(k, "ip", 2) == 0) { + rc = decode_text(v, vn, out->ip, sizeof(out->ip)); + if (rc != TRACKER_OK) return rc; + } else if (kn == 9 && memcmp(k, "trackerid", 9) == 0) { + rc = decode_text(v, vn, out->tracker_id, sizeof(out->tracker_id)); + if (rc != TRACKER_OK) return rc; + } + } + + return (have_info_hash && have_peer_id && have_port && have_uploaded && + have_downloaded && have_left) + ? TRACKER_OK + : TRACKER_EPARSE; +} + +int tracker_http_parse_scrape_query(const char *query, + uint8_t hashes[][20], size_t max_hashes, + size_t *hash_count) +{ + const char *p; + size_t count = 0; + if (!query || !hashes || !hash_count) return TRACKER_EINVAL; + p = query_start(query); + while (*p) { + const char *k; + const char *v; + size_t kn; + size_t vn; + uint8_t decoded[20]; + size_t decoded_len = 0; + int rc; + if (!next_pair(&p, &k, &kn, &v, &vn)) break; + if (kn != 9 || memcmp(k, "info_hash", 9) != 0) continue; + if (count == max_hashes) return TRACKER_ETOOBIG; + rc = url_decode(v, vn, decoded, sizeof(decoded), &decoded_len); + if (rc != TRACKER_OK || decoded_len != 20) return TRACKER_EPARSE; + memcpy(hashes[count++], decoded, 20); + } + *hash_count = count; + return count ? TRACKER_OK : TRACKER_EPARSE; +} + +static int bw_raw(tr_writer *w, const void *src, size_t n) +{ + return tr_put(w, src, n); +} + +static int bw_text(tr_writer *w, const char *s) +{ + return bw_raw(w, s, strlen(s)); +} + +static int bw_uint(tr_writer *w, uint64_t v) +{ + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)v); + if (n < 0 || (size_t)n >= sizeof(tmp)) return TRACKER_EPARSE; + return bw_raw(w, tmp, (size_t)n); +} + +static int bw_int_field(tr_writer *w, const char *key, uint64_t value) +{ + int rc = bw_text(w, key); + if (rc != TRACKER_OK) return rc; + rc = tr_putc(w, 'i'); + if (rc != TRACKER_OK) return rc; + rc = bw_uint(w, value); + if (rc != TRACKER_OK) return rc; + return tr_putc(w, 'e'); +} + +static int bw_string_field(tr_writer *w, const char *key, const void *s, + size_t n) +{ + int rc = bw_text(w, key); + if (rc != TRACKER_OK) return rc; + rc = bw_uint(w, n); + if (rc != TRACKER_OK) return rc; + rc = tr_putc(w, ':'); + if (rc != TRACKER_OK) return rc; + return bw_raw(w, s, n); +} + +static size_t compact_len(const tracker_announce_response *resp, + tracker_addr_family family) +{ + size_t n = 0; + for (size_t i = 0; i < resp->peer_count; i++) { + if (resp->peers[i].family == family) n++; + } + return n * (family == TRACKER_ADDR_IPV4 ? 6u : 18u); +} + +static int bw_compact_peers(tr_writer *w, const tracker_announce_response *resp, + tracker_addr_family family) +{ + uint8_t tmp[TRACKER_MAX_PEERS * 18u]; + size_t len = 0; + size_t stride = family == TRACKER_ADDR_IPV4 ? 6u : 18u; + const char *key = family == TRACKER_ADDR_IPV4 ? "5:peers" : "6:peers6"; + for (size_t i = 0; i < resp->peer_count; i++) { + const tracker_peer *p = &resp->peers[i]; + if (p->family != family) continue; + if (len + stride > sizeof(tmp)) return TRACKER_ETOOBIG; + memcpy(tmp + len, p->addr, family == TRACKER_ADDR_IPV4 ? 4u : 16u); + tr_write_u16(tmp + len + stride - 2u, p->port); + len += stride; + } + if (len == 0 && family == TRACKER_ADDR_IPV6) return TRACKER_OK; + return bw_string_field(w, key, tmp, len); +} + +static int bw_peer_list(tr_writer *w, const tracker_announce_response *resp) +{ + int rc = bw_text(w, "5:peersl"); + if (rc != TRACKER_OK) return rc; + for (size_t i = 0; i < resp->peer_count; i++) { + char ip[INET6_ADDRSTRLEN]; + const tracker_peer *p = &resp->peers[i]; + const void *addr = p->family == TRACKER_ADDR_IPV4 ? (const void *)p->addr + : (const void *)p->addr; + if (!inet_ntop(p->family == TRACKER_ADDR_IPV4 ? AF_INET : AF_INET6, + addr, ip, sizeof(ip))) { + return TRACKER_EINVAL; + } + rc = tr_putc(w, 'd'); + if (rc != TRACKER_OK) return rc; + if (p->has_peer_id) { + rc = bw_string_field(w, "7:peer id", p->peer_id, 20); + if (rc != TRACKER_OK) return rc; + } + rc = bw_string_field(w, "2:ip", ip, strlen(ip)); + if (rc != TRACKER_OK) return rc; + rc = bw_int_field(w, "4:port", p->port); + if (rc != TRACKER_OK) return rc; + rc = tr_putc(w, 'e'); + if (rc != TRACKER_OK) return rc; + } + return tr_putc(w, 'e'); +} + +int tracker_http_write_announce_response(const tracker_announce_response *resp, + uint8_t *buf, size_t cap, + size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!resp || !buf || !written) return TRACKER_EINVAL; + if (resp->peer_count > TRACKER_MAX_PEERS) return TRACKER_ETOOBIG; + + rc = tr_putc(&w, 'd'); + if (rc != TRACKER_OK) return rc; + rc = bw_int_field(&w, "8:complete", resp->complete); + if (rc != TRACKER_OK) return rc; + rc = bw_int_field(&w, "10:incomplete", resp->incomplete); + if (rc != TRACKER_OK) return rc; + rc = bw_int_field(&w, "8:interval", resp->interval); + if (rc != TRACKER_OK) return rc; + if (resp->min_interval) { + rc = bw_int_field(&w, "12:min interval", resp->min_interval); + if (rc != TRACKER_OK) return rc; + } + if (resp->compact) { + rc = bw_compact_peers(&w, resp, TRACKER_ADDR_IPV4); + if (rc != TRACKER_OK) return rc; + if (compact_len(resp, TRACKER_ADDR_IPV6)) { + rc = bw_compact_peers(&w, resp, TRACKER_ADDR_IPV6); + if (rc != TRACKER_OK) return rc; + } + } else { + rc = bw_peer_list(&w, resp); + if (rc != TRACKER_OK) return rc; + } + if (resp->tracker_id) { + rc = bw_string_field(&w, "10:tracker id", resp->tracker_id, + strlen(resp->tracker_id)); + if (rc != TRACKER_OK) return rc; + } + rc = tr_putc(&w, 'e'); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +int tracker_http_write_scrape_response(const tracker_scrape_response *resp, + uint8_t *buf, size_t cap, + size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!resp || !buf || !written) return TRACKER_EINVAL; + rc = bw_text(&w, "d5:filesd"); + if (rc != TRACKER_OK) return rc; + for (size_t i = 0; i < resp->file_count; i++) { + const tracker_scrape_file *f = &resp->files[i]; + rc = bw_string_field(&w, "", f->info_hash, 20); + if (rc != TRACKER_OK) return rc; + rc = tr_putc(&w, 'd'); + if (rc != TRACKER_OK) return rc; + rc = bw_int_field(&w, "8:complete", f->complete); + if (rc != TRACKER_OK) return rc; + rc = bw_int_field(&w, "10:downloaded", f->downloaded); + if (rc != TRACKER_OK) return rc; + rc = bw_int_field(&w, "10:incomplete", f->incomplete); + if (rc != TRACKER_OK) return rc; + rc = tr_putc(&w, 'e'); + if (rc != TRACKER_OK) return rc; + } + rc = bw_text(&w, "ee"); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +int tracker_http_write_failure(const char *message, uint8_t *buf, size_t cap, + size_t *written) +{ + tr_writer w = {buf, cap, 0}; + int rc; + if (!message || !buf || !written) return TRACKER_EINVAL; + rc = tr_putc(&w, 'd'); + if (rc != TRACKER_OK) return rc; + rc = bw_string_field(&w, "14:failure reason", message, strlen(message)); + if (rc != TRACKER_OK) return rc; + rc = tr_putc(&w, 'e'); + if (rc != TRACKER_OK) return rc; + *written = w.len; + return TRACKER_OK; +} + +static int qw_put(char *buf, size_t cap, size_t *len, const char *s, size_t n) +{ + if (n > cap || *len > cap - n) return TRACKER_ENOSPC; + memcpy(buf + *len, s, n); + *len += n; + return TRACKER_OK; +} + +static int qw_puts(char *buf, size_t cap, size_t *len, const char *s) +{ + return qw_put(buf, cap, len, s, strlen(s)); +} + +static int qw_uint(char *buf, size_t cap, size_t *len, uint64_t v) +{ + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)v); + if (n < 0 || (size_t)n >= sizeof(tmp)) return TRACKER_EPARSE; + return qw_put(buf, cap, len, tmp, (size_t)n); +} + +static int url_unreserved(uint8_t c) +{ + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '_' || + c == '.' || c == '~'; +} + +static int qw_encoded(char *buf, size_t cap, size_t *len, + const uint8_t *src, size_t n) +{ + static const char hex[] = "0123456789ABCDEF"; + for (size_t i = 0; i < n; i++) { + char tmp[3]; + if (url_unreserved(src[i])) { + int rc = qw_put(buf, cap, len, (const char *)&src[i], 1); + if (rc != TRACKER_OK) return rc; + } else { + tmp[0] = '%'; + tmp[1] = hex[src[i] >> 4]; + tmp[2] = hex[src[i] & 0x0f]; + int rc = qw_put(buf, cap, len, tmp, sizeof(tmp)); + if (rc != TRACKER_OK) return rc; + } + } + return TRACKER_OK; +} + +static int qw_pair_prefix(char *buf, size_t cap, size_t *len, + const char *key, int *first) +{ + int rc; + if (!*first) { + rc = qw_puts(buf, cap, len, "&"); + if (rc != TRACKER_OK) return rc; + } + *first = 0; + rc = qw_puts(buf, cap, len, key); + if (rc != TRACKER_OK) return rc; + return qw_puts(buf, cap, len, "="); +} + +static int qw_pair_uint(char *buf, size_t cap, size_t *len, + const char *key, uint64_t value, int *first) +{ + int rc = qw_pair_prefix(buf, cap, len, key, first); + if (rc != TRACKER_OK) return rc; + return qw_uint(buf, cap, len, value); +} + +static const char *event_name(tracker_event event) +{ + switch (event) { + case TRACKER_EVENT_COMPLETED: return "completed"; + case TRACKER_EVENT_STARTED: return "started"; + case TRACKER_EVENT_STOPPED: return "stopped"; + case TRACKER_EVENT_NONE: + default: return ""; + } +} + +int tracker_http_write_announce_query(const tracker_announce_request *req, + char *buf, size_t cap, + size_t *written) +{ + size_t len = 0; + int first = 1; + int rc; + const char *ev; + if (!req || !buf || !written) return TRACKER_EINVAL; + + rc = qw_pair_prefix(buf, cap, &len, "info_hash", &first); + if (rc != TRACKER_OK) return rc; + rc = qw_encoded(buf, cap, &len, req->info_hash, 20); + if (rc != TRACKER_OK) return rc; + rc = qw_pair_prefix(buf, cap, &len, "peer_id", &first); + if (rc != TRACKER_OK) return rc; + rc = qw_encoded(buf, cap, &len, req->peer_id, 20); + if (rc != TRACKER_OK) return rc; + rc = qw_pair_uint(buf, cap, &len, "port", req->port, &first); + if (rc != TRACKER_OK) return rc; + rc = qw_pair_uint(buf, cap, &len, "uploaded", req->uploaded, &first); + if (rc != TRACKER_OK) return rc; + rc = qw_pair_uint(buf, cap, &len, "downloaded", req->downloaded, &first); + if (rc != TRACKER_OK) return rc; + rc = qw_pair_uint(buf, cap, &len, "left", req->left, &first); + if (rc != TRACKER_OK) return rc; + rc = qw_pair_uint(buf, cap, &len, "compact", req->compact ? 1u : 0u, &first); + if (rc != TRACKER_OK) return rc; + if (req->no_peer_id) { + rc = qw_pair_uint(buf, cap, &len, "no_peer_id", 1, &first); + if (rc != TRACKER_OK) return rc; + } + if (req->numwant != -1) { + rc = qw_pair_uint(buf, cap, &len, "numwant", (uint32_t)req->numwant, + &first); + if (rc != TRACKER_OK) return rc; + } + if (req->has_key) { + rc = qw_pair_uint(buf, cap, &len, "key", req->key, &first); + if (rc != TRACKER_OK) return rc; + } + ev = event_name(req->event); + if (*ev) { + rc = qw_pair_prefix(buf, cap, &len, "event", &first); + if (rc != TRACKER_OK) return rc; + rc = qw_puts(buf, cap, &len, ev); + if (rc != TRACKER_OK) return rc; + } + if (req->tracker_id[0]) { + rc = qw_pair_prefix(buf, cap, &len, "trackerid", &first); + if (rc != TRACKER_OK) return rc; + rc = qw_encoded(buf, cap, &len, (const uint8_t *)req->tracker_id, + strlen(req->tracker_id)); + if (rc != TRACKER_OK) return rc; + } + if (len < cap) buf[len] = '\0'; + *written = len; + return TRACKER_OK; +} + +int tracker_http_write_scrape_query(const uint8_t hashes[][20], + size_t hash_count, + char *buf, size_t cap, + size_t *written) +{ + size_t len = 0; + int first = 1; + int rc; + if (!hashes || !buf || !written) return TRACKER_EINVAL; + if (hash_count > TRACKER_MAX_SCRAPE) return TRACKER_ETOOBIG; + for (size_t i = 0; i < hash_count; i++) { + rc = qw_pair_prefix(buf, cap, &len, "info_hash", &first); + if (rc != TRACKER_OK) return rc; + rc = qw_encoded(buf, cap, &len, hashes[i], 20); + if (rc != TRACKER_OK) return rc; + } + if (len < cap) buf[len] = '\0'; + *written = len; + return TRACKER_OK; +} + +typedef struct { + const uint8_t *buf; + size_t len; +} bview; + +static int be_string(bview v, size_t *pos, const uint8_t **s, size_t *n) +{ + size_t p = *pos; + size_t value = 0; + if (p >= v.len || !isdigit(v.buf[p])) return TRACKER_EPARSE; + while (p < v.len && isdigit(v.buf[p])) { + value = value * 10u + (size_t)(v.buf[p] - '0'); + p++; + } + if (p >= v.len || v.buf[p] != ':') return TRACKER_EPARSE; + p++; + if (value > v.len - p) return TRACKER_ETOOSMALL; + *s = v.buf + p; + *n = value; + *pos = p + value; + return TRACKER_OK; +} + +static int be_int(bview v, size_t *pos, int64_t *out) +{ + char tmp[32]; + size_t p = *pos; + size_t start; + size_t n; + char *end = NULL; + if (p >= v.len || v.buf[p++] != 'i') return TRACKER_EPARSE; + start = p; + while (p < v.len && v.buf[p] != 'e') p++; + if (p >= v.len) return TRACKER_EPARSE; + n = p - start; + if (n == 0 || n >= sizeof(tmp)) return TRACKER_EPARSE; + memcpy(tmp, v.buf + start, n); + tmp[n] = '\0'; + *out = strtoll(tmp, &end, 10); + if (!end || *end != '\0') return TRACKER_EPARSE; + *pos = p + 1; + return TRACKER_OK; +} + +static int be_skip(bview v, size_t *pos); + +static int be_skip_list_or_dict(bview v, size_t *pos) +{ + uint8_t end = v.buf[*pos] == 'l' ? 'e' : 'e'; + (void)end; + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + int rc; + if (v.buf[*pos] != 'd') { + rc = be_skip(v, pos); + } else { + rc = be_skip(v, pos); + } + if (rc != TRACKER_OK) return rc; + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; +} + +static int be_skip(bview v, size_t *pos) +{ + if (*pos >= v.len) return TRACKER_EPARSE; + if (v.buf[*pos] == 'i') { + int64_t ignored; + return be_int(v, pos, &ignored); + } + if (v.buf[*pos] == 'l' || v.buf[*pos] == 'd') { + return be_skip_list_or_dict(v, pos); + } + if (isdigit(v.buf[*pos])) { + const uint8_t *s; + size_t n; + return be_string(v, pos, &s, &n); + } + return TRACKER_EPARSE; +} + +static int add_compact_peers(const uint8_t *s, size_t n, tracker_addr_family family, + tracker_peer *out, size_t cap, size_t *count) +{ + size_t stride = family == TRACKER_ADDR_IPV4 ? 6u : 18u; + size_t addr_len = family == TRACKER_ADDR_IPV4 ? 4u : 16u; + if (n % stride != 0) return TRACKER_EPARSE; + for (size_t off = 0; off < n; off += stride) { + if (*count >= cap) return TRACKER_ENOSPC; + memset(&out[*count], 0, sizeof(out[*count])); + out[*count].family = (uint8_t)family; + memcpy(out[*count].addr, s + off, addr_len); + out[*count].port = tr_read_u16(s + off + addr_len); + (*count)++; + } + return TRACKER_OK; +} + +static int parse_peer_dict(bview v, size_t *pos, tracker_peer *peer) +{ + memset(peer, 0, sizeof(*peer)); + if (*pos >= v.len || v.buf[*pos] != 'd') return TRACKER_EPARSE; + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + const uint8_t *key; + const uint8_t *s; + size_t key_len; + size_t n; + int64_t iv; + int rc = be_string(v, pos, &key, &key_len); + if (rc != TRACKER_OK) return rc; + if (key_len == 2 && memcmp(key, "ip", 2) == 0) { + char ip[64]; + rc = be_string(v, pos, &s, &n); + if (rc != TRACKER_OK) return rc; + if (n >= sizeof(ip)) return TRACKER_EPARSE; + memcpy(ip, s, n); + ip[n] = '\0'; + if (strchr(ip, ':')) { + peer->family = TRACKER_ADDR_IPV6; + if (inet_pton(AF_INET6, ip, peer->addr) != 1) return TRACKER_EPARSE; + } else { + peer->family = TRACKER_ADDR_IPV4; + if (inet_pton(AF_INET, ip, peer->addr) != 1) return TRACKER_EPARSE; + } + } else if (key_len == 4 && memcmp(key, "port", 4) == 0) { + rc = be_int(v, pos, &iv); + if (rc != TRACKER_OK || iv < 0 || iv > 65535) return TRACKER_EPARSE; + peer->port = (uint16_t)iv; + } else if (key_len == 7 && memcmp(key, "peer id", 7) == 0) { + rc = be_string(v, pos, &s, &n); + if (rc != TRACKER_OK || n != 20) return TRACKER_EPARSE; + memcpy(peer->peer_id, s, 20); + peer->has_peer_id = 1; + } else { + rc = be_skip(v, pos); + if (rc != TRACKER_OK) return rc; + } + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return peer->family && peer->port ? TRACKER_OK : TRACKER_EPARSE; +} + +static int parse_peer_list(bview v, size_t *pos, tracker_peer *out, + size_t cap, size_t *count) +{ + if (*pos >= v.len || v.buf[*pos] != 'l') return TRACKER_EPARSE; + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + if (*count >= cap) return TRACKER_ENOSPC; + int rc = parse_peer_dict(v, pos, &out[*count]); + if (rc != TRACKER_OK) return rc; + (*count)++; + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; +} + +int tracker_http_parse_announce_response(const uint8_t *buf, size_t len, + tracker_peer *out_peers, + size_t out_peer_cap, + tracker_announce_response *resp) +{ + bview v = {buf, len}; + size_t pos = 0; + size_t peer_count = 0; + if (!buf || !resp || (out_peer_cap && !out_peers)) return TRACKER_EINVAL; + memset(resp, 0, sizeof(*resp)); + if (pos >= v.len || v.buf[pos++] != 'd') return TRACKER_EPARSE; + while (pos < v.len && v.buf[pos] != 'e') { + const uint8_t *key; + const uint8_t *s; + size_t key_len; + size_t n; + int64_t iv; + int rc = be_string(v, &pos, &key, &key_len); + if (rc != TRACKER_OK) return rc; + if (key_len == 14 && memcmp(key, "failure reason", 14) == 0) { + return TRACKER_EPARSE; + } else if (key_len == 8 && memcmp(key, "interval", 8) == 0) { + rc = be_int(v, &pos, &iv); + if (rc != TRACKER_OK || iv < 0) return TRACKER_EPARSE; + resp->interval = (uint32_t)iv; + } else if (key_len == 12 && memcmp(key, "min interval", 12) == 0) { + rc = be_int(v, &pos, &iv); + if (rc != TRACKER_OK || iv < 0) return TRACKER_EPARSE; + resp->min_interval = (uint32_t)iv; + } else if (key_len == 8 && memcmp(key, "complete", 8) == 0) { + rc = be_int(v, &pos, &iv); + if (rc != TRACKER_OK || iv < 0) return TRACKER_EPARSE; + resp->complete = (uint32_t)iv; + } else if (key_len == 10 && memcmp(key, "incomplete", 10) == 0) { + rc = be_int(v, &pos, &iv); + if (rc != TRACKER_OK || iv < 0) return TRACKER_EPARSE; + resp->incomplete = (uint32_t)iv; + } else if (key_len == 5 && memcmp(key, "peers", 5) == 0) { + if (pos < v.len && isdigit(v.buf[pos])) { + rc = be_string(v, &pos, &s, &n); + if (rc != TRACKER_OK) return rc; + rc = add_compact_peers(s, n, TRACKER_ADDR_IPV4, out_peers, + out_peer_cap, &peer_count); + } else { + rc = parse_peer_list(v, &pos, out_peers, out_peer_cap, + &peer_count); + } + if (rc != TRACKER_OK) return rc; + resp->compact = 1; + } else if (key_len == 6 && memcmp(key, "peers6", 6) == 0) { + rc = be_string(v, &pos, &s, &n); + if (rc != TRACKER_OK) return rc; + rc = add_compact_peers(s, n, TRACKER_ADDR_IPV6, out_peers, + out_peer_cap, &peer_count); + if (rc != TRACKER_OK) return rc; + resp->compact = 1; + } else { + rc = be_skip(v, &pos); + if (rc != TRACKER_OK) return rc; + } + } + if (pos >= v.len) return TRACKER_EPARSE; + resp->peers = out_peers; + resp->peer_count = peer_count; + return TRACKER_OK; +} + +static int parse_scrape_file(bview v, size_t *pos, tracker_scrape_file *file) +{ + if (*pos >= v.len || v.buf[*pos] != 'd') return TRACKER_EPARSE; + (*pos)++; + while (*pos < v.len && v.buf[*pos] != 'e') { + const uint8_t *key; + size_t key_len; + int64_t iv; + int rc = be_string(v, pos, &key, &key_len); + if (rc != TRACKER_OK) return rc; + if (key_len == 8 && memcmp(key, "complete", 8) == 0) { + rc = be_int(v, pos, &iv); + if (rc != TRACKER_OK || iv < 0) return TRACKER_EPARSE; + file->complete = (uint32_t)iv; + } else if (key_len == 10 && memcmp(key, "downloaded", 10) == 0) { + rc = be_int(v, pos, &iv); + if (rc != TRACKER_OK || iv < 0) return TRACKER_EPARSE; + file->downloaded = (uint32_t)iv; + } else if (key_len == 10 && memcmp(key, "incomplete", 10) == 0) { + rc = be_int(v, pos, &iv); + if (rc != TRACKER_OK || iv < 0) return TRACKER_EPARSE; + file->incomplete = (uint32_t)iv; + } else { + rc = be_skip(v, pos); + if (rc != TRACKER_OK) return rc; + } + } + if (*pos >= v.len) return TRACKER_EPARSE; + (*pos)++; + return TRACKER_OK; +} + +int tracker_http_parse_scrape_response(const uint8_t *buf, size_t len, + tracker_scrape_file *out_files, + size_t out_file_cap, + tracker_scrape_response *resp) +{ + bview v = {buf, len}; + size_t pos = 0; + size_t count = 0; + int found_files = 0; + if (!buf || !out_files || !resp) return TRACKER_EINVAL; + memset(resp, 0, sizeof(*resp)); + if (pos >= v.len || v.buf[pos++] != 'd') return TRACKER_EPARSE; + while (pos < v.len && v.buf[pos] != 'e') { + const uint8_t *key; + size_t key_len; + int rc = be_string(v, &pos, &key, &key_len); + if (rc != TRACKER_OK) return rc; + if (key_len == 14 && memcmp(key, "failure reason", 14) == 0) { + return TRACKER_EPARSE; + } else if (key_len == 5 && memcmp(key, "files", 5) == 0) { + found_files = 1; + if (pos >= v.len || v.buf[pos++] != 'd') return TRACKER_EPARSE; + while (pos < v.len && v.buf[pos] != 'e') { + const uint8_t *hash; + size_t hash_len; + if (count >= out_file_cap) return TRACKER_ENOSPC; + rc = be_string(v, &pos, &hash, &hash_len); + if (rc != TRACKER_OK || hash_len != 20) return TRACKER_EPARSE; + memset(&out_files[count], 0, sizeof(out_files[count])); + memcpy(out_files[count].info_hash, hash, 20); + rc = parse_scrape_file(v, &pos, &out_files[count]); + if (rc != TRACKER_OK) return rc; + count++; + } + if (pos >= v.len) return TRACKER_EPARSE; + pos++; + } else { + rc = be_skip(v, &pos); + if (rc != TRACKER_OK) return rc; + } + } + if (!found_files) return TRACKER_EPARSE; + resp->files = out_files; + resp->file_count = count; + return TRACKER_OK; +} diff --git a/src/tracker_internal.h b/src/tracker_internal.h new file mode 100644 index 0000000..7ac041b --- /dev/null +++ b/src/tracker_internal.h @@ -0,0 +1,63 @@ +#ifndef TORRENT_TRACKER_INTERNAL_H +#define TORRENT_TRACKER_INTERNAL_H + +#include +#include +#include + +static inline uint16_t tr_read_u16(const uint8_t *p) +{ + return (uint16_t)(((uint16_t)p[0] << 8) | p[1]); +} + +static inline uint32_t tr_read_u32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +static inline uint64_t tr_read_u64(const uint8_t *p) +{ + return ((uint64_t)tr_read_u32(p) << 32) | tr_read_u32(p + 4); +} + +static inline void tr_write_u16(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)(v >> 8); + p[1] = (uint8_t)v; +} + +static inline void tr_write_u32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)(v >> 24); + p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); + p[3] = (uint8_t)v; +} + +static inline void tr_write_u64(uint8_t *p, uint64_t v) +{ + tr_write_u32(p, (uint32_t)(v >> 32)); + tr_write_u32(p + 4, (uint32_t)v); +} + +typedef struct { + uint8_t *buf; + size_t cap; + size_t len; +} tr_writer; + +static inline int tr_put(tr_writer *w, const void *src, size_t n) +{ + if (n > w->cap || w->len > w->cap - n) return TRACKER_ENOSPC; + memcpy(w->buf + w->len, src, n); + w->len += n; + return TRACKER_OK; +} + +static inline int tr_putc(tr_writer *w, char c) +{ + return tr_put(w, &c, 1); +} + +#endif /* TORRENT_TRACKER_INTERNAL_H */ diff --git a/src/tracker_store.c b/src/tracker_store.c new file mode 100644 index 0000000..3beabe3 --- /dev/null +++ b/src/tracker_store.c @@ -0,0 +1,383 @@ +#include "tracker.h" + +#include +#include + +#define DEFAULT_INTERVAL 1800u +#define DEFAULT_MIN_INTERVAL 300u +#define DEFAULT_PEER_TIMEOUT 3600u +#define DEFAULT_NUMWANT 50u +#define DEFAULT_MAX_NUMWANT 200u +#define DEFAULT_SEED UINT64_C(0x9e3779b97f4a7c15) + +typedef struct { + tracker_peer peer; + uint8_t info_hash[20]; + uint64_t uploaded; + uint64_t downloaded; + uint64_t left; + uint64_t last_announce; + uint32_t key; + uint8_t has_key; + uint8_t completed_reported; +} store_peer; + +typedef struct { + uint8_t info_hash[20]; + uint32_t complete; + uint32_t incomplete; + uint32_t downloaded; +} store_swarm; + +struct tracker_store { + tracker_store_config cfg; + store_peer *peers; + size_t peer_count; + size_t peer_cap; + store_swarm *swarms; + size_t swarm_count; + size_t swarm_cap; + uint64_t rng; +}; + +static tracker_store_config normalize_config(const tracker_store_config *cfg) +{ + tracker_store_config out; + memset(&out, 0, sizeof(out)); + if (cfg) out = *cfg; + if (!out.interval) out.interval = DEFAULT_INTERVAL; + if (!out.min_interval) out.min_interval = DEFAULT_MIN_INTERVAL; + if (!out.peer_timeout) out.peer_timeout = DEFAULT_PEER_TIMEOUT; + if (!out.default_numwant) out.default_numwant = DEFAULT_NUMWANT; + if (!out.max_numwant) out.max_numwant = DEFAULT_MAX_NUMWANT; + if (out.default_numwant > out.max_numwant) out.default_numwant = out.max_numwant; + if (!out.random_seed) out.random_seed = DEFAULT_SEED; + return out; +} + +tracker_store *tracker_store_create(const tracker_store_config *cfg) +{ + tracker_store *store = calloc(1, sizeof(*store)); + if (!store) return NULL; + store->cfg = normalize_config(cfg); + store->rng = store->cfg.random_seed; + return store; +} + +void tracker_store_destroy(tracker_store *store) +{ + if (!store) return; + free(store->peers); + free(store->swarms); + free(store); +} + +static uint64_t next_rand(tracker_store *store) +{ + uint64_t x = store->rng; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + store->rng = x; + return x * UINT64_C(2685821657736338717); +} + +static int ensure_peers(tracker_store *store, size_t need) +{ + store_peer *p; + size_t cap; + if (need <= store->peer_cap) return TRACKER_OK; + cap = store->peer_cap ? store->peer_cap * 2u : 64u; + while (cap < need) cap *= 2u; + p = realloc(store->peers, cap * sizeof(*p)); + if (!p) return TRACKER_ENOSPC; + store->peers = p; + store->peer_cap = cap; + return TRACKER_OK; +} + +static int ensure_swarms(tracker_store *store, size_t need) +{ + store_swarm *s; + size_t cap; + if (need <= store->swarm_cap) return TRACKER_OK; + cap = store->swarm_cap ? store->swarm_cap * 2u : 16u; + while (cap < need) cap *= 2u; + s = realloc(store->swarms, cap * sizeof(*s)); + if (!s) return TRACKER_ENOSPC; + store->swarms = s; + store->swarm_cap = cap; + return TRACKER_OK; +} + +static store_swarm *find_swarm(tracker_store *store, const uint8_t hash[20]) +{ + for (size_t i = 0; i < store->swarm_count; i++) { + if (memcmp(store->swarms[i].info_hash, hash, 20) == 0) { + return &store->swarms[i]; + } + } + return NULL; +} + +static store_swarm *get_swarm(tracker_store *store, const uint8_t hash[20]) +{ + store_swarm *swarm = find_swarm(store, hash); + if (swarm) return swarm; + if (ensure_swarms(store, store->swarm_count + 1u) != TRACKER_OK) return NULL; + swarm = &store->swarms[store->swarm_count++]; + memset(swarm, 0, sizeof(*swarm)); + memcpy(swarm->info_hash, hash, 20); + return swarm; +} + +static void apply_counts(store_swarm *swarm, const store_peer *peer, int delta) +{ + if (!swarm || !peer || delta == 0) return; + if (peer->left == 0) { + if (delta > 0) swarm->complete += (uint32_t)delta; + else swarm->complete -= (uint32_t)(-delta); + } else { + if (delta > 0) swarm->incomplete += (uint32_t)delta; + else swarm->incomplete -= (uint32_t)(-delta); + } +} + +static int same_endpoint(const tracker_peer *a, const tracker_peer *b) +{ + size_t n; + if (a->family != b->family || a->port != b->port) return 0; + n = a->family == TRACKER_ADDR_IPV4 ? 4u : 16u; + return memcmp(a->addr, b->addr, n) == 0; +} + +static int same_identity(const store_peer *peer, const tracker_announce_request *req, + const tracker_peer *endpoint) +{ + if (memcmp(peer->info_hash, req->info_hash, 20) != 0) return 0; + if (memcmp(peer->peer.peer_id, req->peer_id, 20) == 0) { + if (req->has_key && peer->has_key && req->key == peer->key) return 1; + if (!req->has_key || !peer->has_key) return 1; + } + return same_endpoint(&peer->peer, endpoint); +} + +static store_peer *find_peer(tracker_store *store, const tracker_announce_request *req, + const tracker_peer *endpoint) +{ + for (size_t i = 0; i < store->peer_count; i++) { + if (same_identity(&store->peers[i], req, endpoint)) { + return &store->peers[i]; + } + } + return NULL; +} + +static void remove_peer_at(tracker_store *store, size_t idx) +{ + store_swarm *swarm; + if (idx >= store->peer_count) return; + swarm = find_swarm(store, store->peers[idx].info_hash); + apply_counts(swarm, &store->peers[idx], -1); + if (idx + 1u < store->peer_count) { + store->peers[idx] = store->peers[store->peer_count - 1u]; + } + store->peer_count--; +} + +static int build_endpoint(const tracker_announce_request *req, + const tracker_peer *source_addr, + tracker_peer *out) +{ + size_t n; + if (!req || !source_addr || !out) return TRACKER_EINVAL; + if (source_addr->family != TRACKER_ADDR_IPV4 && + source_addr->family != TRACKER_ADDR_IPV6) { + return TRACKER_EINVAL; + } + if (req->port == 0) return TRACKER_EPARSE; + memset(out, 0, sizeof(*out)); + out->family = source_addr->family; + n = out->family == TRACKER_ADDR_IPV4 ? 4u : 16u; + memcpy(out->addr, source_addr->addr, n); + out->port = req->port; + memcpy(out->peer_id, req->peer_id, 20); + out->has_peer_id = !req->no_peer_id; + return TRACKER_OK; +} + +static size_t requested_numwant(const tracker_store *store, + const tracker_announce_request *req) +{ + uint32_t n; + if (req->numwant < 0) n = store->cfg.default_numwant; + else n = (uint32_t)req->numwant; + if (n > store->cfg.max_numwant) n = store->cfg.max_numwant; + return n; +} + +static int choose_peers(tracker_store *store, + const tracker_announce_request *req, + const tracker_peer *self, + tracker_peer *out, + size_t out_cap, + size_t *out_count) +{ + size_t want = requested_numwant(store, req); + size_t count = 0; + if (want > out_cap) want = out_cap; + for (size_t i = 0; i < store->peer_count; i++) { + store_peer *candidate = &store->peers[i]; + uint64_t j; + if (memcmp(candidate->info_hash, req->info_hash, 20) != 0) continue; + if (same_endpoint(&candidate->peer, self) || + memcmp(candidate->peer.peer_id, req->peer_id, 20) == 0) { + continue; + } + if (count < want) { + out[count++] = candidate->peer; + } else if (want > 0) { + j = next_rand(store) % (i + 1u); + if (j < want) out[j] = candidate->peer; + } + } + *out_count = count; + return TRACKER_OK; +} + +int tracker_store_announce(tracker_store *store, + const tracker_announce_request *req, + const tracker_peer *source_addr, + uint64_t now_sec, + tracker_peer *out_peers, + size_t out_peer_cap, + tracker_announce_response *resp) +{ + tracker_peer endpoint; + store_swarm *swarm; + store_peer *peer; + size_t peer_count = 0; + int rc; + + if (!store || !req || !source_addr || !resp) return TRACKER_EINVAL; + if (out_peer_cap && !out_peers) return TRACKER_EINVAL; + rc = build_endpoint(req, source_addr, &endpoint); + if (rc != TRACKER_OK) return rc; + + swarm = get_swarm(store, req->info_hash); + if (!swarm) return TRACKER_ENOSPC; + + if (req->event == TRACKER_EVENT_STOPPED) { + for (size_t i = 0; i < store->peer_count; i++) { + if (same_identity(&store->peers[i], req, &endpoint)) { + remove_peer_at(store, i); + break; + } + } + memset(resp, 0, sizeof(*resp)); + resp->interval = store->cfg.interval; + resp->min_interval = store->cfg.min_interval; + resp->complete = swarm->complete; + resp->incomplete = swarm->incomplete; + resp->peers = out_peers; + resp->peer_count = 0; + resp->compact = req->compact; + return TRACKER_OK; + } + + peer = find_peer(store, req, &endpoint); + if (!peer) { + if (ensure_peers(store, store->peer_count + 1u) != TRACKER_OK) { + return TRACKER_ENOSPC; + } + peer = &store->peers[store->peer_count++]; + memset(peer, 0, sizeof(*peer)); + memcpy(peer->info_hash, req->info_hash, 20); + peer->peer = endpoint; + peer->has_key = req->has_key; + peer->key = req->key; + peer->left = req->left; + peer->completed_reported = (req->left == 0 && + req->event != TRACKER_EVENT_COMPLETED); + apply_counts(swarm, peer, 1); + } else { + apply_counts(swarm, peer, -1); + peer->peer = endpoint; + peer->left = req->left; + apply_counts(swarm, peer, 1); + } + + peer->uploaded = req->uploaded; + peer->downloaded = req->downloaded; + peer->last_announce = now_sec; + peer->has_key = req->has_key; + peer->key = req->key; + if (req->event == TRACKER_EVENT_COMPLETED && !peer->completed_reported) { + swarm->downloaded++; + peer->completed_reported = 1; + } + + rc = choose_peers(store, req, &endpoint, out_peers, out_peer_cap, &peer_count); + if (rc != TRACKER_OK) return rc; + + memset(resp, 0, sizeof(*resp)); + resp->interval = store->cfg.interval; + resp->min_interval = store->cfg.min_interval; + resp->complete = swarm->complete; + resp->incomplete = swarm->incomplete; + resp->peers = out_peers; + resp->peer_count = peer_count; + resp->compact = req->compact; + return TRACKER_OK; +} + +int tracker_store_scrape(tracker_store *store, + const uint8_t hashes[][20], + size_t hash_count, + tracker_scrape_file *out_files, + size_t out_file_cap, + tracker_scrape_response *resp) +{ + if (!store || !hashes || !out_files || !resp) return TRACKER_EINVAL; + if (hash_count > out_file_cap) return TRACKER_ENOSPC; + for (size_t i = 0; i < hash_count; i++) { + store_swarm *swarm = find_swarm(store, hashes[i]); + memset(&out_files[i], 0, sizeof(out_files[i])); + memcpy(out_files[i].info_hash, hashes[i], 20); + if (swarm) { + out_files[i].complete = swarm->complete; + out_files[i].downloaded = swarm->downloaded; + out_files[i].incomplete = swarm->incomplete; + } + } + resp->files = out_files; + resp->file_count = hash_count; + return TRACKER_OK; +} + +size_t tracker_store_prune(tracker_store *store, uint64_t now_sec) +{ + size_t removed = 0; + if (!store) return 0; + for (size_t i = 0; i < store->peer_count;) { + store_peer *peer = &store->peers[i]; + if (now_sec >= peer->last_announce && + now_sec - peer->last_announce > store->cfg.peer_timeout) { + remove_peer_at(store, i); + removed++; + } else { + i++; + } + } + return removed; +} + +size_t tracker_store_swarm_count(const tracker_store *store) +{ + return store ? store->swarm_count : 0; +} + +size_t tracker_store_peer_count(const tracker_store *store) +{ + return store ? store->peer_count : 0; +} diff --git a/src/tracker_udp.c b/src/tracker_udp.c new file mode 100644 index 0000000..1292ba0 --- /dev/null +++ b/src/tracker_udp.c @@ -0,0 +1,357 @@ +#include "tracker.h" +#include "tracker_internal.h" + +#include +#include +#include + +#define UDP_PROTOCOL_ID UINT64_C(0x0000041727101980) + +static int parse_udp_extensions(const uint8_t *p, size_t n, char *url_data, + size_t cap) +{ + size_t out = 0; + for (size_t i = 0; i < n;) { + uint8_t option = p[i++]; + if (option == 0x00) break; /* EndOfOptions */ + if (option == 0x01) continue; /* NOP */ + if (i >= n) return TRACKER_EPARSE; + uint8_t len = p[i++]; + if (len > n - i) return TRACKER_EPARSE; + if (option == 0x02 && len) { /* URLData */ + if (out + len >= cap) return TRACKER_ENOSPC; + memcpy(url_data + out, p + i, len); + out += len; + } + i += len; + } + url_data[out] = '\0'; + return TRACKER_OK; +} + +int tracker_udp_parse_request(const uint8_t *packet, size_t len, + tracker_addr_family source_family, + tracker_udp_request *out) +{ + uint32_t action; + if (!packet || !out) return TRACKER_EINVAL; + if (len < 16) return TRACKER_ETOOSMALL; + memset(out, 0, sizeof(*out)); + + action = tr_read_u32(packet + 8); + out->action = (tracker_udp_action)action; + out->transaction_id = tr_read_u32(packet + 12); + + if (action == TRACKER_UDP_CONNECT) { + if (tr_read_u64(packet) != UDP_PROTOCOL_ID) return TRACKER_EPARSE; + if (len < 16) return TRACKER_ETOOSMALL; + return TRACKER_OK; + } + + out->connection_id = tr_read_u64(packet); + if (action == TRACKER_UDP_ANNOUNCE) { + tracker_announce_request *a = &out->announce; + if (len < 98) return TRACKER_ETOOSMALL; + memcpy(a->info_hash, packet + 16, 20); + memcpy(a->peer_id, packet + 36, 20); + a->downloaded = tr_read_u64(packet + 56); + a->left = tr_read_u64(packet + 64); + a->uploaded = tr_read_u64(packet + 72); + a->event = (tracker_event)tr_read_u32(packet + 80); + a->ip4 = tr_read_u32(packet + 84); + a->has_ip4 = a->ip4 != 0; + a->key = tr_read_u32(packet + 88); + a->has_key = 1; + a->numwant = (int32_t)tr_read_u32(packet + 92); + a->port = tr_read_u16(packet + 96); + a->compact = 1; + if (a->event < TRACKER_EVENT_NONE || + a->event > TRACKER_EVENT_STOPPED) { + return TRACKER_EPARSE; + } + if (source_family != TRACKER_ADDR_IPV4 && + source_family != TRACKER_ADDR_IPV6) { + return TRACKER_EINVAL; + } + if (len > 98) { + return parse_udp_extensions(packet + 98, len - 98, a->url_data, + sizeof(a->url_data)); + } + return TRACKER_OK; + } + + if (action == TRACKER_UDP_SCRAPE) { + size_t count; + if (len < 36) return TRACKER_ETOOSMALL; + if ((len - 16) % 20 != 0) return TRACKER_EPARSE; + count = (len - 16) / 20; + if (count > TRACKER_MAX_SCRAPE) return TRACKER_ETOOBIG; + for (size_t i = 0; i < count; i++) { + memcpy(out->scrape_hashes[i], packet + 16 + i * 20, 20); + } + out->scrape_count = count; + return TRACKER_OK; + } + + return TRACKER_EPARSE; +} + +int tracker_udp_write_connect_response(uint32_t transaction_id, + uint64_t connection_id, + uint8_t *buf, size_t cap, + size_t *written) +{ + if (!buf || !written) return TRACKER_EINVAL; + if (cap < 16) return TRACKER_ENOSPC; + tr_write_u32(buf, TRACKER_UDP_CONNECT); + tr_write_u32(buf + 4, transaction_id); + tr_write_u64(buf + 8, connection_id); + *written = 16; + return TRACKER_OK; +} + +int tracker_udp_write_announce_response(uint32_t transaction_id, + tracker_addr_family family, + const tracker_announce_response *resp, + uint8_t *buf, size_t cap, + size_t *written) +{ + size_t stride; + size_t count = 0; + size_t need; + if (!resp || !buf || !written) return TRACKER_EINVAL; + if (family != TRACKER_ADDR_IPV4 && family != TRACKER_ADDR_IPV6) { + return TRACKER_EINVAL; + } + stride = family == TRACKER_ADDR_IPV4 ? 6u : 18u; + for (size_t i = 0; i < resp->peer_count; i++) { + if (resp->peers[i].family == family) count++; + } + need = 20 + count * stride; + if (cap < need) return TRACKER_ENOSPC; + + tr_write_u32(buf, TRACKER_UDP_ANNOUNCE); + tr_write_u32(buf + 4, transaction_id); + tr_write_u32(buf + 8, resp->interval); + tr_write_u32(buf + 12, resp->incomplete); + tr_write_u32(buf + 16, resp->complete); + size_t off = 20; + for (size_t i = 0; i < resp->peer_count; i++) { + const tracker_peer *p = &resp->peers[i]; + if (p->family != family) continue; + memcpy(buf + off, p->addr, family == TRACKER_ADDR_IPV4 ? 4u : 16u); + tr_write_u16(buf + off + stride - 2u, p->port); + off += stride; + } + *written = need; + return TRACKER_OK; +} + +int tracker_udp_write_scrape_response(uint32_t transaction_id, + const tracker_scrape_response *resp, + uint8_t *buf, size_t cap, + size_t *written) +{ + size_t need; + if (!resp || !buf || !written) return TRACKER_EINVAL; + need = 8 + resp->file_count * 12u; + if (cap < need) return TRACKER_ENOSPC; + tr_write_u32(buf, TRACKER_UDP_SCRAPE); + tr_write_u32(buf + 4, transaction_id); + for (size_t i = 0; i < resp->file_count; i++) { + const tracker_scrape_file *f = &resp->files[i]; + size_t off = 8 + i * 12u; + tr_write_u32(buf + off, f->complete); + tr_write_u32(buf + off + 4, f->downloaded); + tr_write_u32(buf + off + 8, f->incomplete); + } + *written = need; + return TRACKER_OK; +} + +int tracker_udp_write_error(uint32_t transaction_id, const char *message, + uint8_t *buf, size_t cap, size_t *written) +{ + size_t n; + if (!message || !buf || !written) return TRACKER_EINVAL; + n = strlen(message); + if (cap < 8 + n) return TRACKER_ENOSPC; + tr_write_u32(buf, TRACKER_UDP_ERROR); + tr_write_u32(buf + 4, transaction_id); + memcpy(buf + 8, message, n); + *written = 8 + n; + return TRACKER_OK; +} + +int tracker_udp_write_connect_request(uint32_t transaction_id, + uint8_t *buf, size_t cap, + size_t *written) +{ + if (!buf || !written) return TRACKER_EINVAL; + if (cap < 16) return TRACKER_ENOSPC; + tr_write_u64(buf, UDP_PROTOCOL_ID); + tr_write_u32(buf + 8, TRACKER_UDP_CONNECT); + tr_write_u32(buf + 12, transaction_id); + *written = 16; + return TRACKER_OK; +} + +int tracker_udp_parse_connect_response(const uint8_t *packet, size_t len, + uint32_t transaction_id, + uint64_t *connection_id) +{ + if (!packet || !connection_id) return TRACKER_EINVAL; + if (len < 16) return TRACKER_ETOOSMALL; + if (tr_read_u32(packet) == TRACKER_UDP_ERROR) return TRACKER_EPARSE; + if (tr_read_u32(packet) != TRACKER_UDP_CONNECT) return TRACKER_EPARSE; + if (tr_read_u32(packet + 4) != transaction_id) return TRACKER_EPARSE; + *connection_id = tr_read_u64(packet + 8); + return TRACKER_OK; +} + +static int write_udp_options(const char *url_data, uint8_t *buf, size_t cap, + size_t *off) +{ + size_t n; + if (!url_data || !url_data[0]) return TRACKER_OK; + n = strlen(url_data); + for (size_t pos = 0; pos < n;) { + size_t chunk = n - pos; + if (chunk > 255u) chunk = 255u; + if (*off > cap || cap - *off < chunk + 2u) return TRACKER_ENOSPC; + buf[(*off)++] = 0x02; + buf[(*off)++] = (uint8_t)chunk; + memcpy(buf + *off, url_data + pos, chunk); + *off += chunk; + pos += chunk; + } + return TRACKER_OK; +} + +int tracker_udp_write_announce_request(uint64_t connection_id, + uint32_t transaction_id, + const tracker_announce_request *req, + uint8_t *buf, size_t cap, + size_t *written) +{ + size_t off = 98; + int rc; + if (!req || !buf || !written) return TRACKER_EINVAL; + if (cap < 98) return TRACKER_ENOSPC; + tr_write_u64(buf, connection_id); + tr_write_u32(buf + 8, TRACKER_UDP_ANNOUNCE); + tr_write_u32(buf + 12, transaction_id); + memcpy(buf + 16, req->info_hash, 20); + memcpy(buf + 36, req->peer_id, 20); + tr_write_u64(buf + 56, req->downloaded); + tr_write_u64(buf + 64, req->left); + tr_write_u64(buf + 72, req->uploaded); + tr_write_u32(buf + 80, (uint32_t)req->event); + tr_write_u32(buf + 84, req->has_ip4 ? req->ip4 : 0u); + tr_write_u32(buf + 88, req->key); + tr_write_u32(buf + 92, (uint32_t)req->numwant); + tr_write_u16(buf + 96, req->port); + rc = write_udp_options(req->url_data, buf, cap, &off); + if (rc != TRACKER_OK) return rc; + *written = off; + return TRACKER_OK; +} + +static int read_udp_compact_peers(const uint8_t *packet, size_t len, size_t off, + tracker_addr_family family, + tracker_peer *out, size_t cap, + size_t *count) +{ + size_t stride = family == TRACKER_ADDR_IPV4 ? 6u : 18u; + size_t addr_len = family == TRACKER_ADDR_IPV4 ? 4u : 16u; + if (len < off) return TRACKER_ETOOSMALL; + if ((len - off) % stride != 0) return TRACKER_EPARSE; + for (size_t p = off; p < len; p += stride) { + if (*count >= cap) return TRACKER_ENOSPC; + memset(&out[*count], 0, sizeof(out[*count])); + out[*count].family = (uint8_t)family; + memcpy(out[*count].addr, packet + p, addr_len); + out[*count].port = tr_read_u16(packet + p + addr_len); + (*count)++; + } + return TRACKER_OK; +} + +int tracker_udp_parse_announce_response(const uint8_t *packet, size_t len, + uint32_t transaction_id, + tracker_addr_family family, + tracker_peer *out_peers, + size_t out_peer_cap, + tracker_announce_response *resp) +{ + size_t count = 0; + int rc; + if (!packet || !resp || (out_peer_cap && !out_peers)) return TRACKER_EINVAL; + if (family != TRACKER_ADDR_IPV4 && family != TRACKER_ADDR_IPV6) { + return TRACKER_EINVAL; + } + if (len < 8) return TRACKER_ETOOSMALL; + if (tr_read_u32(packet) == TRACKER_UDP_ERROR) return TRACKER_EPARSE; + if (len < 20) return TRACKER_ETOOSMALL; + if (tr_read_u32(packet) != TRACKER_UDP_ANNOUNCE) return TRACKER_EPARSE; + if (tr_read_u32(packet + 4) != transaction_id) return TRACKER_EPARSE; + memset(resp, 0, sizeof(*resp)); + resp->interval = tr_read_u32(packet + 8); + resp->incomplete = tr_read_u32(packet + 12); + resp->complete = tr_read_u32(packet + 16); + rc = read_udp_compact_peers(packet, len, 20, family, out_peers, + out_peer_cap, &count); + if (rc != TRACKER_OK) return rc; + resp->peers = out_peers; + resp->peer_count = count; + resp->compact = 1; + return TRACKER_OK; +} + +int tracker_udp_write_scrape_request(uint64_t connection_id, + uint32_t transaction_id, + const uint8_t hashes[][20], + size_t hash_count, + uint8_t *buf, size_t cap, + size_t *written) +{ + size_t need = 16u + hash_count * 20u; + if (!hashes || !buf || !written) return TRACKER_EINVAL; + if (hash_count == 0 || hash_count > TRACKER_MAX_SCRAPE) return TRACKER_EINVAL; + if (cap < need) return TRACKER_ENOSPC; + tr_write_u64(buf, connection_id); + tr_write_u32(buf + 8, TRACKER_UDP_SCRAPE); + tr_write_u32(buf + 12, transaction_id); + for (size_t i = 0; i < hash_count; i++) { + memcpy(buf + 16 + i * 20u, hashes[i], 20); + } + *written = need; + return TRACKER_OK; +} + +int tracker_udp_parse_scrape_response(const uint8_t *packet, size_t len, + uint32_t transaction_id, + tracker_scrape_file *out_files, + size_t out_file_cap, + tracker_scrape_response *resp) +{ + size_t count; + if (!packet || !out_files || !resp) return TRACKER_EINVAL; + if (len < 8) return TRACKER_ETOOSMALL; + if (tr_read_u32(packet) == TRACKER_UDP_ERROR) return TRACKER_EPARSE; + if (tr_read_u32(packet) != TRACKER_UDP_SCRAPE) return TRACKER_EPARSE; + if (tr_read_u32(packet + 4) != transaction_id) return TRACKER_EPARSE; + if ((len - 8u) % 12u != 0) return TRACKER_EPARSE; + count = (len - 8u) / 12u; + if (count > out_file_cap) return TRACKER_ENOSPC; + for (size_t i = 0; i < count; i++) { + size_t off = 8u + i * 12u; + memset(&out_files[i], 0, sizeof(out_files[i])); + out_files[i].complete = tr_read_u32(packet + off); + out_files[i].downloaded = tr_read_u32(packet + off + 4u); + out_files[i].incomplete = tr_read_u32(packet + off + 8u); + } + resp->files = out_files; + resp->file_count = count; + return TRACKER_OK; +} diff --git a/tests/test_tracker.c b/tests/test_tracker.c new file mode 100644 index 0000000..f6da3a6 --- /dev/null +++ b/tests/test_tracker.c @@ -0,0 +1,667 @@ +#include "tracker.h" + +#include +#include +#include +#include + +static void put32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)(v >> 24); + p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); + p[3] = (uint8_t)v; +} + +static void put64(uint8_t *p, uint64_t v) +{ + put32(p, (uint32_t)(v >> 32)); + put32(p + 4, (uint32_t)v); +} + +static uint32_t get32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | p[3]; +} + +static void fill_announce(tracker_announce_request *req, const char *hash, + const char *peer_id, uint16_t port, uint64_t left, + tracker_event event) +{ + memset(req, 0, sizeof(*req)); + memcpy(req->info_hash, hash, 20); + memcpy(req->peer_id, peer_id, 20); + req->port = port; + req->left = left; + req->numwant = -1; + req->event = event; + req->compact = 1; + req->has_key = 1; + req->key = (uint32_t)port; +} + +static tracker_peer ipv4_source(uint8_t a, uint8_t b, uint8_t c, uint8_t d) +{ + tracker_peer peer; + memset(&peer, 0, sizeof(peer)); + peer.family = TRACKER_ADDR_IPV4; + peer.addr[0] = a; + peer.addr[1] = b; + peer.addr[2] = c; + peer.addr[3] = d; + return peer; +} + +static void test_dht_queries(void) +{ + uint8_t buf[512]; + size_t written = 0; + dht_message msg; + uint8_t tx[] = {'a', 'a'}; + uint8_t id[20] = "abcdefghij0123456789"; + uint8_t target[20] = "mnopqrstuvwxyz123456"; + uint8_t token[] = "tok"; + + assert(dht_write_ping_query(tx, sizeof(tx), id, buf, sizeof(buf), + &written) == TRACKER_OK); + assert(dht_parse_message(buf, written, &msg) == TRACKER_OK); + assert(msg.type == DHT_MSG_QUERY); + assert(msg.query == DHT_QUERY_PING); + assert(msg.transaction_len == 2); + assert(memcmp(msg.transaction, tx, 2) == 0); + assert(memcmp(msg.id, id, 20) == 0); + + assert(dht_write_find_node_query(tx, sizeof(tx), id, target, 1, 1, + buf, sizeof(buf), + &written) == TRACKER_OK); + assert(dht_parse_message(buf, written, &msg) == TRACKER_OK); + assert(msg.query == DHT_QUERY_FIND_NODE); + assert(memcmp(msg.target, target, 20) == 0); + assert(msg.want_ipv4 == 1); + assert(msg.want_ipv6 == 1); + + assert(dht_write_get_peers_query(tx, sizeof(tx), id, target, 0, 1, + buf, sizeof(buf), + &written) == TRACKER_OK); + assert(dht_parse_message(buf, written, &msg) == TRACKER_OK); + assert(msg.query == DHT_QUERY_GET_PEERS); + assert(memcmp(msg.info_hash, target, 20) == 0); + assert(msg.want_ipv4 == 0); + assert(msg.want_ipv6 == 1); + + assert(dht_write_announce_peer_query(tx, sizeof(tx), id, target, 6881, + token, sizeof(token) - 1, 1, + buf, sizeof(buf), + &written) == TRACKER_OK); + assert(dht_parse_message(buf, written, &msg) == TRACKER_OK); + assert(msg.query == DHT_QUERY_ANNOUNCE_PEER); + assert(memcmp(msg.info_hash, target, 20) == 0); + assert(msg.port == 6881); + assert(msg.implied_port == 1); + assert(msg.token_len == sizeof(token) - 1); + assert(memcmp(msg.token, token, sizeof(token) - 1) == 0); +} + +static void test_dht_responses(void) +{ + uint8_t buf[1024]; + size_t written = 0; + dht_message msg; + uint8_t tx[] = {'b', 'b'}; + uint8_t id[20] = "abcdefghij0123456789"; + uint8_t node_id[20] = "mnopqrstuvwxyz123456"; + uint8_t token[] = "aoeusnth"; + dht_node nodes[2]; + tracker_peer peers[2]; + + assert(dht_write_ping_response(tx, sizeof(tx), id, buf, sizeof(buf), + &written) == TRACKER_OK); + assert(dht_parse_message(buf, written, &msg) == TRACKER_OK); + assert(msg.type == DHT_MSG_RESPONSE); + assert(memcmp(msg.id, id, 20) == 0); + + memset(nodes, 0, sizeof(nodes)); + memcpy(nodes[0].id, node_id, 20); + nodes[0].family = TRACKER_ADDR_IPV4; + nodes[0].addr[0] = 1; + nodes[0].addr[1] = 2; + nodes[0].addr[2] = 3; + nodes[0].addr[3] = 4; + nodes[0].port = 6881; + memcpy(nodes[1].id, id, 20); + nodes[1].family = TRACKER_ADDR_IPV6; + nodes[1].addr[15] = 1; + nodes[1].port = 6882; + assert(dht_write_nodes_response(tx, sizeof(tx), id, token, + sizeof(token) - 1, nodes, 2, + buf, sizeof(buf), + &written) == TRACKER_OK); + assert(dht_parse_message(buf, written, &msg) == TRACKER_OK); + assert(msg.type == DHT_MSG_RESPONSE); + assert(msg.node_count == 2); + assert(msg.nodes[0].family == TRACKER_ADDR_IPV4); + assert(msg.nodes[0].port == 6881); + assert(msg.nodes[1].family == TRACKER_ADDR_IPV6); + assert(msg.nodes[1].port == 6882); + assert(msg.token_len == sizeof(token) - 1); + assert(memcmp(msg.token, token, sizeof(token) - 1) == 0); + + memset(peers, 0, sizeof(peers)); + peers[0].family = TRACKER_ADDR_IPV4; + peers[0].addr[0] = 8; + peers[0].addr[1] = 8; + peers[0].addr[2] = 8; + peers[0].addr[3] = 8; + peers[0].port = 51413; + peers[1].family = TRACKER_ADDR_IPV6; + peers[1].addr[15] = 2; + peers[1].port = 51414; + assert(dht_write_peers_response(tx, sizeof(tx), id, token, + sizeof(token) - 1, peers, 2, + buf, sizeof(buf), + &written) == TRACKER_OK); + assert(dht_parse_message(buf, written, &msg) == TRACKER_OK); + assert(msg.peer_count == 2); + assert(msg.peers[0].family == TRACKER_ADDR_IPV4); + assert(msg.peers[0].port == 51413); + assert(msg.peers[1].family == TRACKER_ADDR_IPV6); + assert(msg.peers[1].port == 51414); +} + +static void test_dht_error(void) +{ + uint8_t buf[256]; + size_t written = 0; + dht_message msg; + uint8_t tx[] = {'e', 'r'}; + assert(dht_write_error(tx, sizeof(tx), DHT_ERR_PROTOCOL, "bad token", + buf, sizeof(buf), &written) == TRACKER_OK); + assert(dht_parse_message(buf, written, &msg) == TRACKER_OK); + assert(msg.type == DHT_MSG_ERROR); + assert(msg.error_code == DHT_ERR_PROTOCOL); + assert(strcmp(msg.error_message, "bad token") == 0); +} + +static void test_http_announce_parse(void) +{ + tracker_announce_request req; + const char *q = + "/announce?info_hash=%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f%10%11%12%13" + "&peer_id=-TT0001-abcdefghijkl" + "&port=6881&uploaded=10&downloaded=20&left=30&compact=1" + "&event=started&numwant=50&key=12345&trackerid=session"; + int rc = tracker_http_parse_announce_query(q, &req); + assert(rc == TRACKER_OK); + assert(req.info_hash[0] == 0); + assert(req.info_hash[19] == 0x13); + assert(memcmp(req.peer_id, "-TT0001-abcdefghijkl", 20) == 0); + assert(req.port == 6881); + assert(req.uploaded == 10); + assert(req.downloaded == 20); + assert(req.left == 30); + assert(req.compact == 1); + assert(req.event == TRACKER_EVENT_STARTED); + assert(req.numwant == 50); + assert(req.has_key == 1 && req.key == 12345); + assert(strcmp(req.tracker_id, "session") == 0); + + assert(tracker_http_parse_announce_query( + "/announce?info_hash=++++++++++++++++++++" + "&peer_id=--------------------&port=1&uploaded=0" + "&downloaded=0&left=0", + &req) == TRACKER_OK); + assert(req.info_hash[0] == '+'); + assert(req.info_hash[19] == '+'); +} + +static void test_http_compact_response(void) +{ + uint8_t buf[512]; + size_t written = 0; + tracker_peer peers[2]; + tracker_announce_response resp; + + memset(peers, 0, sizeof(peers)); + peers[0].family = TRACKER_ADDR_IPV4; + peers[0].addr[0] = 127; + peers[0].addr[1] = 0; + peers[0].addr[2] = 0; + peers[0].addr[3] = 1; + peers[0].port = 6881; + peers[1].family = TRACKER_ADDR_IPV6; + peers[1].addr[15] = 1; + peers[1].port = 51413; + + memset(&resp, 0, sizeof(resp)); + resp.interval = 1800; + resp.complete = 7; + resp.incomplete = 3; + resp.peers = peers; + resp.peer_count = 2; + resp.compact = 1; + + assert(tracker_http_write_announce_response(&resp, buf, sizeof(buf), + &written) == TRACKER_OK); + assert(written > 0); + assert(memmem(buf, written, "5:peers6:", 9) != NULL); + assert(memmem(buf, written, "6:peers618:", 10) != NULL); +} + +static void test_http_scrape(void) +{ + uint8_t hashes[2][20]; + size_t count = 0; + tracker_scrape_file files[1]; + tracker_scrape_response resp; + uint8_t buf[256]; + size_t written = 0; + int rc = tracker_http_parse_scrape_query( + "/scrape?info_hash=aaaaaaaaaaaaaaaaaaaa&info_hash=bbbbbbbbbbbbbbbbbbbb", + hashes, 2, &count); + assert(rc == TRACKER_OK); + assert(count == 2); + assert(memcmp(hashes[0], "aaaaaaaaaaaaaaaaaaaa", 20) == 0); + assert(memcmp(hashes[1], "bbbbbbbbbbbbbbbbbbbb", 20) == 0); + + memset(files, 0, sizeof(files)); + memcpy(files[0].info_hash, hashes[0], 20); + files[0].complete = 11; + files[0].downloaded = 22; + files[0].incomplete = 33; + resp.files = files; + resp.file_count = 1; + assert(tracker_http_write_scrape_response(&resp, buf, sizeof(buf), + &written) == TRACKER_OK); + assert(memmem(buf, written, "5:filesd20:aaaaaaaaaaaaaaaaaaaa", 31) != NULL); + assert(memmem(buf, written, "8:completei11e", 14) != NULL); +} + +static void test_http_client_helpers(void) +{ + tracker_announce_request req; + tracker_announce_request parsed; + tracker_announce_response resp; + tracker_announce_response parsed_resp; + tracker_scrape_response scrape; + tracker_scrape_response parsed_scrape; + tracker_scrape_file files[1]; + tracker_scrape_file parsed_files[2]; + tracker_peer peers[2]; + tracker_peer parsed_peers[4]; + uint8_t buf[512]; + char query[512]; + size_t written = 0; + uint8_t hashes[2][20]; + size_t hash_count = 0; + + fill_announce(&req, "abcdefghijklmnopqrst", "-TC0001-abcdefghijkl", 6881, + 12345, TRACKER_EVENT_STARTED); + req.uploaded = 10; + req.downloaded = 20; + req.numwant = 25; + assert(tracker_http_write_announce_query(&req, query, sizeof(query), + &written) == TRACKER_OK); + assert(written > 0); + assert(tracker_http_parse_announce_query(query, &parsed) == TRACKER_OK); + assert(memcmp(parsed.info_hash, req.info_hash, 20) == 0); + assert(memcmp(parsed.peer_id, req.peer_id, 20) == 0); + assert(parsed.port == 6881); + assert(parsed.left == 12345); + assert(parsed.event == TRACKER_EVENT_STARTED); + assert(parsed.numwant == 25); + + memset(peers, 0, sizeof(peers)); + peers[0].family = TRACKER_ADDR_IPV4; + peers[0].addr[0] = 1; + peers[0].addr[1] = 2; + peers[0].addr[2] = 3; + peers[0].addr[3] = 4; + peers[0].port = 6000; + peers[1].family = TRACKER_ADDR_IPV6; + peers[1].addr[15] = 1; + peers[1].port = 6001; + memset(&resp, 0, sizeof(resp)); + resp.interval = 1800; + resp.min_interval = 60; + resp.complete = 3; + resp.incomplete = 4; + resp.peers = peers; + resp.peer_count = 2; + resp.compact = 1; + assert(tracker_http_write_announce_response(&resp, buf, sizeof(buf), + &written) == TRACKER_OK); + assert(tracker_http_parse_announce_response(buf, written, parsed_peers, 4, + &parsed_resp) == TRACKER_OK); + assert(parsed_resp.interval == 1800); + assert(parsed_resp.min_interval == 60); + assert(parsed_resp.complete == 3); + assert(parsed_resp.incomplete == 4); + assert(parsed_resp.peer_count == 2); + assert(parsed_peers[0].family == TRACKER_ADDR_IPV4); + assert(parsed_peers[0].port == 6000); + assert(parsed_peers[1].family == TRACKER_ADDR_IPV6); + assert(parsed_peers[1].port == 6001); + + memcpy(hashes[0], "aaaaaaaaaaaaaaaaaaaa", 20); + memcpy(hashes[1], "bbbbbbbbbbbbbbbbbbbb", 20); + assert(tracker_http_write_scrape_query(hashes, 2, query, sizeof(query), + &written) == TRACKER_OK); + assert(tracker_http_parse_scrape_query(query, hashes, 2, + &hash_count) == TRACKER_OK); + assert(hash_count == 2); + + memset(files, 0, sizeof(files)); + memcpy(files[0].info_hash, "aaaaaaaaaaaaaaaaaaaa", 20); + files[0].complete = 9; + files[0].downloaded = 8; + files[0].incomplete = 7; + scrape.files = files; + scrape.file_count = 1; + assert(tracker_http_write_scrape_response(&scrape, buf, sizeof(buf), + &written) == TRACKER_OK); + assert(tracker_http_parse_scrape_response(buf, written, parsed_files, 2, + &parsed_scrape) == TRACKER_OK); + assert(parsed_scrape.file_count == 1); + assert(memcmp(parsed_files[0].info_hash, files[0].info_hash, 20) == 0); + assert(parsed_files[0].complete == 9); + assert(parsed_files[0].downloaded == 8); + assert(parsed_files[0].incomplete == 7); +} + +static void test_udp_announce_parse(void) +{ + uint8_t pkt[128]; + tracker_udp_request req; + memset(pkt, 0, sizeof(pkt)); + put64(pkt, 0x0102030405060708ULL); + put32(pkt + 8, TRACKER_UDP_ANNOUNCE); + put32(pkt + 12, 0x11223344); + memcpy(pkt + 16, "aaaaaaaaaaaaaaaaaaaa", 20); + memcpy(pkt + 36, "bbbbbbbbbbbbbbbbbbbb", 20); + put64(pkt + 56, 100); + put64(pkt + 64, 200); + put64(pkt + 72, 300); + put32(pkt + 80, TRACKER_EVENT_COMPLETED); + put32(pkt + 88, 0xaabbccdd); + put32(pkt + 92, 25); + pkt[96] = 0x1a; + pkt[97] = 0xe1; + pkt[98] = 0x02; + pkt[99] = 12; + memcpy(pkt + 100, "/dir?a=b&c=d", 12); + pkt[112] = 0x00; + + assert(tracker_udp_parse_request(pkt, 113, TRACKER_ADDR_IPV4, + &req) == TRACKER_OK); + assert(req.action == TRACKER_UDP_ANNOUNCE); + assert(req.transaction_id == 0x11223344); + assert(req.connection_id == 0x0102030405060708ULL); + assert(memcmp(req.announce.info_hash, "aaaaaaaaaaaaaaaaaaaa", 20) == 0); + assert(memcmp(req.announce.peer_id, "bbbbbbbbbbbbbbbbbbbb", 20) == 0); + assert(req.announce.downloaded == 100); + assert(req.announce.left == 200); + assert(req.announce.uploaded == 300); + assert(req.announce.event == TRACKER_EVENT_COMPLETED); + assert(req.announce.key == 0xaabbccdd); + assert(req.announce.numwant == 25); + assert(req.announce.port == 6881); + assert(strcmp(req.announce.url_data, "/dir?a=b&c=d") == 0); +} + +static void test_udp_responses(void) +{ + uint8_t buf[256]; + size_t written = 0; + tracker_peer peer; + tracker_announce_response announce; + tracker_scrape_file file; + tracker_scrape_response scrape; + + assert(tracker_udp_write_connect_response(0x1234, 0x0102030405060708ULL, + buf, sizeof(buf), + &written) == TRACKER_OK); + assert(written == 16); + assert(get32(buf) == TRACKER_UDP_CONNECT); + assert(get32(buf + 4) == 0x1234); + + memset(&peer, 0, sizeof(peer)); + peer.family = TRACKER_ADDR_IPV4; + peer.addr[0] = 10; + peer.addr[3] = 5; + peer.port = 6000; + memset(&announce, 0, sizeof(announce)); + announce.interval = 900; + announce.complete = 2; + announce.incomplete = 4; + announce.peers = &peer; + announce.peer_count = 1; + assert(tracker_udp_write_announce_response(0x99, TRACKER_ADDR_IPV4, + &announce, buf, sizeof(buf), + &written) == TRACKER_OK); + assert(written == 26); + assert(get32(buf) == TRACKER_UDP_ANNOUNCE); + assert(get32(buf + 8) == 900); + assert(get32(buf + 12) == 4); + assert(get32(buf + 16) == 2); + assert(buf[20] == 10 && buf[23] == 5); + + memset(&file, 0, sizeof(file)); + file.complete = 8; + file.downloaded = 9; + file.incomplete = 10; + scrape.files = &file; + scrape.file_count = 1; + assert(tracker_udp_write_scrape_response(0x77, &scrape, buf, sizeof(buf), + &written) == TRACKER_OK); + assert(written == 20); + assert(get32(buf) == TRACKER_UDP_SCRAPE); + assert(get32(buf + 8) == 8); + assert(get32(buf + 12) == 9); + assert(get32(buf + 16) == 10); +} + +static void test_udp_client_helpers(void) +{ + uint8_t buf[512]; + uint8_t response_buf[512]; + size_t written = 0; + size_t response_written = 0; + tracker_udp_request parsed_req; + tracker_announce_request req; + tracker_announce_response resp; + tracker_announce_response parsed_resp; + tracker_scrape_response scrape; + tracker_scrape_response parsed_scrape; + tracker_scrape_file file; + tracker_scrape_file parsed_files[2]; + tracker_peer peer; + tracker_peer parsed_peers[2]; + uint8_t hashes[1][20]; + uint64_t connection_id = 0; + + assert(tracker_udp_write_connect_request(0x5555, buf, sizeof(buf), + &written) == TRACKER_OK); + assert(tracker_udp_parse_request(buf, written, TRACKER_ADDR_IPV4, + &parsed_req) == TRACKER_OK); + assert(parsed_req.action == TRACKER_UDP_CONNECT); + assert(parsed_req.transaction_id == 0x5555); + assert(tracker_udp_write_connect_response(0x5555, 0x0102030405060708ULL, + response_buf, sizeof(response_buf), + &response_written) == TRACKER_OK); + assert(tracker_udp_parse_connect_response(response_buf, response_written, + 0x5555, + &connection_id) == TRACKER_OK); + assert(connection_id == 0x0102030405060708ULL); + + fill_announce(&req, "abcdefghijklmnopqrst", "-UC0001-abcdefghijkl", 7000, + 42, TRACKER_EVENT_STARTED); + req.downloaded = 5; + req.uploaded = 6; + req.numwant = 10; + strcpy(req.url_data, "/announce?token=abc"); + assert(tracker_udp_write_announce_request(connection_id, 0x6666, &req, + buf, sizeof(buf), + &written) == TRACKER_OK); + assert(tracker_udp_parse_request(buf, written, TRACKER_ADDR_IPV4, + &parsed_req) == TRACKER_OK); + assert(parsed_req.action == TRACKER_UDP_ANNOUNCE); + assert(parsed_req.transaction_id == 0x6666); + assert(parsed_req.connection_id == connection_id); + assert(parsed_req.announce.port == 7000); + assert(parsed_req.announce.left == 42); + assert(strcmp(parsed_req.announce.url_data, "/announce?token=abc") == 0); + + memset(&peer, 0, sizeof(peer)); + peer.family = TRACKER_ADDR_IPV4; + peer.addr[0] = 8; + peer.addr[1] = 8; + peer.addr[2] = 4; + peer.addr[3] = 4; + peer.port = 51413; + memset(&resp, 0, sizeof(resp)); + resp.interval = 900; + resp.complete = 11; + resp.incomplete = 12; + resp.peers = &peer; + resp.peer_count = 1; + assert(tracker_udp_write_announce_response(0x6666, TRACKER_ADDR_IPV4, + &resp, response_buf, + sizeof(response_buf), + &response_written) == TRACKER_OK); + assert(tracker_udp_parse_announce_response(response_buf, response_written, + 0x6666, TRACKER_ADDR_IPV4, + parsed_peers, 2, + &parsed_resp) == TRACKER_OK); + assert(parsed_resp.interval == 900); + assert(parsed_resp.complete == 11); + assert(parsed_resp.incomplete == 12); + assert(parsed_resp.peer_count == 1); + assert(parsed_peers[0].addr[0] == 8); + assert(parsed_peers[0].port == 51413); + + memcpy(hashes[0], "aaaaaaaaaaaaaaaaaaaa", 20); + assert(tracker_udp_write_scrape_request(connection_id, 0x7777, hashes, 1, + buf, sizeof(buf), + &written) == TRACKER_OK); + assert(tracker_udp_parse_request(buf, written, TRACKER_ADDR_IPV4, + &parsed_req) == TRACKER_OK); + assert(parsed_req.action == TRACKER_UDP_SCRAPE); + assert(parsed_req.scrape_count == 1); + assert(memcmp(parsed_req.scrape_hashes[0], hashes[0], 20) == 0); + + memset(&file, 0, sizeof(file)); + file.complete = 1; + file.downloaded = 2; + file.incomplete = 3; + scrape.files = &file; + scrape.file_count = 1; + assert(tracker_udp_write_scrape_response(0x7777, &scrape, response_buf, + sizeof(response_buf), + &response_written) == TRACKER_OK); + assert(tracker_udp_parse_scrape_response(response_buf, response_written, + 0x7777, parsed_files, 2, + &parsed_scrape) == TRACKER_OK); + assert(parsed_scrape.file_count == 1); + assert(parsed_files[0].complete == 1); + assert(parsed_files[0].downloaded == 2); + assert(parsed_files[0].incomplete == 3); +} + +static void test_store_announce_scrape_stop_and_prune(void) +{ + tracker_store_config cfg; + tracker_store *store; + tracker_announce_request req1; + tracker_announce_request req2; + tracker_announce_response resp; + tracker_scrape_response scrape; + tracker_scrape_file files[1]; + tracker_peer source1 = ipv4_source(10, 0, 0, 1); + tracker_peer source2 = ipv4_source(10, 0, 0, 2); + tracker_peer out[8]; + uint8_t hashes[1][20]; + + memset(&cfg, 0, sizeof(cfg)); + cfg.interval = 1200; + cfg.min_interval = 60; + cfg.peer_timeout = 10; + cfg.default_numwant = 50; + cfg.max_numwant = 50; + cfg.random_seed = 1; + store = tracker_store_create(&cfg); + assert(store != NULL); + + fill_announce(&req1, "aaaaaaaaaaaaaaaaaaaa", "peer-000000000000001", 6001, + 100, TRACKER_EVENT_STARTED); + assert(tracker_store_announce(store, &req1, &source1, 100, out, 8, + &resp) == TRACKER_OK); + assert(resp.interval == 1200); + assert(resp.min_interval == 60); + assert(resp.complete == 0); + assert(resp.incomplete == 1); + assert(resp.peer_count == 0); + assert(tracker_store_peer_count(store) == 1); + + fill_announce(&req2, "aaaaaaaaaaaaaaaaaaaa", "peer-000000000000002", 6002, + 0, TRACKER_EVENT_STARTED); + assert(tracker_store_announce(store, &req2, &source2, 101, out, 8, + &resp) == TRACKER_OK); + assert(resp.complete == 1); + assert(resp.incomplete == 1); + assert(resp.peer_count == 1); + assert(out[0].port == 6001); + assert(out[0].addr[3] == 1); + + req1.left = 0; + req1.event = TRACKER_EVENT_COMPLETED; + assert(tracker_store_announce(store, &req1, &source1, 102, out, 8, + &resp) == TRACKER_OK); + assert(resp.complete == 2); + assert(resp.incomplete == 0); + assert(resp.peer_count == 1); + assert(out[0].port == 6002); + + memcpy(hashes[0], "aaaaaaaaaaaaaaaaaaaa", 20); + assert(tracker_store_scrape(store, hashes, 1, files, 1, + &scrape) == TRACKER_OK); + assert(scrape.file_count == 1); + assert(files[0].complete == 2); + assert(files[0].incomplete == 0); + assert(files[0].downloaded == 1); + + req2.event = TRACKER_EVENT_STOPPED; + assert(tracker_store_announce(store, &req2, &source2, 103, out, 8, + &resp) == TRACKER_OK); + assert(resp.complete == 1); + assert(resp.incomplete == 0); + assert(resp.peer_count == 0); + assert(tracker_store_peer_count(store) == 1); + + assert(tracker_store_prune(store, 200) == 1); + assert(tracker_store_peer_count(store) == 0); + assert(tracker_store_scrape(store, hashes, 1, files, 1, + &scrape) == TRACKER_OK); + assert(files[0].complete == 0); + assert(files[0].incomplete == 0); + assert(files[0].downloaded == 1); + + tracker_store_destroy(store); +} + +int main(void) +{ + test_http_announce_parse(); + test_http_compact_response(); + test_http_scrape(); + test_http_client_helpers(); + test_dht_queries(); + test_dht_responses(); + test_dht_error(); + test_udp_announce_parse(); + test_udp_responses(); + test_udp_client_helpers(); + test_store_announce_scrape_stop_and_prune(); + puts("tracker protocol tests passed"); + return 0; +}