nautd/webui: scripting, labels, settings, set-location, pause fix
Session checkpoint on webui-plugin: - engine dump (nautctl dump) + engine endgame integration - per-file move locations persistence; torrent-level "Set location" with reset/keep-relative/leave-separate handling + residual prune - Lua: naut.get_labels, define_settings/get_setting (script_host struct) - daemon-owned labels (category+tags) + taxonomy persistence; webui write-through - fix: pausing a completed/seeding torrent now sticks (stop wins over result) - automation tab responsive layout; anime_sort label gating + settings Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6dc711cf57
commit
b633b7d216
40 changed files with 3305 additions and 3267 deletions
257
src/discovery/tracker_client.c
Normal file
257
src/discovery/tracker_client.c
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
/* 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->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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue