Initial commit: tracker and DHT protocol library
This commit is contained in:
commit
4732ca67ee
13 changed files with 4166 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
build/
|
||||
51
CMakeLists.txt
Normal file
51
CMakeLists.txt
Normal 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
98
PLAN.md
Normal 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
79
README.md
Normal 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 |
|
||||
BIN
harness/__pycache__/tracker_probe.cpython-314.pyc
Normal file
BIN
harness/__pycache__/tracker_probe.cpython-314.pyc
Normal file
Binary file not shown.
563
harness/tracker_probe.py
Normal file
563
harness/tracker_probe.py
Normal 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
338
include/tracker.h
Normal 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
669
src/dht.c
Normal 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
897
src/tracker_http.c
Normal 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
63
src/tracker_internal.h
Normal 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
383
src/tracker_store.c
Normal 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
357
src/tracker_udp.c
Normal 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
667
tests/test_tracker.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue