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:
ookami125 2026-06-21 23:19:41 -04:00
parent 6dc711cf57
commit b633b7d216
40 changed files with 3305 additions and 3267 deletions

169
src/discovery/dht_client.c Normal file
View file

@ -0,0 +1,169 @@
/* dht_client.c — bounded iterative BEP-5 get_peers traversal.
*
* The KRPC message codec comes from the sibling `torrent-tracker` library; this
* file owns the UDP socket, the candidate frontier, and the bounded walk. */
#include "naut/dht.h"
#include "tracker.h" /* torrent-tracker DHT codec (dht_*) */
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <poll.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define DHT_MAX_QUERIES 64
typedef struct {
struct sockaddr_in addr;
bool queried;
} candidate;
static bool parse_endpoint(const char *text, struct sockaddr_in *out) {
const char *colon = strrchr(text, ':');
if (!colon || colon == text) return false;
char host[256], port[16];
size_t host_len = (size_t)(colon - text);
size_t port_len = strlen(colon + 1);
if (host_len >= sizeof host || port_len == 0 || port_len >= sizeof port)
return false;
memcpy(host, text, host_len); host[host_len] = 0;
memcpy(port, colon + 1, port_len + 1);
struct addrinfo hints, *result = NULL;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
if (getaddrinfo(host, port, &hints, &result) != 0) return false;
memcpy(out, result->ai_addr, sizeof(*out));
freeaddrinfo(result);
return true;
}
static bool same_addr(const struct sockaddr_in *a, const struct sockaddr_in *b) {
return a->sin_port == b->sin_port && a->sin_addr.s_addr == b->sin_addr.s_addr;
}
static bool add_candidate(candidate *v, size_t *n, const struct sockaddr_in *addr) {
if (addr->sin_port == 0) return true;
for (size_t i = 0; i < *n; i++)
if (same_addr(&v[i].addr, addr)) return true;
if (*n == NAUT_DHT_MAX_NODES) return false;
v[*n].addr = *addr;
v[*n].queried = false;
(*n)++;
return true;
}
static bool add_peer(naut_peer_addr *v, size_t *n, const naut_peer_addr *peer) {
for (size_t i = 0; i < *n; i++)
if (v[i].port == peer->port && memcmp(v[i].ip, peer->ip, 4) == 0)
return true;
if (*n == NAUT_DHT_MAX_PEERS) return false;
v[(*n)++] = *peer;
return true;
}
static void node_id(uint8_t id[20]) {
int fd = open("/dev/urandom", O_RDONLY);
if (fd >= 0) {
size_t done = 0;
while (done < 20) {
ssize_t n = read(fd, id + done, 20 - done);
if (n <= 0) break;
done += (size_t)n;
}
close(fd);
if (done == 20) return;
}
for (size_t i = 0; i < 20; i++) id[i] = (uint8_t)rand();
}
naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap,
const uint8_t info_hash[20],
naut_peer_addr **peers, size_t *num_peers) {
if (!bootstrap || num_bootstrap == 0 || !info_hash || !peers || !num_peers)
return NAUT_ERR_INVAL;
*peers = NULL; *num_peers = 0;
candidate nodes[NAUT_DHT_MAX_NODES];
size_t node_count = 0;
for (size_t i = 0; i < num_bootstrap; i++) {
struct sockaddr_in addr;
if (parse_endpoint(bootstrap[i], &addr))
add_candidate(nodes, &node_count, &addr);
}
if (node_count == 0) return NAUT_ERR_INVAL;
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) return NAUT_ERR_IO;
dht_message *msg = malloc(sizeof *msg);
if (!msg) { close(fd); return NAUT_ERR_NOMEM; }
naut_peer_addr found[NAUT_DHT_MAX_PEERS];
size_t found_count = 0;
uint8_t id[20];
node_id(id);
uint16_t tx_counter = 1;
size_t queries = 0;
while (queries < DHT_MAX_QUERIES && found_count < NAUT_DHT_MAX_PEERS) {
size_t index = SIZE_MAX;
for (size_t i = 0; i < node_count; i++)
if (!nodes[i].queried) { index = i; break; }
if (index == SIZE_MAX) break;
nodes[index].queried = true;
queries++;
uint8_t tx[2] = { (uint8_t)(tx_counter >> 8), (uint8_t)tx_counter };
tx_counter++;
uint8_t query[256];
size_t query_len = 0;
if (dht_write_get_peers_query(tx, sizeof tx, id, info_hash, 1, 0,
query, sizeof query, &query_len) !=
TRACKER_OK)
continue;
ssize_t sent = sendto(fd, query, query_len, 0,
(struct sockaddr *)&nodes[index].addr,
sizeof(nodes[index].addr));
if (sent < 0) continue;
struct pollfd pfd = { .fd = fd, .events = POLLIN };
if (poll(&pfd, 1, 1000) <= 0) continue;
uint8_t packet[2048];
ssize_t received = recv(fd, packet, sizeof packet, 0);
if (received <= 0) continue;
if (dht_parse_message(packet, (size_t)received, msg) != TRACKER_OK)
continue;
if (msg->type != DHT_MSG_RESPONSE ||
msg->transaction_len != sizeof tx ||
memcmp(msg->transaction, tx, sizeof tx) != 0)
continue;
for (size_t i = 0; i < msg->peer_count; i++) {
if (msg->peers[i].family != TRACKER_ADDR_IPV4) continue;
naut_peer_addr p;
memcpy(p.ip, msg->peers[i].addr, 4);
p.port = msg->peers[i].port;
add_peer(found, &found_count, &p);
}
for (size_t i = 0; i < msg->node_count; i++) {
if (msg->nodes[i].family != TRACKER_ADDR_IPV4) continue;
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
memcpy(&addr.sin_addr, msg->nodes[i].addr, 4);
addr.sin_port = htons(msg->nodes[i].port);
add_candidate(nodes, &node_count, &addr);
}
}
free(msg);
close(fd);
if (found_count == 0) return NAUT_ERR_EMPTY;
naut_peer_addr *result = malloc(found_count * sizeof(*result));
if (!result) return NAUT_ERR_NOMEM;
memcpy(result, found, found_count * sizeof(*result));
*peers = result;
*num_peers = found_count;
return NAUT_OK;
}

View 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;
}