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

View file

@ -1,227 +0,0 @@
#include "naut/dht.h"
#include "naut/bencode.h"
#include <stdlib.h>
#include <string.h>
static naut_err finish(naut_bc_writer *w, uint8_t **out, size_t *out_len) {
if (w->err != NAUT_OK) {
naut_err e = w->err;
naut_bc_w_free(w);
return e;
}
*out = w->buf;
*out_len = w->len;
w->buf = NULL;
naut_bc_w_free(w);
return NAUT_OK;
}
static bool valid_common(const uint8_t *tx, size_t tx_len,
const uint8_t id[20], uint8_t **out, size_t *out_len) {
return tx && tx_len > 0 && tx_len <= 8 && id && out && out_len;
}
naut_err naut_dht_build_ping(const uint8_t *tx, size_t tx_len,
const uint8_t id[20],
uint8_t **out, size_t *out_len) {
if (!valid_common(tx, tx_len, id, out, out_len)) return NAUT_ERR_INVAL;
naut_bc_writer w; naut_bc_w_init(&w);
naut_bc_w_dict_begin(&w);
naut_bc_w_cstr(&w, "a"); naut_bc_w_dict_begin(&w);
naut_bc_w_cstr(&w, "id"); naut_bc_w_bytes(&w, id, 20);
naut_bc_w_end(&w);
naut_bc_w_cstr(&w, "q"); naut_bc_w_cstr(&w, "ping");
naut_bc_w_cstr(&w, "t"); naut_bc_w_bytes(&w, tx, tx_len);
naut_bc_w_cstr(&w, "y"); naut_bc_w_cstr(&w, "q");
naut_bc_w_end(&w);
return finish(&w, out, out_len);
}
static naut_err build_target_query(const char *query, const char *target_key,
const uint8_t *tx, size_t tx_len,
const uint8_t id[20],
const uint8_t target[20],
uint8_t **out, size_t *out_len) {
if (!query || !target_key || !target ||
!valid_common(tx, tx_len, id, out, out_len)) return NAUT_ERR_INVAL;
naut_bc_writer w; naut_bc_w_init(&w);
naut_bc_w_dict_begin(&w);
naut_bc_w_cstr(&w, "a"); naut_bc_w_dict_begin(&w);
naut_bc_w_cstr(&w, "id"); naut_bc_w_bytes(&w, id, 20);
naut_bc_w_cstr(&w, target_key); naut_bc_w_bytes(&w, target, 20);
naut_bc_w_end(&w);
naut_bc_w_cstr(&w, "q"); naut_bc_w_cstr(&w, query);
naut_bc_w_cstr(&w, "t"); naut_bc_w_bytes(&w, tx, tx_len);
naut_bc_w_cstr(&w, "y"); naut_bc_w_cstr(&w, "q");
naut_bc_w_end(&w);
return finish(&w, out, out_len);
}
naut_err naut_dht_build_find_node(const uint8_t *tx, size_t tx_len,
const uint8_t id[20],
const uint8_t target[20],
uint8_t **out, size_t *out_len) {
return build_target_query("find_node", "target", tx, tx_len, id, target,
out, out_len);
}
naut_err naut_dht_build_get_peers(const uint8_t *tx, size_t tx_len,
const uint8_t id[20],
const uint8_t info_hash[20],
uint8_t **out, size_t *out_len) {
return build_target_query("get_peers", "info_hash", tx, tx_len, id,
info_hash, out, out_len);
}
naut_err naut_dht_build_announce_peer(const uint8_t *tx, size_t tx_len,
const uint8_t id[20],
const uint8_t info_hash[20],
uint16_t port, bool implied_port,
const void *token, size_t token_len,
uint8_t **out, size_t *out_len) {
if (!valid_common(tx, tx_len, id, out, out_len) || !info_hash ||
!token || token_len == 0 || token_len > 64 || (!implied_port && port == 0))
return NAUT_ERR_INVAL;
naut_bc_writer w; naut_bc_w_init(&w);
naut_bc_w_dict_begin(&w);
naut_bc_w_cstr(&w, "a"); naut_bc_w_dict_begin(&w);
naut_bc_w_cstr(&w, "id"); naut_bc_w_bytes(&w, id, 20);
naut_bc_w_cstr(&w, "implied_port"); naut_bc_w_int(&w, implied_port ? 1 : 0);
naut_bc_w_cstr(&w, "info_hash"); naut_bc_w_bytes(&w, info_hash, 20);
naut_bc_w_cstr(&w, "port"); naut_bc_w_int(&w, port);
naut_bc_w_cstr(&w, "token"); naut_bc_w_bytes(&w, token, token_len);
naut_bc_w_end(&w);
naut_bc_w_cstr(&w, "q"); naut_bc_w_cstr(&w, "announce_peer");
naut_bc_w_cstr(&w, "t"); naut_bc_w_bytes(&w, tx, tx_len);
naut_bc_w_cstr(&w, "y"); naut_bc_w_cstr(&w, "q");
naut_bc_w_end(&w);
return finish(&w, out, out_len);
}
static naut_err parse_nodes(const uint8_t *p, size_t n,
naut_dht_node **out, size_t *count) {
if (n % 26 != 0 || n / 26 > NAUT_DHT_MAX_NODES) return NAUT_ERR_PROTO;
size_t num = n / 26;
naut_dht_node *nodes = calloc(num ? num : 1, sizeof(*nodes));
if (!nodes) return NAUT_ERR_NOMEM;
for (size_t i = 0; i < num; i++) {
const uint8_t *entry = p + i * 26;
memcpy(nodes[i].id, entry, 20);
memcpy(nodes[i].ip, entry + 20, 4);
nodes[i].port = ((uint16_t)entry[24] << 8) | entry[25];
if (nodes[i].port == 0) {
free(nodes);
return NAUT_ERR_PROTO;
}
}
*out = nodes;
*count = num;
return NAUT_OK;
}
static bool peer_duplicate(const naut_peer_addr *peers, size_t n,
const naut_peer_addr *candidate) {
for (size_t i = 0; i < n; i++)
if (peers[i].port == candidate->port &&
memcmp(peers[i].ip, candidate->ip, 4) == 0)
return true;
return false;
}
static naut_err parse_values(const naut_bc *values,
naut_peer_addr **out, size_t *count) {
if (!values || values->type != NAUT_BC_LIST ||
values->v.list.count > NAUT_DHT_MAX_PEERS) return NAUT_ERR_PROTO;
naut_peer_addr *peers = calloc(values->v.list.count ? values->v.list.count : 1,
sizeof(*peers));
if (!peers) return NAUT_ERR_NOMEM;
size_t num = 0;
for (size_t i = 0; i < values->v.list.count; i++) {
const uint8_t *p; size_t n;
if (!naut_bc_get_str(naut_bc_list_at(values, i), &p, &n) || n != 6) {
free(peers);
return NAUT_ERR_PROTO;
}
naut_peer_addr peer;
memcpy(peer.ip, p, 4);
peer.port = ((uint16_t)p[4] << 8) | p[5];
if (peer.port && !peer_duplicate(peers, num, &peer))
peers[num++] = peer;
}
*out = peers;
*count = num;
return NAUT_OK;
}
naut_err naut_dht_parse_response(const uint8_t *data, size_t len,
naut_dht_response *out) {
if (!data || !out) return NAUT_ERR_INVAL;
memset(out, 0, sizeof(*out));
naut_bc_doc *doc = NULL;
naut_err err = naut_bc_parse(data, len, &doc);
if (err != NAUT_OK) return err;
const naut_bc *root = naut_bc_root(doc);
const uint8_t *p; size_t n;
if (!root || root->type != NAUT_BC_DICT ||
!naut_bc_get_str(naut_bc_dict_get(root, "t"), &p, &n) ||
n == 0 || n > sizeof out->transaction) {
err = NAUT_ERR_PROTO;
goto done;
}
memcpy(out->transaction, p, n);
out->transaction_len = n;
const naut_bc *y = naut_bc_dict_get(root, "y");
if (naut_bc_str_eq(y, "e")) {
const naut_bc *e = naut_bc_dict_get(root, "e");
int64_t code;
if (!e || e->type != NAUT_BC_LIST || e->v.list.count < 1 ||
!naut_bc_get_int(naut_bc_list_at(e, 0), &code)) {
err = NAUT_ERR_PROTO;
goto done;
}
out->type = NAUT_DHT_ERROR;
out->error_code = (int)code;
goto done;
}
if (!naut_bc_str_eq(y, "r")) {
err = NAUT_ERR_PROTO;
goto done;
}
out->type = NAUT_DHT_RESPONSE;
const naut_bc *r = naut_bc_dict_get(root, "r");
if (!r || r->type != NAUT_BC_DICT) {
err = NAUT_ERR_PROTO;
goto done;
}
if (naut_bc_get_str(naut_bc_dict_get(r, "id"), &p, &n)) {
if (n != 20) { err = NAUT_ERR_PROTO; goto done; }
memcpy(out->id, p, 20);
out->has_id = true;
}
if (naut_bc_get_str(naut_bc_dict_get(r, "token"), &p, &n)) {
if (n == 0 || n > sizeof out->token) { err = NAUT_ERR_PROTO; goto done; }
memcpy(out->token, p, n);
out->token_len = n;
}
if (naut_bc_get_str(naut_bc_dict_get(r, "nodes"), &p, &n)) {
err = parse_nodes(p, n, &out->nodes, &out->num_nodes);
if (err != NAUT_OK) goto done;
}
const naut_bc *values = naut_bc_dict_get(r, "values");
if (values) {
err = parse_values(values, &out->peers, &out->num_peers);
if (err != NAUT_OK) goto done;
}
done:
naut_bc_free(doc);
if (err != NAUT_OK) naut_dht_response_free(out);
return err;
}
void naut_dht_response_free(naut_dht_response *response) {
if (!response) return;
free(response->nodes);
free(response->peers);
memset(response, 0, sizeof(*response));
}

