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

4
.gitignore vendored
View file

@ -14,4 +14,6 @@ compile_commands.json
/package/
/package.zip
# Stray torrents dropped at the repo root (fixtures under tests/ stay tracked)
torrents/*.torrent
torrents/*.torrent
# Downloaded torrent data (capital-D dir used at runtime)
/Downloads/

View file

@ -39,6 +39,23 @@ if(NOT URING_LIB OR NOT URING_INC)
endif()
find_package(OpenSSL REQUIRED COMPONENTS Crypto)
# --- external download engine + tracker/DHT protocol libraries --------------
# torrent-peer: multi-peer download engine (engine.h) — replaces Naut's own peer
# poll loop, request pipeline, and MSE transport.
# torrent-tracker: tracker/DHT wire codec (tracker.h) — drives Naut's announce
# and get_peers glue in src/discovery.
set(PEER_NATIVE ${NAUT_NATIVE} CACHE BOOL "" FORCE)
set(TRACKER_NATIVE ${NAUT_NATIVE} CACHE BOOL "" FORCE)
set(TRACKER_TESTS OFF CACHE BOOL "" FORCE)
if(NAUT_SAN STREQUAL "address" OR NAUT_SAN STREQUAL "undefined")
set(PEER_ASAN ON CACHE BOOL "" FORCE)
set(TRACKER_ASAN ON CACHE BOOL "" FORCE)
endif()
add_subdirectory(${CMAKE_SOURCE_DIR}/../torrent-peer
${CMAKE_BINARY_DIR}/torrent-peer)
add_subdirectory(${CMAKE_SOURCE_DIR}/../torrent-tracker
${CMAKE_BINARY_DIR}/torrent-tracker)
if(NAUT_STANDALONE)
include(FetchContent)
@ -141,24 +158,16 @@ target_link_libraries(naut_bencode PUBLIC naut_core)
add_library(naut_metainfo STATIC src/metainfo/metainfo.c src/metainfo/magnet.c)
target_link_libraries(naut_metainfo PUBLIC naut_bencode naut_crypto)
# --- tracker: HTTP + UDP announce (codec + blocking fetch) ------------------
add_library(naut_tracker STATIC
src/tracker/tracker.c src/tracker/udp.c src/tracker/fetch.c)
target_link_libraries(naut_tracker PUBLIC naut_bencode)
# --- discovery: tracker announce + DHT get_peers glue over torrent-tracker ---
add_library(naut_discovery STATIC
src/discovery/tracker_client.c src/discovery/dht_client.c)
target_link_libraries(naut_discovery PUBLIC naut_core torrenttracker)
# --- dht: BEP-5 KRPC codec + bounded iterative peer lookup ------------------
add_library(naut_dht STATIC src/dht/dht.c src/dht/fetch.c)
target_link_libraries(naut_dht PUBLIC naut_bencode naut_tracker)
# --- peer: wire protocol codec (sans-IO) ------------------------------------
# --- peer: wire protocol codec (sans-IO), retained for magnet metadata -------
add_library(naut_peer STATIC
src/peer/wire.c src/peer/extension.c src/peer/metadata.c src/peer/pipeline.c)
src/peer/wire.c src/peer/extension.c src/peer/metadata.c)
target_link_libraries(naut_peer PUBLIC
naut_core naut_crypto naut_bencode naut_tracker m)
# MSE is optional at the application boundary and is the only OpenSSL user.
add_library(naut_mse STATIC src/peer/mse.c)
target_link_libraries(naut_mse PUBLIC naut_peer OpenSSL::Crypto)
naut_core naut_crypto naut_bencode m)
# --- storage: file backend --------------------------------------------------
add_library(naut_storage STATIC src/storage/storage.c)
@ -209,26 +218,12 @@ target_include_directories(naut_webui PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_link_libraries(naut_webui PRIVATE ${NAUT_JANSSON_TARGET} pthread)
set_target_properties(naut_webui PROPERTIES PREFIX "")
# --- echo: Phase 1 gate (io_uring echo server on the buffer pool) -----------
# add_executable(naut_echo apps/echo/main.c)
# target_link_libraries(naut_echo PRIVATE naut_platform naut_core)
# --- leech: Phase 3 gate (single-peer download, byte-correct + verified) ----
# add_executable(naut_leech apps/leech/main.c)
# target_link_libraries(naut_leech PRIVATE
# naut_piece naut_peer naut_mse naut_metainfo)
# --- swarm: Phase 4 gate (multi-peer download, rarest-first + endgame) -------
# --- swarm: multi-peer download driver over the torrent-peer engine ---------
add_library(naut_swarm_engine STATIC apps/swarm/main.c)
target_compile_definitions(naut_swarm_engine PRIVATE NAUT_SWARM_LIBRARY)
target_link_libraries(naut_swarm_engine PUBLIC
naut_piece naut_peer naut_metainfo naut_tracker naut_dht naut_system
naut_session)
# add_executable(naut_swarm apps/swarm/main.c)
# target_link_libraries(naut_swarm PRIVATE
# naut_piece naut_peer naut_metainfo naut_tracker naut_dht naut_system
# naut_session)
naut_piece naut_peer naut_metainfo naut_discovery naut_system
naut_session torrentpeer)
# --- daemon + CLI: Phase 7 extensibility surface ---------------------------
add_executable(nautd apps/nautd/main.c)
@ -250,10 +245,6 @@ add_executable(test_worker tests/unit/test_worker.c)
target_link_libraries(test_worker PRIVATE naut_core naut_crypto)
add_test(NAME test_worker COMMAND test_worker)
add_executable(test_pipeline tests/unit/test_pipeline.c)
target_link_libraries(test_pipeline PRIVATE naut_peer)
add_test(NAME test_pipeline COMMAND test_pipeline)
add_executable(test_rpc tests/unit/test_rpc.c)
target_link_libraries(test_rpc PRIVATE naut_rpc)
add_test(NAME test_rpc COMMAND test_rpc)
@ -300,18 +291,6 @@ add_executable(test_extension tests/unit/test_extension.c)
target_link_libraries(test_extension PRIVATE naut_peer)
add_test(NAME test_extension COMMAND test_extension)
add_executable(test_mse tests/unit/test_mse.c)
target_link_libraries(test_mse PRIVATE naut_mse)
add_test(NAME test_mse COMMAND test_mse)
add_executable(test_tracker tests/unit/test_tracker.c)
target_link_libraries(test_tracker PRIVATE naut_tracker)
add_test(NAME test_tracker COMMAND test_tracker)
add_executable(test_dht tests/unit/test_dht.c)
target_link_libraries(test_dht PRIVATE naut_dht)
add_test(NAME test_dht COMMAND test_dht)
add_executable(test_storage tests/unit/test_storage.c)
target_link_libraries(test_storage PRIVATE naut_storage)
add_test(NAME test_storage COMMAND test_storage)
@ -332,48 +311,6 @@ add_executable(test_picker tests/unit/test_picker.c)
target_link_libraries(test_picker PRIVATE naut_piece)
add_test(NAME test_picker COMMAND test_picker)
# Phase 3 interop gate: download from a real libtorrent seed (SKIPs without it).
# These gates exercise the standalone naut_leech/naut_swarm/naut_echo binaries,
# which are currently folded into the daemon — register them only when built.
if(TARGET naut_leech)
add_test(NAME interop_leech
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_interop.sh $<TARGET_FILE:naut_leech>)
set_tests_properties(interop_leech PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180)
add_test(NAME interop_mse
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_mse.sh $<TARGET_FILE:naut_leech>)
set_tests_properties(interop_mse PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
endif()
if(TARGET naut_swarm)
add_test(NAME interop_magnet_dht
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_magnet_dht.sh
$<TARGET_FILE:naut_swarm>)
set_tests_properties(interop_magnet_dht PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
# Phase 4 interop gate: both libtorrent seeds must contribute to one download.
add_test(NAME interop_swarm
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_swarm.sh $<TARGET_FILE:naut_swarm>)
set_tests_properties(interop_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
add_test(NAME interop_tracker_swarm
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_tracker_swarm.sh
$<TARGET_FILE:naut_swarm> http)
set_tests_properties(interop_tracker_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
add_test(NAME interop_udp_tracker_swarm
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_tracker_swarm.sh
$<TARGET_FILE:naut_swarm> udp)
set_tests_properties(interop_udp_tracker_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
endif()
if(TARGET naut_echo)
add_test(NAME interop_echo_scale
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_echo_scale.sh
$<TARGET_FILE:naut_echo>)
set_tests_properties(interop_echo_scale PROPERTIES TIMEOUT 30)
endif()
add_test(NAME phase7_extensibility
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_phase7.sh
$<TARGET_FILE:nautd> $<TARGET_FILE:nautctl>

10
ISSUES.md Normal file
View file

@ -0,0 +1,10 @@
- ✅ Set Location doesn't work, it should also show the current location.
- ⬛ I need a way to modify a category (including Uncategorozied).
- ⬛ Adding a Category means I can't have none selected on adding a torrent.
- ⬛ Category default download location does nothing as changing it doesn't change the download location. Download location should be greyed out by default with the default location shown. Clicking should allow you to change the location. If the location is set it shouldn't change if the category is changed.
- ✅ Automation variables at half window width makes the script unseeable. It should be displayed above the script if the window is to narrow.
- ⬛ We need to implement the RSS and Search Tabs.
- ⬛ I need to be able to add Tags on adding a torrent.
- ⬛ Categories aren't saved across restart.
- ⬛ Pausing a torrent will go back into Downloading and Seeding.
- ⬛ A torrents data could overlap with another existing torrent. This should be blocked to avoid

View file

@ -1,214 +0,0 @@
/* naut_echo — Phase 1 gate.
*
* A single-reactor io_uring echo server that proves the foundation works end to
* end: multishot accept, recv/send driven entirely off the page-aligned buffer
* pool with ZERO per-operation allocation in steady state. Throughput on
* loopback should be limited by memory bandwidth / the single core, not by the
* allocator or syscalls.
*
* It is intentionally one-in-flight-op-per-connection (recv -> send -> recv).
* The real peer reactor (later phase) uses multishot recv + provided buffers
* and pipelines; this is the minimal honest exercise of the primitives.
*
* usage: naut_echo [port] (default 9000)
*/
#include "naut/uring.h"
#include "naut/net.h"
#include "naut/buf.h"
#include "naut/log.h"
#include "naut/system.h"
#include <liburing.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#define ECHO_BLOCK (128u * 1024u)
#define ECHO_BUFS 4096u
#define RING_ENTRIES 4096u
/* user_data tagging: low 3 bits = op, high bits = conn* (16-byte aligned). */
enum { TAG_ACCEPT = 1, TAG_RECV = 2, TAG_SEND = 3 };
#define UD(p, tag) ((__u64)(uintptr_t)(p) | (unsigned)(tag))
#define UD_TAG(ud) ((unsigned)((ud) & 0x7u))
#define UD_PTR(ud) ((conn *)(uintptr_t)((ud) & ~(__u64)0x7u))
typedef struct conn {
int fd;
uint32_t sent; /* bytes of buf->len already written (partial sends) */
naut_buf *buf;
bool awaiting_notif;
bool recv_fixed; /* the in-flight recv used the fixed buffer */
} conn;
static volatile sig_atomic_t g_stop = 0;
static void on_signal(int s) { (void)s; g_stop = 1; }
static naut_bufpool *g_pool;
static _Atomic uint64_t g_bytes = 0, g_conns = 0, g_zc_copied = 0;
static void arm_recv(naut_ring *owner, conn *c) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&owner->ring);
c->recv_fixed =
naut_ring_prep_recv(owner, sqe, c->fd, c->buf->data, c->buf->cap, 0);
io_uring_sqe_set_data64(sqe, UD(c, TAG_RECV));
}
static void arm_send(naut_ring *owner, conn *c) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&owner->ring);
c->awaiting_notif = naut_ring_prep_send(
owner, sqe, c->fd, c->buf->data + c->sent,
c->buf->len - c->sent, MSG_NOSIGNAL, true);
io_uring_sqe_set_data64(sqe, UD(c, TAG_SEND));
}
static void conn_close(conn *c) {
close(c->fd);
naut_buf_put(c->buf);
free(c);
}
int main(int argc, char **argv) {
uint16_t port = (argc > 1) ? (uint16_t)atoi(argv[1]) : 9000;
int cpu = getenv("NAUT_CPU") ? atoi(getenv("NAUT_CPU")) : -1;
int numa_node =
getenv("NAUT_NUMA_NODE") ? atoi(getenv("NAUT_NUMA_NODE")) : -1;
bool sqpoll = getenv("NAUT_SQPOLL") != NULL;
bool hugepages = getenv("NAUT_HUGEPAGES") != NULL;
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
signal(SIGPIPE, SIG_IGN);
if (cpu >= 0 && naut_pin_current_thread(cpu) != NAUT_OK)
NAUT_WARN("failed to pin reactor to CPU %d", cpu);
naut_ring r;
if (naut_ring_init_cpu(&r, RING_ENTRIES, sqpoll, cpu) != NAUT_OK)
return 1;
if (naut_ring_probe(&r) != NAUT_OK) { naut_ring_close(&r); return 1; }
struct io_uring *ring = &r.ring;
int lfd = naut_net_listen(port, 1024, true);
if (lfd < 0) { naut_ring_close(&r); return 1; }
g_pool = naut_bufpool_create_on_node(
ECHO_BLOCK, ECHO_BUFS, hugepages, numa_node);
if (!g_pool) { close(lfd); naut_ring_close(&r); return 1; }
(void)naut_ring_register_bufpool(&r, g_pool);
/* prime the multishot accept */
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_multishot_accept(sqe, lfd, NULL, NULL, 0);
io_uring_sqe_set_data64(sqe, UD(NULL, TAG_ACCEPT));
NAUT_INFO("echo listening on :%u", port);
while (!g_stop) {
int rc = io_uring_submit_and_wait(ring, 1);
if (rc < 0 && rc != -EINTR) { NAUT_ERROR("submit_and_wait: %s", strerror(-rc)); break; }
unsigned head, count = 0;
struct io_uring_cqe *cqe;
io_uring_for_each_cqe(ring, head, cqe) {
count++;
__u64 ud = cqe->user_data;
int res = cqe->res;
switch (UD_TAG(ud)) {
case TAG_ACCEPT: {
if (res < 0) {
if (res != -ECANCELED) NAUT_WARN("accept: %s", strerror(-res));
} else {
int cfd = res;
naut_net_tune_peer(cfd);
naut_buf *b = naut_buf_get(g_pool);
if (!b) { NAUT_WARN("pool exhausted, dropping conn"); close(cfd); }
else {
conn *c = calloc(1, sizeof(*c));
c->fd = cfd; c->buf = b;
atomic_fetch_add(&g_conns, 1);
arm_recv(&r, c);
}
}
/* re-arm if the kernel dropped the multishot registration */
if (!(cqe->flags & IORING_CQE_F_MORE)) {
struct io_uring_sqe *s = io_uring_get_sqe(ring);
io_uring_prep_multishot_accept(s, lfd, NULL, NULL, 0);
io_uring_sqe_set_data64(s, UD(NULL, TAG_ACCEPT));
}
break;
}
case TAG_RECV: {
conn *c = UD_PTR(ud);
if (res <= 0) {
/* A fixed-buffer recv rejected with -EINVAL means this
* kernel doesn't support IORING_RECVSEND_FIXED_BUF on plain
* recv. Disable it ring-wide and retry THIS connection
* unfixed. We key off the per-conn flag, not the ring flag,
* so every connection that armed a fixed recv before the
* flag flipped recovers too (otherwise all but the first
* would be torn down). */
if (res == -EINVAL && c->recv_fixed) {
if (r.recv_fixed) {
NAUT_WARN("fixed-buffer recv unsupported at runtime; "
"falling back to normal recv");
r.recv_fixed = false;
}
arm_recv(&r, c);
break;
}
if (res < 0)
NAUT_WARN("recv completion: %s", strerror(-res));
conn_close(c);
break;
}
c->buf->len = (uint32_t)res;
c->sent = 0;
arm_send(&r, c);
break;
}
case TAG_SEND: {
conn *c = UD_PTR(ud);
if (cqe->flags & IORING_CQE_F_NOTIF) {
if (res & IORING_NOTIF_USAGE_ZC_COPIED) {
uint64_t copied =
atomic_fetch_add(&g_zc_copied, 1) + 1;
if (copied == 8) {
NAUT_WARN("SEND_ZC is copying on this transport; "
"disabling it for this ring");
r.send_zc = false;
}
}
c->awaiting_notif = false;
if (c->sent < c->buf->len) arm_send(&r, c);
else { c->buf->len = 0; arm_recv(&r, c); }
break;
}
if (res <= 0) { conn_close(c); break; }
c->sent += (uint32_t)res;
atomic_fetch_add(&g_bytes, (uint64_t)res);
if (!c->awaiting_notif) {
if (c->sent < c->buf->len) arm_send(&r, c);
else { c->buf->len = 0; arm_recv(&r, c); }
}
break;
}
default:
NAUT_PANIC("bad user_data tag %u", UD_TAG(ud));
}
}
io_uring_cq_advance(ring, count);
}
NAUT_INFO("shutting down: %llu conns, %llu bytes echoed, %llu SEND_ZC copied notifications",
(unsigned long long)atomic_load(&g_conns),
(unsigned long long)atomic_load(&g_bytes),
(unsigned long long)atomic_load(&g_zc_copied));
close(lfd);
naut_ring_unregister_buffers(&r);
naut_bufpool_destroy(g_pool);
naut_ring_close(&r);
return 0;
}

View file

@ -1,212 +0,0 @@
/* naut_leech — Phase 3 gate: download a torrent from a single peer and write a
* byte-correct, hash-verified file to disk.
*
* Blocking-socket driver around the sans-IO peer codec + download engine. The
* point of this phase is protocol correctness and interop (it downloads from a
* libtorrent seed in the integration test), not peak throughput the io_uring
* reactor that drives thousands of these comes in Phase 6.
*
* usage: naut_leech [--mse] <file.torrent> <output-dir> <ip> <port>
*/
#include "naut/metainfo.h"
#include "naut/storage.h"
#include "naut/piece.h"
#include "naut/peer.h"
#include "naut/mse.h"
#include "naut/log.h"
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#define PIPELINE_DEPTH 512 /* outstanding requests (~8 MiB in flight) */
static double now(void) {
struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec + t.tv_nsec * 1e-9;
}
static uint8_t *slurp(const char *path, size_t *len) {
FILE *f = fopen(path, "rb");
if (!f) { NAUT_ERROR("open %s: %s", path, strerror(errno)); return NULL; }
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
uint8_t *b = malloc(n);
if (fread(b, 1, n, f) != (size_t)n) { fclose(f); free(b); return NULL; }
fclose(f); *len = (size_t)n; return b;
}
static int connect_peer(const char *ip, uint16_t port) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct sockaddr_in a; memset(&a, 0, sizeof a);
a.sin_family = AF_INET; a.sin_port = htons(port);
if (inet_pton(AF_INET, ip, &a.sin_addr) != 1) { close(fd); return -1; }
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) {
NAUT_ERROR("connect %s:%u: %s", ip, port, strerror(errno));
close(fd); return -1;
}
int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
return fd;
}
/* send up to PIPELINE_DEPTH outstanding requests */
static bool refill(int fd, naut_mse_stream *mse,
naut_download *d, int *outstanding) {
uint32_t idx, begin, len;
while (*outstanding < PIPELINE_DEPTH) {
if (!naut_download_next_request(d, &idx, &begin, &len)) break;
uint8_t req[17];
naut_peer_msg_request(req, idx, begin, len);
if (!naut_mse_send_all(fd, mse, req, sizeof req)) return false;
(*outstanding)++;
}
return true;
}
int main(int argc, char **argv) {
bool use_mse = argc > 1 && strcmp(argv[1], "--mse") == 0;
int arg = use_mse ? 2 : 1;
if (argc - arg != 4) {
fprintf(stderr, "usage: %s [--mse] <file.torrent> <output-dir> <ip> <port>\n",
argv[0]);
return 2;
}
naut_log_set_level(NAUT_LOG_INFO);
size_t tlen;
uint8_t *tor = slurp(argv[arg], &tlen);
if (!tor) return 1;
naut_metainfo mi;
if (naut_metainfo_parse(tor, tlen, &mi) != NAUT_OK) { NAUT_ERROR("bad torrent"); return 1; }
free(tor);
char hex[41]; naut_infohash_v1_hex(&mi, hex);
NAUT_INFO("torrent '%s': %u pieces, %lld bytes, infohash %s",
mi.name, mi.num_pieces, (long long)mi.total_length, hex);
naut_err err;
naut_storage_opts storage_opts = {
.direct_io = getenv("NAUT_DIRECT_IO") != NULL,
.preallocate = true,
};
naut_storage *st = naut_storage_open_opts(
mi.files, mi.num_files, argv[arg + 1], &storage_opts, &err);
if (!st) { NAUT_ERROR("storage: %s", naut_strerror(err)); return 1; }
naut_download *d = naut_download_create(&mi, st);
if (!d) return 1;
int fd = connect_peer(argv[arg + 2], (uint16_t)atoi(argv[arg + 3]));
if (fd < 0) return 1;
/* handshake */
uint8_t peerid[20]; memcpy(peerid, "-NT0001-", 8);
for (int i = 8; i < 20; i++) peerid[i] = (uint8_t)(rand() & 0xff);
uint8_t hs[NAUT_HANDSHAKE_LEN];
naut_peer_handshake_build(hs, mi.infohash_v1, peerid, 0);
naut_mse_stream mse = {0};
uint8_t remote_hs[NAUT_HANDSHAKE_LEN];
bool hs_done = false;
if (use_mse) {
naut_err mse_err = naut_mse_client_handshake(
fd, mi.infohash_v1, peerid, 0, &mse, remote_hs);
if (mse_err != NAUT_OK) {
NAUT_ERROR("MSE handshake failed: %s", naut_strerror(mse_err));
return 1;
}
hs_done = true;
NAUT_INFO("MSE/RC4 peer transport established");
uint8_t intr[5];
naut_peer_msg_simple(intr, NAUT_MSG_INTERESTED);
if (!naut_mse_send_all(fd, &mse, intr, sizeof intr)) {
NAUT_ERROR("interested send failed");
return 1;
}
} else if (!naut_mse_send_all(fd, &mse, hs, sizeof hs)) {
NAUT_ERROR("handshake send failed");
return 1;
}
/* recv buffer */
size_t cap = 4u << 20, len = 0;
uint8_t *buf = malloc(cap);
bool unchoked = false;
int outstanding = 0;
double t0 = now();
while (!naut_download_complete(d)) {
if (len == cap) { cap *= 2; buf = realloc(buf, cap); }
ssize_t r = naut_mse_recv(fd, &mse, buf + len, cap - len);
if (r < 0) { NAUT_ERROR("recv: %s", strerror(errno)); break; }
if (r == 0) { NAUT_ERROR("peer closed (%.1f%% done)",
100.0 * naut_download_pieces_done(d) / mi.num_pieces); break; }
len += (size_t)r;
size_t pos = 0;
if (!hs_done) {
if (len < NAUT_HANDSHAKE_LEN) continue;
uint8_t ih[20], pid[20];
if (!naut_peer_handshake_parse(buf, ih, pid, NULL) ||
memcmp(ih, mi.infohash_v1, 20) != 0) {
NAUT_ERROR("handshake mismatch"); break;
}
pos = NAUT_HANDSHAKE_LEN;
hs_done = true;
uint8_t intr[5]; naut_peer_msg_simple(intr, NAUT_MSG_INTERESTED);
if (!naut_mse_send_all(fd, &mse, intr, 5)) break;
}
/* parse all complete messages */
for (;;) {
naut_msg m;
int c = naut_peer_msg_parse(buf + pos, len - pos, &m);
if (c == 0) break;
if (c < 0) { NAUT_ERROR("protocol error"); goto done; }
pos += (size_t)c;
switch (m.type) {
case NAUT_MSG_UNCHOKE: unchoked = true; break;
case NAUT_MSG_CHOKE: unchoked = false; break;
case NAUT_MSG_PIECE: {
outstanding--;
bool pdone = false;
naut_err e = naut_download_on_block(d, m.index, m.begin, m.payload,
(uint32_t)m.payload_len, &pdone);
if (e != NAUT_OK) { NAUT_ERROR("block rejected: %s", naut_strerror(e)); goto done; }
break;
}
default: break; /* bitfield/have/keepalive/port: ignore for a seed */
}
}
/* compact consumed bytes */
memmove(buf, buf + pos, len - pos);
len -= pos;
if (unchoked && !refill(fd, &mse, d, &outstanding)) {
NAUT_ERROR("request send failed"); break;
}
}
done:;
double dt = now() - t0;
bool ok = naut_download_complete(d);
if (ok) {
double mb = (double)mi.total_length / 1e6;
NAUT_INFO("COMPLETE: %u/%u pieces, %.1f MB in %.2fs (%.1f MB/s), all SHA-1 verified",
naut_download_pieces_done(d), mi.num_pieces, mb, dt, mb / dt);
} else {
NAUT_ERROR("INCOMPLETE: %u/%u pieces", naut_download_pieces_done(d), mi.num_pieces);
}
naut_storage_sync(st);
close(fd);
naut_download_destroy(d);
naut_storage_close(st);
naut_metainfo_free(&mi);
free(buf);
return ok ? 0 : 1;
}

View file

@ -17,6 +17,7 @@ static void usage(const char *program) {
" add SOURCE OUTPUT [IP:PORT ...]\n"
" list\n"
" show TORRENT_ID\n"
" dump TORRENT_ID\n"
" remove TORRENT_ID\n"
" script PATH | unscript\n"
" status | events | shutdown\n"
@ -93,6 +94,7 @@ int main(int argc, char **argv) {
return stream_events(socket_path);
json_t *params = NULL;
bool raw_dump = false;
if (strcmp(method, "add") == 0) {
if (arg + 1 >= argc) { usage(argv[0]); return 2; }
method = "add_torrent";
@ -113,14 +115,16 @@ int main(int argc, char **argv) {
method = "torrents";
params = json_object();
} else if (strcmp(method, "show") == 0 ||
strcmp(method, "dump") == 0 ||
strcmp(method, "remove") == 0) {
json_int_t id;
if (arg + 1 != argc || !parse_id(argv[arg], &id)) {
usage(argv[0]);
return 2;
}
method = strcmp(method, "show") == 0 ? "torrent" :
"remove_torrent";
if (strcmp(method, "show") == 0) method = "torrent";
else if (strcmp(method, "dump") == 0) { method = "dump_torrent"; raw_dump = true; }
else method = "remove_torrent";
params = json_pack("{s:I}", "torrent_id", id);
} else if (strcmp(method, "script") == 0) {
if (arg + 1 != argc) { usage(argv[0]); return 2; }
@ -145,8 +149,19 @@ int main(int argc, char **argv) {
fprintf(stderr, "nautctl: RPC failed: %s\n", naut_strerror(error));
return 1;
}
int result = print_json(reply);
bool ok = json_is_true(json_object_get(reply, "ok"));
int result;
/* `dump` returns a multi-line text blob; print it raw instead of escaped JSON. */
const char *dump = raw_dump
? json_string_value(json_object_get(
json_object_get(reply, "result"), "dump"))
: NULL;
if (dump) {
fputs(dump, stdout);
result = 0;
} else {
result = print_json(reply);
}
json_decref(reply);
return result || !ok;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -200,6 +200,78 @@ nautctl --socket /tmp/nautd.sock add file.torrent /downloads/42
If `torrent_id` is unknown or is being removed, `naut.move_file` raises a
`move_file failed` error in the hook.
### `naut.get_labels(torrent_id)`
Return the torrent's labels as a **plain array (table) of strings**. Labels are
the user-assigned tags/category set in the web UI (or via the `set_labels` RPC);
they are stored on the daemon, persisted across restarts, and surfaced here so a
script can branch on them (e.g. route a finished file by its label).
**Arguments**
| # | Name | Lua type | Notes |
|---|---|---|---|
| 1 | `torrent_id` | `integer` | must be ≥ 0 |
**Return value:** a sequence table of strings, e.g. `{"anime", "airing"}`. The
table is **empty** (`#labels == 0`) when the torrent has no labels or is unknown
— it is never `nil`, so it is always safe to iterate.
```lua
function on_file_complete(event)
local labels = naut.get_labels(event.torrent_id)
for _, label in ipairs(labels) do
if label == "anime" then
naut.move_file(event.torrent_id, event.index,
"/archive/anime/" .. event.path)
return
end
end
end
```
Labels reflect the daemon's current state at call time (re-read on every call),
so a script always sees the latest assignment.
### `naut.define_settings({ ... })`
Declare the user-configurable variables the script reads, so they can be edited
in the web UI (**Automation ▸ Settings**) instead of by hand in the source. Pass
an array of entries:
| Field | Lua type | Meaning |
|---|---|---|
| `key` | `string` | identifier passed to `naut.get_setting` |
| `label` | `string` | human label shown in the form (defaults to `key`) |
| `type` | `string` | `"string"`, `"bool"`, or `"number"` (drives the widget + the type returned by `get_setting`) |
| `default` | string/bool/number | value used until the user sets one |
Call it once at load time (re-declaring replaces the schema). The values the user
saves persist across restarts, independently of the script source.
### `naut.get_setting(key)`
Return the current value of a declared setting: the user-saved value if present,
otherwise the declared default. The result is **typed** per the schema — a Lua
`boolean` for `bool`, a `number` for `number`, a `string` otherwise — or `nil`
if the key was never declared. Re-read it on each use so live edits take effect
without reloading the script.
```lua
naut.define_settings({
{ key = "library_root", label = "Library root", type = "string",
default = "/media/anime" },
{ key = "only_video", label = "Only video files", type = "bool",
default = true },
})
function on_file_complete(event)
if naut.get_setting("only_video") and not is_video(event.path) then return end
local root = naut.get_setting("library_root")
naut.move_file(event.torrent_id, event.index, root .. "/" .. basename(event.path))
end
```
---
## 7. Error handling & observability

View file

@ -9,6 +9,9 @@
-- file's last piece verifies, episodes are sorted the moment they're done —
-- without waiting for the rest of the torrent.
--
-- By default it only sorts torrents you have labelled "anime" (REQUIRE_LABEL
-- below), read via naut.get_labels(), so non-anime downloads are left untouched.
--
-- Install:
-- nautd --socket /tmp/nautd.sock
-- nautctl --socket /tmp/nautd.sock script examples/anime_sort.lua
@ -19,13 +22,42 @@
-- test_anime_sort.lua asserts they agree.
----------------------------------------------------------------------
-- CONFIG — edit these
-- CONFIG — these are exposed in the web UI (Automation ▸ Settings) through
-- naut.define_settings, so you can change them there WITHOUT editing this file.
-- The values below are only the defaults used until you set them in the UI.
----------------------------------------------------------------------
local SORTED_ROOT = "/workspaces/source/ai-garbo/Naut-Torrent/sorted" -- destination library root
local ONLY_VIDEO = true -- skip non-video files (subs, nfo, samples)
local KEEP_ORIGINAL_NAME = false -- true: keep the original filename;
-- false: rename to "Title - SNNENN.ext"
local DEFAULTS = {
sorted_root = "/workspaces/source/ai-garbo/Naut-Torrent/Downloads/Sorted/",
only_video = true, -- skip non-video files (subs, nfo, samples)
keep_original_name = false, -- false: rename to "Title - SNNENN.ext"
require_label = "anime", -- only sort torrents with this label
-- (case-insensitive); blank = any torrent
}
-- Read a setting from the host live (so UI edits apply without a reload),
-- falling back to the default when unset or running on an older daemon.
local function setting(key)
if type(naut) == "table" and type(naut.get_setting) == "function" then
local v = naut.get_setting(key)
if v ~= nil then return v end
end
return DEFAULTS[key]
end
-- Declare the configurable variables so the web UI can render a form for them.
if type(naut) == "table" and type(naut.define_settings) == "function" then
naut.define_settings({
{ key = "sorted_root", label = "Library root", type = "string",
default = DEFAULTS.sorted_root },
{ key = "only_video", label = "Only video files", type = "bool",
default = DEFAULTS.only_video },
{ key = "keep_original_name", label = "Keep original filename",
type = "bool", default = DEFAULTS.keep_original_name },
{ key = "require_label", label = "Required label (blank = any)",
type = "string", default = DEFAULTS.require_label },
})
end
----------------------------------------------------------------------
-- embedded anitomy parser (== examples/anitomy.lua)
@ -240,6 +272,8 @@ local function sanitize(s)
end
local function destination(parsed)
local root = setting("sorted_root")
local keep_original = setting("keep_original_name")
local title = sanitize(parsed.title)
local ext = parsed.extension and ("." .. parsed.extension) or ""
local original = sanitize(basename(parsed.file_name))
@ -247,18 +281,18 @@ local function destination(parsed)
if parsed.episode == nil then
-- movie / special: <root>/<title>/<file>
local fname
if KEEP_ORIGINAL_NAME then
if keep_original then
fname = original
else
fname = title .. (parsed.year and (" (" .. parsed.year .. ")") or "") .. ext
end
return SORTED_ROOT .. "/" .. title .. "/" .. fname
return root .. "/" .. title .. "/" .. fname
end
local season = parsed.season or 1
local sdir = string.format("Season %02d", season)
local fname
if KEEP_ORIGINAL_NAME then
if keep_original then
fname = original
else
fname = string.format("%s - S%02dE%02d", title, season, parsed.episode)
@ -267,11 +301,25 @@ local function destination(parsed)
end
fname = fname .. ext
end
return SORTED_ROOT .. "/" .. title .. "/" .. sdir .. "/" .. fname
return root .. "/" .. title .. "/" .. sdir .. "/" .. fname
end
-- True if the torrent carries `want` among its labels (case-insensitive). When
-- `want` is nil the gate is disabled. If the daemon predates naut.get_labels we
-- can't check, so we sort anyway rather than silently dropping every file.
local function has_label(torrent_id, want)
if not want or want == "" then return true end
if type(naut.get_labels) ~= "function" then return true end
want = want:lower()
for _, label in ipairs(naut.get_labels(torrent_id)) do
if label:lower() == want then return true end
end
return false
end
-- exposed for tests; harmless in the daemon
_G.anime_sort = { anitomy = anitomy, destination = destination }
_G.anime_sort = { anitomy = anitomy, destination = destination,
has_label = has_label }
----------------------------------------------------------------------
-- event hook
@ -279,10 +327,13 @@ _G.anime_sort = { anitomy = anitomy, destination = destination }
function on_file_complete(event)
if not event.path then return end
if not has_label(event.torrent_id, setting("require_label")) then
return -- not labelled "anime": leave this torrent's files alone
end
local name = basename(event.path)
local parsed = anitomy.parse(name)
if ONLY_VIDEO and not anitomy.is_video(parsed.extension) then
if setting("only_video") and not anitomy.is_video(parsed.extension) then
return -- leave subtitles, .nfo, samples, etc. where they are
end

View file

@ -7,7 +7,16 @@ local ref = require("anitomy")
-- stub the host API the script calls, and silence its prints
local captured
_G.naut = { move_file = function(tid, idx, dest) captured = dest end }
-- Per-torrent label stub: torrent 1 is labelled "anime"; others are unlabelled.
local LABELS = { [1] = { "anime" } }
-- Optional per-key setting overrides; nil falls back to the script's defaults.
local SETTINGS = {}
_G.naut = {
move_file = function(tid, idx, dest) captured = dest end,
get_labels = function(tid) return LABELS[tid] or {} end,
define_settings = function(_) end, -- schema declaration: no-op here
get_setting = function(key) return SETTINGS[key] end,
}
local realprint = print
_G.print = function() end
@ -23,9 +32,9 @@ local configured_root = _G.anime_sort.destination({
assert(configured_root, "could not derive SORTED_ROOT from anime_sort.lua")
local fails = 0
local function fire(path)
local function fire(path, torrent_id)
captured = nil
on_file_complete({ torrent_id = 1, index = 0, path = path })
on_file_complete({ torrent_id = torrent_id or 1, index = 0, path = path })
return captured
end
local function expect(path, want_dest)
@ -69,6 +78,42 @@ do
end
end
-- label gate: a torrent without the "anime" label is left alone (no move),
-- while an "anime"-labelled torrent is still sorted.
do
local episode = "/downloads/[HorribleSubs] Boku no Hero Academia - 12 [1080p].mkv"
local unlabelled = fire(episode, 2) -- torrent 2 has no labels
if unlabelled ~= nil then
fails = fails + 1
realprint("FAIL unlabelled torrent should be skipped, got: "
.. tostring(unlabelled))
else
realprint("ok unlabelled torrent skipped")
end
local labelled = fire(episode, 1) -- torrent 1 is labelled "anime"
if labelled == nil then
fails = fails + 1
realprint("FAIL anime-labelled torrent should be sorted")
else
realprint("ok anime-labelled torrent sorted")
end
end
-- settings: a value configured in the UI (delivered via naut.get_setting) takes
-- effect live, without editing the script.
do
SETTINGS.sorted_root = "/custom/lib"
local got = fire("/downloads/[HorribleSubs] Boku no Hero Academia - 12 [1080p].mkv", 1)
SETTINGS.sorted_root = nil
local want = "/custom/lib/Boku no Hero Academia/Season 01/Boku no Hero Academia - S01E12.mkv"
if got ~= want then
fails = fails + 1
realprint("FAIL sorted_root override not applied, got: " .. tostring(got))
else
realprint("ok sorted_root setting override applied")
end
end
-- drift guard: embedded parser must agree with anitomy.lua
local embedded = _G.anime_sort.anitomy
local drift = 0

View file

@ -1,4 +1,5 @@
/* dht.h - BEP-5 KRPC codec and bounded IPv4 get_peers traversal. */
/* dht.h - bounded IPv4 BEP-5 get_peers traversal (KRPC codec from
* torrent-tracker; iterative walk + UDP socket in src/discovery). */
#ifndef NAUT_DHT_H
#define NAUT_DHT_H
@ -9,54 +10,6 @@
#define NAUT_DHT_MAX_NODES 256
#define NAUT_DHT_MAX_PEERS 256
typedef struct {
uint8_t id[NAUT_DHT_ID_LEN];
uint8_t ip[4];
uint16_t port;
} naut_dht_node;
typedef enum {
NAUT_DHT_RESPONSE,
NAUT_DHT_ERROR
} naut_dht_message_type;
typedef struct {
naut_dht_message_type type;
uint8_t transaction[8];
size_t transaction_len;
uint8_t id[NAUT_DHT_ID_LEN];
bool has_id;
uint8_t token[64];
size_t token_len;
naut_dht_node *nodes;
size_t num_nodes;
naut_peer_addr *peers;
size_t num_peers;
int error_code;
} naut_dht_response;
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);
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);
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);
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);
naut_err naut_dht_parse_response(const uint8_t *data, size_t len,
naut_dht_response *out);
void naut_dht_response_free(naut_dht_response *response);
/* Query bootstrap endpoints ("host:port") and iteratively follow returned
* compact nodes until peers are found or the bounded traversal is exhausted. */
naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap,

View file

@ -1,88 +0,0 @@
/* mse.h - BitTorrent Message Stream Encryption (MSE/PE) transport. */
#ifndef NAUT_MSE_H
#define NAUT_MSE_H
#include "naut/common.h"
#include "naut/peer.h"
#include "naut/rc4.h"
#include <sys/types.h>
#define NAUT_MSE_DH_LEN 96
typedef struct {
naut_rc4 send;
naut_rc4 recv;
bool active;
} naut_mse_stream;
/* ---- sans-IO handshake state machine ------------------------------------- *
* The outgoing MSE/PE handshake as a pure state machine over byte buffers no
* sockets so the same logic drives the blocking apps and the io_uring reactor
* (where blocking in a handshake would stall a whole core's worth of peers).
*
* Drive it like a codec: pump NEED_WRITE bytes out, feed NEED_READ bytes in,
* repeat until DONE or ERROR, then call _finish().
*
* h = naut_mse_handshake_begin(info_hash, peer_id, reserved);
* for (;;) switch (naut_mse_handshake_status(h)) {
* case NAUT_MSE_HS_NEED_WRITE: pull bytes, write them to the peer; break;
* case NAUT_MSE_HS_NEED_READ: read bytes from the peer, feed them; break;
* case NAUT_MSE_HS_DONE: naut_mse_handshake_finish(h, ...); goto ok;
* case NAUT_MSE_HS_ERROR: ... ; goto err;
* }
*/
typedef enum {
NAUT_MSE_HS_NEED_READ,
NAUT_MSE_HS_NEED_WRITE,
NAUT_MSE_HS_DONE,
NAUT_MSE_HS_ERROR,
} naut_mse_hs_status;
typedef struct naut_mse_handshake naut_mse_handshake;
naut_mse_handshake *naut_mse_handshake_begin(
const uint8_t info_hash[20],
const uint8_t peer_id[NAUT_PEERID_LEN],
uint64_t reserved);
void naut_mse_handshake_free(naut_mse_handshake *h);
naut_mse_hs_status naut_mse_handshake_status(const naut_mse_handshake *h);
/* Copy pending outgoing bytes into buf (up to cap); returns the count, 0 when
* nothing is queued. Call repeatedly until it returns 0. */
size_t naut_mse_handshake_pull(naut_mse_handshake *h, uint8_t *buf, size_t cap);
/* Feed received bytes; *consumed reports how many were absorbed (the rest, if
* any, must be re-fed after DONE that remainder is the start of the encrypted
* payload stream). Returns the new status. */
naut_mse_hs_status naut_mse_handshake_feed(naut_mse_handshake *h,
const uint8_t *data, size_t len,
size_t *consumed);
/* Valid once status is DONE: hand out the negotiated stream and the peer's
* decrypted BitTorrent handshake. */
naut_err naut_mse_handshake_finish(naut_mse_handshake *h,
naut_mse_stream *stream,
uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]);
/* Blocking convenience wrapper over the state machine: perform the whole
* outgoing handshake on a blocking socket, offering RC4 only. The BitTorrent
* handshake is carried as IA; the peer's decrypted handshake is returned in
* remote_handshake. */
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]);
/* Stream I/O after a successful handshake. Encryption/decryption is in-place
* with connection-owned RC4 state. send_all preserves the caller's buffer. */
bool naut_mse_send_all(int fd, naut_mse_stream *stream,
const void *data, size_t len);
ssize_t naut_mse_recv(int fd, naut_mse_stream *stream,
void *data, size_t len);
#endif /* NAUT_MSE_H */

View file

@ -13,11 +13,17 @@
#include "naut/bitfield.h"
#include "naut/worker.h"
#include <stdio.h>
typedef struct naut_download naut_download;
naut_download *naut_download_create(const naut_metainfo *mi, naut_storage *st);
void naut_download_destroy(naut_download *d);
/* Scan existing storage and mark SHA-1 verified pieces complete before
* requesting from peers. Invalid or missing pieces are left for download. */
naut_err naut_download_resume(naut_download *d);
/* Optional hash offload. Completed-piece SHA-1 jobs run on the worker pool;
* naut_download_poll() finalizes verified pieces on the owning engine thread.
* The pool must outlive the download. */
@ -90,4 +96,11 @@ uint64_t naut_download_bytes_done(const naut_download *d);
size_t naut_download_piece_states(const naut_download *d, uint8_t *out,
size_t capacity);
/* Diagnostic: write a human-readable dump of block-assembly state to `out` —
* overall progress plus, for every piece not yet verified, how many of its
* blocks have arrived and how many requests are outstanding. Pairs with
* engine_dump_torrent() (which covers piece selection across peers) to
* investigate pieces that never finish downloading. */
void naut_download_dump(const naut_download *d, FILE *out);
#endif /* NAUT_PIECE_H */

View file

@ -1,29 +0,0 @@
/* pipeline.h - adaptive request window based on observed bandwidth-delay product. */
#ifndef NAUT_PIPELINE_H
#define NAUT_PIPELINE_H
#include "naut/common.h"
typedef struct {
double rtt_seconds;
double bytes_per_second;
double last_sample_at;
uint32_t depth;
uint32_t min_depth;
uint32_t max_depth;
uint32_t block_size;
} naut_pipeline;
void naut_pipeline_init(naut_pipeline *p, uint32_t block_size,
uint32_t min_depth, uint32_t max_depth,
uint32_t initial_depth);
/* Record one completed request. sent_at and received_at are monotonic seconds.
* The controller smooths RTT and delivery rate, then targets 2x BDP to absorb
* scheduling jitter without allowing an unbounded request window. */
void naut_pipeline_on_block(naut_pipeline *p, uint32_t bytes,
double sent_at, double received_at);
uint32_t naut_pipeline_depth(const naut_pipeline *p);
#endif /* NAUT_PIPELINE_H */

View file

@ -11,6 +11,50 @@ typedef naut_err (*naut_script_move_file_cb)(void *context,
uint32_t file_index,
const char *destination);
/* Resolve a torrent's labels for `naut.get_labels(id)`. Returns a heap array of
* `*count` heap strings (caller frees each string then the array), or NULL with
* *count==0 if the torrent has no labels / is unknown. */
typedef char **(*naut_script_labels_cb)(void *context, uint64_t torrent_id,
size_t *count);
/* One user-configurable setting a script declares via naut.define_settings. */
typedef struct {
const char *key; /* stable identifier read by naut.get_setting */
const char *label; /* human label for the web UI form */
const char *type; /* "string" | "bool" | "number" */
const char *default_value; /* stringified default ("true"/"false" for bool)*/
} naut_script_setting_def;
/* The script (re)declared its settings schema. The host stores it and renders a
* form; `defs` is valid only for the duration of the call. */
typedef void (*naut_script_define_settings_cb)(void *context,
const naut_script_setting_def *defs,
size_t count);
/* Setting value kinds, so the Lua side can push the right type. */
typedef enum {
NAUT_SETTING_STRING = 0,
NAUT_SETTING_BOOL = 1,
NAUT_SETTING_NUMBER = 2,
} naut_setting_type;
/* Resolve a setting for `naut.get_setting(key)`: the user-set value if present,
* else the declared default. Returns a heap string (caller frees) and sets
* *type, or NULL if the key is unknown. */
typedef char *(*naut_script_get_setting_cb)(void *context, const char *key,
naut_setting_type *type);
/* Host callbacks the sandboxed script may invoke. Any may be NULL (the matching
* naut.* function then reports it is unavailable). `context` is passed back to
* each callback. */
typedef struct {
naut_script_move_file_cb move_file;
naut_script_labels_cb labels;
naut_script_define_settings_cb define_settings;
naut_script_get_setting_cb get_setting;
void *context;
} naut_script_host;
typedef struct {
uint64_t queued;
uint64_t handled;
@ -20,12 +64,12 @@ typedef struct {
} naut_script_stats;
/* script_path is loaded before the worker starts. queue_capacity bounds copied
* events and must be non-zero. The VM owns no filesystem or process APIs. */
* events and must be non-zero. The VM owns no filesystem or process APIs. `host`
* is copied; its callbacks are invoked from the script worker thread. */
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);
void naut_script_destroy(naut_script *script);

View file

@ -18,6 +18,12 @@ typedef struct naut_storage naut_storage;
typedef struct {
bool direct_io;
bool preallocate;
/* Optional per-file path overrides, e.g. files moved out of `root` on a
* prior run. If non-NULL the array has one entry per file: where overrides[i]
* is non-NULL the file is opened at that path instead of `root`/<rel-path>,
* so a relocated file is picked up in place (no re-download, no placeholder
* recreated under `root`). A NULL entry uses the default location. */
const char *const *overrides;
} naut_storage_opts;
/* Open (creating + preallocating) all files under `root`. */

View file

@ -58,6 +58,7 @@ typedef struct {
uint32_t peers_connecting;
uint32_t peers_active;
uint32_t peers_failed;
bool stalled;
double elapsed_seconds;
uint32_t peer_count;
naut_swarm_peer_stats peer_stats[NAUT_SWARM_MAX_PEER_STATS];
@ -78,17 +79,42 @@ typedef void (*naut_swarm_control_cb)(void *context, naut_storage *storage);
typedef bool (*naut_swarm_stop_cb)(void *context);
/* Optional: return the desired engine-wide download cap in bytes/sec (0 =
* unlimited). Polled on the swarm owner thread; the engine limit is updated
* whenever the returned value changes. */
typedef uint64_t (*naut_swarm_rate_cb)(void *context);
/* Optional diagnostics. should_dump is polled on the swarm owner thread; when it
* returns true the swarm renders a full engine + piece-assembly state dump and
* hands the text to on_dump (also on the owner thread, where engine and download
* state can be read safely). Used by `nautctl dump` to investigate why a few
* pieces never finish downloading. */
typedef bool (*naut_swarm_dump_cb)(void *context);
typedef void (*naut_swarm_dump_sink)(void *context, const char *text);
/* A file's last known on-disk location, from a relocate on a prior run. Passed
* back in so the file is reopened in place instead of re-downloaded. */
typedef struct {
uint32_t file_index;
const char *path;
} naut_swarm_file_location;
typedef struct {
const char *source; /* .torrent path or magnet URI */
const char *output_dir;
const char *const *peers; /* optional explicit ip:port endpoints */
size_t num_peers;
const naut_swarm_file_location *locations; /* optional moved-file locations */
size_t num_locations;
uint64_t torrent_id;
naut_event_bus *events; /* optional */
bool keep_alive; /* retain completed storage until stopped */
naut_swarm_progress_cb on_progress;
naut_swarm_control_cb on_control;
naut_swarm_stop_cb should_stop;
naut_swarm_rate_cb download_rate; /* optional download throttle provider */
naut_swarm_dump_cb should_dump; /* optional state-dump request poll */
naut_swarm_dump_sink on_dump; /* optional rendered-dump sink */
void *context;
} naut_swarm_config;

View file

@ -1,9 +1,9 @@
/* tracker.h — HTTP and UDP tracker clients (BEP-3/BEP-23, BEP-15).
/* tracker.h — HTTP and UDP tracker announce client.
*
* Split into pure codec (URL building, bencode response parsing, UDP packet
* encode/decode all unit-testable without a socket) and thin blocking fetch
* helpers used by the swarm app. HTTPS/TLS is deferred to a later transport
* backend; the built-ins in this phase are plaintext HTTP and UDP.
* The wire codec (query building, bencode/UDP packet encode+decode) lives in the
* sibling `torrent-tracker` library; the implementation here (src/discovery)
* owns only the socket glue. HTTPS/TLS is deferred to a later transport backend;
* the built-ins are plaintext HTTP and UDP.
*/
#ifndef NAUT_TRACKER_H
#define NAUT_TRACKER_H
@ -37,25 +37,10 @@ typedef struct {
void naut_tracker_response_free(naut_tracker_response *r);
/* --- HTTP --- */
/* Build the full announce GET URL (base?...params) with percent-encoded binary
* info_hash/peer_id. Returns bytes written (excl NUL) or 0 on overflow. */
size_t naut_tracker_http_url(const char *base, const naut_announce_req *req,
char *out, size_t outsz);
/* Parse a bencoded HTTP tracker response body (compact or dict peer list). */
naut_err naut_tracker_parse_http(const uint8_t *body, size_t len,
naut_tracker_response *out);
/* --- UDP (BEP-15): pure packet codec --- */
#define NAUT_UDP_CONNECT_REQ_LEN 16
#define NAUT_UDP_ANNOUNCE_REQ_LEN 98
void naut_udp_build_connect(uint8_t out[16], uint32_t txid);
naut_err naut_udp_parse_connect(const uint8_t *in, size_t len, uint32_t txid,
uint64_t *connection_id);
void naut_udp_build_announce(uint8_t out[98], uint64_t connection_id,
uint32_t txid, const naut_announce_req *req);
naut_err naut_udp_parse_announce(const uint8_t *in, size_t len, uint32_t txid,
naut_tracker_response *out);
/* --- live fetch helpers (blocking) --- */
/* HTTP GET the announce URL; fills out. Only http:// (no TLS yet). */

View file

@ -442,6 +442,13 @@ static uint64_t json_u64(const json_t *obj, const char *key) {
? (uint64_t)json_integer_value(value) : 0;
}
static int64_t json_i64_or(const json_t *obj, const char *key,
int64_t fallback) {
json_t *value = json_object_get(obj, key);
return json_is_integer(value) ? (int64_t)json_integer_value(value)
: fallback;
}
static double json_number_or(const json_t *obj, const char *key,
double fallback) {
json_t *value = json_object_get(obj, key);
@ -456,6 +463,11 @@ static const char *base_name(const char *path) {
}
static char *torrent_name(const json_t *torrent) {
/* The daemon persists the display name captured at add time; prefer it so
* restored torrents (where the in-process name cache is empty) read right
* instead of falling back to an upload path's basename. */
const char *saved = json_string_value(json_object_get(torrent, "name"));
if (saved && *saved) return strdup(saved);
const char *source = json_string_or(torrent, "source", "torrent");
if (strncmp(source, "magnet:", 7) == 0) {
const char *dn = strstr(source, "dn=");
@ -475,9 +487,13 @@ static char *torrent_name(const json_t *torrent) {
static const char *ui_state(const char *state, double progress) {
if (!state) return "stalledDL";
if (strcmp(state, "complete") == 0) return "uploading";
if (strcmp(state, "paused") == 0)
return progress >= 1.0 ? "pausedUP" : "pausedDL";
if (strcmp(state, "stopped") == 0)
return progress >= 1.0 ? "pausedUP" : "pausedDL";
if (strcmp(state, "stopping") == 0) return "pausedDL";
if (strcmp(state, "stalled") == 0)
return progress >= 1.0 ? "stalledUP" : "stalledDL";
if (strcmp(state, "queued") == 0) return "queuedDL";
if (strcmp(state, "error") == 0) return "error";
return "downloading";
@ -786,6 +802,89 @@ static void store_set_name(uint64_t id, const char *name) {
pthread_mutex_unlock(&g_webui.meta_lock);
}
static bool parse_id(const char *text, uint64_t *id);
/* Mirror a torrent's category + tags into the daemon (which persists them and
* exposes the flattened set to Lua via naut.get_labels). The web layer is the
* editing surface; the daemon is the source of truth. Snapshots the assignment
* under meta_lock, then RPCs without it held. */
static void webui_sync_labels(uint64_t id) {
char key[32];
snprintf(key, sizeof key, "%llu", (unsigned long long)id);
pthread_mutex_lock(&g_webui.meta_lock);
json_t *entry = json_object_get(g_webui.assignments, key);
char *category = strdup(entry ? json_string_or(entry, "category", "") : "");
json_t *tags_src = entry ? json_object_get(entry, "tags") : NULL;
json_t *tags = tags_src ? json_deep_copy(tags_src) : json_array();
pthread_mutex_unlock(&g_webui.meta_lock);
json_t *params = json_pack("{s:I,s:s,s:o}", "torrent_id", (json_int_t)id,
"category", category ? category : "",
"tags", tags);
free(category);
if (!params) { json_decref(tags); return; }
json_t *reply = rpc_call_json("set_labels", params);
json_decref(params);
if (reply) json_decref(reply);
}
/* Re-push every torrent's labels (after a global category/tag removal that can
* touch many assignments at once). */
static void webui_sync_all_labels(void) {
pthread_mutex_lock(&g_webui.meta_lock);
size_t n = json_object_size(g_webui.assignments);
uint64_t *ids = n ? malloc(n * sizeof *ids) : NULL;
size_t count = 0;
if (ids) {
const char *key;
json_t *entry;
json_object_foreach(g_webui.assignments, key, entry) {
uint64_t id = 0;
if (parse_id(key, &id)) ids[count++] = id;
}
}
pthread_mutex_unlock(&g_webui.meta_lock);
for (size_t i = 0; i < count; i++) webui_sync_labels(ids[i]);
free(ids);
}
/* Persist the full category + tag lists (including unassigned ones) to the
* daemon so they survive restarts. */
static void webui_sync_taxonomy(void) {
pthread_mutex_lock(&g_webui.meta_lock);
json_t *cats = json_deep_copy(g_webui.categories);
json_t *tags = json_deep_copy(g_webui.tags);
pthread_mutex_unlock(&g_webui.meta_lock);
json_t *params = json_pack("{s:o,s:o}",
"categories", cats ? cats : json_array(),
"tags", tags ? tags : json_array());
if (!params) { json_decref(cats); json_decref(tags); return; }
json_t *reply = rpc_call_json("set_label_taxonomy", params);
json_decref(params);
if (reply) json_decref(reply);
}
/* Seed the category + tag lists from the daemon's persisted copy at startup. */
static void webui_load_taxonomy(void) {
json_t *params = json_object();
json_t *reply = rpc_call_json("get_label_taxonomy", params);
json_decref(params);
if (!json_is_object(reply)) { json_decref(reply); return; }
json_t *cats = json_object_get(reply, "categories");
json_t *tags = json_object_get(reply, "tags");
pthread_mutex_lock(&g_webui.meta_lock);
if (json_is_array(cats)) {
json_decref(g_webui.categories);
g_webui.categories = json_deep_copy(cats);
}
if (json_is_array(tags)) {
json_decref(g_webui.tags);
g_webui.tags = json_deep_copy(tags);
}
pthread_mutex_unlock(&g_webui.meta_lock);
json_decref(reply);
}
static bool store_get_name(uint64_t id, char *out, size_t out_size) {
bool found = false;
pthread_mutex_lock(&g_webui.meta_lock);
@ -808,6 +907,35 @@ static void store_forget(uint64_t id) {
pthread_mutex_unlock(&g_webui.meta_lock);
}
/* On first sight of a torrent (e.g. right after a restart, when the in-memory
* store is empty), seed its assignment + the global category/tag lists from the
* daemon's persisted category/tags. Only creates a missing entry, so live web
* edits are never clobbered. */
static void seed_assignment_from_daemon(uint64_t id, json_t *torrent) {
const char *category = json_string_or(torrent, "category", "");
json_t *tags = json_object_get(torrent, "tags");
char key[32];
snprintf(key, sizeof key, "%llu", (unsigned long long)id);
pthread_mutex_lock(&g_webui.meta_lock);
if (!json_object_get(g_webui.assignments, key)) {
json_t *entry = json_pack(
"{s:s,s:o}", "category", category,
"tags", json_is_array(tags) ? json_deep_copy(tags) : json_array());
if (entry) json_object_set_new(g_webui.assignments, key, entry);
if (category && *category && find_category(category) < 0)
json_array_append_new(g_webui.categories, json_pack(
"{s:s,s:s}", "name", category, "savePath", ""));
size_t i;
json_t *v;
json_array_foreach(tags, i, v) {
const char *t = json_string_value(v);
if (t && *t && find_tag(t) < 0)
json_array_append_new(g_webui.tags, json_string(t));
}
}
pthread_mutex_unlock(&g_webui.meta_lock);
}
/* Fill in category + tags for a torrent from the assignment store. */
static void apply_assignment(json_t *out, uint64_t id) {
pthread_mutex_lock(&g_webui.meta_lock);
@ -838,9 +966,13 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
char *better = strdup(override);
if (better) { free(name); name = better; }
}
bool force_start =
json_boolean_value(json_object_get(torrent, "force_start"));
const char *state = ui_state(json_string_value(json_object_get(torrent,
"state")),
progress);
if (force_start && strcmp(state, "downloading") == 0)
state = progress >= 1.0 ? "forcedUP" : "forcedDL";
json_t *trackers = NULL;
json_t *files = NULL;
@ -899,18 +1031,24 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
json_object_set_new(out, "downloaded", json_integer((json_int_t)done));
json_object_set_new(out, "uploaded", json_integer(0));
json_object_set_new(out, "availability", json_real(1.0));
json_object_set_new(out, "priority", json_integer(1));
int64_t queue_pos = json_i64_or(torrent, "queue_pos", 0);
json_object_set_new(out, "priority",
json_integer((json_int_t)(queue_pos < 0
? 1 : queue_pos + 1)));
json_object_set_new(out, "queuePos", json_integer((json_int_t)queue_pos));
json_object_set_new(out, "trackerHosts", hosts ? hosts : json_array());
json_object_set_new(out, "seqDl", json_false());
json_object_set_new(out, "superSeeding", json_false());
json_object_set_new(out, "forceStart", json_false());
json_object_set_new(out, "forceStart", json_boolean(force_start));
json_object_set_new(out, "timeActive",
json_integer((json_int_t)json_u64(torrent, "elapsed_seconds")));
json_object_set_new(out, "pieceSize",
json_integer(pieces ? (json_int_t)(total / pieces) : 0));
json_object_set_new(out, "state", json_string(state));
json_object_set_new(out, "contentPath", json_string(output));
/* category + tags come from the web-layer assignment store */
/* Seed the web-layer store from the daemon's persisted category/tags the
* first time we see a torrent (survives restarts), then apply it. */
seed_assignment_from_daemon(id, torrent);
apply_assignment(out, id);
if (detail) {
json_object_set_new(out, "comment", json_string(""));
@ -934,6 +1072,8 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
return out;
}
static json_t *preferences_json(void);
/* Build a fresh snapshot (grid + global stats) with live download rates. */
static json_t *build_snapshot(void) {
json_t *params = json_object();
@ -945,6 +1085,7 @@ static json_t *build_snapshot(void) {
}
speed_retain(torrents);
json_t *prefs = preferences_json();
json_t *items = json_array();
uint64_t active = 0;
uint64_t total_rate = 0;
@ -958,20 +1099,35 @@ static json_t *build_snapshot(void) {
json_t *mapped = map_torrent(torrent, false, dlspeed);
if (!mapped) continue;
const char *state = json_string_value(json_object_get(mapped, "state"));
if (state && strcmp(state, "downloading") == 0) active++;
if (state && (strcmp(state, "downloading") == 0 ||
strcmp(state, "forcedDL") == 0)) active++;
total_rate += (uint64_t)dlspeed;
total_data += done;
json_array_append_new(items, mapped);
int64_t q = json_i64_or(mapped, "queuePos", 0);
size_t pos = 0;
for (; pos < json_array_size(items); pos++) {
json_t *cur = json_array_get(items, pos);
if (q < json_i64_or(cur, "queuePos", 0)) break;
}
if (json_array_insert_new(items, pos, mapped) != 0)
json_decref(mapped);
}
json_decref(torrents);
bool alt_speed = prefs &&
json_boolean_value(json_object_get(prefs, "alt_speed_enabled"));
uint64_t dl_limit = prefs ? json_u64(prefs, alt_speed ? "alt_dl_limit"
: "dl_limit") : 0;
uint64_t up_limit = prefs ? json_u64(prefs, alt_speed ? "alt_up_limit"
: "up_limit") : 0;
json_t *server = json_pack(
"{s:I,s:i,s:I,s:i,s:i,s:i,s:f,s:i,s:s,s:i,s:I,s:i,s:i,s:s,s:i}",
"{s:I,s:i,s:I,s:i,s:I,s:I,s:b,s:f,s:i,s:s,s:i,s:I,s:i,s:i,s:s,s:i}",
"dl_info_speed", (json_int_t)total_rate,
"up_info_speed", 0,
"dl_info_data", (json_int_t)total_data,
"up_info_data", 0,
"dl_rate_limit", 0,
"up_rate_limit", 0,
"dl_rate_limit", (json_int_t)dl_limit,
"up_rate_limit", (json_int_t)up_limit,
"alt_speed_enabled", alt_speed,
"global_ratio", 0.0,
"dht_nodes", 0,
"connection_status", "connected",
@ -981,6 +1137,7 @@ static json_t *build_snapshot(void) {
"total_torrents", (int)json_array_size(items),
"read_cache_hits", "0.0",
"queued_io_jobs", 0);
json_decref(prefs);
return json_pack("{s:I,s:o,s:o}", "ts", (json_int_t)time(NULL) * 1000,
"server", server, "torrents", items);
}
@ -1043,9 +1200,49 @@ static json_t *full_torrent_by_hash(const char *hash) {
return mapped;
}
static json_t *preferences_json(void) {
json_t *params = json_object();
json_t *prefs = rpc_call_json("get_preferences", params);
json_decref(params);
if (!json_is_object(prefs)) {
json_decref(prefs);
prefs = json_object();
}
if (!prefs) return NULL;
json_t *max_active = json_object_get(prefs, "max_active");
if (json_is_integer(max_active) &&
!json_object_get(prefs, "max_active_downloads")) {
json_object_set_new(prefs, "max_active_downloads",
json_integer(json_integer_value(max_active)));
}
if (!json_object_get(prefs, "save_path"))
json_object_set_new(prefs, "save_path",
json_string(getenv("NAUT_WEBUI_SAVE_PATH")
? getenv("NAUT_WEBUI_SAVE_PATH") : "."));
if (!json_object_get(prefs, "dl_limit"))
json_object_set_new(prefs, "dl_limit", json_integer(0));
if (!json_object_get(prefs, "up_limit"))
json_object_set_new(prefs, "up_limit", json_integer(0));
if (!json_object_get(prefs, "alt_dl_limit"))
json_object_set_new(prefs, "alt_dl_limit", json_integer(0));
if (!json_object_get(prefs, "alt_up_limit"))
json_object_set_new(prefs, "alt_up_limit", json_integer(0));
if (!json_object_get(prefs, "alt_speed_enabled"))
json_object_set_new(prefs, "alt_speed_enabled", json_false());
json_object_set_new(prefs, "max_connec", json_integer(500));
json_object_set_new(prefs, "max_connec_per_torrent", json_integer(100));
json_object_set_new(prefs, "max_uploads", json_integer(20));
json_object_set_new(prefs, "max_active_uploads", json_integer(10));
json_object_set_new(prefs, "max_active_torrents",
json_integer((json_int_t)json_i64_or(
prefs, "max_active_downloads", 5)));
return prefs;
}
static void api_meta(int fd) {
json_t *json = json_object();
json_t *preferences = json_object();
json_t *preferences = preferences_json();
if (!json || !preferences) {
json_decref(json);
json_decref(preferences);
@ -1063,12 +1260,6 @@ static void api_meta(int fd) {
json_t *trackers = tracker_summary(torrents);
json_decref(torrents);
json_object_set_new(json, "trackers", trackers ? trackers : json_array());
json_object_set_new(preferences, "save_path",
json_string(getenv("NAUT_WEBUI_SAVE_PATH")
? getenv("NAUT_WEBUI_SAVE_PATH") : "."));
json_object_set_new(preferences, "dl_limit", json_integer(0));
json_object_set_new(preferences, "up_limit", json_integer(0));
json_object_set_new(preferences, "alt_speed_enabled", json_false());
json_object_set_new(json, "preferences", preferences);
json_object_set_new(json, "searchPlugins", json_array());
http_json(fd, 200, json);
@ -1190,6 +1381,13 @@ static void api_add(int fd, const char *body, size_t len) {
json_object_set_new(params, "output", json_string(save_path));
if (source && *source) json_object_set_new(params, "source", json_string(source));
if (data && *data) json_object_set_new(params, "data", json_string(data));
if (json_object_get(req, "paused"))
json_object_set_new(params, "paused",
json_boolean(json_boolean_value(
json_object_get(req, "paused"))));
/* Forward the display name so the daemon persists it for restore. */
if (display_name[0])
json_object_set_new(params, "name", json_string(display_name));
json_t *result = rpc_call_json("add_torrent", params);
json_decref(params);
json_decref(req);
@ -1236,10 +1434,33 @@ static void api_delete(int fd, const char *body, size_t len) {
if (removed) publish_snapshot();
}
/* Category/tag assignment is web-layer state the plugin owns, so those verbs
* are honored here. Engine-level verbs (pause/resume/recheck/queue/rate
* limits) have no daemon support yet, so we return 501 instead of pretending
* they worked; the front end shows that as an honest "Action failed" toast. */
static bool rpc_for_torrent(const char *method, uint64_t id, json_t *extra) {
json_t *params = json_object();
if (!params) return false;
json_object_set_new(params, "torrent_id", json_integer((json_int_t)id));
if (json_is_object(extra)) {
const char *key;
json_t *value;
json_object_foreach(extra, key, value)
json_object_set(params, key, value);
}
json_t *result = rpc_call_json(method, params);
json_decref(params);
if (!result) return false;
json_decref(result);
return true;
}
static const char *queue_op_for_action(const char *action) {
if (strcmp(action, "topPriority") == 0) return "top";
if (strcmp(action, "bottomPriority") == 0) return "bottom";
if (strcmp(action, "increasePriority") == 0) return "up";
if (strcmp(action, "decreasePriority") == 0) return "down";
return NULL;
}
/* Category/tag assignment is web-layer state. Engine-backed verbs delegate to
* nautd RPCs so toolbar actions mutate the real queue/lifecycle state. */
static void api_action(int fd, const char *body, size_t len) {
json_t *req = read_body_json(body, len);
const char *raw_action = json_string_value(json_object_get(req, "action"));
@ -1248,6 +1469,7 @@ static void api_action(int fd, const char *body, size_t len) {
json_t *hashes = json_object_get(req, "hashes");
json_t *params = json_object_get(req, "params");
bool handled = false;
int affected = 0;
if (json_is_array(hashes) &&
(strcmp(action, "setCategory") == 0 ||
strcmp(action, "addTags") == 0 ||
@ -1263,12 +1485,54 @@ static void api_action(int fd, const char *body, size_t len) {
else
store_update_tags(id, json_object_get(params, "tags"),
strcmp(action, "addTags") == 0);
webui_sync_labels(id); /* mirror to the daemon (persist + Lua) */
affected++;
}
handled = true;
}
if (json_is_array(hashes) && !handled) {
const char *rpc = NULL;
json_t *extra = NULL;
if (strcmp(action, "pause") == 0) {
rpc = "pause_torrent";
} else if (strcmp(action, "resume") == 0) {
rpc = "resume_torrent";
} else if (strcmp(action, "forceStart") == 0) {
rpc = "resume_torrent";
extra = json_pack("{s:b}", "force", 1);
} else if (strcmp(action, "recheck") == 0) {
rpc = "recheck_torrent";
} else if (strcmp(action, "setSavePath") == 0) {
const char *sp =
json_string_value(json_object_get(params, "savePath"));
if (sp && *sp) {
rpc = "set_save_path";
extra = json_pack("{s:s,s:b}", "savePath", sp, "reset",
json_boolean_value(
json_object_get(params, "reset")));
}
} else {
const char *op = queue_op_for_action(action);
if (op) {
rpc = "queue_move";
extra = json_pack("{s:s}", "op", op);
}
}
if (rpc) {
size_t index;
json_t *hash;
json_array_foreach(hashes, index, hash) {
uint64_t id = 0;
if (!parse_id(json_string_value(hash), &id)) continue;
if (rpc_for_torrent(rpc, id, extra)) affected++;
}
json_decref(extra);
handled = true;
}
}
json_decref(req);
if (handled) {
json_t *json = json_pack("{s:b}", "ok", 1);
json_t *json = json_pack("{s:b,s:i}", "ok", 1, "affected", affected);
http_json(fd, 200, json);
json_decref(json);
publish_snapshot();
@ -1282,14 +1546,143 @@ static void api_action(int fd, const char *body, size_t len) {
json_decref(json);
}
static void api_preferences(int fd, const char *method,
const char *body, size_t len) {
if (strcmp(method, "GET") == 0) {
json_t *prefs = preferences_json();
if (!prefs) {
http_text(fd, 502, "Bad Gateway", "get_preferences failed");
return;
}
http_json(fd, 200, prefs);
json_decref(prefs);
return;
}
if (strcmp(method, "POST") != 0) {
http_text(fd, 405, "Method Not Allowed", "method not allowed");
return;
}
json_t *req = read_body_json(body, len);
json_t *params = json_object();
if (!req || !params) {
json_decref(req);
json_decref(params);
http_text(fd, 500, "Internal Server Error", "oom");
return;
}
const char *keys[] = {
"dl_limit", "up_limit", "alt_dl_limit", "alt_up_limit",
"alt_speed_enabled", "max_active"
};
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
json_t *v = json_object_get(req, keys[i]);
if (v) json_object_set(params, keys[i], v);
}
json_t *max = json_object_get(req, "max_active_downloads");
if (max) json_object_set(params, "max_active", max);
json_t *result = rpc_call_json("set_preferences", params);
json_decref(params);
json_decref(req);
if (!json_is_object(result)) {
json_decref(result);
http_text(fd, 502, "Bad Gateway", "set_preferences failed");
return;
}
json_decref(result);
json_t *prefs = preferences_json();
http_json(fd, 200, prefs);
json_decref(prefs);
publish_snapshot();
}
static void api_altspeed(int fd) {
json_t *params = json_object();
json_t *result = rpc_call_json("toggle_altspeed", params);
json_decref(params);
if (!json_is_object(result)) {
json_decref(result);
http_text(fd, 502, "Bad Gateway", "toggle_altspeed failed");
return;
}
http_json(fd, 200, result);
json_decref(result);
publish_snapshot();
}
/* POST /api/script/settings — persist user-edited script setting values. Body:
* { "settings": { "<key>": "<value>", ... } }. */
static void api_script_settings(int fd, const char *method, const char *body,
size_t len) {
if (strcmp(method, "POST") != 0) {
http_text(fd, 405, "Method Not Allowed", "method not allowed");
return;
}
json_t *req = read_body_json(body, len);
json_t *settings = req ? json_object_get(req, "settings") : NULL;
if (!json_is_object(settings)) {
json_decref(req);
http_text(fd, 400, "Bad Request", "missing settings object");
return;
}
json_t *params = json_object();
json_object_set(params, "settings", settings);
json_decref(req);
json_t *result = rpc_call_json("set_script_settings", params);
json_decref(params);
if (!json_is_object(result)) {
json_decref(result);
http_text(fd, 502, "Bad Gateway", "set_script_settings failed");
return;
}
http_json(fd, 200, result);
json_decref(result);
}
static void api_script(int fd, const char *method, const char *body, size_t len) {
json_t *params = NULL;
json_t *result = NULL;
const char *rpc_name = "script_status";
if (strcmp(method, "GET") == 0) {
params = json_object();
result = rpc_call_json("script_status", params);
} else if (strcmp(method, "POST") == 0) {
rpc_name = "update_script";
json_t *req = read_body_json(body, len);
const char *source = json_string_value(json_object_get(req, "source"));
if (!source) {
json_decref(req);
http_text(fd, 400, "Bad Request", "missing source");
return;
}
params = json_object();
json_object_set(params, "source", json_object_get(req, "source"));
json_decref(req);
result = rpc_call_json("update_script", params);
} else {
http_text(fd, 405, "Method Not Allowed", "method not allowed");
return;
}
json_decref(params);
if (!json_is_object(result)) {
json_decref(result);
char msg[96];
snprintf(msg, sizeof msg, "%s failed", rpc_name);
http_text(fd, 502, "Bad Gateway", msg);
return;
}
http_json(fd, 200, result);
json_decref(result);
}
/* POST /api/categories and /api/categories/delete */
static void api_categories(int fd, const char *body, size_t len, bool remove) {
json_t *req = read_body_json(body, len);
const char *name = json_string_value(json_object_get(req, "name"));
if (name && *name) {
if (remove) store_remove_category(name);
if (remove) { store_remove_category(name); webui_sync_all_labels(); }
else store_add_category(name,
json_string_value(json_object_get(req, "savePath")));
webui_sync_taxonomy(); /* persist the category list via the daemon */
}
json_decref(req);
pthread_mutex_lock(&g_webui.meta_lock);
@ -1304,8 +1697,9 @@ static void api_tags(int fd, const char *body, size_t len, bool remove) {
json_t *req = read_body_json(body, len);
const char *name = json_string_value(json_object_get(req, "name"));
if (name && *name) {
if (remove) store_remove_tag(name);
if (remove) { store_remove_tag(name); webui_sync_all_labels(); }
else store_add_tag(name);
webui_sync_taxonomy(); /* persist the tag list via the daemon */
}
json_decref(req);
pthread_mutex_lock(&g_webui.meta_lock);
@ -1428,11 +1822,13 @@ static void handle_api(int fd, const char *method, char *path,
} else if (strcmp(path, "/api/meta") == 0 && strcmp(method, "GET") == 0) {
api_meta(fd);
} else if (strcmp(path, "/api/preferences") == 0) {
api_meta(fd);
api_preferences(fd, method, body, body_len);
} else if (strcmp(path, "/api/altspeed") == 0 && strcmp(method, "POST") == 0) {
json_t *json = json_pack("{s:b}", "alt_speed_enabled", 0);
http_json(fd, 200, json);
json_decref(json);
api_altspeed(fd);
} else if (strcmp(path, "/api/script/settings") == 0) {
api_script_settings(fd, method, body, body_len);
} else if (strcmp(path, "/api/script") == 0) {
api_script(fd, method, body, body_len);
} else if (strcmp(path, "/api/categories") == 0 &&
strcmp(method, "POST") == 0) {
api_categories(fd, body, body_len, false);
@ -1720,6 +2116,7 @@ naut_err naut_plugin_register(const naut_host_api *host) {
if (error != NAUT_OK) goto fail_store;
error = start_server();
if (error != NAUT_OK) goto fail_store;
webui_load_taxonomy(); /* restore category + tag lists from the daemon */
return NAUT_OK;
fail_store:

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

View file

@ -3,5 +3,8 @@ function on_torrent_finished(event)
end
function on_file_complete(event)
naut.move_file(event.torrent_id, event.index, event.path .. ".moved")
-- Labels surface to Lua as a plain array of strings; use the first to route.
local labels = naut.get_labels(event.torrent_id)
local suffix = labels[1] or "moved"
naut.move_file(event.torrent_id, event.index, event.path .. "." .. suffix)
end

View file

@ -54,7 +54,7 @@ for _ in $(seq 1 100); do
done
[[ -n "$port" && "$port" != 0 ]]
"$daemon" --socket "$socket" --plugin "$plugin" \
"$daemon" --socket "$socket" --plugin "$plugin" --state-dir "$tmp/state" \
>"$daemon_log" 2>&1 &
daemon_pid=$!

View file

@ -1,98 +0,0 @@
#include "naut/bencode.h"
#include "naut/dht.h"
#include "test.h"
#include <stdlib.h>
#include <string.h>
static void check_get_peers_query(void) {
uint8_t tx[2] = { 0x12, 0x34 };
uint8_t id[20], hash[20];
for (size_t i = 0; i < 20; i++) {
id[i] = (uint8_t)i;
hash[i] = (uint8_t)(0x80 + i);
}
uint8_t *query = NULL;
size_t query_len = 0;
CHECK(naut_dht_build_get_peers(tx, sizeof tx, id, hash,
&query, &query_len) == NAUT_OK);
naut_bc_doc *doc = NULL;
CHECK(naut_bc_parse(query, query_len, &doc) == NAUT_OK);
const naut_bc *root = naut_bc_root(doc);
CHECK(naut_bc_str_eq(naut_bc_dict_get(root, "y"), "q"));
CHECK(naut_bc_str_eq(naut_bc_dict_get(root, "q"), "get_peers"));
const naut_bc *args = naut_bc_dict_get(root, "a");
const uint8_t *p = NULL;
size_t n = 0;
CHECK(naut_bc_get_str(naut_bc_dict_get(args, "id"), &p, &n));
CHECK(n == 20 && memcmp(p, id, 20) == 0);
CHECK(naut_bc_get_str(naut_bc_dict_get(args, "info_hash"), &p, &n));
CHECK(n == 20 && memcmp(p, hash, 20) == 0);
naut_bc_free(doc);
free(query);
}
static void check_response(void) {
uint8_t packet[256];
size_t len = 0;
const char *prefix = "d1:rd2:id20:";
memcpy(packet + len, prefix, strlen(prefix));
len += strlen(prefix);
for (size_t i = 0; i < 20; i++) packet[len++] = (uint8_t)(0x20 + i);
const char *nodes = "5:nodes26:";
memcpy(packet + len, nodes, strlen(nodes));
len += strlen(nodes);
for (size_t i = 0; i < 20; i++) packet[len++] = (uint8_t)(0x40 + i);
packet[len++] = 192; packet[len++] = 0; packet[len++] = 2; packet[len++] = 9;
packet[len++] = 0x1a; packet[len++] = 0xe1;
const char *suffix = "5:token3:abc6:valuesl6:";
memcpy(packet + len, suffix, strlen(suffix));
len += strlen(suffix);
packet[len++] = 203; packet[len++] = 0; packet[len++] = 113; packet[len++] = 7;
packet[len++] = 0xc8; packet[len++] = 0xd5;
const char *tail = "ee1:t2:aa1:y1:re";
memcpy(packet + len, tail, strlen(tail));
len += strlen(tail);
naut_dht_response response;
CHECK(naut_dht_parse_response(packet, len, &response) == NAUT_OK);
CHECK(response.type == NAUT_DHT_RESPONSE);
CHECK(response.transaction_len == 2 &&
memcmp(response.transaction, "aa", 2) == 0);
CHECK(response.has_id && response.id[0] == 0x20);
CHECK(response.token_len == 3 &&
memcmp(response.token, "abc", 3) == 0);
CHECK(response.num_nodes == 1);
CHECK(response.nodes[0].ip[0] == 192 &&
response.nodes[0].port == 6881);
CHECK(response.num_peers == 1);
CHECK(response.peers[0].ip[0] == 203 &&
response.peers[0].port == 51413);
naut_dht_response_free(&response);
}
int main(void) {
check_get_peers_query();
check_response();
const char error[] = "d1:eli203e12:Server errore1:t2:zz1:y1:ee";
naut_dht_response response;
CHECK(naut_dht_parse_response((const uint8_t *)error, sizeof error - 1,
&response) == NAUT_OK);
CHECK(response.type == NAUT_DHT_ERROR && response.error_code == 203);
naut_dht_response_free(&response);
const char malformed[] =
"d1:rd2:id20:abcdefghijklmnopqrst5:nodes1:xe1:t1:a1:y1:re";
CHECK(naut_dht_parse_response((const uint8_t *)malformed,
sizeof malformed - 1,
&response) == NAUT_ERR_PROTO);
TEST_MAIN_END();
}

View file

@ -69,6 +69,20 @@ int main(void) {
CHECK(got && glen == olen && memcmp(got, orig, olen) == 0);
free(got);
/* Existing verified data should be reflected in progress before any peer
* requests are made. */
st = naut_storage_open(mi.files, mi.num_files, root, &err);
CHECK(st && err == NAUT_OK);
d = naut_download_create(&mi, st);
CHECK(d != NULL);
CHECK(naut_download_resume(d) == NAUT_OK);
CHECK(naut_download_complete(d));
CHECK_EQ(naut_download_pieces_done(d), naut_download_num_pieces(d));
CHECK_EQ((long long)naut_download_bytes_done(d), (long long)olen);
CHECK(!naut_download_next_request(d, &idx, &begin, &len));
naut_download_destroy(d);
naut_storage_close(st);
/* The same path with SHA-1 verification offloaded to bounded workers. */
{
char t2[] = "/tmp/naut_async_XXXXXX";

View file

@ -108,6 +108,22 @@ int main(void) {
memcmp(got1, global + mi.files[0].length, l1) == 0);
free(got1);
/* Simulate a daemon restart: file 0 now lives at `dest`, not under `root`.
* Reopening with its saved location as an override must pick it up in place
* so resume verifies every piece -- if the override were ignored, file 0
* would open as an empty placeholder under root and resume would fail. */
const char *overrides[2] = { dest, NULL };
naut_storage_opts ropts = { .preallocate = true, .overrides = overrides };
naut_storage *st2 =
naut_storage_open_opts(mi.files, mi.num_files, root, &ropts, &err);
CHECK(st2 && err == NAUT_OK);
naut_download *d2 = naut_download_create(&mi, st2);
CHECK(d2 != NULL);
CHECK(naut_download_resume(d2) == NAUT_OK);
CHECK(naut_download_complete(d2)); /* both files verified from their homes */
naut_download_destroy(d2);
naut_storage_close(st2);
naut_download_destroy(d);
free(global); free(tor); naut_metainfo_free(&mi);
char cmd[600]; snprintf(cmd, sizeof cmd, "rm -rf '%s' '%s'", root, destdir);

View file

@ -1,56 +0,0 @@
/* Drives the MSE handshake state machine without a socket. Full-handshake
* correctness is proven against libtorrent in interop_mse; this guards the
* sans-IO plumbing (state transitions, fragmented pull, DH validation) so it
* stays covered even where libtorrent is unavailable. */
#include "naut/mse.h"
#include "test.h"
#include <string.h>
int main(void) {
uint8_t info_hash[20], peer_id[NAUT_PEERID_LEN];
memset(info_hash, 0xAB, sizeof info_hash);
memset(peer_id, 0xCD, sizeof peer_id);
/* begin → must want to write its 96-byte public key first. */
naut_mse_handshake *h = naut_mse_handshake_begin(info_hash, peer_id, 0);
CHECK(h != NULL);
CHECK_EQ(naut_mse_handshake_status(h), NAUT_MSE_HS_NEED_WRITE);
/* Drain the public key one byte at a time; it must be exactly 96 bytes,
* after which the machine flips to waiting for the peer's key. */
uint8_t pub[128];
size_t total = 0, n;
while ((n = naut_mse_handshake_pull(h, pub + total, 1)) > 0) total += n;
CHECK_EQ((int)total, NAUT_MSE_DH_LEN);
CHECK_EQ(naut_mse_handshake_status(h), NAUT_MSE_HS_NEED_READ);
/* A real DH public key is never all-zero. */
uint8_t zero[NAUT_MSE_DH_LEN] = {0};
CHECK(memcmp(pub, zero, NAUT_MSE_DH_LEN) != 0);
/* finish() before completion must refuse rather than hand out junk. */
naut_mse_stream stream;
uint8_t remote_hs[NAUT_HANDSHAKE_LEN];
CHECK_EQ(naut_mse_handshake_finish(h, &stream, remote_hs), NAUT_ERR_AGAIN);
/* Feed an invalid (zero) peer public key fragmented across calls; the DH
* validation must reject it (0 < 2) and latch the error state. */
size_t consumed_total = 0;
naut_mse_hs_status st = NAUT_MSE_HS_NEED_READ;
for (int i = 0; i < NAUT_MSE_DH_LEN; i++) {
size_t consumed = 0;
uint8_t b = 0;
st = naut_mse_handshake_feed(h, &b, 1, &consumed);
consumed_total += consumed;
if (st == NAUT_MSE_HS_ERROR) break;
}
CHECK_EQ(st, NAUT_MSE_HS_ERROR);
CHECK(consumed_total <= NAUT_MSE_DH_LEN);
CHECK_EQ(naut_mse_handshake_finish(h, &stream, remote_hs), NAUT_ERR_PROTO);
naut_mse_handshake_free(h);
/* Bad arguments are rejected, not crashed on. */
CHECK(naut_mse_handshake_begin(NULL, peer_id, 0) == NULL);
CHECK(naut_mse_handshake_begin(info_hash, NULL, 0) == NULL);
TEST_MAIN_END();
}

View file

@ -1,30 +0,0 @@
#include "naut/pipeline.h"
#include "test.h"
int main(void) {
naut_pipeline pipeline;
naut_pipeline_init(&pipeline, NAUT_BLOCK, 4, 1024, 32);
CHECK_EQ(naut_pipeline_depth(&pipeline), 32);
/* 16 KiB every 100 us with 20 ms RTT is about 164 MB/s and a 200-block
* BDP. Repeated samples should grow the window substantially. */
double now = 1.0;
for (int i = 0; i < 100; i++) {
now += 0.0001;
naut_pipeline_on_block(&pipeline, NAUT_BLOCK, now - 0.020, now);
}
CHECK(naut_pipeline_depth(&pipeline) > 128);
CHECK(naut_pipeline_depth(&pipeline) <= 1024);
uint32_t high = naut_pipeline_depth(&pipeline);
for (int i = 0; i < 100; i++) {
now += 0.050;
naut_pipeline_on_block(&pipeline, NAUT_BLOCK, now - 0.005, now);
}
CHECK(naut_pipeline_depth(&pipeline) < high);
CHECK(naut_pipeline_depth(&pipeline) >= 4);
naut_pipeline_init(&pipeline, 0, 0, 0, 0);
CHECK_EQ(naut_pipeline_depth(&pipeline), 1);
TEST_MAIN_END();
}

View file

@ -3,6 +3,7 @@
#include <pthread.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
@ -30,13 +31,29 @@ static naut_err capture_move(void *opaque, uint64_t torrent_id,
return NAUT_OK;
}
/* Hand the script a single label "anime" so the fixture can route on it. */
static char **capture_labels(void *opaque, uint64_t torrent_id, size_t *count) {
(void)opaque;
(void)torrent_id;
char **labels = malloc(sizeof *labels);
if (!labels) { *count = 0; return NULL; }
labels[0] = strdup("anime");
*count = labels[0] ? 1 : 0;
return labels;
}
int main(void) {
pthread_t owner = pthread_self();
move_capture capture = {0};
naut_event_bus *events = naut_event_bus_create();
naut_err error;
naut_script_host host = {
.move_file = capture_move,
.labels = capture_labels,
.context = &capture,
};
naut_script *script = naut_script_create(
events, NAUT_PHASE7_SCRIPT, 8, capture_move, &capture, &error);
events, NAUT_PHASE7_SCRIPT, 8, &host, &error);
CHECK(script && error == NAUT_OK);
naut_event event = {
@ -65,7 +82,8 @@ int main(void) {
CHECK(!pthread_equal(owner, capture.caller));
CHECK_EQ(capture.torrent_id, 42);
CHECK_EQ(capture.file_index, 3);
CHECK(strcmp(capture.destination, "/tmp/completed-file.moved") == 0);
/* Proves naut.get_labels surfaced the label string into Lua. */
CHECK(strcmp(capture.destination, "/tmp/completed-file.anime") == 0);
naut_script_stats stats;
naut_script_get_stats(script, &stats);

View file

@ -1,115 +0,0 @@
#include "naut/tracker.h"
#include "naut/bencode.h"
#include "test.h"
#include <string.h>
int main(void) {
naut_announce_req req;
memset(&req, 0, sizeof req);
for (int i = 0; i < 20; i++) { req.info_hash[i] = (uint8_t)i; req.peer_id[i] = (uint8_t)(0x80 + i); }
req.port = 6881; req.left = 1000; req.numwant = -1; req.key = 0xdeadbeef;
req.event = NAUT_TEV_STARTED;
/* --- HTTP announce URL --- */
char url[1024];
size_t n = naut_tracker_http_url("http://t.example/announce", &req, url, sizeof url);
CHECK(n > 0);
CHECK(strstr(url, "info_hash=%00%01%02") != NULL); /* binary pct-encoded */
CHECK(strstr(url, "port=6881") != NULL);
CHECK(strstr(url, "compact=1") != NULL);
CHECK(strstr(url, "event=started") != NULL);
/* base already having a query uses '&' */
naut_tracker_http_url("http://t.example/announce?x=1", &req, url, sizeof url);
CHECK(strstr(url, "announce?x=1&info_hash=") != NULL);
req.event = (naut_tracker_event)99;
CHECK(naut_tracker_http_url("http://t.example/announce", &req,
url, sizeof url) == 0);
req.event = NAUT_TEV_STARTED;
/* --- HTTP response parse: compact peers --- */
{
/* d8:intervali1800e5:peers12:<two 6-byte peers>e */
uint8_t body[128]; size_t b = 0;
const char *pre = "d8:intervali1800e8:completei5e10:incompletei2e5:peers12:";
memcpy(body, pre, strlen(pre)); b = strlen(pre);
uint8_t peers[12] = { 1,2,3,4, 0x1a,0xe1, 10,0,0,1, 0x1a,0xe2 };
memcpy(body + b, peers, 12); b += 12;
body[b++] = 'e';
naut_tracker_response r;
CHECK(naut_tracker_parse_http(body, b, &r) == NAUT_OK);
CHECK_EQ(r.interval, 1800);
CHECK_EQ(r.seeders, 5);
CHECK_EQ(r.leechers, 2);
CHECK_EQ(r.num_peers, 2);
CHECK(r.peers[0].ip[0]==1 && r.peers[0].ip[3]==4 && r.peers[0].port==0x1ae1);
CHECK(r.peers[1].ip[0]==10 && r.peers[1].port==0x1ae2);
naut_tracker_response_free(&r);
}
/* --- failure reason --- */
{
const char *body = "d14:failure reason17:torrent not founde";
naut_tracker_response r;
CHECK(naut_tracker_parse_http((const uint8_t *)body, strlen(body), &r) == NAUT_ERR_PROTO);
CHECK(r.failure && strcmp(r.failure, "torrent not found") == 0);
naut_tracker_response_free(&r);
}
/* --- UDP connect codec --- */
{
uint8_t pkt[98];
naut_udp_build_connect(pkt, 0x11223344);
/* protocol id 0x41727101980, action 0, txid */
CHECK(pkt[0]==0 && pkt[1]==0 && pkt[2]==0x04 && pkt[3]==0x17 &&
pkt[4]==0x27 && pkt[5]==0x10 && pkt[6]==0x19 && pkt[7]==0x80);
CHECK(pkt[8]==0 && pkt[11]==0); /* action connect */
CHECK(pkt[12]==0x11 && pkt[15]==0x44); /* txid */
/* build a fake connect response and parse it */
uint8_t resp[16] = {0};
resp[3] = 0; /* action connect */
resp[4]=0x11; resp[5]=0x22; resp[6]=0x33; resp[7]=0x44; /* txid */
for (int i = 0; i < 8; i++) resp[8+i] = (uint8_t)(0xA0 + i); /* conn id */
uint64_t cid = 0;
CHECK(naut_udp_parse_connect(resp, 16, 0x11223344, &cid) == NAUT_OK);
CHECK(cid == 0xA0A1A2A3A4A5A6A7ULL);
CHECK(naut_udp_parse_connect(resp, 16, 0x99999999, &cid) == NAUT_ERR_PROTO); /* wrong txid */
}
/* --- UDP announce codec round-trip --- */
{
uint8_t pkt[98];
naut_udp_build_announce(pkt, 0xA0A1A2A3A4A5A6A7ULL, 0x55667788, &req);
CHECK(pkt[11] == 1); /* action announce */
CHECK(memcmp(pkt + 16, req.info_hash, 20) == 0);
CHECK(memcmp(pkt + 36, req.peer_id, 20) == 0);
CHECK(pkt[83] == NAUT_TEV_STARTED); /* event low byte */
CHECK((pkt[96]<<8 | pkt[97]) == 6881); /* port */
/* fake announce response: action=1, txid, interval, leech, seed, 1 peer */
uint8_t resp[26] = {0};
resp[3] = 1;
resp[4]=0x55; resp[5]=0x66; resp[6]=0x77; resp[7]=0x88;
resp[11] = 0x84; /* interval 0x84 = 132 */
resp[15] = 3; /* leechers */
resp[19] = 7; /* seeders */
resp[20]=192; resp[21]=168; resp[22]=0; resp[23]=5; resp[24]=0x1a; resp[25]=0xe1;
naut_tracker_response r;
CHECK(naut_udp_parse_announce(resp, 26, 0x55667788, &r) == NAUT_OK);
CHECK_EQ(r.interval, 132);
CHECK_EQ(r.leechers, 3);
CHECK_EQ(r.seeders, 7);
CHECK_EQ(r.num_peers, 1);
CHECK(r.peers[0].ip[0]==192 && r.peers[0].ip[3]==5 && r.peers[0].port==0x1ae1);
naut_tracker_response_free(&r);
uint8_t malformed[27];
memcpy(malformed, resp, sizeof resp);
malformed[26] = 0;
CHECK(naut_udp_parse_announce(malformed, sizeof malformed,
0x55667788, &r) == NAUT_ERR_PROTO);
}
TEST_MAIN_END();
}