The peer top-up loop re-announced to trackers only at the full advertised interval (~30 min), so a torrent that dropped to a handful of peers would sit there for up to half an hour with only the 5-minute DHT refresh to help — looking like the re-announce system was dead. Now, when fewer than LOW_PEER_THRESHOLD (10) peers are connected, the next tracker announce is scheduled at the tracker's min_interval floor (never below 60s) instead of the full interval, so a thin swarm actually tries to recover. Once peers recover the full interval is used again. Capture the tracker's min_interval (was being dropped) to stay announce-compliant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
258 lines
9.1 KiB
C
258 lines
9.1 KiB
C
/* tracker_client.c — HTTP/UDP tracker announce client.
|
|
*
|
|
* The wire codec (query building, bencode/UDP packet encode+decode) comes from
|
|
* the sibling `torrent-tracker` library; this file owns only the socket glue and
|
|
* the conversion between Naut's announce types and torrent-tracker's. */
|
|
#include "naut/tracker.h"
|
|
#include "naut/log.h"
|
|
|
|
#include "tracker.h" /* torrent-tracker public ABI */
|
|
|
|
#include <errno.h>
|
|
#include <netdb.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/time.h>
|
|
|
|
#define TRACKER_RESPONSE_MAX (16u << 20)
|
|
|
|
void naut_tracker_response_free(naut_tracker_response *r) {
|
|
if (!r) return;
|
|
free(r->peers);
|
|
free(r->failure);
|
|
r->peers = NULL;
|
|
r->failure = NULL;
|
|
r->num_peers = 0;
|
|
}
|
|
|
|
/* naut_announce_req -> torrent-tracker request (compact IPv4 announce). */
|
|
static void to_tracker_request(const naut_announce_req *req,
|
|
tracker_announce_request *out) {
|
|
memset(out, 0, sizeof *out);
|
|
memcpy(out->info_hash, req->info_hash, 20);
|
|
memcpy(out->peer_id, req->peer_id, 20);
|
|
out->port = req->port;
|
|
out->uploaded = req->uploaded;
|
|
out->downloaded = req->downloaded;
|
|
out->left = req->left;
|
|
out->numwant = req->numwant;
|
|
out->key = req->key;
|
|
out->has_key = 1;
|
|
out->compact = 1;
|
|
out->event = (tracker_event)req->event; /* codes match BEP-15 */
|
|
}
|
|
|
|
/* Copy torrent-tracker IPv4 peers into a freshly malloc'd naut_peer_addr array. */
|
|
static naut_err collect_peers(const tracker_peer *peers, size_t count,
|
|
const tracker_announce_response *resp,
|
|
naut_tracker_response *out) {
|
|
out->interval = (int32_t)resp->interval;
|
|
out->min_interval = (int32_t)resp->min_interval;
|
|
out->seeders = (int32_t)resp->complete;
|
|
out->leechers = (int32_t)resp->incomplete;
|
|
out->peers = NULL;
|
|
out->num_peers = 0;
|
|
if (count == 0) return NAUT_OK;
|
|
naut_peer_addr *v = malloc(count * sizeof *v);
|
|
if (!v) return NAUT_ERR_NOMEM;
|
|
size_t n = 0;
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (peers[i].family != TRACKER_ADDR_IPV4) continue; /* IPv4 only */
|
|
memcpy(v[n].ip, peers[i].addr, 4);
|
|
v[n].port = peers[i].port;
|
|
n++;
|
|
}
|
|
out->peers = v;
|
|
out->num_peers = n;
|
|
return NAUT_OK;
|
|
}
|
|
|
|
size_t naut_tracker_http_url(const char *base, const naut_announce_req *req,
|
|
char *out, size_t outsz) {
|
|
tracker_announce_request treq;
|
|
to_tracker_request(req, &treq);
|
|
char query[2048];
|
|
size_t qlen = 0;
|
|
if (tracker_http_write_announce_query(&treq, query, sizeof query, &qlen) !=
|
|
TRACKER_OK)
|
|
return 0;
|
|
const char sep = strchr(base, '?') ? '&' : '?';
|
|
int n = snprintf(out, outsz, "%s%c%.*s", base, sep, (int)qlen, query);
|
|
if (n < 0 || (size_t)n >= outsz) return 0;
|
|
return (size_t)n;
|
|
}
|
|
|
|
/* --- HTTP --------------------------------------------------------------- */
|
|
|
|
static int dial(const char *host, const char *port, int socktype) {
|
|
struct addrinfo hints, *res = NULL, *ai;
|
|
memset(&hints, 0, sizeof hints);
|
|
hints.ai_family = AF_INET; /* IPv4 (compact peers are v4) */
|
|
hints.ai_socktype = socktype;
|
|
if (getaddrinfo(host, port, &hints, &res) != 0) return -1;
|
|
int fd = -1;
|
|
for (ai = res; ai; ai = ai->ai_next) {
|
|
fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
|
|
if (fd < 0) continue;
|
|
struct timeval tv = { .tv_sec = 10, .tv_usec = 0 };
|
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
|
if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break;
|
|
close(fd); fd = -1;
|
|
}
|
|
freeaddrinfo(res);
|
|
return fd;
|
|
}
|
|
|
|
/* split "http://host[:port]/path" */
|
|
static bool parse_http_url(const char *url, char *host, size_t hostsz,
|
|
char *port, size_t portsz, const char **path) {
|
|
if (strncmp(url, "http://", 7) != 0) return false;
|
|
const char *h = url + 7;
|
|
const char *slash = strchr(h, '/');
|
|
const char *hostend = slash ? slash : h + strlen(h);
|
|
const char *colon = memchr(h, ':', (size_t)(hostend - h));
|
|
size_t hlen = colon ? (size_t)(colon - h) : (size_t)(hostend - h);
|
|
if (hlen >= hostsz) return false;
|
|
memcpy(host, h, hlen); host[hlen] = 0;
|
|
if (colon) {
|
|
size_t plen = (size_t)(hostend - colon - 1);
|
|
if (plen >= portsz) return false;
|
|
memcpy(port, colon + 1, plen); port[plen] = 0;
|
|
} else { snprintf(port, portsz, "80"); }
|
|
*path = slash ? slash : "/";
|
|
return true;
|
|
}
|
|
|
|
static bool write_all(int fd, const void *data, size_t len) {
|
|
const uint8_t *p = data;
|
|
while (len) {
|
|
ssize_t n = write(fd, p, len);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
return false;
|
|
}
|
|
p += (size_t)n;
|
|
len -= (size_t)n;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
naut_err naut_tracker_announce_http(const char *url, naut_tracker_response *out) {
|
|
char host[256], port[16]; const char *path;
|
|
if (!parse_http_url(url, host, sizeof host, port, sizeof port, &path))
|
|
return NAUT_ERR_INVAL;
|
|
int fd = dial(host, port, SOCK_STREAM);
|
|
if (fd < 0) { NAUT_WARN("tracker connect %s:%s failed", host, port); return NAUT_ERR_IO; }
|
|
|
|
char req[4096];
|
|
int rn = snprintf(req, sizeof req,
|
|
"GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: Naut/0.1\r\nAccept: */*\r\n\r\n",
|
|
path, host);
|
|
if (rn < 0 || (size_t)rn >= sizeof req ||
|
|
!write_all(fd, req, (size_t)rn)) {
|
|
close(fd);
|
|
return NAUT_ERR_IO;
|
|
}
|
|
|
|
/* read whole response (server closes on HTTP/1.0) */
|
|
size_t cap = 1 << 16, len = 0;
|
|
uint8_t *buf = malloc(cap);
|
|
if (!buf) { close(fd); return NAUT_ERR_NOMEM; }
|
|
naut_err read_error = NAUT_OK;
|
|
for (;;) {
|
|
if (len == cap) {
|
|
if (cap == TRACKER_RESPONSE_MAX) { read_error = NAUT_ERR_FULL; break; }
|
|
size_t next_cap = NAUT_MIN(cap * 2, (size_t)TRACKER_RESPONSE_MAX);
|
|
uint8_t *next = realloc(buf, next_cap);
|
|
if (!next) { read_error = NAUT_ERR_NOMEM; break; }
|
|
buf = next;
|
|
cap = next_cap;
|
|
}
|
|
ssize_t r = read(fd, buf + len, cap - len);
|
|
if (r < 0) {
|
|
if (errno == EINTR) continue;
|
|
read_error = NAUT_ERR_IO;
|
|
break;
|
|
}
|
|
if (r == 0) break;
|
|
len += (size_t)r;
|
|
}
|
|
close(fd);
|
|
if (read_error != NAUT_OK) { free(buf); return read_error; }
|
|
|
|
/* find body after CRLFCRLF */
|
|
uint8_t *body = NULL; size_t blen = 0;
|
|
for (size_t i = 0; i + 3 < len; i++)
|
|
if (buf[i]=='\r'&&buf[i+1]=='\n'&&buf[i+2]=='\r'&&buf[i+3]=='\n') {
|
|
body = buf + i + 4; blen = len - (i + 4); break;
|
|
}
|
|
bool ok = len >= 12 && memcmp(buf, "HTTP/", 5) == 0 && buf[9] == '2';
|
|
if (!ok || !body) { free(buf); return NAUT_ERR_PROTO; }
|
|
|
|
tracker_peer peers[TRACKER_MAX_PEERS];
|
|
tracker_announce_response resp;
|
|
memset(&resp, 0, sizeof resp);
|
|
naut_err e = NAUT_ERR_PROTO;
|
|
if (tracker_http_parse_announce_response(body, blen, peers,
|
|
TRACKER_MAX_PEERS, &resp) ==
|
|
TRACKER_OK)
|
|
e = collect_peers(resp.peers, resp.peer_count, &resp, out);
|
|
free(buf);
|
|
return e;
|
|
}
|
|
|
|
/* --- UDP (BEP-15) ------------------------------------------------------- */
|
|
|
|
naut_err naut_tracker_announce_udp(const char *host, uint16_t port,
|
|
const naut_announce_req *req,
|
|
naut_tracker_response *out) {
|
|
char portstr[16]; snprintf(portstr, sizeof portstr, "%u", port);
|
|
int fd = dial(host, portstr, SOCK_DGRAM);
|
|
if (fd < 0) return NAUT_ERR_IO;
|
|
|
|
srand((unsigned)time(NULL) ^ (unsigned)getpid());
|
|
uint32_t txid = (uint32_t)rand();
|
|
|
|
uint8_t pkt[128], resp[2048];
|
|
size_t written = 0;
|
|
if (tracker_udp_write_connect_request(txid, pkt, sizeof pkt, &written) !=
|
|
TRACKER_OK ||
|
|
!write_all(fd, pkt, written)) {
|
|
close(fd); return NAUT_ERR_IO;
|
|
}
|
|
ssize_t r = read(fd, resp, sizeof resp);
|
|
uint64_t cid = 0;
|
|
if (r < 0 ||
|
|
tracker_udp_parse_connect_response(resp, (size_t)r, txid, &cid) !=
|
|
TRACKER_OK) {
|
|
close(fd); return NAUT_ERR_IO;
|
|
}
|
|
|
|
txid++;
|
|
tracker_announce_request treq;
|
|
to_tracker_request(req, &treq);
|
|
if (tracker_udp_write_announce_request(cid, txid, &treq, pkt, sizeof pkt,
|
|
&written) != TRACKER_OK ||
|
|
!write_all(fd, pkt, written)) {
|
|
close(fd); return NAUT_ERR_IO;
|
|
}
|
|
r = read(fd, resp, sizeof resp);
|
|
naut_err e = NAUT_ERR_IO;
|
|
if (r >= 0) {
|
|
tracker_peer peers[TRACKER_MAX_PEERS];
|
|
tracker_announce_response tresp;
|
|
memset(&tresp, 0, sizeof tresp);
|
|
e = NAUT_ERR_PROTO;
|
|
if (tracker_udp_parse_announce_response(resp, (size_t)r, txid,
|
|
TRACKER_ADDR_IPV4, peers,
|
|
TRACKER_MAX_PEERS, &tresp) ==
|
|
TRACKER_OK)
|
|
e = collect_peers(tresp.peers, tresp.peer_count, &tresp, out);
|
|
}
|
|
close(fd);
|
|
return e;
|
|
}
|