View file

@ -1,5 +1,11 @@
/* 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>
@ -10,6 +16,8 @@
#include <sys/socket.h>
#include <unistd.h>
#define DHT_MAX_QUERIES 64
typedef struct {
struct sockaddr_in addr;
bool queried;
@ -91,6 +99,9 @@ naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap,
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];
@ -98,7 +109,7 @@ naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap,
uint16_t tx_counter = 1;
size_t queries = 0;
while (queries < 64 && found_count < NAUT_DHT_MAX_PEERS) {
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; }
@ -107,42 +118,46 @@ naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap,
queries++;
uint8_t tx[2] = { (uint8_t)(tx_counter >> 8), (uint8_t)tx_counter };
tx_counter++;
uint8_t *query = NULL; size_t query_len = 0;
if (naut_dht_build_get_peers(tx, sizeof tx, id, info_hash,
&query, &query_len) != NAUT_OK)
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));
free(query);
if (sent < 0) continue;
struct pollfd pfd = { .fd = fd, .events = POLLIN };
if (poll(&pfd, 1, 1000) <= 0) continue;
uint8_t packet[65536];
uint8_t packet[2048];
ssize_t received = recv(fd, packet, sizeof packet, 0);
if (received <= 0) continue;
naut_dht_response response;
if (naut_dht_parse_response(packet, (size_t)received, &response) != NAUT_OK)
if (dht_parse_message(packet, (size_t)received, msg) != TRACKER_OK)
continue;
if (response.transaction_len != sizeof tx ||
memcmp(response.transaction, tx, sizeof tx) != 0 ||
response.type != NAUT_DHT_RESPONSE) {
naut_dht_response_free(&response);
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 < response.num_peers; i++)
add_peer(found, &found_count, &response.peers[i]);
for (size_t i = 0; i < response.num_nodes; i++) {
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, response.nodes[i].ip, 4);
addr.sin_port = htons(response.nodes[i].port);
memcpy(&addr.sin_addr, msg->nodes[i].addr, 4);
addr.sin_port = htons(msg->nodes[i].port);
add_candidate(nodes, &node_count, &addr);
}
naut_dht_response_free(&response);
}
free(msg);
close(fd);
if (found_count == 0) return NAUT_ERR_EMPTY;
naut_peer_addr *result = malloc(found_count * sizeof(*result));

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

View file

@ -1,466 +0,0 @@
#include "naut/mse.h"
#include "naut/hash.h"
#include <errno.h>
#include <openssl/bn.h>
#include <openssl/rand.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#define MSE_PAD_MAX 512
#define MSE_CRYPTO_RC4 2u
static const char DH_PRIME_HEX[] =
"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC"
"74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF2"
"5F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A63A3621000000"
"0000090563";
static void wr16(uint8_t *p, uint16_t value) {
p[0] = (uint8_t)(value >> 8);
p[1] = (uint8_t)value;
}
static void wr32(uint8_t *p, uint32_t value) {
p[0] = (uint8_t)(value >> 24);
p[1] = (uint8_t)(value >> 16);
p[2] = (uint8_t)(value >> 8);
p[3] = (uint8_t)value;
}
static uint16_t rd16(const uint8_t *p) {
return ((uint16_t)p[0] << 8) | p[1];
}
static uint32_t rd32(const uint8_t *p) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | p[3];
}
static void hash_parts(const char label[4],
const uint8_t *first, size_t first_len,
const uint8_t *second, size_t second_len,
uint8_t out[20]) {
naut_sha1_ctx sha;
naut_sha1_init(&sha);
naut_sha1_update(&sha, label, 4);
naut_sha1_update(&sha, first, first_len);
if (second && second_len) naut_sha1_update(&sha, second, second_len);
naut_sha1_final(&sha, out);
}
static void init_rc4(const uint8_t secret[NAUT_MSE_DH_LEN],
const uint8_t info_hash[20],
naut_mse_stream *stream) {
uint8_t key_a[20], key_b[20];
hash_parts("keyA", secret, NAUT_MSE_DH_LEN, info_hash, 20, key_a);
hash_parts("keyB", secret, NAUT_MSE_DH_LEN, info_hash, 20, key_b);
naut_rc4_init(&stream->send, key_a, sizeof key_a, 1024);
naut_rc4_init(&stream->recv, key_b, sizeof key_b, 1024);
memset(key_a, 0, sizeof key_a);
memset(key_b, 0, sizeof key_b);
}
/* ---- sans-IO handshake state machine ------------------------------------- */
enum {
PH_RECV_PUBKEY, /* waiting for the peer's 96-byte DH public key */
PH_SYNC_VC, /* scanning past PadB for the encrypted VC */
PH_RECV_SELECT, /* crypto_select + len(PadD) */
PH_RECV_PAD, /* PadD bytes (discarded) */
PH_RECV_HS, /* the peer's encrypted BitTorrent handshake */
};
struct naut_mse_handshake {
int phase;
naut_err err;
bool done;
uint8_t info_hash[20];
uint8_t peer_id[NAUT_PEERID_LEN];
uint64_t reserved;
/* DH state retained until the shared secret is computed. */
BN_CTX *ctx;
BIGNUM *prime;
BIGNUM *priv;
naut_mse_stream stream;
uint8_t expected_vc[8];
size_t vc_scanned;
size_t pad_remaining;
uint8_t remote_handshake[NAUT_HANDSHAKE_LEN];
uint8_t out[256];
size_t out_len, out_off;
uint8_t in[1024];
size_t in_len;
};
static void dh_free(naut_mse_handshake *h) {
BN_CTX_free(h->ctx); h->ctx = NULL;
BN_free(h->prime); h->prime = NULL;
BN_clear_free(h->priv); h->priv = NULL;
}
/* Generate our private key and public value, writing the 96-byte public key
* into the outgoing buffer. Retains prime/priv/ctx for dh_complete(). */
static naut_err dh_begin(naut_mse_handshake *h) {
naut_err result = NAUT_ERR_IO;
BIGNUM *generator = BN_new();
BIGNUM *local = BN_new();
h->ctx = BN_CTX_new();
h->priv = BN_new();
if (!generator || !local || !h->ctx || !h->priv ||
!BN_hex2bn(&h->prime, DH_PRIME_HEX) || !BN_set_word(generator, 2))
goto done;
do {
if (!BN_rand_range(h->priv, h->prime)) goto done;
} while (BN_cmp(h->priv, generator) < 0);
if (!BN_mod_exp(local, generator, h->priv, h->prime, h->ctx) ||
BN_bn2binpad(local, h->out, NAUT_MSE_DH_LEN) != NAUT_MSE_DH_LEN)
goto done;
h->out_len = NAUT_MSE_DH_LEN;
h->out_off = 0;
result = NAUT_OK;
done:
BN_free(generator);
BN_free(local);
if (result != NAUT_OK) dh_free(h);
return result;
}
/* Validate the peer's public key and derive the shared secret. */
static naut_err dh_complete(naut_mse_handshake *h, const uint8_t remote_bytes[96],
uint8_t secret[NAUT_MSE_DH_LEN]) {
naut_err result = NAUT_ERR_IO;
BIGNUM *remote = BN_new();
BIGNUM *shared = BN_new();
BIGNUM *limit = BN_new();
BIGNUM *two = BN_new();
if (!remote || !shared || !limit || !two ||
!BN_bin2bn(remote_bytes, NAUT_MSE_DH_LEN, remote) ||
!BN_set_word(two, 2) || !BN_copy(limit, h->prime) ||
!BN_sub_word(limit, 1))
goto done;
if (BN_cmp(remote, two) < 0 || BN_cmp(remote, limit) >= 0) {
result = NAUT_ERR_PROTO;
goto done;
}
if (!BN_mod_exp(shared, remote, h->priv, h->prime, h->ctx) ||
BN_bn2binpad(shared, secret, NAUT_MSE_DH_LEN) != NAUT_MSE_DH_LEN)
goto done;
result = NAUT_OK;
done:
BN_free(remote);
BN_clear_free(shared);
BN_free(limit);
BN_free(two);
return result;
}
naut_mse_handshake *naut_mse_handshake_begin(
const uint8_t info_hash[20],
const uint8_t peer_id[NAUT_PEERID_LEN],
uint64_t reserved) {
if (!info_hash || !peer_id) return NULL;
naut_mse_handshake *h = calloc(1, sizeof(*h));
if (!h) return NULL;
memcpy(h->info_hash, info_hash, 20);
memcpy(h->peer_id, peer_id, NAUT_PEERID_LEN);
h->reserved = reserved;
h->phase = PH_RECV_PUBKEY;
if (dh_begin(h) != NAUT_OK) {
naut_mse_handshake_free(h);
return NULL;
}
return h;
}
void naut_mse_handshake_free(naut_mse_handshake *h) {
if (!h) return;
dh_free(h);
/* keystream state is sensitive; scrub before release */
memset(h, 0, sizeof(*h));
free(h);
}
static void consume(naut_mse_handshake *h, size_t n) {
memmove(h->in, h->in + n, h->in_len - n);
h->in_len -= n;
}
/* Build req1/req2 + encrypted offer (VC, crypto_provide, PadC, IA) into out. */
static void build_request(naut_mse_handshake *h, const uint8_t secret[96]) {
uint8_t req1[20], req2[20], req3[20];
hash_parts("req1", secret, NAUT_MSE_DH_LEN, NULL, 0, req1);
hash_parts("req2", h->info_hash, 20, NULL, 0, req2);
hash_parts("req3", secret, NAUT_MSE_DH_LEN, NULL, 0, req3);
for (size_t i = 0; i < sizeof req2; i++) req2[i] ^= req3[i];
init_rc4(secret, h->info_hash, &h->stream);
uint8_t *p = h->out;
memcpy(p, req1, 20);
memcpy(p + 20, req2, 20);
p += 40;
uint8_t *offer = p; /* VC(8) crypto_provide(4) padlen(2) ialen(2) IA */
memset(offer, 0, 8);
wr32(offer + 8, MSE_CRYPTO_RC4);
wr16(offer + 12, 0);
wr16(offer + 14, NAUT_HANDSHAKE_LEN);
naut_peer_handshake_build(offer + 16, h->info_hash, h->peer_id, h->reserved);
size_t offer_len = 16 + NAUT_HANDSHAKE_LEN;
naut_rc4_xor(&h->stream.send, offer, offer_len);
h->out_len = 40 + offer_len;
h->out_off = 0;
/* expected_vc = our recv keystream applied to 8 zero bytes at position 0,
* without advancing the real recv state (we resync on it). */
naut_rc4 probe = h->stream.recv;
uint8_t vc[8] = {0};
naut_rc4_xor(&probe, vc, sizeof vc);
memcpy(h->expected_vc, vc, sizeof vc);
h->vc_scanned = 0;
}
static void advance(naut_mse_handshake *h) {
for (;;) {
switch (h->phase) {
case PH_RECV_PUBKEY: {
if (h->in_len < NAUT_MSE_DH_LEN) return;
uint8_t secret[NAUT_MSE_DH_LEN];
naut_err e = dh_complete(h, h->in, secret);
if (e != NAUT_OK) { h->err = e; return; }
consume(h, NAUT_MSE_DH_LEN);
dh_free(h); /* DH no longer needed */
build_request(h, secret);
memset(secret, 0, sizeof secret);
h->phase = PH_SYNC_VC;
return; /* out now holds req+offer: NEED_WRITE */
}
case PH_SYNC_VC: {
while (h->in_len >= sizeof h->expected_vc) {
if (memcmp(h->in, h->expected_vc, sizeof h->expected_vc) == 0) {
uint8_t vc[8];
memcpy(vc, h->in, sizeof vc);
naut_rc4_xor(&h->stream.recv, vc, sizeof vc);
static const uint8_t zero8[8] = {0};
if (memcmp(vc, zero8, sizeof vc) != 0) {
h->err = NAUT_ERR_PROTO;
return;
}
consume(h, sizeof vc);
h->phase = PH_RECV_SELECT;
break;
}
consume(h, 1);
if (++h->vc_scanned > MSE_PAD_MAX) {
h->err = NAUT_ERR_PROTO;
return;
}
}
if (h->phase == PH_SYNC_VC) return; /* need more bytes */
continue;
}
case PH_RECV_SELECT: {
if (h->in_len < 6) return;
uint8_t hdr[6];
memcpy(hdr, h->in, sizeof hdr);
naut_rc4_xor(&h->stream.recv, hdr, sizeof hdr);
consume(h, sizeof hdr);
if (rd32(hdr) != MSE_CRYPTO_RC4) { h->err = NAUT_ERR_PROTO; return; }
h->pad_remaining = rd16(hdr + 4);
if (h->pad_remaining > MSE_PAD_MAX) { h->err = NAUT_ERR_PROTO; return; }
h->phase = PH_RECV_PAD;
continue;
}
case PH_RECV_PAD: {
if (h->pad_remaining > 0) {
size_t n = h->pad_remaining < h->in_len ? h->pad_remaining
: h->in_len;
if (n == 0) return;
naut_rc4_xor(&h->stream.recv, h->in, n); /* advance keystream */
consume(h, n);
h->pad_remaining -= n;
if (h->pad_remaining > 0) return;
}
h->phase = PH_RECV_HS;
continue;
}
case PH_RECV_HS: {
if (h->in_len < NAUT_HANDSHAKE_LEN) return;
memcpy(h->remote_handshake, h->in, NAUT_HANDSHAKE_LEN);
naut_rc4_xor(&h->stream.recv, h->remote_handshake, NAUT_HANDSHAKE_LEN);
consume(h, NAUT_HANDSHAKE_LEN);
uint8_t remote_hash[20], remote_id[20];
if (!naut_peer_handshake_parse(h->remote_handshake, remote_hash,
remote_id, NULL) ||
memcmp(remote_hash, h->info_hash, 20) != 0) {
h->err = NAUT_ERR_PROTO;
return;
}
h->stream.active = true;
h->done = true;
return;
}
default:
h->err = NAUT_ERR_PROTO;
return;
}
}
}
naut_mse_hs_status naut_mse_handshake_status(const naut_mse_handshake *h) {
if (!h || h->err != NAUT_OK) return NAUT_MSE_HS_ERROR;
if (h->done) return NAUT_MSE_HS_DONE;
if (h->out_off < h->out_len) return NAUT_MSE_HS_NEED_WRITE;
return NAUT_MSE_HS_NEED_READ;
}
size_t naut_mse_handshake_pull(naut_mse_handshake *h, uint8_t *buf, size_t cap) {
if (!h || !buf) return 0;
size_t avail = h->out_len - h->out_off;
size_t n = avail < cap ? avail : cap;
if (n) {
memcpy(buf, h->out + h->out_off, n);
h->out_off += n;
if (h->out_off == h->out_len) h->out_len = h->out_off = 0;
}
return n;
}
naut_mse_hs_status naut_mse_handshake_feed(naut_mse_handshake *h,
const uint8_t *data, size_t len,
size_t *consumed) {
if (consumed) *consumed = 0;
if (!h) return NAUT_MSE_HS_ERROR;
if (h->err == NAUT_OK && !h->done && data && len) {
size_t space = sizeof h->in - h->in_len;
size_t take = len < space ? len : space;
memcpy(h->in + h->in_len, data, take);
h->in_len += take;
if (consumed) *consumed = take;
advance(h);
}
return naut_mse_handshake_status(h);
}
naut_err naut_mse_handshake_finish(naut_mse_handshake *h,
naut_mse_stream *stream,
uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]) {
if (!h || !stream || !remote_handshake) return NAUT_ERR_INVAL;
if (h->err != NAUT_OK) return h->err;
if (!h->done) return NAUT_ERR_AGAIN;
*stream = h->stream;
memcpy(remote_handshake, h->remote_handshake, NAUT_HANDSHAKE_LEN);
return NAUT_OK;
}
/* ---- blocking I/O helpers + convenience wrapper -------------------------- */
static bool raw_send_all(int fd, const void *data, size_t len) {
const uint8_t *p = data;
while (len) {
ssize_t n = send(fd, p, len, MSG_NOSIGNAL);
if (n < 0) {
if (errno == EINTR) continue;
return false;
}
if (n == 0) return false;
p += n;
len -= (size_t)n;
}
return true;
}
static bool raw_recv_exact(int fd, void *data, size_t len) {
uint8_t *p = data;
while (len) {
ssize_t n = recv(fd, p, len, 0);
if (n < 0) {
if (errno == EINTR) continue;
return false;
}
if (n == 0) return false;
p += n;
len -= (size_t)n;
}
return true;
}
naut_err naut_mse_client_handshake(
int fd,
const uint8_t info_hash[20],
const uint8_t peer_id[NAUT_PEERID_LEN],
uint64_t reserved,
naut_mse_stream *stream,
uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]) {
if (fd < 0 || !info_hash || !peer_id || !stream || !remote_handshake)
return NAUT_ERR_INVAL;
memset(stream, 0, sizeof(*stream));
naut_mse_handshake *h =
naut_mse_handshake_begin(info_hash, peer_id, reserved);
if (!h) return NAUT_ERR_NOMEM;
naut_err rc = NAUT_ERR_PROTO;
for (;;) {
naut_mse_hs_status st = naut_mse_handshake_status(h);
if (st == NAUT_MSE_HS_NEED_WRITE) {
uint8_t buf[256];
size_t n;
bool ok = true;
while ((n = naut_mse_handshake_pull(h, buf, sizeof buf)) > 0)
if (!raw_send_all(fd, buf, n)) { ok = false; break; }
if (!ok) { rc = NAUT_ERR_IO; break; }
} else if (st == NAUT_MSE_HS_NEED_READ) {
/* One byte at a time: the handshake is tiny and one-shot, and this
* keeps the wrapper from over-reading into the payload stream. */
uint8_t byte;
if (!raw_recv_exact(fd, &byte, 1)) { rc = NAUT_ERR_IO; break; }
naut_mse_handshake_feed(h, &byte, 1, NULL);
} else if (st == NAUT_MSE_HS_DONE) {
rc = naut_mse_handshake_finish(h, stream, remote_handshake);
break;
} else {
rc = h->err != NAUT_OK ? h->err : NAUT_ERR_PROTO;
break;
}
}
naut_mse_handshake_free(h);
return rc;
}
/* ---- post-handshake stream I/O ------------------------------------------- */
bool naut_mse_send_all(int fd, naut_mse_stream *stream,
const void *data, size_t len) {
if (!stream || !stream->active) return raw_send_all(fd, data, len);
const uint8_t *p = data;
uint8_t block[16 * 1024];
while (len) {
size_t n = len < sizeof block ? len : sizeof block;
memcpy(block, p, n);
naut_rc4_xor(&stream->send, block, n);
if (!raw_send_all(fd, block, n)) return false;
p += n;
len -= n;
}
return true;
}
ssize_t naut_mse_recv(int fd, naut_mse_stream *stream,
void *data, size_t len) {
ssize_t n;
do {
n = recv(fd, data, len, 0);
} while (n < 0 && errno == EINTR);
if (n > 0 && stream && stream->active)
naut_rc4_xor(&stream->recv, data, (size_t)n);
return n;
}

View file

@ -1,64 +0,0 @@
#include "naut/pipeline.h"
#include <math.h>
static uint32_t clamp_depth(const naut_pipeline *p, uint32_t depth) {
if (depth < p->min_depth) return p->min_depth;
if (depth > p->max_depth) return p->max_depth;
return depth;
}
void naut_pipeline_init(naut_pipeline *p, uint32_t block_size,
uint32_t min_depth, uint32_t max_depth,
uint32_t initial_depth) {
if (!p) return;
if (block_size == 0) block_size = NAUT_BLOCK;
if (min_depth == 0) min_depth = 1;
if (max_depth < min_depth) max_depth = min_depth;
p->rtt_seconds = 0;
p->bytes_per_second = 0;
p->last_sample_at = 0;
p->min_depth = min_depth;
p->max_depth = max_depth;
p->block_size = block_size;
p->depth = clamp_depth(p, initial_depth);
}
void naut_pipeline_on_block(naut_pipeline *p, uint32_t bytes,
double sent_at, double received_at) {
if (!p || bytes == 0 || sent_at <= 0 || received_at <= sent_at) return;
double rtt = received_at - sent_at;
if (rtt > 60.0) return;
if (p->rtt_seconds == 0) p->rtt_seconds = rtt;
else p->rtt_seconds = p->rtt_seconds * 0.875 + rtt * 0.125;
double interval = p->last_sample_at > 0
? received_at - p->last_sample_at : rtt;
if (interval <= 0) interval = rtt;
double rate = bytes / interval;
if (p->bytes_per_second == 0) p->bytes_per_second = rate;
else p->bytes_per_second = p->bytes_per_second * 0.8 + rate * 0.2;
p->last_sample_at = received_at;
double blocks = (2.0 * p->bytes_per_second * p->rtt_seconds) /
p->block_size;
uint32_t target = blocks >= UINT32_MAX ? UINT32_MAX :
(uint32_t)ceil(blocks);
target = clamp_depth(p, target);
/* Grow quickly enough to fill a fast path; shrink one eighth at a time so
* transient delayed samples do not collapse the pipe. */
if (target > p->depth) {
uint32_t step = p->depth / 4 + 1;
p->depth = clamp_depth(p, NAUT_MIN(target, p->depth + step));
} else if (target < p->depth) {
uint32_t step = p->depth / 8 + 1;
p->depth = clamp_depth(p, target > p->depth - step
? target : p->depth - step);
}
}
uint32_t naut_pipeline_depth(const naut_pipeline *p) {
return p ? p->depth : 0;
}

View file

@ -158,6 +158,48 @@ void naut_download_destroy(naut_download *d) {
free(d);
}
static void mark_piece_complete(naut_download *d, uint32_t p,
bool count_blocks, bool emit);
naut_err naut_download_resume(naut_download *d) {
if (!d) return NAUT_ERR_INVAL;
uint64_t max_piece = d->piece_len;
uint64_t last_piece = piece_size(d, d->num_pieces - 1);
if (last_piece > max_piece) max_piece = last_piece;
if (max_piece > (uint64_t)SIZE_MAX) return NAUT_ERR_INVAL;
uint8_t *buf = malloc((size_t)max_piece);
if (!buf) return NAUT_ERR_NOMEM;
uint32_t resumed = 0;
uint8_t digest[NAUT_SHA1_LEN];
for (uint32_t p = 0; p < d->num_pieces; p++) {
uint64_t ps = piece_size(d, p);
if (ps > (uint64_t)SIZE_MAX) {
free(buf);
return NAUT_ERR_INVAL;
}
naut_err e = naut_storage_read(
d->st, (int64_t)p * (int64_t)d->piece_len, buf, (size_t)ps);
if (e != NAUT_OK) {
free(buf);
return e;
}
naut_sha1(buf, ps, digest);
if (memcmp(digest,
d->mi->piece_hashes + (size_t)p * NAUT_SHA1_LEN,
NAUT_SHA1_LEN) != 0)
continue;
mark_piece_complete(d, p, true, false);
resumed++;
}
free(buf);
if (resumed)
NAUT_INFO("resume: verified %u/%u pieces from disk",
resumed, d->num_pieces);
return NAUT_OK;
}
void naut_download_set_worker_pool(naut_download *d, naut_worker_pool *pool) {
if (d) d->workers = pool;
}
@ -174,7 +216,7 @@ bool naut_download_file_complete(const naut_download *d, uint32_t f) {
return f < d->num_files && d->file_done[f];
}
static void notify_files(naut_download *d, uint32_t p) {
static void notify_files(naut_download *d, uint32_t p, bool emit) {
size_t lo = 0, hi = d->num_files;
while (lo < hi) { size_t mid = (lo + hi) / 2;
if (d->file_last[mid] < p) lo = mid + 1; else hi = mid; }
@ -182,11 +224,26 @@ static void notify_files(naut_download *d, uint32_t p) {
if (d->file_done[f]) continue;
if (--d->file_remain[f] == 0) {
d->file_done[f] = true;
if (d->file_cb) d->file_cb(d->file_cb_ctx, (uint32_t)f, d->mi->files[f].path);
if (emit && d->file_cb)
d->file_cb(d->file_cb_ctx, (uint32_t)f,
d->mi->files[f].path);
}
}
}
static void mark_piece_complete(naut_download *d, uint32_t p,
bool count_blocks, bool emit) {
if (naut_bitfield_test(&d->have, p)) return;
naut_bitfield_set(&d->have, p);
d->pieces_done++;
d->bytes_done += piece_size(d, p);
if (count_blocks)
d->recv_blocks += nblocks(d, p);
if (emit && d->piece_cb)
d->piece_cb(d->piece_cb_ctx, p);
notify_files(d, p, emit);
}
/* --- availability -------------------------------------------------------- */
void naut_download_inc_avail(naut_download *d, uint32_t p) {
if (p < d->num_pieces) d->avail[p]++;
@ -323,13 +380,9 @@ static naut_err finish_verified(naut_download *d, uint32_t p,
}
naut_err e = naut_storage_write(d->st, (int64_t)p * (int64_t)d->piece_len, s->buf, ps);
if (e != NAUT_OK) return e;
naut_bitfield_set(&d->have, p);
d->pieces_done++;
d->bytes_done += ps;
mark_piece_complete(d, p, false, true);
free_ps(d, p);
*done = true;
if (d->piece_cb) d->piece_cb(d->piece_cb_ctx, p);
notify_files(d, p);
return NAUT_OK;
}
@ -412,6 +465,40 @@ bool naut_download_in_endgame(const naut_download *d) { return d->endgame; }
uint32_t naut_download_num_pieces(const naut_download *d) { return d->num_pieces; }
uint32_t naut_download_pieces_done(const naut_download *d) { return d->pieces_done; }
uint64_t naut_download_bytes_done(const naut_download *d) { return d->bytes_done; }
void naut_download_dump(const naut_download *d, FILE *out) {
if (!d || !out) return;
fprintf(out, "=== download dump: %u/%u pieces verified, %llu/%llu bytes ===\n",
d->pieces_done, d->num_pieces,
(unsigned long long)d->bytes_done, (unsigned long long)d->total);
fprintf(out, "blocks: %llu/%llu received, active_pieces=%u, endgame=%d\n",
(unsigned long long)d->recv_blocks,
(unsigned long long)d->total_blocks,
d->active_pieces, d->endgame);
/* Per-piece assembly state for everything not yet verified. The pieces with
* blocks stuck in flight (or none requested at all) are the ones to chase. */
uint32_t missing = 0, in_progress = 0;
for (uint32_t p = 0; p < d->num_pieces; p++) {
if (naut_bitfield_test(&d->have, p)) continue;
missing++;
pstate *s = d->ps[p];
if (!s) continue;
in_progress++;
uint32_t requested = 0, idle = 0;
for (uint32_t b = 0; b < s->nblocks; b++) {
if (bget(s->recv_bits, b)) continue;
if (s->req_count[b]) requested++;
else idle++;
}
fprintf(out,
" piece %u: %u/%u blocks in, %u requested, %u not requested%s\n",
p, s->nrecv, s->nblocks, requested, idle,
s->verifying ? ", verifying" : "");
}
fprintf(out, "incomplete pieces: %u (%u being assembled, %u untouched)\n",
missing, in_progress, missing - in_progress);
}
size_t naut_download_piece_states(const naut_download *d, uint8_t *out,
size_t capacity) {
if (!d || !out || capacity == 0) return 0;

View file

@ -29,8 +29,7 @@ struct naut_script {
size_t head;
size_t count;
bool stopping;
naut_script_move_file_cb move_file;
void *move_context;
naut_script_host host;
_Atomic uint64_t queued;
_Atomic uint64_t handled;
_Atomic uint64_t dropped;
@ -67,12 +66,12 @@ static int lua_move_file(lua_State *lua) {
if (torrent_id < 0 || file_index < 0 ||
(uint64_t)file_index > UINT32_MAX)
return luaL_error(lua, "move_file arguments out of range");
if (!script->move_file)
if (!script->host.move_file)
return luaL_error(lua, "move_file is unavailable");
naut_err error = script->move_file(script->move_context,
(uint64_t)torrent_id,
(uint32_t)file_index,
destination);
naut_err error = script->host.move_file(script->host.context,
(uint64_t)torrent_id,
(uint32_t)file_index,
destination);
if (error != NAUT_OK)
return luaL_error(lua, "move_file failed: %d", error);
atomic_fetch_add_explicit(&script->move_requests, 1,
@ -80,6 +79,98 @@ static int lua_move_file(lua_State *lua) {
return 0;
}
/* naut.get_labels(torrent_id) -> { "label", ... } (empty table if none). */
static int lua_get_labels(lua_State *lua) {
naut_script *script = lua_script(lua);
lua_Integer torrent_id = luaL_checkinteger(lua, 1);
if (torrent_id < 0)
return luaL_error(lua, "get_labels: torrent id out of range");
size_t count = 0;
char **labels = script->host.labels
? script->host.labels(script->host.context, (uint64_t)torrent_id, &count)
: NULL;
lua_createtable(lua, (int)count, 0);
for (size_t i = 0; i < count; i++) {
lua_pushstring(lua, labels[i]);
lua_rawseti(lua, -2, (int)i + 1);
free(labels[i]);
}
free(labels);
return 1;
}
/* naut.define_settings({ {key=,label=,type=,default=}, ... }) — declare the
* user-configurable variables this script reads, so the host can render a form
* and persist values. Re-declaring replaces the schema. */
static int lua_define_settings(lua_State *lua) {
naut_script *script = lua_script(lua);
luaL_checktype(lua, 1, LUA_TTABLE);
if (!script->host.define_settings) return 0;
size_t count = lua_rawlen(lua, 1);
naut_script_setting_def *defs =
count ? calloc(count, sizeof *defs) : NULL;
/* Stringified defaults need to outlive the per-entry stack churn. */
char **owned = count ? calloc(count, sizeof *owned) : NULL;
if (count && (!defs || !owned)) {
free(defs); free(owned);
return luaL_error(lua, "define_settings: out of memory");
}
size_t n = 0;
for (size_t i = 0; i < count; i++) {
lua_rawgeti(lua, 1, (int)i + 1); /* entry table */
if (!lua_istable(lua, -1)) { lua_pop(lua, 1); continue; }
lua_getfield(lua, -1, "key");
const char *key = lua_tostring(lua, -1);
lua_getfield(lua, -2, "label");
const char *label = lua_tostring(lua, -1);
lua_getfield(lua, -3, "type");
const char *type = lua_tostring(lua, -1);
lua_getfield(lua, -4, "default");
const char *defv;
if (lua_isboolean(lua, -1))
defv = lua_toboolean(lua, -1) ? "true" : "false";
else
defv = lua_tostring(lua, -1); /* nil -> NULL */
if (key) {
defs[n].key = key; /* table strings stay valid while the entry
* table is on the stack (popped after call) */
defs[n].label = label ? label : key;
defs[n].type = type ? type : "string";
owned[n] = defv ? strdup(defv) : NULL;
defs[n].default_value = owned[n];
n++;
}
lua_pop(lua, 5); /* default,type,label,key,entry */
}
script->host.define_settings(script->host.context, defs, n);
for (size_t i = 0; i < count; i++) free(owned[i]);
free(owned);
free(defs);
return 0;
}
/* naut.get_setting(key) -> value (typed) or nil. */
static int lua_get_setting(lua_State *lua) {
naut_script *script = lua_script(lua);
const char *key = luaL_checkstring(lua, 1);
if (!script->host.get_setting) { lua_pushnil(lua); return 1; }
naut_setting_type type = NAUT_SETTING_STRING;
char *value = script->host.get_setting(script->host.context, key, &type);
if (!value) { lua_pushnil(lua); return 1; }
if (type == NAUT_SETTING_BOOL)
lua_pushboolean(lua, strcmp(value, "true") == 0 ||
strcmp(value, "1") == 0);
else if (type == NAUT_SETTING_NUMBER)
lua_pushnumber(lua, strtod(value, NULL));
else
lua_pushstring(lua, value);
free(value);
return 1;
}
static void sandbox(lua_State *lua) {
/* Remove every documented route to the filesystem, subprocesses, native
* module loading, and raw chunk compilation. `load`/`loadstring` are
@ -103,6 +194,15 @@ static void install_api(naut_script *script) {
lua_pushlightuserdata(lua, script);
lua_pushcclosure(lua, lua_move_file, 1);
lua_setfield(lua, -2, "move_file");
lua_pushlightuserdata(lua, script);
lua_pushcclosure(lua, lua_get_labels, 1);
lua_setfield(lua, -2, "get_labels");
lua_pushlightuserdata(lua, script);
lua_pushcclosure(lua, lua_define_settings, 1);
lua_setfield(lua, -2, "define_settings");
lua_pushlightuserdata(lua, script);
lua_pushcclosure(lua, lua_get_setting, 1);
lua_setfield(lua, -2, "get_setting");
lua_setglobal(lua, "naut");
}
@ -206,8 +306,7 @@ static void queue_event(void *opaque, const naut_event *event) {
naut_script *naut_script_create(naut_event_bus *events,
const char *script_path,
size_t queue_capacity,
naut_script_move_file_cb move_file,
void *move_context,
const naut_script_host *host,
naut_err *error) {
if (error) *error = NAUT_ERR_INVAL;
if (!events || !script_path || !*script_path || queue_capacity == 0)
@ -219,8 +318,7 @@ naut_script *naut_script_create(naut_event_bus *events,
}
script->events = events;
script->capacity = queue_capacity;
script->move_file = move_file;
script->move_context = move_context;
if (host) script->host = *host;
script->queue = calloc(queue_capacity, sizeof(*script->queue));
if (!script->queue) {
if (error) *error = NAUT_ERR_NOMEM;

View file

@ -62,11 +62,24 @@ naut_storage *naut_storage_open_opts(const naut_file *files, size_t nfiles,
s->files[i].fd = -1;
s->files[i].direct_fd = -1;
char path[4096];
int n = snprintf(path, sizeof path, "%s/%s", root, files[i].path);
const char *override = opts->overrides ? opts->overrides[i] : NULL;
int n = override
? snprintf(path, sizeof path, "%s", override)
: snprintf(path, sizeof path, "%s/%s", root, files[i].path);
if (n < 0 || n >= (int)sizeof path) goto fail_io;
if (make_parents(path) != NAUT_OK) goto fail_io;
int fd = open(path, O_RDWR | O_CREAT, 0666);
if (fd < 0 && override) {
/* The relocated copy is gone (e.g. external drive absent); fall back
* to the default location and let resume re-download it. */
NAUT_WARN("open relocated %s: %s; falling back to %s root",
path, strerror(errno), files[i].path);
n = snprintf(path, sizeof path, "%s/%s", root, files[i].path);
if (n < 0 || n >= (int)sizeof path || make_parents(path) != NAUT_OK)
goto fail_io;
fd = open(path, O_RDWR | O_CREAT, 0666);
}
if (fd < 0) { NAUT_ERROR("open %s: %s", path, strerror(errno)); goto fail_io; }
if (ftruncate(fd, files[i].length) != 0) {
NAUT_ERROR("ftruncate %s: %s", path, strerror(errno));

View file

@ -1,162 +0,0 @@
#include "naut/tracker.h"
#include "naut/log.h"
#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)
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 for now (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[2048];
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 success = len >= 12 && memcmp(buf, "HTTP/", 5) == 0 &&
buf[9] == '2';
naut_err e = success && body
? naut_tracker_parse_http(body, blen, out)
: NAUT_ERR_PROTO;
free(buf);
return e;
}
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[98], resp[1500];
naut_udp_build_connect(pkt, txid);
if (write(fd, pkt, 16) != 16) { close(fd); return NAUT_ERR_IO; }
ssize_t r = read(fd, resp, sizeof resp);
uint64_t cid;
if (r < 0 || naut_udp_parse_connect(resp, (size_t)r, txid, &cid) != NAUT_OK) {
close(fd); return NAUT_ERR_IO;
}
txid++;
naut_udp_build_announce(pkt, cid, txid, req);
if (write(fd, pkt, 98) != 98) { close(fd); return NAUT_ERR_IO; }
r = read(fd, resp, sizeof resp);
naut_err e = (r < 0) ? NAUT_ERR_IO
: naut_udp_parse_announce(resp, (size_t)r, txid, out);
close(fd);
return e;
}

View file

@ -1,120 +0,0 @@
#include "naut/tracker.h"
#include "naut/bencode.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void naut_tracker_response_free(naut_tracker_response *r) {
free(r->peers); r->peers = NULL; r->num_peers = 0;
free(r->failure); r->failure = NULL;
}
/* percent-encode raw bytes per RFC 3986 (unreserved chars pass through) */
static size_t pct_encode(const uint8_t *in, size_t n, char *out, size_t outsz) {
static const char hx[] = "0123456789ABCDEF";
size_t o = 0;
for (size_t i = 0; i < n; i++) {
uint8_t c = in[i];
bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' ||
c == '.' || c == '~';
if (unreserved) {
if (o + 1 >= outsz) return 0;
out[o++] = (char)c;
} else {
if (o + 3 >= outsz) return 0;
out[o++] = '%'; out[o++] = hx[c >> 4]; out[o++] = hx[c & 15];
}
}
return o;
}
size_t naut_tracker_http_url(const char *base, const naut_announce_req *req,
char *out, size_t outsz) {
static const char *ev[] = { "", "completed", "started", "stopped" };
if (req->event < NAUT_TEV_NONE || req->event > NAUT_TEV_STOPPED) return 0;
char ih[61], pid[61]; /* 20*3 = 60 worst case + NUL */
size_t ihn = pct_encode(req->info_hash, 20, ih, sizeof ih);
size_t pidn = pct_encode(req->peer_id, 20, pid, sizeof pid);
if (!ihn || !pidn) return 0;
ih[ihn] = 0; pid[pidn] = 0;
const char *sep = strchr(base, '?') ? "&" : "?";
int n = snprintf(out, outsz,
"%s%sinfo_hash=%s&peer_id=%s&port=%u&uploaded=%llu&downloaded=%llu"
"&left=%llu&compact=1&numwant=%d%s%s&key=%u",
base, sep, ih, pid, req->port,
(unsigned long long)req->uploaded, (unsigned long long)req->downloaded,
(unsigned long long)req->left, req->numwant < 0 ? 50 : req->numwant,
req->event ? "&event=" : "", ev[req->event], req->key);
if (n < 0 || (size_t)n >= outsz) return 0;
return (size_t)n;
}
static naut_err parse_peers(const naut_bc *peers, naut_tracker_response *out) {
const uint8_t *p; size_t n;
if (naut_bc_get_str(peers, &p, &n)) { /* compact: 6 bytes each */
if (n % 6 != 0) return NAUT_ERR_PROTO;
out->num_peers = n / 6;
out->peers = calloc(out->num_peers ? out->num_peers : 1, sizeof(naut_peer_addr));
if (!out->peers) return NAUT_ERR_NOMEM;
for (size_t i = 0; i < out->num_peers; i++) {
memcpy(out->peers[i].ip, p + i*6, 4);
out->peers[i].port = ((uint16_t)p[i*6+4] << 8) | p[i*6+5];
}
return NAUT_OK;
}
if (peers && peers->type == NAUT_BC_LIST) { /* dict form */
out->peers = calloc(peers->v.list.count ? peers->v.list.count : 1, sizeof(naut_peer_addr));
if (!out->peers) return NAUT_ERR_NOMEM;
for (size_t i = 0; i < peers->v.list.count; i++) {
const naut_bc *pe = naut_bc_list_at(peers, i);
const uint8_t *ips; size_t ipn; int64_t port;
if (!naut_bc_get_str(naut_bc_dict_get(pe, "ip"), &ips, &ipn)) continue;
if (!naut_bc_get_int(naut_bc_dict_get(pe, "port"), &port)) continue;
unsigned a, b, c, dd;
char tmp[64];
if (ipn >= sizeof tmp) continue;
memcpy(tmp, ips, ipn); tmp[ipn] = 0;
if (sscanf(tmp, "%u.%u.%u.%u", &a, &b, &c, &dd) != 4) continue;
if (a > 255 || b > 255 || c > 255 || dd > 255 ||
port <= 0 || port > UINT16_MAX) continue;
naut_peer_addr *pa = &out->peers[out->num_peers++];
pa->ip[0]=(uint8_t)a; pa->ip[1]=(uint8_t)b; pa->ip[2]=(uint8_t)c; pa->ip[3]=(uint8_t)dd;
pa->port = (uint16_t)port;
}
return NAUT_OK;
}
return NAUT_ERR_PROTO;
}
naut_err naut_tracker_parse_http(const uint8_t *body, size_t len,
naut_tracker_response *out) {
memset(out, 0, sizeof(*out));
out->seeders = out->leechers = -1;
naut_bc_doc *doc = NULL;
naut_err e = naut_bc_parse(body, len, &doc);
if (e != NAUT_OK) return e;
const naut_bc *root = naut_bc_root(doc);
const uint8_t *fp; size_t fn;
if (naut_bc_get_str(naut_bc_dict_get(root, "failure reason"), &fp, &fn)) {
out->failure = malloc(fn + 1);
if (out->failure) { memcpy(out->failure, fp, fn); out->failure[fn] = 0; }
naut_bc_free(doc);
return NAUT_ERR_PROTO; /* tracker reported failure */
}
int64_t iv = 0;
naut_bc_get_int(naut_bc_dict_get(root, "interval"), &iv);
out->interval = (int32_t)iv;
int64_t sc;
if (naut_bc_get_int(naut_bc_dict_get(root, "complete"), &sc)) out->seeders = (int32_t)sc;
if (naut_bc_get_int(naut_bc_dict_get(root, "incomplete"), &sc)) out->leechers = (int32_t)sc;
e = parse_peers(naut_bc_dict_get(root, "peers"), out);
naut_bc_free(doc);
if (e != NAUT_OK) { naut_tracker_response_free(out); return e; }
return NAUT_OK;
}

View file

@ -1,81 +0,0 @@
#include "naut/tracker.h"
#include <stdlib.h>
#include <string.h>
#define UDP_PROTOCOL_ID 0x41727101980ULL /* BEP-15 magic */
#define ACTION_CONNECT 0
#define ACTION_ANNOUNCE 1
#define ACTION_ERROR 3
static void wr16(uint8_t *p, uint16_t v) { p[0]=(uint8_t)(v>>8); p[1]=(uint8_t)v; }
static void wr32(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 wr64(uint8_t *p, uint64_t v) { wr32(p, (uint32_t)(v>>32)); wr32(p+4, (uint32_t)v); }
static uint32_t rd32(const uint8_t *p) {
return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)|((uint32_t)p[2]<<8)|p[3];
}
static uint64_t rd64(const uint8_t *p) { return ((uint64_t)rd32(p)<<32) | rd32(p+4); }
void naut_udp_build_connect(uint8_t out[16], uint32_t txid) {
wr64(out, UDP_PROTOCOL_ID);
wr32(out + 8, ACTION_CONNECT);
wr32(out + 12, txid);
}
naut_err naut_udp_parse_connect(const uint8_t *in, size_t len, uint32_t txid,
uint64_t *connection_id) {
if (len < 16) return NAUT_ERR_PROTO;
if (rd32(in) != ACTION_CONNECT) return NAUT_ERR_PROTO;
if (rd32(in + 4) != txid) return NAUT_ERR_PROTO;
*connection_id = rd64(in + 8);
return NAUT_OK;
}
void naut_udp_build_announce(uint8_t out[98], uint64_t connection_id,
uint32_t txid, const naut_announce_req *req) {
wr64(out + 0, connection_id);
wr32(out + 8, ACTION_ANNOUNCE);
wr32(out + 12, txid);
memcpy(out + 16, req->info_hash, 20);
memcpy(out + 36, req->peer_id, 20);
wr64(out + 56, req->downloaded);
wr64(out + 64, req->left);
wr64(out + 72, req->uploaded);
wr32(out + 80, (uint32_t)req->event);
wr32(out + 84, 0); /* IP: 0 = source */
wr32(out + 88, req->key);
wr32(out + 92, (uint32_t)(req->numwant < 0 ? 50 : req->numwant));
wr16(out + 96, req->port);
}
naut_err naut_udp_parse_announce(const uint8_t *in, size_t len, uint32_t txid,
naut_tracker_response *out) {
memset(out, 0, sizeof(*out));
out->seeders = out->leechers = -1;
if (len < 8) return NAUT_ERR_PROTO;
uint32_t action = rd32(in);
if (rd32(in + 4) != txid) return NAUT_ERR_PROTO;
if (action == ACTION_ERROR) {
size_t mn = len - 8;
out->failure = malloc(mn + 1);
if (out->failure) { memcpy(out->failure, in + 8, mn); out->failure[mn] = 0; }
return NAUT_ERR_PROTO;
}
if (action != ACTION_ANNOUNCE || len < 20 || (len - 20) % 6 != 0)
return NAUT_ERR_PROTO;
out->interval = (int32_t)rd32(in + 8);
out->leechers = (int32_t)rd32(in + 12);
out->seeders = (int32_t)rd32(in + 16);
size_t avail = (len - 20) / 6;
out->peers = calloc(avail ? avail : 1, sizeof(naut_peer_addr));
if (!out->peers) return NAUT_ERR_NOMEM;
for (size_t i = 0; i < avail; i++) {
const uint8_t *p = in + 20 + i*6;
memcpy(out->peers[i].ip, p, 4);
out->peers[i].port = ((uint16_t)p[4] << 8) | p[5];
}
out->num_peers = avail;
return NAUT_OK;
}