Compare commits
10 commits
66ec5d6c3d
...
252bc8140b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
252bc8140b | ||
|
|
067b62c23a | ||
|
|
096535292d | ||
|
|
a0265cc1ea | ||
|
|
91d4b99aee | ||
|
|
9474e6f31a | ||
|
|
1b79fe8079 | ||
|
|
ab51774554 | ||
|
|
a4ec585aed | ||
|
|
ab733cb573 |
9 changed files with 1761 additions and 612 deletions
|
|
@ -220,9 +220,14 @@ add_library(naut_example MODULE plugins/example/example.c)
|
||||||
target_include_directories(naut_example PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
target_include_directories(naut_example PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||||
set_target_properties(naut_example PROPERTIES PREFIX "")
|
set_target_properties(naut_example PROPERTIES PREFIX "")
|
||||||
|
|
||||||
add_library(naut_webui MODULE plugins/webui/webui.c)
|
# SQLite backs the webui account store.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(SQLITE3 REQUIRED IMPORTED_TARGET sqlite3)
|
||||||
|
|
||||||
|
add_library(naut_webui MODULE plugins/webui/webui.c plugins/webui/webui_store.c)
|
||||||
target_include_directories(naut_webui PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
target_include_directories(naut_webui PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||||
target_link_libraries(naut_webui PRIVATE ${NAUT_JANSSON_TARGET} naut_net pthread)
|
target_link_libraries(naut_webui PRIVATE ${NAUT_JANSSON_TARGET} naut_net
|
||||||
|
PkgConfig::SQLITE3 OpenSSL::Crypto pthread)
|
||||||
set_target_properties(naut_webui PROPERTIES PREFIX "")
|
set_target_properties(naut_webui PROPERTIES PREFIX "")
|
||||||
|
|
||||||
# --- swarm: multi-peer download driver over the torrent-peer engine ---------
|
# --- swarm: multi-peer download driver over the torrent-peer engine ---------
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,5 @@
|
||||||
- ✅ RSS should have a manual repull
|
- ✅ RSS should have a manual repull
|
||||||
- ✅ I should be able to force re-run a rule for cases where it was modified.
|
- ✅ I should be able to force re-run a rule for cases where it was modified.
|
||||||
- ✅ RSS manual download button doesn't work — there's no + next to articles (feeds whose items only carry a <link>/Atom href had no source).
|
- ✅ RSS manual download button doesn't work — there's no + next to articles (feeds whose items only carry a <link>/Atom href had no source).
|
||||||
- ✅ Rules should show their current matches.
|
- ✅ Rules should show their current matches.
|
||||||
|
- ✅ We need a real login system backed by a database.
|
||||||
|
|
@ -114,15 +114,6 @@ struct daemon_state {
|
||||||
json_t *script_settings_schema; /* array of {key,label,type,default} */
|
json_t *script_settings_schema; /* array of {key,label,type,default} */
|
||||||
json_t *script_settings; /* object: key -> value string (user-set)*/
|
json_t *script_settings; /* object: key -> value string (user-set)*/
|
||||||
char settings_file[PATH_MAX]; /* <state_dir>/script_settings.json */
|
char settings_file[PATH_MAX]; /* <state_dir>/script_settings.json */
|
||||||
/* Label taxonomy: the web UI's full category + tag lists (including ones
|
|
||||||
* created but not yet assigned). Web-layer schema; the daemon just persists
|
|
||||||
* it so they survive restarts. */
|
|
||||||
pthread_mutex_t taxonomy_lock;
|
|
||||||
json_t *label_categories; /* array of {name, savePath} */
|
|
||||||
json_t *label_tags; /* array of tag name strings */
|
|
||||||
char taxonomy_file[PATH_MAX]; /* <state_dir>/labels.json */
|
|
||||||
char data_dir[PATH_MAX]; /* the resolved state dir (for blobs) */
|
|
||||||
pthread_mutex_t blob_lock; /* guards webui_<key>.json blob files */
|
|
||||||
pthread_mutex_t torrent_lock;
|
pthread_mutex_t torrent_lock;
|
||||||
torrent_task *torrents[MAX_TORRENTS];
|
torrent_task *torrents[MAX_TORRENTS];
|
||||||
size_t torrent_count;
|
size_t torrent_count;
|
||||||
|
|
@ -1320,153 +1311,6 @@ static json_t *rpc_set_script_settings(void *opaque, const json_t *params,
|
||||||
return script_status_json(state);
|
return script_status_json(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- label taxonomy (web-layer category + tag lists, persisted here) ------- */
|
|
||||||
|
|
||||||
static void persist_taxonomy(daemon_state *state) {
|
|
||||||
if (!state->persist_enabled) return;
|
|
||||||
pthread_mutex_lock(&state->taxonomy_lock);
|
|
||||||
json_t *cats = state->label_categories
|
|
||||||
? json_deep_copy(state->label_categories) : json_array();
|
|
||||||
json_t *tags = state->label_tags
|
|
||||||
? json_deep_copy(state->label_tags) : json_array();
|
|
||||||
pthread_mutex_unlock(&state->taxonomy_lock);
|
|
||||||
json_t *doc = json_object();
|
|
||||||
if (!doc) { json_decref(cats); json_decref(tags); return; }
|
|
||||||
json_object_set_new(doc, "categories", cats ? cats : json_array());
|
|
||||||
json_object_set_new(doc, "tags", tags ? tags : json_array());
|
|
||||||
char tmp[PATH_MAX + 8];
|
|
||||||
snprintf(tmp, sizeof tmp, "%s.tmp", state->taxonomy_file);
|
|
||||||
if (json_dump_file(doc, tmp, JSON_INDENT(2)) != 0 ||
|
|
||||||
rename(tmp, state->taxonomy_file) != 0) {
|
|
||||||
NAUT_WARN("persist: write %s failed", state->taxonomy_file);
|
|
||||||
unlink(tmp);
|
|
||||||
}
|
|
||||||
json_decref(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void load_taxonomy(daemon_state *state) {
|
|
||||||
if (!state->persist_enabled) return;
|
|
||||||
json_error_t jerr;
|
|
||||||
json_t *doc = json_load_file(state->taxonomy_file, 0, &jerr);
|
|
||||||
if (!doc) return;
|
|
||||||
json_t *cats = json_object_get(doc, "categories");
|
|
||||||
json_t *tags = json_object_get(doc, "tags");
|
|
||||||
pthread_mutex_lock(&state->taxonomy_lock);
|
|
||||||
if (json_is_array(cats)) {
|
|
||||||
json_decref(state->label_categories);
|
|
||||||
state->label_categories = json_deep_copy(cats);
|
|
||||||
}
|
|
||||||
if (json_is_array(tags)) {
|
|
||||||
json_decref(state->label_tags);
|
|
||||||
state->label_tags = json_deep_copy(tags);
|
|
||||||
}
|
|
||||||
pthread_mutex_unlock(&state->taxonomy_lock);
|
|
||||||
json_decref(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
static json_t *rpc_get_label_taxonomy(void *opaque, const json_t *params,
|
|
||||||
naut_err *error) {
|
|
||||||
(void)params;
|
|
||||||
daemon_state *state = opaque;
|
|
||||||
pthread_mutex_lock(&state->taxonomy_lock);
|
|
||||||
json_t *cats = state->label_categories
|
|
||||||
? json_deep_copy(state->label_categories) : json_array();
|
|
||||||
json_t *tags = state->label_tags
|
|
||||||
? json_deep_copy(state->label_tags) : json_array();
|
|
||||||
pthread_mutex_unlock(&state->taxonomy_lock);
|
|
||||||
json_t *out = json_object();
|
|
||||||
if (!out) { json_decref(cats); json_decref(tags); *error = NAUT_ERR_NOMEM; return NULL; }
|
|
||||||
json_object_set_new(out, "categories", cats ? cats : json_array());
|
|
||||||
json_object_set_new(out, "tags", tags ? tags : json_array());
|
|
||||||
*error = NAUT_OK;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
static json_t *rpc_set_label_taxonomy(void *opaque, const json_t *params,
|
|
||||||
naut_err *error) {
|
|
||||||
daemon_state *state = opaque;
|
|
||||||
if (!json_is_object(params)) { *error = NAUT_ERR_INVAL; return NULL; }
|
|
||||||
json_t *cats = json_object_get(params, "categories");
|
|
||||||
json_t *tags = json_object_get(params, "tags");
|
|
||||||
pthread_mutex_lock(&state->taxonomy_lock);
|
|
||||||
if (json_is_array(cats)) {
|
|
||||||
json_decref(state->label_categories);
|
|
||||||
state->label_categories = json_deep_copy(cats);
|
|
||||||
}
|
|
||||||
if (json_is_array(tags)) {
|
|
||||||
json_decref(state->label_tags);
|
|
||||||
state->label_tags = json_deep_copy(tags);
|
|
||||||
}
|
|
||||||
pthread_mutex_unlock(&state->taxonomy_lock);
|
|
||||||
persist_taxonomy(state);
|
|
||||||
*error = NAUT_OK;
|
|
||||||
return json_object();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- generic web-UI blob store (RSS feeds, indexer config, ...) ------------ *
|
|
||||||
* The web layer owns these schemas; the daemon only persists them, one JSON
|
|
||||||
* document per key, under <state_dir>/webui_<key>.json. Keys are sanitized to a
|
|
||||||
* safe filename charset so a key can never escape the state directory. */
|
|
||||||
|
|
||||||
static bool blob_path(daemon_state *state, const char *key, char *out, size_t n) {
|
|
||||||
if (!key || !*key || !state->data_dir[0]) return false;
|
|
||||||
char safe[64];
|
|
||||||
size_t j = 0;
|
|
||||||
for (size_t i = 0; key[i] && j + 1 < sizeof safe; i++) {
|
|
||||||
char c = key[i];
|
|
||||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
||||||
(c >= '0' && c <= '9') || c == '_' || c == '-')
|
|
||||||
safe[j++] = c;
|
|
||||||
}
|
|
||||||
safe[j] = 0;
|
|
||||||
if (j == 0) return false;
|
|
||||||
return (size_t)snprintf(out, n, "%s/webui_%s.json", state->data_dir, safe) < n;
|
|
||||||
}
|
|
||||||
|
|
||||||
static json_t *rpc_get_webui_blob(void *opaque, const json_t *params,
|
|
||||||
naut_err *error) {
|
|
||||||
daemon_state *state = opaque;
|
|
||||||
const char *key = json_string_value(json_object_get(params, "key"));
|
|
||||||
char path[PATH_MAX];
|
|
||||||
if (!blob_path(state, key, path, sizeof path)) {
|
|
||||||
*error = NAUT_ERR_INVAL;
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
pthread_mutex_lock(&state->blob_lock);
|
|
||||||
json_error_t jerr;
|
|
||||||
json_t *value = json_load_file(path, 0, &jerr);
|
|
||||||
pthread_mutex_unlock(&state->blob_lock);
|
|
||||||
json_t *out = json_object();
|
|
||||||
if (!out) { json_decref(value); *error = NAUT_ERR_NOMEM; return NULL; }
|
|
||||||
json_object_set_new(out, "value", value ? value : json_null());
|
|
||||||
*error = NAUT_OK;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
static json_t *rpc_set_webui_blob(void *opaque, const json_t *params,
|
|
||||||
naut_err *error) {
|
|
||||||
daemon_state *state = opaque;
|
|
||||||
const char *key = json_string_value(json_object_get(params, "key"));
|
|
||||||
json_t *value = json_object_get(params, "value");
|
|
||||||
char path[PATH_MAX];
|
|
||||||
if (!value || !blob_path(state, key, path, sizeof path)) {
|
|
||||||
*error = NAUT_ERR_INVAL;
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
*error = NAUT_OK;
|
|
||||||
if (!state->persist_enabled) return json_object();
|
|
||||||
pthread_mutex_lock(&state->blob_lock);
|
|
||||||
char tmp[PATH_MAX + 8];
|
|
||||||
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
|
||||||
if (json_dump_file(value, tmp, JSON_INDENT(2)) != 0 ||
|
|
||||||
rename(tmp, path) != 0) {
|
|
||||||
NAUT_WARN("persist: write %s failed", path);
|
|
||||||
unlink(tmp);
|
|
||||||
}
|
|
||||||
pthread_mutex_unlock(&state->blob_lock);
|
|
||||||
return json_object();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- data-overlap guard (block torrents that would write the same files) --- */
|
/* --- data-overlap guard (block torrents that would write the same files) --- */
|
||||||
|
|
||||||
static uint8_t *slurp_file(const char *path, size_t *len) {
|
static uint8_t *slurp_file(const char *path, size_t *len) {
|
||||||
|
|
@ -2393,10 +2237,6 @@ static bool register_commands(daemon_state *state) {
|
||||||
naut_rpc_register(state->rpc, "script_status", rpc_script_status, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "script_status", rpc_script_status, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "update_script", rpc_update_script, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "update_script", rpc_update_script, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "set_script_settings", rpc_set_script_settings, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "set_script_settings", rpc_set_script_settings, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "get_label_taxonomy", rpc_get_label_taxonomy, state) == NAUT_OK &&
|
|
||||||
naut_rpc_register(state->rpc, "set_label_taxonomy", rpc_set_label_taxonomy, state) == NAUT_OK &&
|
|
||||||
naut_rpc_register(state->rpc, "get_webui_blob", rpc_get_webui_blob, state) == NAUT_OK &&
|
|
||||||
naut_rpc_register(state->rpc, "set_webui_blob", rpc_set_webui_blob, state) == NAUT_OK &&
|
|
||||||
naut_rpc_register(state->rpc, "unload_script", rpc_unload_script, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "unload_script", rpc_unload_script, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "shutdown", rpc_shutdown, state) == NAUT_OK;
|
naut_rpc_register(state->rpc, "shutdown", rpc_shutdown, state) == NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
@ -2685,12 +2525,6 @@ static bool resolve_state_dir(daemon_state *state, const char *override) {
|
||||||
"%s/script_settings.json", dir) >=
|
"%s/script_settings.json", dir) >=
|
||||||
sizeof state->settings_file)
|
sizeof state->settings_file)
|
||||||
return false;
|
return false;
|
||||||
if ((size_t)snprintf(state->taxonomy_file, sizeof state->taxonomy_file,
|
|
||||||
"%s/labels.json", dir) >= sizeof state->taxonomy_file)
|
|
||||||
return false;
|
|
||||||
if ((size_t)snprintf(state->data_dir, sizeof state->data_dir, "%s", dir) >=
|
|
||||||
sizeof state->data_dir)
|
|
||||||
return false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2796,9 +2630,6 @@ int main(int argc, char **argv) {
|
||||||
pthread_mutex_init(&state.script_lock, NULL);
|
pthread_mutex_init(&state.script_lock, NULL);
|
||||||
pthread_mutex_init(&state.settings_lock, NULL);
|
pthread_mutex_init(&state.settings_lock, NULL);
|
||||||
load_script_settings(&state); /* user-set values; schema comes from the script */
|
load_script_settings(&state); /* user-set values; schema comes from the script */
|
||||||
pthread_mutex_init(&state.taxonomy_lock, NULL);
|
|
||||||
load_taxonomy(&state); /* persisted category + tag lists for the web UI */
|
|
||||||
pthread_mutex_init(&state.blob_lock, NULL);
|
|
||||||
state.events = naut_event_bus_create();
|
state.events = naut_event_bus_create();
|
||||||
state.rpc = naut_rpc_registry_create();
|
state.rpc = naut_rpc_registry_create();
|
||||||
state.plugins = naut_plugin_manager_create(state.rpc, state.events);
|
state.plugins = naut_plugin_manager_create(state.rpc, state.events);
|
||||||
|
|
@ -2881,9 +2712,5 @@ int main(int argc, char **argv) {
|
||||||
json_decref(state.script_settings);
|
json_decref(state.script_settings);
|
||||||
json_decref(state.script_settings_schema);
|
json_decref(state.script_settings_schema);
|
||||||
pthread_mutex_destroy(&state.settings_lock);
|
pthread_mutex_destroy(&state.settings_lock);
|
||||||
json_decref(state.label_categories);
|
|
||||||
json_decref(state.label_tags);
|
|
||||||
pthread_mutex_destroy(&state.taxonomy_lock);
|
|
||||||
pthread_mutex_destroy(&state.blob_lock);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ static uint32_t target_peer_count(void) {
|
||||||
return (uint32_t)NAUT_MIN(value, MAX_TARGET_PEERS);
|
return (uint32_t)NAUT_MIN(value, MAX_TARGET_PEERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Below this many connected peers the swarm is "starved": re-announce as often
|
||||||
|
* as the tracker's min interval allows instead of waiting the full interval. */
|
||||||
|
#define LOW_PEER_THRESHOLD 10
|
||||||
|
|
||||||
static double tracker_delay_seconds(int32_t interval) {
|
static double tracker_delay_seconds(int32_t interval) {
|
||||||
if (interval <= 0) return TRACKER_DEFAULT_INTERVAL;
|
if (interval <= 0) return TRACKER_DEFAULT_INTERVAL;
|
||||||
if (interval < (int32_t)TRACKER_MIN_INTERVAL)
|
if (interval < (int32_t)TRACKER_MIN_INTERVAL)
|
||||||
|
|
@ -65,6 +69,20 @@ static double tracker_delay_seconds(int32_t interval) {
|
||||||
return (double)interval;
|
return (double)interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Seconds to wait before the next tracker announce. Normally the tracker's full
|
||||||
|
* advertised interval, but when the swarm is starved (< LOW_PEER_THRESHOLD
|
||||||
|
* connected peers) we re-announce sooner — down to the tracker's min_interval,
|
||||||
|
* never below our 60s floor — so a thin swarm can actually recover. */
|
||||||
|
static double next_announce_delay(int32_t interval, int32_t min_interval,
|
||||||
|
uint32_t peers_connected) {
|
||||||
|
double full = interval > 0 ? tracker_delay_seconds(interval)
|
||||||
|
: TRACKER_FAILURE_RETRY_INTERVAL;
|
||||||
|
if (peers_connected >= LOW_PEER_THRESHOLD) return full;
|
||||||
|
double floor_s = min_interval > 0 ? (double)min_interval : TRACKER_MIN_INTERVAL;
|
||||||
|
if (floor_s < TRACKER_MIN_INTERVAL) floor_s = TRACKER_MIN_INTERVAL;
|
||||||
|
return floor_s < full ? floor_s : full;
|
||||||
|
}
|
||||||
|
|
||||||
static void random_bytes(uint8_t *output, size_t length) {
|
static void random_bytes(uint8_t *output, size_t length) {
|
||||||
int fd = open("/dev/urandom", O_RDONLY);
|
int fd = open("/dev/urandom", O_RDONLY);
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
|
|
@ -385,6 +403,7 @@ static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length
|
||||||
naut_tracker_event event,
|
naut_tracker_event event,
|
||||||
endpoint_t **eps, size_t *neps, size_t *cap,
|
endpoint_t **eps, size_t *neps, size_t *cap,
|
||||||
int32_t *announce_interval,
|
int32_t *announce_interval,
|
||||||
|
int32_t *announce_min_interval,
|
||||||
naut_swarm_tracker_stats *tracker_stats,
|
naut_swarm_tracker_stats *tracker_stats,
|
||||||
uint32_t tracker_count) {
|
uint32_t tracker_count) {
|
||||||
naut_announce_req req;
|
naut_announce_req req;
|
||||||
|
|
@ -399,6 +418,7 @@ static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length
|
||||||
req.numwant = 100;
|
req.numwant = 100;
|
||||||
memcpy(&req.key, peerid + 8, sizeof req.key);
|
memcpy(&req.key, peerid + 8, sizeof req.key);
|
||||||
if (announce_interval) *announce_interval = 0;
|
if (announce_interval) *announce_interval = 0;
|
||||||
|
if (announce_min_interval) *announce_min_interval = 0;
|
||||||
|
|
||||||
size_t tier_start = 0;
|
size_t tier_start = 0;
|
||||||
while (tier_start < num_trackers) {
|
while (tier_start < num_trackers) {
|
||||||
|
|
@ -451,6 +471,8 @@ static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length
|
||||||
tier_succeeded = true;
|
tier_succeeded = true;
|
||||||
if (announce_interval && response.interval > 0)
|
if (announce_interval && response.interval > 0)
|
||||||
*announce_interval = response.interval;
|
*announce_interval = response.interval;
|
||||||
|
if (announce_min_interval && response.min_interval > 0)
|
||||||
|
*announce_min_interval = response.min_interval;
|
||||||
if (tracker_stat) {
|
if (tracker_stat) {
|
||||||
tracker_set_status(tracker_stat, "working", "");
|
tracker_set_status(tracker_stat, "working", "");
|
||||||
tracker_stat->seeds = response.seeders;
|
tracker_stat->seeds = response.seeders;
|
||||||
|
|
@ -574,7 +596,7 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
||||||
endpoint_t *endpoints = NULL;
|
endpoint_t *endpoints = NULL;
|
||||||
size_t neps = 0, epcap = 0;
|
size_t neps = 0, epcap = 0;
|
||||||
uint32_t target_peers = target_peer_count();
|
uint32_t target_peers = target_peer_count();
|
||||||
int32_t tracker_interval = 0;
|
int32_t tracker_interval = 0, tracker_min_interval = 0;
|
||||||
naut_swarm_tracker_stats tracker_stats[NAUT_SWARM_MAX_TRACKER_STATS];
|
naut_swarm_tracker_stats tracker_stats[NAUT_SWARM_MAX_TRACKER_STATS];
|
||||||
uint32_t tracker_count = 0;
|
uint32_t tracker_count = 0;
|
||||||
if (config->num_peers > 0) {
|
if (config->num_peers > 0) {
|
||||||
|
|
@ -608,8 +630,8 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
||||||
if (!discover_trackers(hash, 0, trackers, num_trackers,
|
if (!discover_trackers(hash, 0, trackers, num_trackers,
|
||||||
tracker_tiers, peerid, 0, 0,
|
tracker_tiers, peerid, 0, 0,
|
||||||
NAUT_TEV_STARTED, &endpoints, &neps, &epcap,
|
NAUT_TEV_STARTED, &endpoints, &neps, &epcap,
|
||||||
&tracker_interval, tracker_stats,
|
&tracker_interval, &tracker_min_interval,
|
||||||
tracker_count) ||
|
tracker_stats, tracker_count) ||
|
||||||
(neps < target_peers &&
|
(neps < target_peers &&
|
||||||
!discover_dht(hash, &endpoints, &neps, &epcap))) {
|
!discover_dht(hash, &endpoints, &neps, &epcap))) {
|
||||||
NAUT_ERROR("out of memory collecting discovered peers");
|
NAUT_ERROR("out of memory collecting discovered peers");
|
||||||
|
|
@ -731,8 +753,8 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
||||||
mi.tracker_tiers, peerid,
|
mi.tracker_tiers, peerid,
|
||||||
resumed_bytes, resumed_left,
|
resumed_bytes, resumed_left,
|
||||||
NAUT_TEV_STARTED, &endpoints, &neps, &epcap,
|
NAUT_TEV_STARTED, &endpoints, &neps, &epcap,
|
||||||
&tracker_interval, tracker_stats,
|
&tracker_interval, &tracker_min_interval,
|
||||||
tracker_count) ||
|
tracker_stats, tracker_count) ||
|
||||||
(neps < target_peers &&
|
(neps < target_peers &&
|
||||||
!discover_dht(mi.infohash_v1, &endpoints, &neps, &epcap))) {
|
!discover_dht(mi.infohash_v1, &endpoints, &neps, &epcap))) {
|
||||||
NAUT_ERROR("out of memory collecting discovered peers");
|
NAUT_ERROR("out of memory collecting discovered peers");
|
||||||
|
|
@ -787,10 +809,11 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
||||||
feed_engine(eng, torrent_id, endpoints, neps, &fed);
|
feed_engine(eng, torrent_id, endpoints, neps, &fed);
|
||||||
|
|
||||||
double t0 = now();
|
double t0 = now();
|
||||||
|
/* Use the discovered endpoint count as the initial peer proxy: if we start
|
||||||
|
* thin, schedule a quick re-announce instead of waiting the full interval. */
|
||||||
double next_tracker_announce =
|
double next_tracker_announce =
|
||||||
t0 + (tracker_interval > 0
|
t0 + next_announce_delay(tracker_interval, tracker_min_interval,
|
||||||
? tracker_delay_seconds(tracker_interval)
|
(uint32_t)neps);
|
||||||
: TRACKER_FAILURE_RETRY_INTERVAL);
|
|
||||||
double next_dht_lookup = t0 + DHT_REFRESH_INTERVAL;
|
double next_dht_lookup = t0 + DHT_REFRESH_INTERVAL;
|
||||||
naut_err run_error = NAUT_OK;
|
naut_err run_error = NAUT_OK;
|
||||||
bool cancelled = false;
|
bool cancelled = false;
|
||||||
|
|
@ -827,21 +850,20 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
||||||
uint64_t downloaded = naut_download_bytes_done(d);
|
uint64_t downloaded = naut_download_bytes_done(d);
|
||||||
uint64_t left = (uint64_t)mi.total_length > downloaded
|
uint64_t left = (uint64_t)mi.total_length > downloaded
|
||||||
? (uint64_t)mi.total_length - downloaded : 0;
|
? (uint64_t)mi.total_length - downloaded : 0;
|
||||||
int32_t interval = 0;
|
int32_t interval = 0, min_interval = 0;
|
||||||
if (!discover_trackers(mi.infohash_v1,
|
if (!discover_trackers(mi.infohash_v1,
|
||||||
(uint64_t)mi.total_length,
|
(uint64_t)mi.total_length,
|
||||||
mi.trackers, mi.num_trackers,
|
mi.trackers, mi.num_trackers,
|
||||||
mi.tracker_tiers, peerid,
|
mi.tracker_tiers, peerid,
|
||||||
downloaded, left, NAUT_TEV_NONE,
|
downloaded, left, NAUT_TEV_NONE,
|
||||||
&endpoints, &neps, &epcap,
|
&endpoints, &neps, &epcap,
|
||||||
&interval, tracker_stats,
|
&interval, &min_interval,
|
||||||
tracker_count)) {
|
tracker_stats, tracker_count)) {
|
||||||
run_error = NAUT_ERR_NOMEM;
|
run_error = NAUT_ERR_NOMEM;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
next_tracker_announce = t + (interval > 0
|
next_tracker_announce = t + next_announce_delay(
|
||||||
? tracker_delay_seconds(interval)
|
interval, min_interval, ts.peers_connected);
|
||||||
: TRACKER_FAILURE_RETRY_INTERVAL);
|
|
||||||
feed_engine(eng, torrent_id, endpoints, neps, &fed);
|
feed_engine(eng, torrent_id, endpoints, neps, &fed);
|
||||||
}
|
}
|
||||||
if (t >= next_dht_lookup) {
|
if (t >= next_dht_lookup) {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ typedef struct {
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int32_t interval;
|
int32_t interval;
|
||||||
|
int32_t min_interval; /* tracker's floor, 0 if not advertised */
|
||||||
int32_t seeders, leechers; /* -1 if absent */
|
int32_t seeders, leechers; /* -1 if absent */
|
||||||
naut_peer_addr *peers;
|
naut_peer_addr *peers;
|
||||||
size_t num_peers;
|
size_t num_peers;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
1135
plugins/webui/webui_store.c
Normal file
1135
plugins/webui/webui_store.c
Normal file
File diff suppressed because it is too large
Load diff
107
plugins/webui/webui_store.h
Normal file
107
plugins/webui/webui_store.h
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
/* webui_store.h — SQLite-backed persistence for all web-UI-owned state:
|
||||||
|
* accounts, the category/tag taxonomy, and RSS feeds/rules/indexers.
|
||||||
|
*
|
||||||
|
* Owned entirely by the webui plugin (the daemon persists none of this).
|
||||||
|
* Account passwords are PBKDF2-HMAC-SHA256 with a per-user random salt. All
|
||||||
|
* calls are thread-safe (the store serializes access to its SQLite handle). */
|
||||||
|
#ifndef NAUT_WEBUI_STORE_H
|
||||||
|
#define NAUT_WEBUI_STORE_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <jansson.h>
|
||||||
|
|
||||||
|
typedef struct webui_store webui_store;
|
||||||
|
|
||||||
|
/* Open (creating if needed) the account database at `path`. Returns NULL on
|
||||||
|
* failure. The schema is created/migrated on open. */
|
||||||
|
webui_store *webui_store_open(const char *path);
|
||||||
|
void webui_store_close(webui_store *s);
|
||||||
|
|
||||||
|
/* Number of accounts, or -1 on error. */
|
||||||
|
int webui_store_user_count(webui_store *s);
|
||||||
|
/* Number of admin accounts, or -1 on error. */
|
||||||
|
int webui_store_admin_count(webui_store *s);
|
||||||
|
bool webui_store_user_exists(webui_store *s, const char *username);
|
||||||
|
|
||||||
|
/* Verify a username/password pair (constant-time). On success, copies the
|
||||||
|
* account's role ("admin"/"user") into role_out. */
|
||||||
|
bool webui_store_verify(webui_store *s, const char *username,
|
||||||
|
const char *password, char *role_out, size_t role_sz);
|
||||||
|
|
||||||
|
/* Create an account. `role` must be "admin" or "user" (defaults to "user" if
|
||||||
|
* NULL/invalid). Returns false if the username already exists or on error. */
|
||||||
|
bool webui_store_create_user(webui_store *s, const char *username,
|
||||||
|
const char *password, const char *role);
|
||||||
|
|
||||||
|
bool webui_store_set_password(webui_store *s, const char *username,
|
||||||
|
const char *password);
|
||||||
|
/* Change an account's role ("admin"/"user"). */
|
||||||
|
bool webui_store_set_role(webui_store *s, const char *username, const char *role);
|
||||||
|
bool webui_store_delete_user(webui_store *s, const char *username);
|
||||||
|
|
||||||
|
/* Append {username, role, createdAt} objects (sorted by username) to the
|
||||||
|
* json array `out`. Returns false on error. */
|
||||||
|
bool webui_store_list_users(webui_store *s, json_t *out);
|
||||||
|
|
||||||
|
/* --- sessions (persisted so logins survive daemon restarts) --------------- *
|
||||||
|
* Only a SHA-256 of the bearer token is stored, so a DB read can't be replayed
|
||||||
|
* as a live cookie. `expires` is an absolute unix time. */
|
||||||
|
bool webui_store_session_create(webui_store *s, const char *token,
|
||||||
|
const char *user, const char *role, long expires);
|
||||||
|
/* On a live (unexpired) session, copies username/role and the stored expiry. */
|
||||||
|
bool webui_store_session_lookup(webui_store *s, const char *token,
|
||||||
|
char *user, size_t user_sz,
|
||||||
|
char *role, size_t role_sz, long *expires_out);
|
||||||
|
bool webui_store_session_touch(webui_store *s, const char *token, long expires);
|
||||||
|
bool webui_store_session_delete(webui_store *s, const char *token);
|
||||||
|
bool webui_store_sessions_delete_user(webui_store *s, const char *user);
|
||||||
|
void webui_store_sessions_prune(webui_store *s, long now);
|
||||||
|
|
||||||
|
/* --- category / tag taxonomy (web-UI organization, owned here) ------------- *
|
||||||
|
* The save_* calls replace the whole list atomically; the load_* calls append
|
||||||
|
* to the (array) `out`. Categories are {name, savePath}; tags are strings. */
|
||||||
|
bool webui_store_save_categories(webui_store *s, json_t *cats);
|
||||||
|
bool webui_store_load_categories(webui_store *s, json_t *out);
|
||||||
|
bool webui_store_save_tags(webui_store *s, json_t *tags);
|
||||||
|
bool webui_store_load_tags(webui_store *s, json_t *out);
|
||||||
|
|
||||||
|
/* --- RSS: feeds, articles, auto-download rules, Torznab indexers ----------- *
|
||||||
|
* Fully relational: articles live in their own table (deduped by feed+key,
|
||||||
|
* indexed), and a rule's feed scope lives in a rule_feeds join table. The web
|
||||||
|
* layer operates on rows, not whole-list blobs. */
|
||||||
|
|
||||||
|
/* Feeds. upsert preserves an existing feed's lastUpdate (only the url changes);
|
||||||
|
* remove also drops the feed's articles. feed_list appends
|
||||||
|
* {name,url,lastUpdate,articles:[...]} (newest article first). feed_targets
|
||||||
|
* appends lightweight {name,url} objects for the poller. */
|
||||||
|
bool webui_store_feed_upsert(webui_store *s, const char *name, const char *url);
|
||||||
|
bool webui_store_feed_remove(webui_store *s, const char *name);
|
||||||
|
bool webui_store_feed_set_updated(webui_store *s, const char *name, long ts);
|
||||||
|
bool webui_store_feed_list(webui_store *s, json_t *out);
|
||||||
|
bool webui_store_feed_targets(webui_store *s, json_t *out);
|
||||||
|
bool webui_store_feed_exists(webui_store *s, const char *name);
|
||||||
|
|
||||||
|
/* Articles. add inserts unless (feed,key) already exists: returns 1 if newly
|
||||||
|
* inserted, 0 if a duplicate, -1 on error. trim keeps the newest `keep` for a
|
||||||
|
* feed. mark_grabbed flags every article with this key. ungrabbed appends
|
||||||
|
* {feed,key,title,magnet,torrentUrl} for not-yet-grabbed articles. */
|
||||||
|
int webui_store_article_add(webui_store *s, const char *feed, json_t *article);
|
||||||
|
bool webui_store_article_trim(webui_store *s, const char *feed, int keep);
|
||||||
|
bool webui_store_article_mark_grabbed(webui_store *s, const char *key);
|
||||||
|
bool webui_store_articles_ungrabbed(webui_store *s, json_t *out);
|
||||||
|
|
||||||
|
/* Rules. upsert replaces the rule row and its feed scope; list/get assemble the
|
||||||
|
* rule with its affectedFeeds array. */
|
||||||
|
bool webui_store_rule_upsert(webui_store *s, json_t *rule);
|
||||||
|
bool webui_store_rule_remove(webui_store *s, const char *name);
|
||||||
|
bool webui_store_rule_list(webui_store *s, json_t *out);
|
||||||
|
json_t *webui_store_rule_get(webui_store *s, const char *name);
|
||||||
|
bool webui_store_rule_set_match(webui_store *s, const char *name, long ts);
|
||||||
|
|
||||||
|
/* Torznab indexers. */
|
||||||
|
bool webui_store_indexer_upsert(webui_store *s, json_t *indexer);
|
||||||
|
bool webui_store_indexer_remove(webui_store *s, const char *name);
|
||||||
|
bool webui_store_indexer_list(webui_store *s, json_t *out);
|
||||||
|
|
||||||
|
#endif /* NAUT_WEBUI_STORE_H */
|
||||||
|
|
@ -51,6 +51,7 @@ static naut_err collect_peers(const tracker_peer *peers, size_t count,
|
||||||
const tracker_announce_response *resp,
|
const tracker_announce_response *resp,
|
||||||
naut_tracker_response *out) {
|
naut_tracker_response *out) {
|
||||||
out->interval = (int32_t)resp->interval;
|
out->interval = (int32_t)resp->interval;
|
||||||
|
out->min_interval = (int32_t)resp->min_interval;
|
||||||
out->seeders = (int32_t)resp->complete;
|
out->seeders = (int32_t)resp->complete;
|
||||||
out->leechers = (int32_t)resp->incomplete;
|
out->leechers = (int32_t)resp->incomplete;
|
||||||
out->peers = NULL;
|
out->peers = NULL;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue