Initial commit: tracker and DHT protocol library

This commit is contained in:
ookami125 2026-06-24 23:40:18 -04:00
commit 4732ca67ee
13 changed files with 4166 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
build/

51
CMakeLists.txt Normal file
View file

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

98
PLAN.md Normal file
View file

@ -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.

79
README.md Normal file
View file

@ -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 |

Binary file not shown.

563
harness/tracker_probe.py Normal file
View file

@ -0,0 +1,563 @@
"""
Real-world tracker probe for torrent-tracker.
Given a .torrent file, this harness parses the metainfo, computes the tracker
info_hash value, and speaks HTTP(S) and UDP tracker announce/scrape protocols
directly. It is intentionally stdlib-only so it can run anywhere the C library
builds, without requiring libtorrent.
"""
from __future__ import annotations
import argparse
import hashlib
import os
import random
import socket
import ssl
import struct
import sys
import time
from dataclasses import dataclass
from typing import Any
from urllib.parse import quote_from_bytes, urlsplit, urlunsplit
from urllib.request import Request, urlopen
UDP_PROTOCOL_ID = 0x0000041727101980
UDP_CONNECT = 0
UDP_ANNOUNCE = 1
UDP_SCRAPE = 2
UDP_ERROR = 3
class BencodeError(ValueError):
pass
class BDecoder:
def __init__(self, data: bytes):
self.data = data
self.info_span: tuple[int, int] | None = None
def parse(self) -> 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())

338
include/tracker.h Normal file
View file

@ -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 <stddef.h>
#include <stdint.h>
#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 */

669
src/dht.c Normal file
View file

@ -0,0 +1,669 @@
#include "tracker.h"
#include "tracker_internal.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

897
src/tracker_http.c Normal file
View file

@ -0,0 +1,897 @@
#include "tracker.h"
#include "tracker_internal.h"
#include <arpa/inet.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

63
src/tracker_internal.h Normal file
View file

@ -0,0 +1,63 @@
#ifndef TORRENT_TRACKER_INTERNAL_H
#define TORRENT_TRACKER_INTERNAL_H
#include <stddef.h>
#include <stdint.h>
#include <string.h>
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 */

383
src/tracker_store.c Normal file
View file

@ -0,0 +1,383 @@
#include "tracker.h"
#include <stdlib.h>
#include <string.h>
#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;
}

357
src/tracker_udp.c Normal file
View file

@ -0,0 +1,357 @@
#include "tracker.h"
#include "tracker_internal.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#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;
}

667
tests/test_tracker.c Normal file
View file

@ -0,0 +1,667 @@
#include "tracker.h"
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
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;
}