Merge webui-plugin into main
This commit is contained in:
commit
252bc8140b
57 changed files with 9342 additions and 2954 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -8,3 +8,12 @@ compile_commands.json
|
||||||
|
|
||||||
# Test scratch
|
# Test scratch
|
||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
|
# Local downloads and packaging scratch
|
||||||
|
/downloads/
|
||||||
|
/package/
|
||||||
|
/package.zip
|
||||||
|
# Stray torrents dropped at the repo root (fixtures under tests/ stay tracked)
|
||||||
|
torrents/*.torrent
|
||||||
|
# Downloaded torrent data (capital-D dir used at runtime)
|
||||||
|
/Downloads/
|
||||||
|
|
|
||||||
Binary file not shown.
215
CMakeLists.txt
215
CMakeLists.txt
|
|
@ -5,6 +5,11 @@ set(CMAKE_C_STANDARD 11)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
option(NAUT_STANDALONE
|
||||||
|
"Statically embed Jansson and Lua in the daemon and client" OFF)
|
||||||
|
option(NAUT_NATIVE
|
||||||
|
"Optimize Release builds for the build machine's CPU" OFF)
|
||||||
|
|
||||||
if(NOT CMAKE_BUILD_TYPE)
|
if(NOT CMAKE_BUILD_TYPE)
|
||||||
set(CMAKE_BUILD_TYPE Release)
|
set(CMAKE_BUILD_TYPE Release)
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -12,8 +17,11 @@ endif()
|
||||||
add_compile_definitions(_GNU_SOURCE)
|
add_compile_definitions(_GNU_SOURCE)
|
||||||
add_compile_options(-Wall -Wextra -Wshadow -Wvla -Wpointer-arith
|
add_compile_options(-Wall -Wextra -Wshadow -Wvla -Wpointer-arith
|
||||||
-fno-omit-frame-pointer)
|
-fno-omit-frame-pointer)
|
||||||
set(CMAKE_C_FLAGS_RELEASE "-O3 -march=native -DNDEBUG")
|
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")
|
||||||
set(CMAKE_C_FLAGS_DEBUG "-O0 -g3")
|
set(CMAKE_C_FLAGS_DEBUG "-O0 -g3")
|
||||||
|
if(NAUT_NATIVE)
|
||||||
|
add_compile_options($<$<CONFIG:Release>:-march=native>)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Sanitizer convenience build: -DNAUT_SAN=address|thread|undefined
|
# Sanitizer convenience build: -DNAUT_SAN=address|thread|undefined
|
||||||
if(NAUT_SAN)
|
if(NAUT_SAN)
|
||||||
|
|
@ -29,10 +37,98 @@ find_path(URING_INC liburing.h)
|
||||||
if(NOT URING_LIB OR NOT URING_INC)
|
if(NOT URING_LIB OR NOT URING_INC)
|
||||||
message(FATAL_ERROR "liburing not found (install liburing-dev)")
|
message(FATAL_ERROR "liburing not found (install liburing-dev)")
|
||||||
endif()
|
endif()
|
||||||
find_package(OpenSSL REQUIRED COMPONENTS Crypto)
|
find_package(OpenSSL REQUIRED COMPONENTS Crypto SSL)
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
pkg_check_modules(JANSSON REQUIRED IMPORTED_TARGET jansson)
|
# --- external download engine + tracker/DHT protocol libraries --------------
|
||||||
pkg_check_modules(LUA REQUIRED IMPORTED_TARGET lua)
|
# 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)
|
||||||
|
|
||||||
|
# Jansson 2.14.1 predates CMake 4's removal of pre-3.5 policy defaults.
|
||||||
|
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
||||||
|
set(JANSSON_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JANSSON_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JANSSON_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JANSSON_INSTALL OFF CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_Declare(jansson
|
||||||
|
URL https://github.com/akheron/jansson/archive/refs/tags/v2.14.1.tar.gz
|
||||||
|
URL_HASH
|
||||||
|
SHA256=979210eaffdffbcf54cfc34d047fccde13f21b529a381df26db871d886f729a4
|
||||||
|
DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
|
||||||
|
FetchContent_MakeAvailable(jansson)
|
||||||
|
target_include_directories(jansson INTERFACE
|
||||||
|
${jansson_SOURCE_DIR}/src
|
||||||
|
${jansson_BINARY_DIR}/include)
|
||||||
|
|
||||||
|
FetchContent_Declare(lua
|
||||||
|
URL https://www.lua.org/ftp/lua-5.4.8.tar.gz
|
||||||
|
URL_HASH
|
||||||
|
SHA256=4f18ddae154e793e46eeab727c59ef1c0c0c2b744e7b94219710d76f530629ae
|
||||||
|
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||||
|
SOURCE_SUBDIR cmake-unused)
|
||||||
|
FetchContent_MakeAvailable(lua)
|
||||||
|
set(LUA_SRC_DIR ${lua_SOURCE_DIR}/src)
|
||||||
|
add_library(naut_lua STATIC
|
||||||
|
${LUA_SRC_DIR}/lapi.c
|
||||||
|
${LUA_SRC_DIR}/lauxlib.c
|
||||||
|
${LUA_SRC_DIR}/lbaselib.c
|
||||||
|
${LUA_SRC_DIR}/lcode.c
|
||||||
|
${LUA_SRC_DIR}/lcorolib.c
|
||||||
|
${LUA_SRC_DIR}/lctype.c
|
||||||
|
${LUA_SRC_DIR}/ldblib.c
|
||||||
|
${LUA_SRC_DIR}/ldebug.c
|
||||||
|
${LUA_SRC_DIR}/ldo.c
|
||||||
|
${LUA_SRC_DIR}/ldump.c
|
||||||
|
${LUA_SRC_DIR}/lfunc.c
|
||||||
|
${LUA_SRC_DIR}/lgc.c
|
||||||
|
${LUA_SRC_DIR}/linit.c
|
||||||
|
${LUA_SRC_DIR}/liolib.c
|
||||||
|
${LUA_SRC_DIR}/llex.c
|
||||||
|
${LUA_SRC_DIR}/lmathlib.c
|
||||||
|
${LUA_SRC_DIR}/lmem.c
|
||||||
|
${LUA_SRC_DIR}/loadlib.c
|
||||||
|
${LUA_SRC_DIR}/lobject.c
|
||||||
|
${LUA_SRC_DIR}/lopcodes.c
|
||||||
|
${LUA_SRC_DIR}/loslib.c
|
||||||
|
${LUA_SRC_DIR}/lparser.c
|
||||||
|
${LUA_SRC_DIR}/lstate.c
|
||||||
|
${LUA_SRC_DIR}/lstring.c
|
||||||
|
${LUA_SRC_DIR}/lstrlib.c
|
||||||
|
${LUA_SRC_DIR}/ltable.c
|
||||||
|
${LUA_SRC_DIR}/ltablib.c
|
||||||
|
${LUA_SRC_DIR}/ltm.c
|
||||||
|
${LUA_SRC_DIR}/lundump.c
|
||||||
|
${LUA_SRC_DIR}/lutf8lib.c
|
||||||
|
${LUA_SRC_DIR}/lvm.c
|
||||||
|
${LUA_SRC_DIR}/lzio.c)
|
||||||
|
target_compile_definitions(naut_lua PRIVATE LUA_USE_LINUX)
|
||||||
|
target_include_directories(naut_lua PUBLIC ${LUA_SRC_DIR})
|
||||||
|
target_link_libraries(naut_lua PUBLIC m ${CMAKE_DL_LIBS})
|
||||||
|
|
||||||
|
set(NAUT_JANSSON_TARGET jansson)
|
||||||
|
set(NAUT_LUA_TARGET naut_lua)
|
||||||
|
else()
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(JANSSON REQUIRED IMPORTED_TARGET jansson)
|
||||||
|
pkg_check_modules(LUA REQUIRED IMPORTED_TARGET lua)
|
||||||
|
set(NAUT_JANSSON_TARGET PkgConfig::JANSSON)
|
||||||
|
set(NAUT_LUA_TARGET PkgConfig::LUA)
|
||||||
|
endif()
|
||||||
|
|
||||||
# --- core: zero-dependency foundation ---------------------------------------
|
# --- core: zero-dependency foundation ---------------------------------------
|
||||||
add_library(naut_core STATIC
|
add_library(naut_core STATIC
|
||||||
|
|
@ -62,21 +158,23 @@ target_link_libraries(naut_bencode PUBLIC naut_core)
|
||||||
add_library(naut_metainfo STATIC src/metainfo/metainfo.c src/metainfo/magnet.c)
|
add_library(naut_metainfo STATIC src/metainfo/metainfo.c src/metainfo/magnet.c)
|
||||||
target_link_libraries(naut_metainfo PUBLIC naut_bencode naut_crypto)
|
target_link_libraries(naut_metainfo PUBLIC naut_bencode naut_crypto)
|
||||||
|
|
||||||
# --- tracker: HTTP + UDP announce (codec + blocking fetch) ------------------
|
# --- discovery: tracker announce + DHT get_peers glue over torrent-tracker ---
|
||||||
add_library(naut_tracker STATIC
|
add_library(naut_discovery STATIC
|
||||||
src/tracker/tracker.c src/tracker/udp.c src/tracker/fetch.c)
|
src/discovery/tracker_client.c src/discovery/dht_client.c)
|
||||||
target_link_libraries(naut_tracker PUBLIC naut_bencode)
|
target_link_libraries(naut_discovery PUBLIC naut_core torrenttracker)
|
||||||
|
|
||||||
# --- dht: BEP-5 KRPC codec + bounded iterative peer lookup ------------------
|
# --- net: blocking HTTP/HTTPS client (RSS feeds, Torznab search) -------------
|
||||||
add_library(naut_dht STATIC src/dht/dht.c src/dht/fetch.c)
|
# PIC so it can be linked into the webui plugin module; naut_log symbols resolve
|
||||||
target_link_libraries(naut_dht PUBLIC naut_bencode naut_tracker)
|
# from the host executable at load time, like the rest of the plugin.
|
||||||
|
add_library(naut_net STATIC src/net/http_client.c)
|
||||||
|
set_target_properties(naut_net PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||||
|
target_link_libraries(naut_net PUBLIC OpenSSL::SSL OpenSSL::Crypto)
|
||||||
|
|
||||||
# --- peer: wire protocol codec (sans-IO) ------------------------------------
|
# --- peer: wire protocol codec (sans-IO), retained for magnet metadata -------
|
||||||
add_library(naut_peer STATIC
|
add_library(naut_peer STATIC
|
||||||
src/peer/wire.c src/peer/extension.c src/peer/metadata.c src/peer/mse.c
|
src/peer/wire.c src/peer/extension.c src/peer/metadata.c)
|
||||||
src/peer/pipeline.c)
|
|
||||||
target_link_libraries(naut_peer PUBLIC
|
target_link_libraries(naut_peer PUBLIC
|
||||||
naut_core naut_crypto naut_bencode naut_tracker OpenSSL::Crypto m)
|
naut_core naut_crypto naut_bencode m)
|
||||||
|
|
||||||
# --- storage: file backend --------------------------------------------------
|
# --- storage: file backend --------------------------------------------------
|
||||||
add_library(naut_storage STATIC src/storage/storage.c)
|
add_library(naut_storage STATIC src/storage/storage.c)
|
||||||
|
|
@ -95,6 +193,10 @@ add_library(naut_platform STATIC
|
||||||
target_include_directories(naut_platform PUBLIC ${URING_INC})
|
target_include_directories(naut_platform PUBLIC ${URING_INC})
|
||||||
target_link_libraries(naut_platform PUBLIC naut_core ${URING_LIB})
|
target_link_libraries(naut_platform PUBLIC naut_core ${URING_LIB})
|
||||||
|
|
||||||
|
# CPU topology helpers do not require the io_uring platform backend.
|
||||||
|
add_library(naut_system STATIC src/platform/system.c)
|
||||||
|
target_link_libraries(naut_system PUBLIC naut_core)
|
||||||
|
|
||||||
# --- session + extensibility control plane ---------------------------------
|
# --- session + extensibility control plane ---------------------------------
|
||||||
add_library(naut_session STATIC src/session/event.c)
|
add_library(naut_session STATIC src/session/event.c)
|
||||||
target_link_libraries(naut_session PUBLIC naut_core)
|
target_link_libraries(naut_session PUBLIC naut_core)
|
||||||
|
|
@ -105,36 +207,40 @@ target_link_libraries(naut_torrents PUBLIC naut_storage)
|
||||||
|
|
||||||
add_library(naut_rpc STATIC src/rpc/rpc.c)
|
add_library(naut_rpc STATIC src/rpc/rpc.c)
|
||||||
target_link_libraries(naut_rpc PUBLIC
|
target_link_libraries(naut_rpc PUBLIC
|
||||||
naut_session naut_core PkgConfig::JANSSON)
|
naut_session naut_core ${NAUT_JANSSON_TARGET})
|
||||||
|
|
||||||
add_library(naut_plugin STATIC src/plugin/plugin.c)
|
add_library(naut_plugin STATIC src/plugin/plugin.c)
|
||||||
target_link_libraries(naut_plugin PUBLIC naut_rpc naut_session dl)
|
target_link_libraries(naut_plugin PUBLIC naut_rpc naut_session dl)
|
||||||
|
|
||||||
add_library(naut_script STATIC src/script/script.c)
|
add_library(naut_script STATIC src/script/script.c)
|
||||||
target_link_libraries(naut_script PUBLIC
|
target_link_libraries(naut_script PUBLIC
|
||||||
naut_session naut_core PkgConfig::LUA)
|
naut_session naut_core ${NAUT_LUA_TARGET})
|
||||||
|
|
||||||
add_library(naut_example MODULE plugins/example/example.c)
|
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 "")
|
||||||
|
|
||||||
# --- echo: Phase 1 gate (io_uring echo server on the buffer pool) -----------
|
# SQLite backs the webui account store.
|
||||||
add_executable(naut_echo apps/echo/main.c)
|
find_package(PkgConfig REQUIRED)
|
||||||
target_link_libraries(naut_echo PRIVATE naut_platform naut_core)
|
pkg_check_modules(SQLITE3 REQUIRED IMPORTED_TARGET sqlite3)
|
||||||
|
|
||||||
# --- leech: Phase 3 gate (single-peer download, byte-correct + verified) ----
|
add_library(naut_webui MODULE plugins/webui/webui.c plugins/webui/webui_store.c)
|
||||||
add_executable(naut_leech apps/leech/main.c)
|
target_include_directories(naut_webui PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||||
target_link_libraries(naut_leech PRIVATE naut_piece naut_peer naut_metainfo)
|
target_link_libraries(naut_webui PRIVATE ${NAUT_JANSSON_TARGET} naut_net
|
||||||
|
PkgConfig::SQLITE3 OpenSSL::Crypto pthread)
|
||||||
|
set_target_properties(naut_webui PROPERTIES PREFIX "")
|
||||||
|
|
||||||
# --- swarm: Phase 4 gate (multi-peer download, rarest-first + endgame) -------
|
# --- swarm: multi-peer download driver over the torrent-peer engine ---------
|
||||||
add_executable(naut_swarm apps/swarm/main.c)
|
add_library(naut_swarm_engine STATIC apps/swarm/main.c)
|
||||||
target_link_libraries(naut_swarm PRIVATE
|
target_compile_definitions(naut_swarm_engine PRIVATE NAUT_SWARM_LIBRARY)
|
||||||
naut_piece naut_peer naut_metainfo naut_tracker naut_dht naut_platform)
|
target_link_libraries(naut_swarm_engine PUBLIC
|
||||||
|
naut_piece naut_peer naut_metainfo naut_discovery naut_system
|
||||||
|
naut_session torrentpeer)
|
||||||
|
|
||||||
# --- daemon + CLI: Phase 7 extensibility surface ---------------------------
|
# --- daemon + CLI: Phase 7 extensibility surface ---------------------------
|
||||||
add_executable(nautd apps/nautd/main.c)
|
add_executable(nautd apps/nautd/main.c)
|
||||||
target_link_libraries(nautd PRIVATE
|
target_link_libraries(nautd PRIVATE
|
||||||
naut_plugin naut_script naut_rpc naut_session naut_torrents naut_metainfo)
|
naut_plugin naut_script naut_rpc naut_session naut_metainfo naut_swarm_engine)
|
||||||
|
|
||||||
add_executable(nautctl apps/nautctl/main.c)
|
add_executable(nautctl apps/nautctl/main.c)
|
||||||
target_link_libraries(nautctl PRIVATE naut_rpc)
|
target_link_libraries(nautctl PRIVATE naut_rpc)
|
||||||
|
|
@ -151,10 +257,6 @@ add_executable(test_worker tests/unit/test_worker.c)
|
||||||
target_link_libraries(test_worker PRIVATE naut_core naut_crypto)
|
target_link_libraries(test_worker PRIVATE naut_core naut_crypto)
|
||||||
add_test(NAME test_worker COMMAND test_worker)
|
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)
|
add_executable(test_rpc tests/unit/test_rpc.c)
|
||||||
target_link_libraries(test_rpc PRIVATE naut_rpc)
|
target_link_libraries(test_rpc PRIVATE naut_rpc)
|
||||||
add_test(NAME test_rpc COMMAND test_rpc)
|
add_test(NAME test_rpc COMMAND test_rpc)
|
||||||
|
|
@ -201,18 +303,6 @@ add_executable(test_extension tests/unit/test_extension.c)
|
||||||
target_link_libraries(test_extension PRIVATE naut_peer)
|
target_link_libraries(test_extension PRIVATE naut_peer)
|
||||||
add_test(NAME test_extension COMMAND test_extension)
|
add_test(NAME test_extension COMMAND test_extension)
|
||||||
|
|
||||||
add_executable(test_mse tests/unit/test_mse.c)
|
|
||||||
target_link_libraries(test_mse PRIVATE naut_peer)
|
|
||||||
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)
|
add_executable(test_storage tests/unit/test_storage.c)
|
||||||
target_link_libraries(test_storage PRIVATE naut_storage)
|
target_link_libraries(test_storage PRIVATE naut_storage)
|
||||||
add_test(NAME test_storage COMMAND test_storage)
|
add_test(NAME test_storage COMMAND test_storage)
|
||||||
|
|
@ -233,46 +323,13 @@ add_executable(test_picker tests/unit/test_picker.c)
|
||||||
target_link_libraries(test_picker PRIVATE naut_piece)
|
target_link_libraries(test_picker PRIVATE naut_piece)
|
||||||
add_test(NAME test_picker COMMAND test_picker)
|
add_test(NAME test_picker COMMAND test_picker)
|
||||||
|
|
||||||
# Phase 3 interop gate: download from a real libtorrent seed (SKIPs without it).
|
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
add_test(NAME phase7_extensibility
|
add_test(NAME phase7_extensibility
|
||||||
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_phase7.sh
|
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_phase7.sh
|
||||||
$<TARGET_FILE:nautd> $<TARGET_FILE:nautctl>
|
$<TARGET_FILE:nautd> $<TARGET_FILE:nautctl>
|
||||||
$<TARGET_FILE:naut_example>
|
$<TARGET_FILE:naut_example>
|
||||||
${CMAKE_SOURCE_DIR}/tests/fixtures/phase7.lua)
|
${CMAKE_SOURCE_DIR}/tests/fixtures/phase7.lua)
|
||||||
set_tests_properties(phase7_extensibility PROPERTIES TIMEOUT 15)
|
set_tests_properties(phase7_extensibility PROPERTIES TIMEOUT 15)
|
||||||
|
set_tests_properties(phase7_extensibility PROPERTIES SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
# Example Lua scripts: parser battery + end-to-end sort path building. Only
|
# Example Lua scripts: parser battery + end-to-end sort path building. Only
|
||||||
# registered when a standalone lua interpreter is available.
|
# registered when a standalone lua interpreter is available.
|
||||||
|
|
|
||||||
18
ISSUES.md
Normal file
18
ISSUES.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
- ✅ 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
|
||||||
|
- ✅ A paused torrent should still do a full piece check.
|
||||||
|
- ✅ Something appears to have broken the peers info tab, nothing shows up.
|
||||||
|
- ✅ RSS should have a manual download button
|
||||||
|
- ✅ RSS should have a manual repull
|
||||||
|
- ✅ 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).
|
||||||
|
- ✅ Rules should show their current matches.
|
||||||
|
- ✅ We need a real login system backed by a database.
|
||||||
113
README.md
113
README.md
|
|
@ -25,6 +25,8 @@ src/peer/ wire protocol, MSE/RC4, BEP-10, ut_metadata, and PEX
|
||||||
apps/echo/ Phase 1 gate: io_uring echo server on the buffer pool
|
apps/echo/ Phase 1 gate: io_uring echo server on the buffer pool
|
||||||
apps/leech/ Phase 3 gate: verified single-peer download
|
apps/leech/ Phase 3 gate: verified single-peer download
|
||||||
apps/swarm/ tracker/DHT discovery, magnets, and concurrent peers
|
apps/swarm/ tracker/DHT discovery, magnets, and concurrent peers
|
||||||
|
apps/nautctl/ thin CLI frontend over daemon RPC
|
||||||
|
plugins/webui/ daemon plugin that serves ../torrent-ui as the web panel
|
||||||
tests/unit/ unit + concurrency tests
|
tests/unit/ unit + concurrency tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -35,6 +37,13 @@ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||||
ninja -C build
|
ninja -C build
|
||||||
ctest --test-dir build --output-on-failure
|
ctest --test-dir build --output-on-failure
|
||||||
|
|
||||||
|
# portable daemon/client build with Jansson and Lua embedded
|
||||||
|
cmake -S . -B build-standalone -G Ninja \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release -DNAUT_STANDALONE=ON
|
||||||
|
ninja -C build-standalone nautd nautctl
|
||||||
|
ldd build-standalone/nautd
|
||||||
|
ldd build-standalone/nautctl
|
||||||
|
|
||||||
# sanitizer build (address|thread|undefined)
|
# sanitizer build (address|thread|undefined)
|
||||||
cmake -S . -B build-tsan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DNAUT_SAN=thread
|
cmake -S . -B build-tsan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DNAUT_SAN=thread
|
||||||
ninja -C build-tsan && ./build-tsan/test_buf
|
ninja -C build-tsan && ./build-tsan/test_buf
|
||||||
|
|
@ -42,13 +51,16 @@ ninja -C build-tsan && ./build-tsan/test_buf
|
||||||
# run the Phase 1 echo gate
|
# run the Phase 1 echo gate
|
||||||
./build/naut_echo 9000
|
./build/naut_echo 9000
|
||||||
|
|
||||||
# download from explicit peers, or omit them to use the torrent's trackers
|
# start the engine, then add and inspect downloads through its RPC frontend
|
||||||
./build/naut_swarm file.torrent output/ 192.0.2.10:6881 192.0.2.11:6881
|
./build/nautd
|
||||||
./build/naut_swarm file.torrent output/
|
./build/nautctl add file.torrent output/
|
||||||
|
./build/nautctl list
|
||||||
|
./build/nautctl show 1
|
||||||
|
./build/nautctl events
|
||||||
|
|
||||||
# trackerless magnet start through DHT (override bootstraps when needed)
|
# explicit peers and trackerless magnets use the same daemon workflow
|
||||||
./build/naut_swarm 'magnet:?xt=urn:btih:...' output/
|
./build/nautctl add file.torrent output/ 192.0.2.10:6881
|
||||||
NAUT_DHT_BOOTSTRAP=127.0.0.1:6881 ./build/naut_swarm 'magnet:?xt=urn:btih:...' output/
|
./build/nautctl add 'magnet:?xt=urn:btih:...' output/
|
||||||
|
|
||||||
# force an encrypted single-peer MSE/RC4 connection
|
# force an encrypted single-peer MSE/RC4 connection
|
||||||
./build/naut_leech --mse file.torrent output/ 192.0.2.10 6881
|
./build/naut_leech --mse file.torrent output/ 192.0.2.10 6881
|
||||||
|
|
@ -59,41 +71,86 @@ NAUT_DHT_BOOTSTRAP=127.0.0.1:6881 ./build/naut_swarm 'magnet:?xt=urn:btih:...' o
|
||||||
bash tests/integration/run_echo_scale.sh ./build/naut_echo
|
bash tests/integration/run_echo_scale.sh ./build/naut_echo
|
||||||
|
|
||||||
# optional data-path tuning
|
# optional data-path tuning
|
||||||
NAUT_DIRECT_IO=1 NAUT_WORKERS=8 ./build/naut_swarm file.torrent output/
|
NAUT_DIRECT_IO=1 NAUT_WORKERS=8 ./build/nautd
|
||||||
NAUT_CPU=2 NAUT_SQPOLL=1 NAUT_HUGEPAGES=1 NAUT_NUMA_NODE=0 ./build/naut_echo 9000
|
NAUT_CPU=2 NAUT_SQPOLL=1 NAUT_HUGEPAGES=1 NAUT_NUMA_NODE=0 ./build/naut_echo 9000
|
||||||
```
|
```
|
||||||
|
|
||||||
Requirements: Linux ≥ 6.0, `liburing` (≥ 2.x), OpenSSL `libcrypto`, Jansson,
|
Requirements: Linux ≥ 6.0, `liburing` (≥ 2.x), OpenSSL `libcrypto`, Jansson,
|
||||||
Lua, CMake ≥ 3.20, gcc/clang, Ninja.
|
Lua, CMake ≥ 3.20, gcc/clang, Ninja.
|
||||||
|
|
||||||
|
`NAUT_STANDALONE=ON` downloads hash-pinned Jansson 2.14.1 and Lua 5.4.8
|
||||||
|
sources at configure time and statically embeds them in `nautd` and `nautctl`.
|
||||||
|
The resulting executables still use the host's glibc/ELF loader intentionally:
|
||||||
|
fully static glibc breaks normal DNS/NSS behavior and native `.so` plugins.
|
||||||
|
Release builds target a portable CPU baseline. Use `-DNAUT_NATIVE=ON` only for
|
||||||
|
a local build that will run on the same CPU family as the build machine.
|
||||||
|
|
||||||
## Daemon, RPC, plugins, and scripts
|
## Daemon, RPC, plugins, and scripts
|
||||||
|
|
||||||
Phase 7 adds a headless control process and thin CLI over a versioned,
|
`nautd` is the application engine: it owns torrent workers, storage, scripts,
|
||||||
length-prefixed JSON protocol on a Unix socket:
|
plugins, progress, and lifecycle. `nautctl` is one thin frontend over a
|
||||||
|
versioned, length-prefixed JSON protocol on a Unix socket; a desktop or web
|
||||||
|
panel can use the same RPC surface.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./build/nautd \
|
./build/nautd \
|
||||||
--socket /tmp/nautd.sock \
|
--socket /tmp/nautd.sock \
|
||||||
--plugin ./build/naut_example.so \
|
--plugin ./build/naut_example.so
|
||||||
--script ./tests/fixtures/phase7.lua
|
|
||||||
|
|
||||||
./build/nautctl ping
|
./build/nautctl ping
|
||||||
./build/nautctl plugins
|
./build/nautctl plugins
|
||||||
./build/nautctl status
|
./build/nautctl status
|
||||||
|
./build/nautctl script ./examples/anime_sort.lua
|
||||||
|
./build/nautctl add show.torrent /downloads/show
|
||||||
|
./build/nautctl list
|
||||||
./build/nautctl events
|
./build/nautctl events
|
||||||
```
|
```
|
||||||
|
|
||||||
`nautctl` accepts an optional JSON value after the method:
|
The convenience commands cover normal operation:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# register a torrent's storage so a move command can resolve + relocate its files
|
./build/nautctl add file.torrent output/ [IP:PORT ...]
|
||||||
./build/nautctl add_torrent \
|
./build/nautctl list
|
||||||
'{"torrent_id":7,"torrent":"file.torrent","root":"output/"}'
|
./build/nautctl show 1
|
||||||
./build/nautctl emit \
|
./build/nautctl remove 1
|
||||||
'{"type":"torrent_finished","torrent_id":7}'
|
./build/nautctl script rules.lua
|
||||||
|
./build/nautctl unscript
|
||||||
./build/nautctl shutdown
|
./build/nautctl shutdown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For tooling and plugin methods, the generic form remains
|
||||||
|
`nautctl METHOD [PARAMS_JSON]`.
|
||||||
|
|
||||||
|
### Web panel
|
||||||
|
|
||||||
|
The web panel is a daemon plugin, not part of `nautctl`. It serves the static
|
||||||
|
frontend from `../torrent-ui/public` by default and adapts that UI's `/api/*`
|
||||||
|
contract to Naut's daemon RPC surface:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
NAUT_WEBUI_ROOT=../torrent-ui/public \
|
||||||
|
NAUT_AUTH_PASSWORD='change-me' \
|
||||||
|
./build/nautd --socket /tmp/nautd.sock --plugin ./build/naut_webui.so
|
||||||
|
# open http://127.0.0.1:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
NAUT_WEBUI_HOST=127.0.0.1 # default
|
||||||
|
NAUT_WEBUI_PORT=8080 # default
|
||||||
|
NAUT_WEBUI_ROOT=../torrent-ui/public
|
||||||
|
NAUT_AUTH_USER=admin # default
|
||||||
|
NAUT_AUTH_PASSWORD=change-me # generated and logged if omitted
|
||||||
|
NAUT_WEBUI_SAVE_PATH=/downloads # default add-torrent destination
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin implements the stable `torrent-ui` API surface: cookie login,
|
||||||
|
`/api/snapshot`, `/api/stream` Server-Sent Events, `/api/meta`, torrent detail
|
||||||
|
tabs, add/remove, and `/api/plugins` loading ES modules from
|
||||||
|
`public/plugins/plugins.json`. Some advanced qBittorrent-style controls in the
|
||||||
|
UI are accepted as no-ops until Naut grows matching daemon RPC methods.
|
||||||
|
|
||||||
The native ABI is declared in `include/naut/naut_plugin.h`. Plugins export
|
The native ABI is declared in `include/naut/naut_plugin.h`. Plugins export
|
||||||
`naut_plugin_register()`, receive the versioned host API, and may register RPC
|
`naut_plugin_register()`, receive the versioned host API, and may register RPC
|
||||||
methods, storage backends, and event handlers. `plugins/example/example.c`
|
methods, storage backends, and event handlers. `plugins/example/example.c`
|
||||||
|
|
@ -105,12 +162,11 @@ hooks are `on_torrent_added`, `on_piece_complete`, `on_file_complete`,
|
||||||
filesystem, process, package-loading, debug, and raw chunk-loading globals
|
filesystem, process, package-loading, debug, and raw chunk-loading globals
|
||||||
(`os`, `io`, `package`/`require`, `debug`, `dofile`/`loadfile`, and
|
(`os`, `io`, `package`/`require`, `debug`, `dofile`/`loadfile`, and
|
||||||
`load`/`loadstring` — the bytecode loaders are denied so a crafted binary chunk
|
`load`/`loadstring` — the bytecode loaders are denied so a crafted binary chunk
|
||||||
can't escape the VM). `naut.move_file()` submits a bounded command from the
|
can't escape the VM). `naut.move_file()` submits a bounded command to the
|
||||||
script thread to the daemon owner thread; the owner resolves it through the
|
worker that owns the torrent. That worker performs
|
||||||
torrent registry (`naut_session`) and performs the relocate with
|
`naut_storage_relocate()` and keeps tracking the file at its new path.
|
||||||
`naut_storage_relocate()`. Register a torrent's storage first with the
|
`phase7_extensibility` drives a real daemon-owned download end to end and
|
||||||
`add_torrent` RPC so the id resolves. `phase7_extensibility` drives this
|
asserts the moved file byte-for-byte.
|
||||||
end to end and asserts the file actually moves on disk.
|
|
||||||
|
|
||||||
The full script-visible surface — every event hook, the `event` object's
|
The full script-visible surface — every event hook, the `event` object's
|
||||||
fields, and the `naut` API table — is documented in
|
fields, and the `naut` API table — is documented in
|
||||||
|
|
@ -211,9 +267,8 @@ embedded Anitomy-style filename parser ([`examples/`](examples/)).
|
||||||
piece verifies — before the torrent finishes — and `naut_storage_relocate()`
|
piece verifies — before the torrent finishes — and `naut_storage_relocate()`
|
||||||
moves that file out safely (even mid-download, while other files' pieces are
|
moves that file out safely (even mid-download, while other files' pieces are
|
||||||
still arriving). `test_filemove` proves a file is relocated mid-download with
|
still arriving). `test_filemove` proves a file is relocated mid-download with
|
||||||
no corruption. The scripting layer (Phase 7) forwards the event to an
|
no corruption. The scripting layer forwards the event to an
|
||||||
`on_file_complete` hook and exposes `move_file`; the daemon resolves the
|
`on_file_complete` hook and exposes `move_file`; the daemon queues the command
|
||||||
command through the `naut_session` torrent registry (`src/session/session.c`)
|
back to the worker that owns the torrent's storage.
|
||||||
and calls `naut_storage_relocate()` on its owner thread. `phase7_extensibility`
|
`phase7_extensibility` exercises the whole chain — download worker → script
|
||||||
exercises the whole chain — script thread → bounded queue → owner thread →
|
thread → bounded command queue → download worker — and checks the moved bytes.
|
||||||
storage — and asserts the file moves on disk.
|
|
||||||
|
|
|
||||||
214
apps/echo/main.c
214
apps/echo/main.c
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -11,8 +11,17 @@
|
||||||
|
|
||||||
static void usage(const char *program) {
|
static void usage(const char *program) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"usage: %s [--socket PATH] METHOD [PARAMS_JSON]\n"
|
"usage: %s [--socket PATH] COMMAND [ARGS]\n"
|
||||||
" %s [--socket PATH] events\n", program, program);
|
"\n"
|
||||||
|
"commands:\n"
|
||||||
|
" 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"
|
||||||
|
" METHOD [PARAMS_JSON] (raw RPC)\n", program);
|
||||||
}
|
}
|
||||||
|
|
||||||
static json_t *parse_params(const char *text) {
|
static json_t *parse_params(const char *text) {
|
||||||
|
|
@ -54,6 +63,15 @@ static int stream_events(const char *socket_path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool parse_id(const char *text, json_int_t *id) {
|
||||||
|
if (!text || !*text || *text == '-') return false;
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long long value = strtoull(text, &end, 10);
|
||||||
|
if (!end || *end || value > (unsigned long long)INT64_MAX) return false;
|
||||||
|
*id = (json_int_t)value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
const char *socket_path = getenv("NAUT_SOCKET");
|
const char *socket_path = getenv("NAUT_SOCKET");
|
||||||
if (!socket_path || !*socket_path) socket_path = DEFAULT_SOCKET;
|
if (!socket_path || !*socket_path) socket_path = DEFAULT_SOCKET;
|
||||||
|
|
@ -66,7 +84,7 @@ int main(int argc, char **argv) {
|
||||||
socket_path = argv[arg + 1];
|
socket_path = argv[arg + 1];
|
||||||
arg += 2;
|
arg += 2;
|
||||||
}
|
}
|
||||||
if (arg >= argc || arg + 2 < argc) {
|
if (arg >= argc) {
|
||||||
usage(argv[0]);
|
usage(argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +92,52 @@ int main(int argc, char **argv) {
|
||||||
const char *method = argv[arg++];
|
const char *method = argv[arg++];
|
||||||
if (strcmp(method, "events") == 0)
|
if (strcmp(method, "events") == 0)
|
||||||
return stream_events(socket_path);
|
return stream_events(socket_path);
|
||||||
json_t *params = parse_params(arg < argc ? argv[arg] : NULL);
|
|
||||||
|
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";
|
||||||
|
params = json_pack("{s:s,s:s}", "source", argv[arg],
|
||||||
|
"output", argv[arg + 1]);
|
||||||
|
arg += 2;
|
||||||
|
json_t *peers = json_array();
|
||||||
|
if (!params || !peers) {
|
||||||
|
json_decref(params);
|
||||||
|
json_decref(peers);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
while (arg < argc)
|
||||||
|
json_array_append_new(peers, json_string(argv[arg++]));
|
||||||
|
json_object_set_new(params, "peers", peers);
|
||||||
|
} else if (strcmp(method, "list") == 0) {
|
||||||
|
if (arg != argc) { usage(argv[0]); return 2; }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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; }
|
||||||
|
method = "load_script";
|
||||||
|
params = json_pack("{s:s}", "path", argv[arg]);
|
||||||
|
} else if (strcmp(method, "unscript") == 0) {
|
||||||
|
if (arg != argc) { usage(argv[0]); return 2; }
|
||||||
|
method = "unload_script";
|
||||||
|
params = json_object();
|
||||||
|
} else {
|
||||||
|
if (arg + 1 < argc) { usage(argv[0]); return 2; }
|
||||||
|
params = parse_params(arg < argc ? argv[arg] : NULL);
|
||||||
|
}
|
||||||
if (!params) {
|
if (!params) {
|
||||||
fprintf(stderr, "nautctl: invalid JSON parameters\n");
|
fprintf(stderr, "nautctl: invalid JSON parameters\n");
|
||||||
return 2;
|
return 2;
|
||||||
|
|
@ -86,8 +149,19 @@ int main(int argc, char **argv) {
|
||||||
fprintf(stderr, "nautctl: RPC failed: %s\n", naut_strerror(error));
|
fprintf(stderr, "nautctl: RPC failed: %s\n", naut_strerror(error));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
int result = print_json(reply);
|
|
||||||
bool ok = json_is_true(json_object_get(reply, "ok"));
|
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);
|
json_decref(reply);
|
||||||
return result || !ok;
|
return result || !ok;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2461
apps/nautd/main.c
2461
apps/nautd/main.c
File diff suppressed because it is too large
Load diff
1096
apps/swarm/main.c
1096
apps/swarm/main.c
File diff suppressed because it is too large
Load diff
|
|
@ -16,18 +16,19 @@ them, and the `naut` API table.
|
||||||
|
|
||||||
## 1. Loading a script
|
## 1. Loading a script
|
||||||
|
|
||||||
Pass a script to the daemon with `--script`:
|
Load a script through the control API:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nautd --socket /tmp/nautd.sock --script ./my-rules.lua
|
nautctl --socket /tmp/nautd.sock script ./my-rules.lua
|
||||||
```
|
```
|
||||||
|
|
||||||
- **One script per daemon.** If `--script` is given more than once, the last
|
- **One active script per daemon.** Loading another script replaces the current
|
||||||
path wins.
|
one. `nautctl unscript` unloads it. `nautd --script PATH` is also available
|
||||||
- The file is **loaded and executed once at startup**, on the main thread,
|
for startup configuration.
|
||||||
before the event worker starts. Use this top-level run to define your hook
|
- The file is **loaded and executed once**, synchronously on the control thread,
|
||||||
functions (and any state they need). If the file fails to load or its
|
before its event worker starts. Use this top-level run to define your hook
|
||||||
top-level code raises, the daemon refuses to start and prints the Lua error.
|
functions (and any state they need). A load or top-level error rejects the
|
||||||
|
request (or prevents startup when `--script` is used).
|
||||||
- After startup the script is **event-driven**: the functions you defined are
|
- After startup the script is **event-driven**: the functions you defined are
|
||||||
called as matching events occur.
|
called as matching events occur.
|
||||||
|
|
||||||
|
|
@ -161,9 +162,9 @@ Move one **completed** file of a torrent to `destination` (the headline
|
||||||
**Return value:** none on success.
|
**Return value:** none on success.
|
||||||
|
|
||||||
**Asynchronous semantics — important.** `move_file` does **not** perform the
|
**Asynchronous semantics — important.** `move_file` does **not** perform the
|
||||||
move inline on the script thread. It enqueues a bounded command that the daemon
|
move inline on the script thread. It enqueues a bounded command that the
|
||||||
**owner thread** executes shortly after (this preserves the engine's
|
torrent's **owner thread** executes shortly after (the script thread never
|
||||||
shared-nothing threading: the script thread never touches storage directly).
|
touches storage directly).
|
||||||
So a successful call means *"the move was accepted"*, not *"the file has moved."*
|
So a successful call means *"the move was accepted"*, not *"the file has moved."*
|
||||||
The actual relocate (a `rename`, or copy+unlink across filesystems) runs later;
|
The actual relocate (a `rename`, or copy+unlink across filesystems) runs later;
|
||||||
its success or failure is reported in the **daemon log** and reflected in the
|
its success or failure is reported in the **daemon log** and reflected in the
|
||||||
|
|
@ -189,19 +190,87 @@ function on_file_complete(event)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
**Prerequisite — register the torrent's storage first.** `move_file` resolves
|
**Prerequisite — the torrent must be loaded.** Add the torrent to the daemon;
|
||||||
`torrent_id` through the daemon's torrent registry (`naut_session`). Register a
|
the same worker that downloads it owns and executes its move commands:
|
||||||
torrent's storage with the `add_torrent` RPC before any move command can take
|
|
||||||
effect:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nautctl --socket /tmp/nautd.sock add_torrent \
|
nautctl --socket /tmp/nautd.sock add file.torrent /downloads/42
|
||||||
'{"torrent_id":42,"torrent":"file.torrent","root":"/downloads/42"}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If `torrent_id` is unknown when the move drains, the relocate fails with
|
If `torrent_id` is unknown or is being removed, `naut.move_file` raises a
|
||||||
"not found" in the daemon log (the Lua call itself still succeeded, because it
|
`move_file failed` error in the hook.
|
||||||
only *queued* the command).
|
|
||||||
|
### `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
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -283,7 +352,7 @@ end
|
||||||
Run it:
|
Run it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nautd --socket /tmp/nautd.sock --script ./archive-on-finish.lua &
|
nautd --socket /tmp/nautd.sock &
|
||||||
nautctl --socket /tmp/nautd.sock add_torrent \
|
nautctl --socket /tmp/nautd.sock script ./archive-on-finish.lua
|
||||||
'{"torrent_id":1,"torrent":"big.torrent","root":"/downloads/1"}'
|
nautctl --socket /tmp/nautd.sock add big.torrent /downloads/1
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,10 @@ Movies (no detectable episode) go to `<SORTED_ROOT>/<Title>/<Title> (year).<ext>
|
||||||
### Use it
|
### Use it
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# 1. edit SORTED_ROOT (and options) at the top of the script
|
# Start the engine, load the script, and add a download.
|
||||||
# 2. start the daemon with the script
|
nautd --socket /tmp/nautd.sock
|
||||||
nautd --socket /tmp/nautd.sock --script examples/anime_sort.lua
|
nautctl --socket /tmp/nautd.sock script examples/anime_sort.lua
|
||||||
|
nautctl --socket /tmp/nautd.sock add show.torrent /downloads/1
|
||||||
# 3. register each torrent's storage so the move can resolve + relocate its files
|
|
||||||
nautctl --socket /tmp/nautd.sock add_torrent \
|
|
||||||
'{"torrent_id":1,"torrent":"show.torrent","root":"/downloads/1"}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
As each file completes you'll see, e.g.:
|
As each file completes you'll see, e.g.:
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,55 @@
|
||||||
-- file's last piece verifies, episodes are sorted the moment they're done —
|
-- file's last piece verifies, episodes are sorted the moment they're done —
|
||||||
-- without waiting for the rest of the torrent.
|
-- 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:
|
-- Install:
|
||||||
-- nautd --socket /tmp/nautd.sock --script examples/anime_sort.lua
|
-- nautd --socket /tmp/nautd.sock
|
||||||
-- nautctl --socket /tmp/nautd.sock add_torrent \
|
-- nautctl --socket /tmp/nautd.sock script examples/anime_sort.lua
|
||||||
-- '{"torrent_id":1,"torrent":"show.torrent","root":"/downloads/1"}'
|
-- nautctl --socket /tmp/nautd.sock add show.torrent /downloads/1
|
||||||
--
|
--
|
||||||
-- The parser below is a VERBATIM COPY of examples/anitomy.lua (the sandbox has
|
-- The parser below is a VERBATIM COPY of examples/anitomy.lua (the sandbox has
|
||||||
-- no `require`, so it must be inlined). Keep the two in sync; examples/
|
-- no `require`, so it must be inlined). Keep the two in sync; examples/
|
||||||
-- test_anime_sort.lua asserts they agree.
|
-- 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 = "/sorted" -- destination library root
|
local DEFAULTS = {
|
||||||
local ONLY_VIDEO = true -- skip non-video files (subs, nfo, samples)
|
sorted_root = "/workspaces/source/ai-garbo/Naut-Torrent/Downloads/Sorted/",
|
||||||
local KEEP_ORIGINAL_NAME = false -- true: keep the original filename;
|
only_video = true, -- skip non-video files (subs, nfo, samples)
|
||||||
-- false: rename to "Title - SNNENN.ext"
|
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)
|
-- embedded anitomy parser (== examples/anitomy.lua)
|
||||||
|
|
@ -240,6 +272,8 @@ local function sanitize(s)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function destination(parsed)
|
local function destination(parsed)
|
||||||
|
local root = setting("sorted_root")
|
||||||
|
local keep_original = setting("keep_original_name")
|
||||||
local title = sanitize(parsed.title)
|
local title = sanitize(parsed.title)
|
||||||
local ext = parsed.extension and ("." .. parsed.extension) or ""
|
local ext = parsed.extension and ("." .. parsed.extension) or ""
|
||||||
local original = sanitize(basename(parsed.file_name))
|
local original = sanitize(basename(parsed.file_name))
|
||||||
|
|
@ -247,18 +281,18 @@ local function destination(parsed)
|
||||||
if parsed.episode == nil then
|
if parsed.episode == nil then
|
||||||
-- movie / special: <root>/<title>/<file>
|
-- movie / special: <root>/<title>/<file>
|
||||||
local fname
|
local fname
|
||||||
if KEEP_ORIGINAL_NAME then
|
if keep_original then
|
||||||
fname = original
|
fname = original
|
||||||
else
|
else
|
||||||
fname = title .. (parsed.year and (" (" .. parsed.year .. ")") or "") .. ext
|
fname = title .. (parsed.year and (" (" .. parsed.year .. ")") or "") .. ext
|
||||||
end
|
end
|
||||||
return SORTED_ROOT .. "/" .. title .. "/" .. fname
|
return root .. "/" .. title .. "/" .. fname
|
||||||
end
|
end
|
||||||
|
|
||||||
local season = parsed.season or 1
|
local season = parsed.season or 1
|
||||||
local sdir = string.format("Season %02d", season)
|
local sdir = string.format("Season %02d", season)
|
||||||
local fname
|
local fname
|
||||||
if KEEP_ORIGINAL_NAME then
|
if keep_original then
|
||||||
fname = original
|
fname = original
|
||||||
else
|
else
|
||||||
fname = string.format("%s - S%02dE%02d", title, season, parsed.episode)
|
fname = string.format("%s - S%02dE%02d", title, season, parsed.episode)
|
||||||
|
|
@ -267,11 +301,25 @@ local function destination(parsed)
|
||||||
end
|
end
|
||||||
fname = fname .. ext
|
fname = fname .. ext
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- exposed for tests; harmless in the daemon
|
-- 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
|
-- event hook
|
||||||
|
|
@ -279,10 +327,13 @@ _G.anime_sort = { anitomy = anitomy, destination = destination }
|
||||||
|
|
||||||
function on_file_complete(event)
|
function on_file_complete(event)
|
||||||
if not event.path then return end
|
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 name = basename(event.path)
|
||||||
local parsed = anitomy.parse(name)
|
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
|
return -- leave subtitles, .nfo, samples, etc. where they are
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@ local ref = require("anitomy")
|
||||||
|
|
||||||
-- stub the host API the script calls, and silence its prints
|
-- stub the host API the script calls, and silence its prints
|
||||||
local captured
|
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
|
local realprint = print
|
||||||
_G.print = function() end
|
_G.print = function() end
|
||||||
|
|
||||||
|
|
@ -15,10 +24,17 @@ dofile(dir .. "anime_sort.lua") -- defines on_file_complete + _G.anime_sort
|
||||||
|
|
||||||
_G.print = realprint
|
_G.print = realprint
|
||||||
|
|
||||||
|
local configured_root = _G.anime_sort.destination({
|
||||||
|
title = "__ROOT_PROBE__",
|
||||||
|
file_name = "__ROOT_PROBE__.mkv",
|
||||||
|
extension = "mkv",
|
||||||
|
}):match("^(.*)/__ROOT_PROBE__/__ROOT_PROBE__%.mkv$")
|
||||||
|
assert(configured_root, "could not derive SORTED_ROOT from anime_sort.lua")
|
||||||
|
|
||||||
local fails = 0
|
local fails = 0
|
||||||
local function fire(path)
|
local function fire(path, torrent_id)
|
||||||
captured = nil
|
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
|
return captured
|
||||||
end
|
end
|
||||||
local function expect(path, want_dest)
|
local function expect(path, want_dest)
|
||||||
|
|
@ -35,21 +51,21 @@ end
|
||||||
|
|
||||||
-- episodes
|
-- episodes
|
||||||
expect("/downloads/1/[HorribleSubs] Boku no Hero Academia - 12 [1080p].mkv",
|
expect("/downloads/1/[HorribleSubs] Boku no Hero Academia - 12 [1080p].mkv",
|
||||||
"/sorted/Boku no Hero Academia/Season 01/Boku no Hero Academia - S01E12.mkv")
|
configured_root .. "/Boku no Hero Academia/Season 01/Boku no Hero Academia - S01E12.mkv")
|
||||||
expect("/downloads/2/[Group] Attack on Titan S04E28 [BD 1080p HEVC].mkv",
|
expect("/downloads/2/[Group] Attack on Titan S04E28 [BD 1080p HEVC].mkv",
|
||||||
"/sorted/Attack on Titan/Season 04/Attack on Titan - S04E28.mkv")
|
configured_root .. "/Attack on Titan/Season 04/Attack on Titan - S04E28.mkv")
|
||||||
expect("/dl/[Commie] Steins;Gate 0 - 12 [BD 1080p].mkv",
|
expect("/dl/[Commie] Steins;Gate 0 - 12 [BD 1080p].mkv",
|
||||||
"/sorted/Steins;Gate 0/Season 01/Steins;Gate 0 - S01E12.mkv")
|
configured_root .. "/Steins;Gate 0/Season 01/Steins;Gate 0 - S01E12.mkv")
|
||||||
expect("Demon.Slayer.Kimetsu.no.Yaiba.S03E11.1080p.mkv",
|
expect("Demon.Slayer.Kimetsu.no.Yaiba.S03E11.1080p.mkv",
|
||||||
"/sorted/Demon Slayer Kimetsu no Yaiba/Season 03/Demon Slayer Kimetsu no Yaiba - S03E11.mkv")
|
configured_root .. "/Demon Slayer Kimetsu no Yaiba/Season 03/Demon Slayer Kimetsu no Yaiba - S03E11.mkv")
|
||||||
expect("/x/[Erai-raws] Jujutsu Kaisen 2nd Season - 17 [1080p].mkv",
|
expect("/x/[Erai-raws] Jujutsu Kaisen 2nd Season - 17 [1080p].mkv",
|
||||||
"/sorted/Jujutsu Kaisen/Season 02/Jujutsu Kaisen - S02E17.mkv")
|
configured_root .. "/Jujutsu Kaisen/Season 02/Jujutsu Kaisen - S02E17.mkv")
|
||||||
|
|
||||||
-- movies (no episode)
|
-- movies (no episode)
|
||||||
expect("/m/[Group] A Silent Voice [BD 1080p FLAC].mkv",
|
expect("/m/[Group] A Silent Voice [BD 1080p FLAC].mkv",
|
||||||
"/sorted/A Silent Voice/A Silent Voice.mkv")
|
configured_root .. "/A Silent Voice/A Silent Voice.mkv")
|
||||||
expect("/m/Spirited.Away.2001.1080p.BluRay.x264.mkv",
|
expect("/m/Spirited.Away.2001.1080p.BluRay.x264.mkv",
|
||||||
"/sorted/Spirited Away/Spirited Away (2001).mkv")
|
configured_root .. "/Spirited Away/Spirited Away (2001).mkv")
|
||||||
|
|
||||||
-- non-video file must be skipped (no move queued)
|
-- non-video file must be skipped (no move queued)
|
||||||
do
|
do
|
||||||
|
|
@ -62,6 +78,42 @@ do
|
||||||
end
|
end
|
||||||
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
|
-- drift guard: embedded parser must agree with anitomy.lua
|
||||||
local embedded = _G.anime_sort.anitomy
|
local embedded = _G.anime_sort.anitomy
|
||||||
local drift = 0
|
local drift = 0
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ enum {
|
||||||
NAUT_ERR_FULL = -8,
|
NAUT_ERR_FULL = -8,
|
||||||
NAUT_ERR_EMPTY = -9,
|
NAUT_ERR_EMPTY = -9,
|
||||||
NAUT_ERR_NOTFOUND = -10,
|
NAUT_ERR_NOTFOUND = -10,
|
||||||
|
NAUT_ERR_EXIST = -11, /* already exists / data would overlap */
|
||||||
};
|
};
|
||||||
|
|
||||||
const char *naut_strerror(naut_err e);
|
const char *naut_strerror(naut_err e);
|
||||||
|
|
|
||||||
|
|
@ -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
|
#ifndef NAUT_DHT_H
|
||||||
#define NAUT_DHT_H
|
#define NAUT_DHT_H
|
||||||
|
|
||||||
|
|
@ -9,54 +10,6 @@
|
||||||
#define NAUT_DHT_MAX_NODES 256
|
#define NAUT_DHT_MAX_NODES 256
|
||||||
#define NAUT_DHT_MAX_PEERS 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
|
/* Query bootstrap endpoints ("host:port") and iteratively follow returned
|
||||||
* compact nodes until peers are found or the bounded traversal is exhausted. */
|
* 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,
|
naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap,
|
||||||
|
|
|
||||||
26
include/naut/http_client.h
Normal file
26
include/naut/http_client.h
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/* http_client.h — minimal blocking HTTP/HTTPS GET client.
|
||||||
|
*
|
||||||
|
* Used by the web UI for RSS feed polling and Torznab indexer search. Supports
|
||||||
|
* plain HTTP and TLS (via OpenSSL), follows redirects, and returns the decoded
|
||||||
|
* response body. Intended for occasional fetches off the hot path. */
|
||||||
|
#ifndef NAUT_HTTP_CLIENT_H
|
||||||
|
#define NAUT_HTTP_CLIENT_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "naut/common.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
long status; /* HTTP status code (e.g. 200) */
|
||||||
|
char *body; /* malloc'd, NUL-terminated response body */
|
||||||
|
size_t body_len; /* length of body, excluding the NUL */
|
||||||
|
} naut_http_response;
|
||||||
|
|
||||||
|
/* GET `url` (http:// or https://), following up to a handful of redirects.
|
||||||
|
* On NAUT_OK, `out` owns `out->body` (free with naut_http_response_free).
|
||||||
|
* Returns NAUT_ERR_* on transport/protocol failure. A non-2xx HTTP status is
|
||||||
|
* still returned as NAUT_OK with out->status set, so callers can inspect it. */
|
||||||
|
naut_err naut_http_get(const char *url, naut_http_response *out);
|
||||||
|
|
||||||
|
void naut_http_response_free(naut_http_response *r);
|
||||||
|
|
||||||
|
#endif /* NAUT_HTTP_CLIENT_H */
|
||||||
|
|
@ -35,7 +35,9 @@ typedef struct naut_metainfo {
|
||||||
const uint8_t *piece_hashes;
|
const uint8_t *piece_hashes;
|
||||||
|
|
||||||
naut_file *files; size_t num_files;
|
naut_file *files; size_t num_files;
|
||||||
char **trackers; size_t num_trackers; /* announce + announce-list, flattened */
|
char **trackers; size_t num_trackers;
|
||||||
|
/* Parallel to trackers. Equal values belong to one BEP-12 tier. */
|
||||||
|
uint32_t *tracker_tiers;
|
||||||
|
|
||||||
/* internals kept alive so piece_hashes/name stay valid */
|
/* internals kept alive so piece_hashes/name stay valid */
|
||||||
void *_owned;
|
void *_owned;
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
|
||||||
|
|
@ -25,6 +25,7 @@ typedef naut_err (*naut_plugin_rpc_fn)(void *context,
|
||||||
char **response_json);
|
char **response_json);
|
||||||
typedef void (*naut_plugin_event_fn)(void *context,
|
typedef void (*naut_plugin_event_fn)(void *context,
|
||||||
const naut_event *event);
|
const naut_event *event);
|
||||||
|
typedef naut_err (*naut_plugin_shutdown_fn)(void);
|
||||||
|
|
||||||
typedef struct naut_host_api {
|
typedef struct naut_host_api {
|
||||||
uint32_t abi_version;
|
uint32_t abi_version;
|
||||||
|
|
@ -41,6 +42,8 @@ typedef struct naut_host_api {
|
||||||
void *context);
|
void *context);
|
||||||
void (*emit_event)(void *host_context, const naut_event *event);
|
void (*emit_event)(void *host_context, const naut_event *event);
|
||||||
void (*log)(void *host_context, int level, const char *message);
|
void (*log)(void *host_context, int level, const char *message);
|
||||||
|
naut_err (*call_rpc)(void *host_context, const char *method,
|
||||||
|
const char *request_json, char **response_json);
|
||||||
} naut_host_api;
|
} naut_host_api;
|
||||||
|
|
||||||
/* Every plugin exports this exact symbol. */
|
/* Every plugin exports this exact symbol. */
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,17 @@
|
||||||
#include "naut/bitfield.h"
|
#include "naut/bitfield.h"
|
||||||
#include "naut/worker.h"
|
#include "naut/worker.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
typedef struct naut_download naut_download;
|
typedef struct naut_download naut_download;
|
||||||
|
|
||||||
naut_download *naut_download_create(const naut_metainfo *mi, naut_storage *st);
|
naut_download *naut_download_create(const naut_metainfo *mi, naut_storage *st);
|
||||||
void naut_download_destroy(naut_download *d);
|
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;
|
/* Optional hash offload. Completed-piece SHA-1 jobs run on the worker pool;
|
||||||
* naut_download_poll() finalizes verified pieces on the owning engine thread.
|
* naut_download_poll() finalizes verified pieces on the owning engine thread.
|
||||||
* The pool must outlive the download. */
|
* The pool must outlive the download. */
|
||||||
|
|
@ -67,6 +73,11 @@ typedef void (*naut_file_complete_cb)(void *ctx, uint32_t file_index, const char
|
||||||
void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx);
|
void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx);
|
||||||
bool naut_download_file_complete(const naut_download *d, uint32_t file_index);
|
bool naut_download_file_complete(const naut_download *d, uint32_t file_index);
|
||||||
|
|
||||||
|
/* Optional owner-thread notification after a piece verifies and is persisted. */
|
||||||
|
typedef void (*naut_piece_complete_cb)(void *ctx, uint32_t piece_index);
|
||||||
|
void naut_download_set_piece_cb(naut_download *d, naut_piece_complete_cb cb,
|
||||||
|
void *ctx);
|
||||||
|
|
||||||
/* Hand out the next block to request. false => nothing left to hand out right
|
/* Hand out the next block to request. false => nothing left to hand out right
|
||||||
* now (all blocks have been requested). */
|
* now (all blocks have been requested). */
|
||||||
bool naut_download_next_request(naut_download *d,
|
bool naut_download_next_request(naut_download *d,
|
||||||
|
|
@ -82,5 +93,14 @@ bool naut_download_complete(const naut_download *d);
|
||||||
uint32_t naut_download_num_pieces(const naut_download *d);
|
uint32_t naut_download_num_pieces(const naut_download *d);
|
||||||
uint32_t naut_download_pieces_done(const naut_download *d);
|
uint32_t naut_download_pieces_done(const naut_download *d);
|
||||||
uint64_t naut_download_bytes_done(const naut_download *d);
|
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 */
|
#endif /* NAUT_PIECE_H */
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
#include <jansson.h>
|
#include <jansson.h>
|
||||||
|
|
||||||
#define NAUT_RPC_VERSION 1
|
#define NAUT_RPC_VERSION 1
|
||||||
#define NAUT_RPC_MAX_PAYLOAD (1u << 20)
|
/* Generous enough to carry a base64-encoded .torrent upload in add_torrent. */
|
||||||
|
#define NAUT_RPC_MAX_PAYLOAD (8u << 20)
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
NAUT_RPC_REQUEST = 1,
|
NAUT_RPC_REQUEST = 1,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,50 @@ typedef naut_err (*naut_script_move_file_cb)(void *context,
|
||||||
uint32_t file_index,
|
uint32_t file_index,
|
||||||
const char *destination);
|
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 {
|
typedef struct {
|
||||||
uint64_t queued;
|
uint64_t queued;
|
||||||
uint64_t handled;
|
uint64_t handled;
|
||||||
|
|
@ -20,12 +64,12 @@ typedef struct {
|
||||||
} naut_script_stats;
|
} naut_script_stats;
|
||||||
|
|
||||||
/* script_path is loaded before the worker starts. queue_capacity bounds copied
|
/* 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,
|
naut_script *naut_script_create(naut_event_bus *events,
|
||||||
const char *script_path,
|
const char *script_path,
|
||||||
size_t queue_capacity,
|
size_t queue_capacity,
|
||||||
naut_script_move_file_cb move_file,
|
const naut_script_host *host,
|
||||||
void *move_context,
|
|
||||||
naut_err *error);
|
naut_err *error);
|
||||||
void naut_script_destroy(naut_script *script);
|
void naut_script_destroy(naut_script *script);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ typedef struct naut_storage naut_storage;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool direct_io;
|
bool direct_io;
|
||||||
bool preallocate;
|
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;
|
} naut_storage_opts;
|
||||||
|
|
||||||
/* Open (creating + preallocating) all files under `root`. */
|
/* Open (creating + preallocating) all files under `root`. */
|
||||||
|
|
@ -33,11 +39,12 @@ naut_err naut_storage_write(naut_storage *s, int64_t offset, const void *buf, si
|
||||||
naut_err naut_storage_read(naut_storage *s, int64_t offset, void *buf, size_t len);
|
naut_err naut_storage_read(naut_storage *s, int64_t offset, void *buf, size_t len);
|
||||||
naut_err naut_storage_sync(naut_storage *s);
|
naut_err naut_storage_sync(naut_storage *s);
|
||||||
|
|
||||||
/* Move one completed file out to `dest` (rename, or copy+unlink across file
|
/* Move one file to `dest` (rename, or copy+unlink across file systems) and keep
|
||||||
* systems). The caller must guarantee the file is complete — every piece
|
* tracking it there: the slot's path is updated and its fd reopened, so the
|
||||||
* overlapping it verified — so no further writes target it. After this the slot
|
* engine can still read/write/seed the file at its new location. Typically
|
||||||
* is "externalized": subsequent I/O to its region returns NAUT_ERR_RANGE. This
|
* called the moment a file completes (the storage half of "move files as they
|
||||||
* is the storage half of the "move files as they finish" feature. */
|
* finish"), but safe at any time. The owning process therefore never loses
|
||||||
|
* track of a moved file. Returns NAUT_ERR_IO if the move or reopen fails. */
|
||||||
naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest);
|
naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest);
|
||||||
|
|
||||||
int64_t naut_storage_total(const naut_storage *s);
|
int64_t naut_storage_total(const naut_storage *s);
|
||||||
|
|
|
||||||
126
include/naut/swarm.h
Normal file
126
include/naut/swarm.h
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
/* swarm.h - reusable multi-peer download driver.
|
||||||
|
*
|
||||||
|
* naut_swarm is a small CLI wrapper around this API. Long-running applications
|
||||||
|
* such as nautd own the worker thread and use callbacks for progress, control
|
||||||
|
* commands, cancellation, and event delivery.
|
||||||
|
*/
|
||||||
|
#ifndef NAUT_SWARM_H
|
||||||
|
#define NAUT_SWARM_H
|
||||||
|
|
||||||
|
#include "naut/common.h"
|
||||||
|
#include "naut/event.h"
|
||||||
|
#include "naut/storage.h"
|
||||||
|
|
||||||
|
#define NAUT_SWARM_MAX_PEER_STATS 64
|
||||||
|
#define NAUT_SWARM_MAX_PIECE_STATS 4000
|
||||||
|
#define NAUT_SWARM_MAX_TRACKER_STATS 32
|
||||||
|
#define NAUT_SWARM_MAX_FILE_STATS 1024
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char ip[46];
|
||||||
|
uint16_t port;
|
||||||
|
char client[64];
|
||||||
|
char connection[16];
|
||||||
|
char flags[16];
|
||||||
|
double progress;
|
||||||
|
double relevance;
|
||||||
|
double dlspeed;
|
||||||
|
double upspeed;
|
||||||
|
uint64_t downloaded;
|
||||||
|
uint64_t uploaded;
|
||||||
|
} naut_swarm_peer_stats;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char url[256];
|
||||||
|
int32_t tier;
|
||||||
|
char status[32];
|
||||||
|
int32_t seeds;
|
||||||
|
int32_t peers;
|
||||||
|
int32_t leeches;
|
||||||
|
int32_t downloaded;
|
||||||
|
char message[128];
|
||||||
|
} naut_swarm_tracker_stats;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char path[512];
|
||||||
|
uint64_t size;
|
||||||
|
double progress;
|
||||||
|
int32_t priority;
|
||||||
|
double availability;
|
||||||
|
} naut_swarm_file_stats;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t total_bytes;
|
||||||
|
uint64_t bytes_done;
|
||||||
|
uint32_t total_pieces;
|
||||||
|
uint32_t pieces_done;
|
||||||
|
uint32_t peers_total; /* discovered endpoints */
|
||||||
|
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];
|
||||||
|
uint32_t tracker_count;
|
||||||
|
naut_swarm_tracker_stats tracker_stats[NAUT_SWARM_MAX_TRACKER_STATS];
|
||||||
|
uint32_t file_count;
|
||||||
|
naut_swarm_file_stats file_stats[NAUT_SWARM_MAX_FILE_STATS];
|
||||||
|
uint32_t piece_state_count;
|
||||||
|
uint8_t piece_states[NAUT_SWARM_MAX_PIECE_STATS];
|
||||||
|
} naut_swarm_stats;
|
||||||
|
|
||||||
|
typedef void (*naut_swarm_progress_cb)(void *context,
|
||||||
|
const naut_swarm_stats *stats);
|
||||||
|
|
||||||
|
/* Called on the swarm owner thread. The callback may safely operate on storage,
|
||||||
|
* including relocating completed files. */
|
||||||
|
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 */
|
||||||
|
bool check_only; /* hash-verify existing data + report, then return;
|
||||||
|
* no peers, no engine, no download (paused recheck)*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
/* Blocks until the torrent completes, is cancelled, or fails. */
|
||||||
|
naut_err naut_swarm_run(const naut_swarm_config *config);
|
||||||
|
|
||||||
|
#endif /* NAUT_SWARM_H */
|
||||||
|
|
@ -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
|
* The wire codec (query building, bencode/UDP packet encode+decode) lives in the
|
||||||
* encode/decode — all unit-testable without a socket) and thin blocking fetch
|
* sibling `torrent-tracker` library; the implementation here (src/discovery)
|
||||||
* helpers used by the swarm app. HTTPS/TLS is deferred to a later transport
|
* owns only the socket glue. HTTPS/TLS is deferred to a later transport backend;
|
||||||
* backend; the built-ins in this phase are plaintext HTTP and UDP.
|
* the built-ins are plaintext HTTP and UDP.
|
||||||
*/
|
*/
|
||||||
#ifndef NAUT_TRACKER_H
|
#ifndef NAUT_TRACKER_H
|
||||||
#define NAUT_TRACKER_H
|
#define NAUT_TRACKER_H
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -37,25 +38,10 @@ typedef struct {
|
||||||
|
|
||||||
void naut_tracker_response_free(naut_tracker_response *r);
|
void naut_tracker_response_free(naut_tracker_response *r);
|
||||||
|
|
||||||
/* --- HTTP --- */
|
|
||||||
/* Build the full announce GET URL (base?...params) with percent-encoded binary
|
/* 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. */
|
* 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,
|
size_t naut_tracker_http_url(const char *base, const naut_announce_req *req,
|
||||||
char *out, size_t outsz);
|
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) --- */
|
/* --- live fetch helpers (blocking) --- */
|
||||||
/* HTTP GET the announce URL; fills out. Only http:// (no TLS yet). */
|
/* HTTP GET the announce URL; fills out. Only http:// (no TLS yet). */
|
||||||
|
|
|
||||||
3193
plugins/webui/webui.c
Normal file
3193
plugins/webui/webui.c
Normal file
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 */
|
||||||
|
|
@ -13,6 +13,7 @@ const char *naut_strerror(naut_err e) {
|
||||||
case NAUT_ERR_FULL: return "full";
|
case NAUT_ERR_FULL: return "full";
|
||||||
case NAUT_ERR_EMPTY: return "empty";
|
case NAUT_ERR_EMPTY: return "empty";
|
||||||
case NAUT_ERR_NOTFOUND: return "not found";
|
case NAUT_ERR_NOTFOUND: return "not found";
|
||||||
|
case NAUT_ERR_EXIST: return "already exists / data would overlap";
|
||||||
default: return "unknown error";
|
default: return "unknown error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
227
src/dht/dht.c
227
src/dht/dht.c
|
|
@ -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));
|
|
||||||
}
|
|
||||||
|
|
@ -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 "naut/dht.h"
|
||||||
|
|
||||||
|
#include "tracker.h" /* torrent-tracker DHT codec (dht_*) */
|
||||||
|
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
|
@ -10,6 +16,8 @@
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define DHT_MAX_QUERIES 64
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
struct sockaddr_in addr;
|
struct sockaddr_in addr;
|
||||||
bool queried;
|
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);
|
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
if (fd < 0) return NAUT_ERR_IO;
|
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];
|
naut_peer_addr found[NAUT_DHT_MAX_PEERS];
|
||||||
size_t found_count = 0;
|
size_t found_count = 0;
|
||||||
uint8_t id[20];
|
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;
|
uint16_t tx_counter = 1;
|
||||||
size_t queries = 0;
|
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;
|
size_t index = SIZE_MAX;
|
||||||
for (size_t i = 0; i < node_count; i++)
|
for (size_t i = 0; i < node_count; i++)
|
||||||
if (!nodes[i].queried) { index = i; break; }
|
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++;
|
queries++;
|
||||||
uint8_t tx[2] = { (uint8_t)(tx_counter >> 8), (uint8_t)tx_counter };
|
uint8_t tx[2] = { (uint8_t)(tx_counter >> 8), (uint8_t)tx_counter };
|
||||||
tx_counter++;
|
tx_counter++;
|
||||||
uint8_t *query = NULL; size_t query_len = 0;
|
uint8_t query[256];
|
||||||
if (naut_dht_build_get_peers(tx, sizeof tx, id, info_hash,
|
size_t query_len = 0;
|
||||||
&query, &query_len) != NAUT_OK)
|
if (dht_write_get_peers_query(tx, sizeof tx, id, info_hash, 1, 0,
|
||||||
|
query, sizeof query, &query_len) !=
|
||||||
|
TRACKER_OK)
|
||||||
continue;
|
continue;
|
||||||
ssize_t sent = sendto(fd, query, query_len, 0,
|
ssize_t sent = sendto(fd, query, query_len, 0,
|
||||||
(struct sockaddr *)&nodes[index].addr,
|
(struct sockaddr *)&nodes[index].addr,
|
||||||
sizeof(nodes[index].addr));
|
sizeof(nodes[index].addr));
|
||||||
free(query);
|
|
||||||
if (sent < 0) continue;
|
if (sent < 0) continue;
|
||||||
|
|
||||||
struct pollfd pfd = { .fd = fd, .events = POLLIN };
|
struct pollfd pfd = { .fd = fd, .events = POLLIN };
|
||||||
if (poll(&pfd, 1, 1000) <= 0) continue;
|
if (poll(&pfd, 1, 1000) <= 0) continue;
|
||||||
uint8_t packet[65536];
|
uint8_t packet[2048];
|
||||||
ssize_t received = recv(fd, packet, sizeof packet, 0);
|
ssize_t received = recv(fd, packet, sizeof packet, 0);
|
||||||
if (received <= 0) continue;
|
if (received <= 0) continue;
|
||||||
naut_dht_response response;
|
if (dht_parse_message(packet, (size_t)received, msg) != TRACKER_OK)
|
||||||
if (naut_dht_parse_response(packet, (size_t)received, &response) != NAUT_OK)
|
|
||||||
continue;
|
continue;
|
||||||
if (response.transaction_len != sizeof tx ||
|
if (msg->type != DHT_MSG_RESPONSE ||
|
||||||
memcmp(response.transaction, tx, sizeof tx) != 0 ||
|
msg->transaction_len != sizeof tx ||
|
||||||
response.type != NAUT_DHT_RESPONSE) {
|
memcmp(msg->transaction, tx, sizeof tx) != 0)
|
||||||
naut_dht_response_free(&response);
|
|
||||||
continue;
|
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++)
|
for (size_t i = 0; i < msg->node_count; i++) {
|
||||||
add_peer(found, &found_count, &response.peers[i]);
|
if (msg->nodes[i].family != TRACKER_ADDR_IPV4) continue;
|
||||||
for (size_t i = 0; i < response.num_nodes; i++) {
|
|
||||||
struct sockaddr_in addr;
|
struct sockaddr_in addr;
|
||||||
memset(&addr, 0, sizeof addr);
|
memset(&addr, 0, sizeof addr);
|
||||||
addr.sin_family = AF_INET;
|
addr.sin_family = AF_INET;
|
||||||
memcpy(&addr.sin_addr, response.nodes[i].ip, 4);
|
memcpy(&addr.sin_addr, msg->nodes[i].addr, 4);
|
||||||
addr.sin_port = htons(response.nodes[i].port);
|
addr.sin_port = htons(msg->nodes[i].port);
|
||||||
add_candidate(nodes, &node_count, &addr);
|
add_candidate(nodes, &node_count, &addr);
|
||||||
}
|
}
|
||||||
naut_dht_response_free(&response);
|
|
||||||
}
|
}
|
||||||
|
free(msg);
|
||||||
close(fd);
|
close(fd);
|
||||||
if (found_count == 0) return NAUT_ERR_EMPTY;
|
if (found_count == 0) return NAUT_ERR_EMPTY;
|
||||||
naut_peer_addr *result = malloc(found_count * sizeof(*result));
|
naut_peer_addr *result = malloc(found_count * sizeof(*result));
|
||||||
258
src/discovery/tracker_client.c
Normal file
258
src/discovery/tracker_client.c
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
/* 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->min_interval = (int32_t)resp->min_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;
|
||||||
|
}
|
||||||
|
|
@ -20,9 +20,37 @@ static char *dup_cstr(const uint8_t *p, size_t n) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* collect a single announce string or an announce-list (list of tiers) */
|
static naut_err add_tracker(naut_metainfo *mi, size_t *capacity,
|
||||||
static void collect_trackers(const naut_bc *root, naut_metainfo *mi) {
|
const uint8_t *url, size_t url_len,
|
||||||
size_t cap = 0;
|
uint32_t tier) {
|
||||||
|
for (size_t i = 0; i < mi->num_trackers; i++)
|
||||||
|
if (strlen(mi->trackers[i]) == url_len &&
|
||||||
|
memcmp(mi->trackers[i], url, url_len) == 0)
|
||||||
|
return NAUT_OK;
|
||||||
|
if (mi->num_trackers == *capacity) {
|
||||||
|
size_t next_capacity = *capacity ? *capacity * 2 : 8;
|
||||||
|
char **next_trackers =
|
||||||
|
realloc(mi->trackers, next_capacity * sizeof(*next_trackers));
|
||||||
|
if (!next_trackers) return NAUT_ERR_NOMEM;
|
||||||
|
mi->trackers = next_trackers;
|
||||||
|
uint32_t *next_tiers =
|
||||||
|
realloc(mi->tracker_tiers,
|
||||||
|
next_capacity * sizeof(*next_tiers));
|
||||||
|
if (!next_tiers) return NAUT_ERR_NOMEM;
|
||||||
|
mi->tracker_tiers = next_tiers;
|
||||||
|
*capacity = next_capacity;
|
||||||
|
}
|
||||||
|
char *copy = dup_cstr(url, url_len);
|
||||||
|
if (!copy) return NAUT_ERR_NOMEM;
|
||||||
|
mi->trackers[mi->num_trackers] = copy;
|
||||||
|
mi->tracker_tiers[mi->num_trackers] = tier;
|
||||||
|
mi->num_trackers++;
|
||||||
|
return NAUT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preserve the outer announce-list as BEP-12 failover tiers. */
|
||||||
|
static naut_err collect_trackers(const naut_bc *root, naut_metainfo *mi) {
|
||||||
|
size_t capacity = 0;
|
||||||
const naut_bc *al = naut_bc_dict_get(root, "announce-list");
|
const naut_bc *al = naut_bc_dict_get(root, "announce-list");
|
||||||
if (al && al->type == NAUT_BC_LIST) {
|
if (al && al->type == NAUT_BC_LIST) {
|
||||||
for (size_t t = 0; t < al->v.list.count; t++) {
|
for (size_t t = 0; t < al->v.list.count; t++) {
|
||||||
|
|
@ -32,21 +60,20 @@ static void collect_trackers(const naut_bc *root, naut_metainfo *mi) {
|
||||||
const naut_bc *url = naut_bc_list_at(tier, u);
|
const naut_bc *url = naut_bc_list_at(tier, u);
|
||||||
const uint8_t *p; size_t n;
|
const uint8_t *p; size_t n;
|
||||||
if (!naut_bc_get_str(url, &p, &n)) continue;
|
if (!naut_bc_get_str(url, &p, &n)) continue;
|
||||||
if (mi->num_trackers == cap) {
|
naut_err error =
|
||||||
cap = cap ? cap * 2 : 8;
|
add_tracker(mi, &capacity, p, n, (uint32_t)t);
|
||||||
mi->trackers = realloc(mi->trackers, cap * sizeof(char *));
|
if (error != NAUT_OK) return error;
|
||||||
}
|
|
||||||
mi->trackers[mi->num_trackers++] = dup_cstr(p, n);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mi->num_trackers == 0) {
|
if (mi->num_trackers == 0) {
|
||||||
const uint8_t *p; size_t n;
|
const uint8_t *p; size_t n;
|
||||||
if (naut_bc_get_str(naut_bc_dict_get(root, "announce"), &p, &n)) {
|
if (naut_bc_get_str(naut_bc_dict_get(root, "announce"), &p, &n)) {
|
||||||
mi->trackers = malloc(sizeof(char *));
|
naut_err error = add_tracker(mi, &capacity, p, n, 0);
|
||||||
mi->trackers[mi->num_trackers++] = dup_cstr(p, n);
|
if (error != NAUT_OK) return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* v1 file list: single-file (info.length) or multi-file (info.files[]) */
|
/* v1 file list: single-file (info.length) or multi-file (info.files[]) */
|
||||||
|
|
@ -220,7 +247,12 @@ naut_err naut_metainfo_parse(const uint8_t *data, size_t len, naut_metainfo *out
|
||||||
} else {
|
} else {
|
||||||
collect_files_v2(info, out); /* v2-only: walk the file tree */
|
collect_files_v2(info, out); /* v2-only: walk the file tree */
|
||||||
}
|
}
|
||||||
collect_trackers(root, out);
|
e = collect_trackers(root, out);
|
||||||
|
if (e != NAUT_OK) {
|
||||||
|
out->_owned = o;
|
||||||
|
naut_metainfo_free(out);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
out->_owned = o;
|
out->_owned = o;
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
|
|
@ -245,7 +277,9 @@ naut_err naut_metainfo_parse_info(const uint8_t *info, size_t info_len,
|
||||||
|
|
||||||
if (num_trackers) {
|
if (num_trackers) {
|
||||||
out->trackers = calloc(num_trackers, sizeof(*out->trackers));
|
out->trackers = calloc(num_trackers, sizeof(*out->trackers));
|
||||||
if (!out->trackers) {
|
out->tracker_tiers =
|
||||||
|
calloc(num_trackers, sizeof(*out->tracker_tiers));
|
||||||
|
if (!out->trackers || !out->tracker_tiers) {
|
||||||
naut_metainfo_free(out);
|
naut_metainfo_free(out);
|
||||||
return NAUT_ERR_NOMEM;
|
return NAUT_ERR_NOMEM;
|
||||||
}
|
}
|
||||||
|
|
@ -257,6 +291,9 @@ naut_err naut_metainfo_parse_info(const uint8_t *info, size_t info_len,
|
||||||
naut_metainfo_free(out);
|
naut_metainfo_free(out);
|
||||||
return NAUT_ERR_NOMEM;
|
return NAUT_ERR_NOMEM;
|
||||||
}
|
}
|
||||||
|
/* Magnet tr= parameters have no tier metadata. Treat them as
|
||||||
|
* ordered failover entries instead of announcing to all at once. */
|
||||||
|
out->tracker_tiers[i] = (uint32_t)i;
|
||||||
}
|
}
|
||||||
out->num_trackers = num_trackers;
|
out->num_trackers = num_trackers;
|
||||||
}
|
}
|
||||||
|
|
@ -270,6 +307,7 @@ void naut_metainfo_free(naut_metainfo *mi) {
|
||||||
free(mi->files);
|
free(mi->files);
|
||||||
for (size_t i = 0; i < mi->num_trackers; i++) free(mi->trackers[i]);
|
for (size_t i = 0; i < mi->num_trackers; i++) free(mi->trackers[i]);
|
||||||
free(mi->trackers);
|
free(mi->trackers);
|
||||||
|
free(mi->tracker_tiers);
|
||||||
if (mi->_owned) {
|
if (mi->_owned) {
|
||||||
owned *o = mi->_owned;
|
owned *o = mi->_owned;
|
||||||
naut_bc_free(o->doc);
|
naut_bc_free(o->doc);
|
||||||
|
|
|
||||||
269
src/net/http_client.c
Normal file
269
src/net/http_client.c
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
/* http_client.c — blocking HTTP/HTTPS GET with redirect handling.
|
||||||
|
*
|
||||||
|
* A small, dependency-light client: raw sockets for HTTP, OpenSSL for HTTPS.
|
||||||
|
* It reads the whole response into memory (capped), handles both Content-Length
|
||||||
|
* and chunked transfer-encoding, and follows 3xx redirects. This is deliberately
|
||||||
|
* simple — it serves RSS/Torznab fetches, not a general-purpose user agent. */
|
||||||
|
#include "naut/http_client.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include <openssl/ssl.h>
|
||||||
|
#include <openssl/err.h>
|
||||||
|
|
||||||
|
/* This lib is linked into the plugin module too, which can't resolve the host's
|
||||||
|
* naut_log symbols, so keep diagnostics dependency-free. */
|
||||||
|
#define HTTP_WARN(...) (void)fprintf(stderr, "http: " __VA_ARGS__)
|
||||||
|
|
||||||
|
#define HTTP_MAX_BODY (16 * 1024 * 1024) /* 16 MiB cap */
|
||||||
|
#define HTTP_MAX_REDIR 5
|
||||||
|
|
||||||
|
/* A transport: either a plain fd or an SSL session over it. */
|
||||||
|
typedef struct {
|
||||||
|
int fd;
|
||||||
|
SSL_CTX *ctx;
|
||||||
|
SSL *ssl;
|
||||||
|
} conn_t;
|
||||||
|
|
||||||
|
static void conn_close(conn_t *c) {
|
||||||
|
if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); c->ssl = NULL; }
|
||||||
|
if (c->ctx) { SSL_CTX_free(c->ctx); c->ctx = NULL; }
|
||||||
|
if (c->fd >= 0) { close(c->fd); c->fd = -1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static int dial(const char *host, const char *port) {
|
||||||
|
struct addrinfo hints, *res = NULL, *ai;
|
||||||
|
memset(&hints, 0, sizeof hints);
|
||||||
|
hints.ai_family = AF_UNSPEC;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
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 = 15, .tv_usec = 0 };
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||||
|
if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break;
|
||||||
|
close(fd); fd = -1;
|
||||||
|
}
|
||||||
|
freeaddrinfo(res);
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool conn_open(conn_t *c, const char *host, const char *port, bool tls) {
|
||||||
|
memset(c, 0, sizeof *c);
|
||||||
|
c->fd = dial(host, port);
|
||||||
|
if (c->fd < 0) { HTTP_WARN("connect %s:%s failed\n", host, port); return false; }
|
||||||
|
if (!tls) return true;
|
||||||
|
|
||||||
|
c->ctx = SSL_CTX_new(TLS_client_method());
|
||||||
|
if (!c->ctx) { conn_close(c); return false; }
|
||||||
|
SSL_CTX_set_verify(c->ctx, SSL_VERIFY_NONE, NULL); /* best-effort fetch */
|
||||||
|
c->ssl = SSL_new(c->ctx);
|
||||||
|
if (!c->ssl) { conn_close(c); return false; }
|
||||||
|
SSL_set_fd(c->ssl, c->fd);
|
||||||
|
SSL_set_tlsext_host_name(c->ssl, host); /* SNI */
|
||||||
|
if (SSL_connect(c->ssl) != 1) {
|
||||||
|
HTTP_WARN("TLS handshake with %s failed\n", host);
|
||||||
|
conn_close(c);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool conn_write(conn_t *c, const void *data, size_t len) {
|
||||||
|
const char *p = data;
|
||||||
|
while (len) {
|
||||||
|
int n = c->ssl ? SSL_write(c->ssl, p, (int)len)
|
||||||
|
: (int)write(c->fd, p, len);
|
||||||
|
if (n <= 0) {
|
||||||
|
if (!c->ssl && n < 0 && errno == EINTR) continue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p += n; len -= (size_t)n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int conn_read(conn_t *c, void *buf, size_t len) {
|
||||||
|
for (;;) {
|
||||||
|
int n = c->ssl ? SSL_read(c->ssl, buf, (int)len)
|
||||||
|
: (int)read(c->fd, buf, len);
|
||||||
|
if (n < 0 && !c->ssl && errno == EINTR) continue;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse "scheme://host[:port]/path". Fills host/port/path; sets *tls. */
|
||||||
|
static bool parse_url(const char *url, char *host, size_t hostsz,
|
||||||
|
char *port, size_t portsz, char *path, size_t pathsz,
|
||||||
|
bool *tls) {
|
||||||
|
const char *h;
|
||||||
|
if (strncasecmp(url, "https://", 8) == 0) { *tls = true; h = url + 8; }
|
||||||
|
else if (strncasecmp(url, "http://", 7) == 0) { *tls = false; h = url + 7; }
|
||||||
|
else return false;
|
||||||
|
|
||||||
|
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 == 0 || hlen >= hostsz) return false;
|
||||||
|
memcpy(host, h, hlen); host[hlen] = 0;
|
||||||
|
if (colon) {
|
||||||
|
size_t plen = (size_t)(hostend - colon - 1);
|
||||||
|
if (plen == 0 || plen >= portsz) return false;
|
||||||
|
memcpy(port, colon + 1, plen); port[plen] = 0;
|
||||||
|
} else {
|
||||||
|
snprintf(port, portsz, "%s", *tls ? "443" : "80");
|
||||||
|
}
|
||||||
|
if (slash) { if (strlen(slash) >= pathsz) return false; snprintf(path, pathsz, "%s", slash); }
|
||||||
|
else snprintf(path, pathsz, "/");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decode a chunked-transfer body in place; returns new length. */
|
||||||
|
static size_t dechunk(char *body, size_t len) {
|
||||||
|
char *out = body;
|
||||||
|
const char *in = body, *end = body + len;
|
||||||
|
while (in < end) {
|
||||||
|
char *nl = (char *)memchr(in, '\n', (size_t)(end - in));
|
||||||
|
if (!nl) break;
|
||||||
|
long sz = strtol(in, NULL, 16);
|
||||||
|
in = nl + 1;
|
||||||
|
if (sz <= 0) break;
|
||||||
|
if (in + sz > end) sz = (long)(end - in);
|
||||||
|
memmove(out, in, (size_t)sz);
|
||||||
|
out += sz;
|
||||||
|
in += sz;
|
||||||
|
/* skip trailing CRLF after the chunk */
|
||||||
|
if (in < end && *in == '\r') in++;
|
||||||
|
if (in < end && *in == '\n') in++;
|
||||||
|
}
|
||||||
|
*out = 0;
|
||||||
|
return (size_t)(out - body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One request/response round-trip. On a 3xx with Location, writes the target
|
||||||
|
* into `redirect` (caller retries) and returns NAUT_OK with out->body == NULL. */
|
||||||
|
static naut_err fetch_once(const char *url, naut_http_response *out,
|
||||||
|
char *redirect, size_t redirsz) {
|
||||||
|
char host[256], port[16], path[2048];
|
||||||
|
bool tls;
|
||||||
|
if (!parse_url(url, host, sizeof host, port, sizeof port,
|
||||||
|
path, sizeof path, &tls))
|
||||||
|
return NAUT_ERR_INVAL;
|
||||||
|
|
||||||
|
conn_t c;
|
||||||
|
if (!conn_open(&c, host, port, tls)) return NAUT_ERR_IO;
|
||||||
|
|
||||||
|
char req[3072];
|
||||||
|
int rn = snprintf(req, sizeof req,
|
||||||
|
"GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: Naut/0.1\r\n"
|
||||||
|
"Accept: */*\r\nConnection: close\r\n\r\n", path, host);
|
||||||
|
if (rn < 0 || (size_t)rn >= sizeof req || !conn_write(&c, req, (size_t)rn)) {
|
||||||
|
conn_close(&c); return NAUT_ERR_IO;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t cap = 1 << 16, len = 0;
|
||||||
|
char *buf = malloc(cap);
|
||||||
|
if (!buf) { conn_close(&c); return NAUT_ERR_NOMEM; }
|
||||||
|
for (;;) {
|
||||||
|
if (len + 1 >= cap) {
|
||||||
|
if (cap >= HTTP_MAX_BODY) break;
|
||||||
|
size_t ncap = cap * 2 > HTTP_MAX_BODY ? HTTP_MAX_BODY : cap * 2;
|
||||||
|
char *nb = realloc(buf, ncap);
|
||||||
|
if (!nb) { free(buf); conn_close(&c); return NAUT_ERR_NOMEM; }
|
||||||
|
buf = nb; cap = ncap;
|
||||||
|
}
|
||||||
|
int r = conn_read(&c, buf + len, cap - len - 1);
|
||||||
|
if (r < 0) { free(buf); conn_close(&c); return NAUT_ERR_IO; }
|
||||||
|
if (r == 0) break;
|
||||||
|
len += (size_t)r;
|
||||||
|
}
|
||||||
|
conn_close(&c);
|
||||||
|
buf[len] = 0;
|
||||||
|
|
||||||
|
if (len < 12 || memcmp(buf, "HTTP/", 5) != 0) { free(buf); return NAUT_ERR_PROTO; }
|
||||||
|
long status = strtol(buf + 9, NULL, 10);
|
||||||
|
|
||||||
|
char *hdr_end = NULL;
|
||||||
|
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') {
|
||||||
|
hdr_end = buf + i + 4; break;
|
||||||
|
}
|
||||||
|
if (!hdr_end) { free(buf); return NAUT_ERR_PROTO; }
|
||||||
|
|
||||||
|
/* Headers are everything before hdr_end; scan them case-insensitively. */
|
||||||
|
size_t hdr_len = (size_t)(hdr_end - buf);
|
||||||
|
bool chunked = false;
|
||||||
|
char *loc = NULL;
|
||||||
|
for (char *p = buf; p < buf + hdr_len; ) {
|
||||||
|
char *eol = memchr(p, '\n', (size_t)(buf + hdr_len - p));
|
||||||
|
size_t line = eol ? (size_t)(eol - p) : (size_t)(buf + hdr_len - p);
|
||||||
|
if (strncasecmp(p, "Transfer-Encoding:", 18) == 0 &&
|
||||||
|
line < 256 && memmem(p, line, "chunked", 7))
|
||||||
|
chunked = true;
|
||||||
|
if (strncasecmp(p, "Location:", 9) == 0) loc = p + 9;
|
||||||
|
if (!eol) break;
|
||||||
|
p = eol + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status >= 300 && status < 400 && loc && redirect) {
|
||||||
|
while (*loc == ' ' || *loc == '\t') loc++;
|
||||||
|
size_t n = strcspn(loc, "\r\n");
|
||||||
|
if (n && n < redirsz) { memcpy(redirect, loc, n); redirect[n] = 0; }
|
||||||
|
else redirect[0] = 0;
|
||||||
|
free(buf);
|
||||||
|
out->body = NULL; out->status = status; out->body_len = 0;
|
||||||
|
return NAUT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Move body to the front of the allocation so the caller owns one buffer. */
|
||||||
|
size_t blen = len - hdr_len;
|
||||||
|
memmove(buf, hdr_end, blen);
|
||||||
|
buf[blen] = 0;
|
||||||
|
if (chunked) blen = dechunk(buf, blen);
|
||||||
|
|
||||||
|
out->status = status;
|
||||||
|
out->body = buf;
|
||||||
|
out->body_len = blen;
|
||||||
|
if (redirect) redirect[0] = 0;
|
||||||
|
return NAUT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
naut_err naut_http_get(const char *url, naut_http_response *out) {
|
||||||
|
if (!url || !out) return NAUT_ERR_INVAL;
|
||||||
|
out->body = NULL; out->status = 0; out->body_len = 0;
|
||||||
|
|
||||||
|
char current[2048];
|
||||||
|
if (strlen(url) >= sizeof current) return NAUT_ERR_INVAL;
|
||||||
|
snprintf(current, sizeof current, "%s", url);
|
||||||
|
|
||||||
|
for (int hop = 0; hop <= HTTP_MAX_REDIR; hop++) {
|
||||||
|
char redirect[2048] = {0};
|
||||||
|
naut_err e = fetch_once(current, out, redirect, sizeof redirect);
|
||||||
|
if (e != NAUT_OK) return e;
|
||||||
|
if (out->body) return NAUT_OK; /* got a real response */
|
||||||
|
if (!redirect[0]) return NAUT_ERR_PROTO;
|
||||||
|
/* Relative redirect: only absolute URLs are followed here. */
|
||||||
|
if (strncasecmp(redirect, "http", 4) != 0) return NAUT_ERR_PROTO;
|
||||||
|
snprintf(current, sizeof current, "%s", redirect);
|
||||||
|
}
|
||||||
|
return NAUT_ERR_PROTO; /* too many redirects */
|
||||||
|
}
|
||||||
|
|
||||||
|
void naut_http_response_free(naut_http_response *r) {
|
||||||
|
if (!r) return;
|
||||||
|
free(r->body);
|
||||||
|
r->body = NULL; r->body_len = 0; r->status = 0;
|
||||||
|
}
|
||||||
466
src/peer/mse.c
466
src/peer/mse.c
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
#define BLK NAUT_BLOCK /* 16 KiB */
|
#define BLK NAUT_BLOCK /* 16 KiB */
|
||||||
#define ENDGAME_BLOCKS 8 /* switch to endgame when this few remain */
|
#define ENDGAME_BLOCKS 8 /* switch to endgame when this few remain */
|
||||||
#define ENDGAME_COPIES 2 /* at most two peers race a missing block */
|
#define ENDGAME_COPIES 2 /* at most two peers race a missing block */
|
||||||
|
#define PIECE_INFLIGHT_SOFT_CAP 64
|
||||||
|
#define ACTIVE_PIECE_SOFT_CAP 64
|
||||||
|
|
||||||
/* per-piece in-progress state, lazily allocated and freed on completion */
|
/* per-piece in-progress state, lazily allocated and freed on completion */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -35,8 +37,10 @@ struct naut_download {
|
||||||
naut_bitfield have;
|
naut_bitfield have;
|
||||||
uint32_t *avail; /* [num_pieces] swarm availability count */
|
uint32_t *avail; /* [num_pieces] swarm availability count */
|
||||||
pstate **ps; /* [num_pieces] in-progress state or NULL */
|
pstate **ps; /* [num_pieces] in-progress state or NULL */
|
||||||
|
uint32_t active_pieces;
|
||||||
|
|
||||||
uint32_t cur_piece; /* sequential cursor for next_request() */
|
uint32_t cur_piece; /* sequential cursor for next_request() */
|
||||||
|
uint32_t pick_cursor; /* rotating start point for rarest-first ties */
|
||||||
|
|
||||||
uint64_t total_blocks, recv_blocks;
|
uint64_t total_blocks, recv_blocks;
|
||||||
uint32_t pieces_done;
|
uint32_t pieces_done;
|
||||||
|
|
@ -49,6 +53,8 @@ struct naut_download {
|
||||||
bool *file_done;
|
bool *file_done;
|
||||||
naut_file_complete_cb file_cb;
|
naut_file_complete_cb file_cb;
|
||||||
void *file_cb_ctx;
|
void *file_cb_ctx;
|
||||||
|
naut_piece_complete_cb piece_cb;
|
||||||
|
void *piece_cb_ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
static bool bget(const uint8_t *a, uint32_t i) { return (a[i>>3] >> (i&7)) & 1; }
|
static bool bget(const uint8_t *a, uint32_t i) { return (a[i>>3] >> (i&7)) & 1; }
|
||||||
|
|
@ -84,6 +90,7 @@ static pstate *ensure_ps(naut_download *d, uint32_t p) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
d->ps[p] = s;
|
d->ps[p] = s;
|
||||||
|
d->active_pieces++;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
static void free_ps(naut_download *d, uint32_t p) {
|
static void free_ps(naut_download *d, uint32_t p) {
|
||||||
|
|
@ -91,6 +98,7 @@ static void free_ps(naut_download *d, uint32_t p) {
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
free(s->recv_bits); free(s->req_count); free(s->buf); free(s);
|
free(s->recv_bits); free(s->req_count); free(s->buf); free(s);
|
||||||
d->ps[p] = NULL;
|
d->ps[p] = NULL;
|
||||||
|
if (d->active_pieces) d->active_pieces--;
|
||||||
}
|
}
|
||||||
|
|
||||||
naut_download *naut_download_create(const naut_metainfo *mi, naut_storage *st) {
|
naut_download *naut_download_create(const naut_metainfo *mi, naut_storage *st) {
|
||||||
|
|
@ -150,6 +158,48 @@ void naut_download_destroy(naut_download *d) {
|
||||||
free(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) {
|
void naut_download_set_worker_pool(naut_download *d, naut_worker_pool *pool) {
|
||||||
if (d) d->workers = pool;
|
if (d) d->workers = pool;
|
||||||
}
|
}
|
||||||
|
|
@ -157,11 +207,16 @@ void naut_download_set_worker_pool(naut_download *d, naut_worker_pool *pool) {
|
||||||
void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx) {
|
void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx) {
|
||||||
d->file_cb = cb; d->file_cb_ctx = ctx;
|
d->file_cb = cb; d->file_cb_ctx = ctx;
|
||||||
}
|
}
|
||||||
|
void naut_download_set_piece_cb(naut_download *d, naut_piece_complete_cb cb,
|
||||||
|
void *ctx) {
|
||||||
|
d->piece_cb = cb;
|
||||||
|
d->piece_cb_ctx = ctx;
|
||||||
|
}
|
||||||
bool naut_download_file_complete(const naut_download *d, uint32_t f) {
|
bool naut_download_file_complete(const naut_download *d, uint32_t f) {
|
||||||
return f < d->num_files && d->file_done[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;
|
size_t lo = 0, hi = d->num_files;
|
||||||
while (lo < hi) { size_t mid = (lo + hi) / 2;
|
while (lo < hi) { size_t mid = (lo + hi) / 2;
|
||||||
if (d->file_last[mid] < p) lo = mid + 1; else hi = mid; }
|
if (d->file_last[mid] < p) lo = mid + 1; else hi = mid; }
|
||||||
|
|
@ -169,11 +224,26 @@ static void notify_files(naut_download *d, uint32_t p) {
|
||||||
if (d->file_done[f]) continue;
|
if (d->file_done[f]) continue;
|
||||||
if (--d->file_remain[f] == 0) {
|
if (--d->file_remain[f] == 0) {
|
||||||
d->file_done[f] = true;
|
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 -------------------------------------------------------- */
|
/* --- availability -------------------------------------------------------- */
|
||||||
void naut_download_inc_avail(naut_download *d, uint32_t p) {
|
void naut_download_inc_avail(naut_download *d, uint32_t p) {
|
||||||
if (p < d->num_pieces) d->avail[p]++;
|
if (p < d->num_pieces) d->avail[p]++;
|
||||||
|
|
@ -198,6 +268,12 @@ static uint32_t first_unreq(const pstate *s) {
|
||||||
return UINT32_MAX;
|
return UINT32_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint32_t piece_inflight(const pstate *s) {
|
||||||
|
uint32_t n = 0;
|
||||||
|
for (uint32_t b = 0; b < s->nblocks; b++) n += s->req_count[b];
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
static bool hand_out(naut_download *d, uint32_t p, uint32_t b,
|
static bool hand_out(naut_download *d, uint32_t p, uint32_t b,
|
||||||
uint32_t *index, uint32_t *begin, uint32_t *length) {
|
uint32_t *index, uint32_t *begin, uint32_t *length) {
|
||||||
d->ps[p]->req_count[b]++;
|
d->ps[p]->req_count[b]++;
|
||||||
|
|
@ -211,15 +287,22 @@ bool naut_download_pick_for_peer(naut_download *d, const naut_bitfield *peer_hav
|
||||||
d->endgame = (d->total_blocks - d->recv_blocks) <= ENDGAME_BLOCKS;
|
d->endgame = (d->total_blocks - d->recv_blocks) <= ENDGAME_BLOCKS;
|
||||||
|
|
||||||
/* pass 1: finish an in-progress piece the peer has (reduces fragmentation) */
|
/* pass 1: finish an in-progress piece the peer has (reduces fragmentation) */
|
||||||
for (uint32_t p = 0; p < d->num_pieces; p++) {
|
for (uint32_t n = 0; n < d->num_pieces; n++) {
|
||||||
|
uint32_t p = (d->pick_cursor + n) % d->num_pieces;
|
||||||
if (naut_bitfield_test(&d->have, p) || !d->ps[p]) continue;
|
if (naut_bitfield_test(&d->have, p) || !d->ps[p]) continue;
|
||||||
if (p >= peer_have->nbits || !naut_bitfield_test(peer_have, p)) continue;
|
if (p >= peer_have->nbits || !naut_bitfield_test(peer_have, p)) continue;
|
||||||
|
if (!d->endgame && d->active_pieces < ACTIVE_PIECE_SOFT_CAP &&
|
||||||
|
piece_inflight(d->ps[p]) >= PIECE_INFLIGHT_SOFT_CAP)
|
||||||
|
continue;
|
||||||
uint32_t b = first_unreq(d->ps[p]);
|
uint32_t b = first_unreq(d->ps[p]);
|
||||||
if (b != UINT32_MAX) return hand_out(d, p, b, index, begin, length);
|
if (b != UINT32_MAX) return hand_out(d, p, b, index, begin, length);
|
||||||
}
|
}
|
||||||
/* pass 2: start the rarest new piece the peer has */
|
/* pass 2: start the rarest new piece the peer has */
|
||||||
|
if (!d->endgame && d->active_pieces >= ACTIVE_PIECE_SOFT_CAP)
|
||||||
|
return false;
|
||||||
uint32_t best = UINT32_MAX, best_av = UINT32_MAX;
|
uint32_t best = UINT32_MAX, best_av = UINT32_MAX;
|
||||||
for (uint32_t p = 0; p < d->num_pieces; p++) {
|
for (uint32_t n = 0; n < d->num_pieces; n++) {
|
||||||
|
uint32_t p = (d->pick_cursor + n) % d->num_pieces;
|
||||||
if (naut_bitfield_test(&d->have, p) || d->ps[p]) continue;
|
if (naut_bitfield_test(&d->have, p) || d->ps[p]) continue;
|
||||||
if (p >= peer_have->nbits || !naut_bitfield_test(peer_have, p) ||
|
if (p >= peer_have->nbits || !naut_bitfield_test(peer_have, p) ||
|
||||||
d->avail[p] == 0) continue;
|
d->avail[p] == 0) continue;
|
||||||
|
|
@ -227,12 +310,14 @@ bool naut_download_pick_for_peer(naut_download *d, const naut_bitfield *peer_hav
|
||||||
}
|
}
|
||||||
if (best != UINT32_MAX) {
|
if (best != UINT32_MAX) {
|
||||||
if (!ensure_ps(d, best)) return false;
|
if (!ensure_ps(d, best)) return false;
|
||||||
|
d->pick_cursor = (best + 1) % d->num_pieces;
|
||||||
return hand_out(d, best, 0, index, begin, length);
|
return hand_out(d, best, 0, index, begin, length);
|
||||||
}
|
}
|
||||||
/* pass 3: endgame — race each missing block on at most two distinct peers */
|
/* pass 3: endgame — race each missing block on at most two distinct peers */
|
||||||
if (d->endgame) {
|
if (d->endgame) {
|
||||||
for (uint8_t copies = 1; copies < ENDGAME_COPIES; copies++) {
|
for (uint8_t copies = 1; copies < ENDGAME_COPIES; copies++) {
|
||||||
for (uint32_t p = 0; p < d->num_pieces; p++) {
|
for (uint32_t n = 0; n < d->num_pieces; n++) {
|
||||||
|
uint32_t p = (d->pick_cursor + n) % d->num_pieces;
|
||||||
if (naut_bitfield_test(&d->have, p) ||
|
if (naut_bitfield_test(&d->have, p) ||
|
||||||
p >= peer_have->nbits ||
|
p >= peer_have->nbits ||
|
||||||
!naut_bitfield_test(peer_have, p)) continue;
|
!naut_bitfield_test(peer_have, p)) continue;
|
||||||
|
|
@ -295,12 +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);
|
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;
|
if (e != NAUT_OK) return e;
|
||||||
naut_bitfield_set(&d->have, p);
|
mark_piece_complete(d, p, false, true);
|
||||||
d->pieces_done++;
|
|
||||||
d->bytes_done += ps;
|
|
||||||
free_ps(d, p);
|
free_ps(d, p);
|
||||||
*done = true;
|
*done = true;
|
||||||
notify_files(d, p);
|
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,3 +465,51 @@ 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_num_pieces(const naut_download *d) { return d->num_pieces; }
|
||||||
uint32_t naut_download_pieces_done(const naut_download *d) { return d->pieces_done; }
|
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; }
|
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;
|
||||||
|
size_t count = NAUT_MIN((size_t)d->num_pieces, capacity);
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
if (naut_bitfield_test(&d->have, i))
|
||||||
|
out[i] = 2;
|
||||||
|
else if (d->ps[i])
|
||||||
|
out[i] = 1;
|
||||||
|
else
|
||||||
|
out[i] = 0;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ typedef struct {
|
||||||
void *handle;
|
void *handle;
|
||||||
char *path;
|
char *path;
|
||||||
char *name;
|
char *name;
|
||||||
|
naut_plugin_shutdown_fn shutdown;
|
||||||
uint64_t *subscriptions;
|
uint64_t *subscriptions;
|
||||||
size_t subscription_count;
|
size_t subscription_count;
|
||||||
size_t subscription_capacity;
|
size_t subscription_capacity;
|
||||||
|
|
@ -209,6 +210,36 @@ static void host_log(void *opaque, int level, const char *message) {
|
||||||
else NAUT_INFO("plugin: %s", message);
|
else NAUT_INFO("plugin: %s", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static naut_err host_call_rpc(void *opaque, const char *method,
|
||||||
|
const char *request_json,
|
||||||
|
char **response_json) {
|
||||||
|
naut_plugin_manager *manager = opaque;
|
||||||
|
if (!manager || !method || !*method || !response_json)
|
||||||
|
return NAUT_ERR_INVAL;
|
||||||
|
*response_json = NULL;
|
||||||
|
json_t *params = NULL;
|
||||||
|
if (request_json && *request_json) {
|
||||||
|
json_error_t json_error;
|
||||||
|
params = json_loads(request_json,
|
||||||
|
JSON_REJECT_DUPLICATES | JSON_DECODE_ANY,
|
||||||
|
&json_error);
|
||||||
|
if (!params) return NAUT_ERR_PROTO;
|
||||||
|
}
|
||||||
|
naut_err error = NAUT_OK;
|
||||||
|
json_t *result = naut_rpc_dispatch(manager->rpc, method, params, &error);
|
||||||
|
json_decref(params);
|
||||||
|
if (error != NAUT_OK) {
|
||||||
|
json_decref(result);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
char *text = json_dumps(result ? result : json_null(),
|
||||||
|
JSON_COMPACT | JSON_ENCODE_ANY);
|
||||||
|
json_decref(result);
|
||||||
|
if (!text) return NAUT_ERR_NOMEM;
|
||||||
|
*response_json = text;
|
||||||
|
return NAUT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
naut_plugin_manager *naut_plugin_manager_create(
|
naut_plugin_manager *naut_plugin_manager_create(
|
||||||
naut_rpc_registry *rpc, naut_event_bus *events) {
|
naut_rpc_registry *rpc, naut_event_bus *events) {
|
||||||
if (!rpc || !events) return NULL;
|
if (!rpc || !events) return NULL;
|
||||||
|
|
@ -225,6 +256,7 @@ void naut_plugin_manager_destroy(naut_plugin_manager *manager) {
|
||||||
naut_rpc_unregister(manager->rpc, manager->rpc_adapters[i]->method);
|
naut_rpc_unregister(manager->rpc, manager->rpc_adapters[i]->method);
|
||||||
for (size_t i = 0; i < manager->plugin_count; i++) {
|
for (size_t i = 0; i < manager->plugin_count; i++) {
|
||||||
loaded_plugin *plugin = &manager->plugins[i];
|
loaded_plugin *plugin = &manager->plugins[i];
|
||||||
|
if (plugin->shutdown) plugin->shutdown();
|
||||||
for (size_t s = 0; s < plugin->subscription_count; s++)
|
for (size_t s = 0; s < plugin->subscription_count; s++)
|
||||||
naut_event_unsubscribe(manager->events, plugin->subscriptions[s]);
|
naut_event_unsubscribe(manager->events, plugin->subscriptions[s]);
|
||||||
free(plugin->subscriptions);
|
free(plugin->subscriptions);
|
||||||
|
|
@ -280,6 +312,10 @@ naut_err naut_plugin_load(naut_plugin_manager *manager, const char *path) {
|
||||||
memset(plugin, 0, sizeof(*plugin));
|
memset(plugin, 0, sizeof(*plugin));
|
||||||
return NAUT_ERR_PROTO;
|
return NAUT_ERR_PROTO;
|
||||||
}
|
}
|
||||||
|
dlerror();
|
||||||
|
plugin->shutdown = (naut_plugin_shutdown_fn)dlsym(plugin->handle,
|
||||||
|
"naut_plugin_shutdown");
|
||||||
|
dlerror();
|
||||||
naut_host_api host = {
|
naut_host_api host = {
|
||||||
.abi_version = NAUT_PLUGIN_ABI_VERSION,
|
.abi_version = NAUT_PLUGIN_ABI_VERSION,
|
||||||
.struct_size = sizeof(host),
|
.struct_size = sizeof(host),
|
||||||
|
|
@ -290,6 +326,7 @@ naut_err naut_plugin_load(naut_plugin_manager *manager, const char *path) {
|
||||||
.subscribe_event = host_subscribe_event,
|
.subscribe_event = host_subscribe_event,
|
||||||
.emit_event = host_emit_event,
|
.emit_event = host_emit_event,
|
||||||
.log = host_log,
|
.log = host_log,
|
||||||
|
.call_rpc = host_call_rpc,
|
||||||
};
|
};
|
||||||
size_t rpc_start = manager->rpc_count;
|
size_t rpc_start = manager->rpc_count;
|
||||||
size_t event_start = manager->event_count;
|
size_t event_start = manager->event_count;
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,7 @@ struct naut_script {
|
||||||
size_t head;
|
size_t head;
|
||||||
size_t count;
|
size_t count;
|
||||||
bool stopping;
|
bool stopping;
|
||||||
naut_script_move_file_cb move_file;
|
naut_script_host host;
|
||||||
void *move_context;
|
|
||||||
_Atomic uint64_t queued;
|
_Atomic uint64_t queued;
|
||||||
_Atomic uint64_t handled;
|
_Atomic uint64_t handled;
|
||||||
_Atomic uint64_t dropped;
|
_Atomic uint64_t dropped;
|
||||||
|
|
@ -67,9 +66,9 @@ static int lua_move_file(lua_State *lua) {
|
||||||
if (torrent_id < 0 || file_index < 0 ||
|
if (torrent_id < 0 || file_index < 0 ||
|
||||||
(uint64_t)file_index > UINT32_MAX)
|
(uint64_t)file_index > UINT32_MAX)
|
||||||
return luaL_error(lua, "move_file arguments out of range");
|
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");
|
return luaL_error(lua, "move_file is unavailable");
|
||||||
naut_err error = script->move_file(script->move_context,
|
naut_err error = script->host.move_file(script->host.context,
|
||||||
(uint64_t)torrent_id,
|
(uint64_t)torrent_id,
|
||||||
(uint32_t)file_index,
|
(uint32_t)file_index,
|
||||||
destination);
|
destination);
|
||||||
|
|
@ -80,6 +79,98 @@ static int lua_move_file(lua_State *lua) {
|
||||||
return 0;
|
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) {
|
static void sandbox(lua_State *lua) {
|
||||||
/* Remove every documented route to the filesystem, subprocesses, native
|
/* Remove every documented route to the filesystem, subprocesses, native
|
||||||
* module loading, and raw chunk compilation. `load`/`loadstring` are
|
* 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_pushlightuserdata(lua, script);
|
||||||
lua_pushcclosure(lua, lua_move_file, 1);
|
lua_pushcclosure(lua, lua_move_file, 1);
|
||||||
lua_setfield(lua, -2, "move_file");
|
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");
|
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,
|
naut_script *naut_script_create(naut_event_bus *events,
|
||||||
const char *script_path,
|
const char *script_path,
|
||||||
size_t queue_capacity,
|
size_t queue_capacity,
|
||||||
naut_script_move_file_cb move_file,
|
const naut_script_host *host,
|
||||||
void *move_context,
|
|
||||||
naut_err *error) {
|
naut_err *error) {
|
||||||
if (error) *error = NAUT_ERR_INVAL;
|
if (error) *error = NAUT_ERR_INVAL;
|
||||||
if (!events || !script_path || !*script_path || queue_capacity == 0)
|
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->events = events;
|
||||||
script->capacity = queue_capacity;
|
script->capacity = queue_capacity;
|
||||||
script->move_file = move_file;
|
if (host) script->host = *host;
|
||||||
script->move_context = move_context;
|
|
||||||
script->queue = calloc(queue_capacity, sizeof(*script->queue));
|
script->queue = calloc(queue_capacity, sizeof(*script->queue));
|
||||||
if (!script->queue) {
|
if (!script->queue) {
|
||||||
if (error) *error = NAUT_ERR_NOMEM;
|
if (error) *error = NAUT_ERR_NOMEM;
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ typedef struct {
|
||||||
int direct_fd;
|
int direct_fd;
|
||||||
int64_t start; /* global offset of this file's first byte */
|
int64_t start; /* global offset of this file's first byte */
|
||||||
int64_t length;
|
int64_t length;
|
||||||
char *path; /* full on-disk path (for relocate) */
|
char *path; /* current on-disk path (updated by relocate) */
|
||||||
bool externalized; /* moved out; region no longer backed here */
|
|
||||||
} file_slot;
|
} file_slot;
|
||||||
|
|
||||||
struct naut_storage {
|
struct naut_storage {
|
||||||
|
|
@ -63,11 +62,24 @@ naut_storage *naut_storage_open_opts(const naut_file *files, size_t nfiles,
|
||||||
s->files[i].fd = -1;
|
s->files[i].fd = -1;
|
||||||
s->files[i].direct_fd = -1;
|
s->files[i].direct_fd = -1;
|
||||||
char path[4096];
|
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 (n < 0 || n >= (int)sizeof path) goto fail_io;
|
||||||
|
|
||||||
if (make_parents(path) != NAUT_OK) goto fail_io;
|
if (make_parents(path) != NAUT_OK) goto fail_io;
|
||||||
int fd = open(path, O_RDWR | O_CREAT, 0666);
|
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 (fd < 0) { NAUT_ERROR("open %s: %s", path, strerror(errno)); goto fail_io; }
|
||||||
if (ftruncate(fd, files[i].length) != 0) {
|
if (ftruncate(fd, files[i].length) != 0) {
|
||||||
NAUT_ERROR("ftruncate %s: %s", path, strerror(errno));
|
NAUT_ERROR("ftruncate %s: %s", path, strerror(errno));
|
||||||
|
|
@ -140,7 +152,6 @@ static naut_err io_at(naut_storage *s, int64_t offset, void *buf, size_t len, bo
|
||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
const file_slot *f = locate(s, offset);
|
const file_slot *f = locate(s, offset);
|
||||||
if (!f) return NAUT_ERR_RANGE; /* zero-length file region */
|
if (!f) return NAUT_ERR_RANGE; /* zero-length file region */
|
||||||
if (f->externalized) return NAUT_ERR_RANGE; /* moved out; not backed here */
|
|
||||||
off_t fo = (off_t)(offset - f->start);
|
off_t fo = (off_t)(offset - f->start);
|
||||||
size_t chunk = len;
|
size_t chunk = len;
|
||||||
int64_t avail = f->length - fo;
|
int64_t avail = f->length - fo;
|
||||||
|
|
@ -193,11 +204,31 @@ done:
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static naut_err reopen_slot(file_slot *file, const char *path,
|
||||||
|
bool direct_io) {
|
||||||
|
file->fd = open(path, O_RDWR);
|
||||||
|
if (file->fd < 0) return NAUT_ERR_IO;
|
||||||
|
#ifdef O_DIRECT
|
||||||
|
if (direct_io && file->length > 0) {
|
||||||
|
file->direct_fd = open(path, O_RDWR | O_DIRECT);
|
||||||
|
if (file->direct_fd < 0)
|
||||||
|
NAUT_WARN("O_DIRECT reopen unavailable for %s: %s", path,
|
||||||
|
strerror(errno));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void)direct_io;
|
||||||
|
#endif
|
||||||
|
return NAUT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest) {
|
naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest) {
|
||||||
|
if (!s || !dest || !*dest) return NAUT_ERR_INVAL;
|
||||||
if (file_index >= s->nfiles) return NAUT_ERR_RANGE;
|
if (file_index >= s->nfiles) return NAUT_ERR_RANGE;
|
||||||
file_slot *f = &s->files[file_index];
|
file_slot *f = &s->files[file_index];
|
||||||
if (f->externalized) return NAUT_ERR_INVAL;
|
|
||||||
|
|
||||||
|
char *newpath = strdup(dest);
|
||||||
|
if (!newpath) return NAUT_ERR_NOMEM;
|
||||||
|
bool had_direct = f->direct_fd >= 0;
|
||||||
if (f->direct_fd >= 0) {
|
if (f->direct_fd >= 0) {
|
||||||
fsync(f->direct_fd);
|
fsync(f->direct_fd);
|
||||||
close(f->direct_fd);
|
close(f->direct_fd);
|
||||||
|
|
@ -207,17 +238,45 @@ naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *d
|
||||||
|
|
||||||
/* ensure the destination directory exists */
|
/* ensure the destination directory exists */
|
||||||
char dcopy[4096];
|
char dcopy[4096];
|
||||||
if ((size_t)snprintf(dcopy, sizeof dcopy, "%s", dest) >= sizeof dcopy) return NAUT_ERR_INVAL;
|
if ((size_t)snprintf(dcopy, sizeof dcopy, "%s", dest) >= sizeof dcopy) {
|
||||||
if (make_parents(dcopy) != NAUT_OK) return NAUT_ERR_IO;
|
free(newpath);
|
||||||
|
(void)reopen_slot(f, f->path, had_direct);
|
||||||
|
return NAUT_ERR_INVAL;
|
||||||
|
}
|
||||||
|
if (make_parents(dcopy) != NAUT_OK) {
|
||||||
|
free(newpath);
|
||||||
|
(void)reopen_slot(f, f->path, had_direct);
|
||||||
|
return NAUT_ERR_IO;
|
||||||
|
}
|
||||||
|
|
||||||
if (rename(f->path, dest) != 0) {
|
if (rename(f->path, dest) != 0) {
|
||||||
if (errno != EXDEV) { NAUT_ERROR("rename %s -> %s: %s", f->path, dest, strerror(errno)); return NAUT_ERR_IO; }
|
if (errno != EXDEV) {
|
||||||
|
NAUT_ERROR("rename %s -> %s: %s", f->path, dest,
|
||||||
|
strerror(errno));
|
||||||
|
free(newpath);
|
||||||
|
(void)reopen_slot(f, f->path, had_direct);
|
||||||
|
return NAUT_ERR_IO;
|
||||||
|
}
|
||||||
naut_err e = copy_file(f->path, dest); /* cross-filesystem */
|
naut_err e = copy_file(f->path, dest); /* cross-filesystem */
|
||||||
if (e != NAUT_OK) return e;
|
if (e != NAUT_OK) {
|
||||||
|
free(newpath);
|
||||||
|
(void)reopen_slot(f, f->path, had_direct);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
if (unlink(f->path) != 0) NAUT_WARN("unlink %s after copy: %s", f->path, strerror(errno));
|
if (unlink(f->path) != 0) NAUT_WARN("unlink %s after copy: %s", f->path, strerror(errno));
|
||||||
}
|
}
|
||||||
f->externalized = true;
|
|
||||||
NAUT_INFO("relocated file %zu -> %s", file_index, dest);
|
/* Keep tracking the file at its new home: update the path and reopen so the
|
||||||
|
* engine can still read/write/seed it from the new location (no externalize,
|
||||||
|
* so the owning process never loses track of a moved file). */
|
||||||
|
free(f->path);
|
||||||
|
f->path = newpath;
|
||||||
|
|
||||||
|
if (reopen_slot(f, dest, had_direct) != NAUT_OK) {
|
||||||
|
NAUT_ERROR("reopen %s after move: %s", dest, strerror(errno));
|
||||||
|
return NAUT_ERR_IO;
|
||||||
|
}
|
||||||
|
NAUT_INFO("relocated file %zu -> %s (still tracked)", file_index, dest);
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
7
tests/fixtures/phase7.lua
vendored
7
tests/fixtures/phase7.lua
vendored
|
|
@ -1,7 +1,10 @@
|
||||||
function on_torrent_finished(event)
|
function on_torrent_finished(event)
|
||||||
naut.move_file(event.torrent_id, 0, "/tmp/naut-phase7-finished")
|
print("torrent " .. event.torrent_id .. " finished")
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_file_complete(event)
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ tmp=$(mktemp -d)
|
||||||
socket="$tmp/nautd.sock"
|
socket="$tmp/nautd.sock"
|
||||||
daemon_log="$tmp/nautd.log"
|
daemon_log="$tmp/nautd.log"
|
||||||
events_log="$tmp/events.log"
|
events_log="$tmp/events.log"
|
||||||
|
seeder_log="$tmp/seeder.log"
|
||||||
|
root_dir=$(cd "$(dirname "$script")/../.." && pwd)
|
||||||
|
seeder="$root_dir/tests/integration/seeder.py"
|
||||||
|
torrent="$(dirname "$script")/single_v1.torrent"
|
||||||
|
seed_data="$(dirname "$script")/data"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
result=$?
|
result=$?
|
||||||
|
|
@ -20,16 +25,36 @@ cleanup() {
|
||||||
kill "$events_pid" 2>/dev/null || true
|
kill "$events_pid" 2>/dev/null || true
|
||||||
wait "$events_pid" 2>/dev/null || true
|
wait "$events_pid" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
if [[ -n "${seeder_pid:-}" ]]; then
|
||||||
|
kill "$seeder_pid" 2>/dev/null || true
|
||||||
|
wait "$seeder_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
if [[ "$result" -ne 0 ]]; then
|
if [[ "$result" -ne 0 ]]; then
|
||||||
cat "$daemon_log" >&2 2>/dev/null || true
|
cat "$daemon_log" >&2 2>/dev/null || true
|
||||||
cat "$events_log" >&2 2>/dev/null || true
|
cat "$events_log" >&2 2>/dev/null || true
|
||||||
|
cat "$seeder_log" >&2 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
rm -rf "$tmp"
|
rm -rf "$tmp"
|
||||||
return "$result"
|
return "$result"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
"$daemon" --socket "$socket" --plugin "$plugin" --script "$script" \
|
python3 -c 'import libtorrent' 2>/dev/null || {
|
||||||
|
echo "SKIP: python libtorrent not available"
|
||||||
|
exit 77
|
||||||
|
}
|
||||||
|
|
||||||
|
python3 "$seeder" "$torrent" "$seed_data" >"$seeder_log" 2>&1 &
|
||||||
|
seeder_pid=$!
|
||||||
|
port=
|
||||||
|
for _ in $(seq 1 100); do
|
||||||
|
port=$(grep -oP 'PORT \K[0-9]+' "$seeder_log" 2>/dev/null || true)
|
||||||
|
[[ -n "$port" && "$port" != 0 ]] && break
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
[[ -n "$port" && "$port" != 0 ]]
|
||||||
|
|
||||||
|
"$daemon" --socket "$socket" --plugin "$plugin" --state-dir "$tmp/state" \
|
||||||
>"$daemon_log" 2>&1 &
|
>"$daemon_log" 2>&1 &
|
||||||
daemon_pid=$!
|
daemon_pid=$!
|
||||||
|
|
||||||
|
|
@ -41,24 +66,28 @@ done
|
||||||
|
|
||||||
"$ctl" --socket "$socket" ping | grep -q '"service": "nautd"'
|
"$ctl" --socket "$socket" ping | grep -q '"service": "nautd"'
|
||||||
"$ctl" --socket "$socket" plugins | grep -q '"memory"'
|
"$ctl" --socket "$socket" plugins | grep -q '"memory"'
|
||||||
|
"$ctl" --socket "$socket" script "$script" | grep -q '"ok": true'
|
||||||
|
|
||||||
timeout 5 "$ctl" --socket "$socket" events >"$events_log" &
|
timeout 5 "$ctl" --socket "$socket" events >"$events_log" &
|
||||||
events_pid=$!
|
events_pid=$!
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
"$ctl" --socket "$socket" emit \
|
root="$tmp/torrent-data"
|
||||||
'{"type":"torrent_finished","torrent_id":7}' >/dev/null
|
"$ctl" --socket "$socket" add "$torrent" "$root" "127.0.0.1:$port" \
|
||||||
|
| grep -q '"ok": true'
|
||||||
|
|
||||||
status=
|
listing=
|
||||||
for _ in $(seq 1 100); do
|
for _ in $(seq 1 200); do
|
||||||
status=$("$ctl" --socket "$socket" status)
|
listing=$("$ctl" --socket "$socket" list)
|
||||||
if grep -q '"move_commands": 1' <<<"$status" &&
|
grep -q '"state": "complete"' <<<"$listing" &&
|
||||||
grep -q '"handled": 1' <<<"$status"; then
|
[[ -f "$root/single.bin.moved" ]] && break
|
||||||
break
|
sleep 0.05
|
||||||
fi
|
|
||||||
sleep 0.02
|
|
||||||
done
|
done
|
||||||
|
grep -q '"state": "complete"' <<<"$listing"
|
||||||
|
cmp "$root/single.bin.moved" "$seed_data/single.bin"
|
||||||
|
|
||||||
|
status=$("$ctl" --socket "$socket" status)
|
||||||
grep -q '"move_commands": 1' <<<"$status"
|
grep -q '"move_commands": 1' <<<"$status"
|
||||||
grep -q '"handled": 1' <<<"$status"
|
grep -q '"handled": 2' <<<"$status"
|
||||||
grep -q '"errors": 0' <<<"$status"
|
grep -q '"errors": 0' <<<"$status"
|
||||||
|
|
||||||
plugin_status=$("$ctl" --socket "$socket" example.events)
|
plugin_status=$("$ctl" --socket "$socket" example.events)
|
||||||
|
|
@ -70,28 +99,13 @@ for _ in $(seq 1 100); do
|
||||||
done
|
done
|
||||||
grep -q '"event": "torrent_finished"' "$events_log"
|
grep -q '"event": "torrent_finished"' "$events_log"
|
||||||
|
|
||||||
# --- end-to-end move-as-you-finish: register a real torrent's storage, fire a
|
"$ctl" --socket "$socket" remove 1 >/dev/null
|
||||||
# file_complete event, and confirm the script-driven move actually relocates the
|
|
||||||
# file on disk (script thread -> bounded queue -> owner thread -> storage). ----
|
|
||||||
fixtures=$(dirname "$script")
|
|
||||||
root="$tmp/torrent-data"
|
|
||||||
"$ctl" --socket "$socket" add_torrent \
|
|
||||||
"{\"torrent_id\":42,\"torrent\":\"$fixtures/single_v1.torrent\",\"root\":\"$root\"}" \
|
|
||||||
| grep -q '"ok": true'
|
|
||||||
|
|
||||||
src=$(find "$root" -type f | head -n1)
|
|
||||||
[[ -n "$src" ]]
|
|
||||||
|
|
||||||
"$ctl" --socket "$socket" emit \
|
|
||||||
"{\"type\":\"file_complete\",\"torrent_id\":42,\"index\":0,\"path\":\"$src\"}" \
|
|
||||||
>/dev/null
|
|
||||||
|
|
||||||
for _ in $(seq 1 100); do
|
for _ in $(seq 1 100); do
|
||||||
[[ -f "$src.moved" ]] && break
|
listing=$("$ctl" --socket "$socket" list)
|
||||||
|
grep -q '"result": \[\]' <<<"$listing" && break
|
||||||
sleep 0.02
|
sleep 0.02
|
||||||
done
|
done
|
||||||
[[ -f "$src.moved" ]]
|
grep -q '"result": \[\]' <<<"$listing"
|
||||||
[[ ! -f "$src" ]]
|
|
||||||
|
|
||||||
"$ctl" --socket "$socket" shutdown >/dev/null
|
"$ctl" --socket "$socket" shutdown >/dev/null
|
||||||
wait "$daemon_pid"
|
wait "$daemon_pid"
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ if [ "$MODE" = "udp" ]; then
|
||||||
else
|
else
|
||||||
announce="http://127.0.0.1:$tracker_port/announce"
|
announce="http://127.0.0.1:$tracker_port/announce"
|
||||||
fi
|
fi
|
||||||
python3 - "$SOURCE_TOR" "$tor" "$announce" <<'PY'
|
backup="http://127.0.0.1:1/lower-tier-should-not-be-contacted"
|
||||||
|
python3 - "$SOURCE_TOR" "$tor" "$announce" "$backup" <<'PY'
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,7 +91,8 @@ def skip(data, pos):
|
||||||
return colon + 1 + size
|
return colon + 1 + size
|
||||||
|
|
||||||
|
|
||||||
source, target, announce = sys.argv[1], sys.argv[2], sys.argv[3].encode()
|
source, target = sys.argv[1], sys.argv[2]
|
||||||
|
announce, backup = sys.argv[3].encode(), sys.argv[4].encode()
|
||||||
data = open(source, "rb").read()
|
data = open(source, "rb").read()
|
||||||
pos = 1
|
pos = 1
|
||||||
raw_info = None
|
raw_info = None
|
||||||
|
|
@ -107,6 +109,8 @@ while data[pos] != ord("e"):
|
||||||
assert raw_info is not None
|
assert raw_info is not None
|
||||||
rewritten = (
|
rewritten = (
|
||||||
b"d8:announce" + str(len(announce)).encode() + b":" + announce
|
b"d8:announce" + str(len(announce)).encode() + b":" + announce
|
||||||
|
+ b"13:announce-listll" + str(len(announce)).encode() + b":" + announce
|
||||||
|
+ b"el" + str(len(backup)).encode() + b":" + backup + b"ee"
|
||||||
+ b"4:info" + raw_info + b"e"
|
+ b"4:info" + raw_info + b"e"
|
||||||
)
|
)
|
||||||
open(target, "wb").write(rewritten)
|
open(target, "wb").write(rewritten)
|
||||||
|
|
@ -125,5 +129,10 @@ if ! grep -q "REQUEST" "$tracker_log"; then
|
||||||
echo "FAIL: tracker received no announce"
|
echo "FAIL: tracker received no announce"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if grep -q "$backup" "$swarm_log"; then
|
||||||
|
echo "FAIL: lower tracker tier was contacted after primary success"
|
||||||
|
cat "$swarm_log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "PASS: $MODE tracker discovery produced byte-identical output"
|
echo "PASS: $MODE tracker discovery produced byte-identical output"
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -69,6 +69,20 @@ int main(void) {
|
||||||
CHECK(got && glen == olen && memcmp(got, orig, olen) == 0);
|
CHECK(got && glen == olen && memcmp(got, orig, olen) == 0);
|
||||||
free(got);
|
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. */
|
/* The same path with SHA-1 verification offloaded to bounded workers. */
|
||||||
{
|
{
|
||||||
char t2[] = "/tmp/naut_async_XXXXXX";
|
char t2[] = "/tmp/naut_async_XXXXXX";
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,22 @@ int main(void) {
|
||||||
memcmp(got1, global + mi.files[0].length, l1) == 0);
|
memcmp(got1, global + mi.files[0].length, l1) == 0);
|
||||||
free(got1);
|
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);
|
naut_download_destroy(d);
|
||||||
free(global); free(tor); naut_metainfo_free(&mi);
|
free(global); free(tor); naut_metainfo_free(&mi);
|
||||||
char cmd[600]; snprintf(cmd, sizeof cmd, "rm -rf '%s' '%s'", root, destdir);
|
char cmd[600]; snprintf(cmd, sizeof cmd, "rm -rf '%s' '%s'", root, destdir);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ int main(void) {
|
||||||
CHECK_EQ(mi.total_length, 200000);
|
CHECK_EQ(mi.total_length, 200000);
|
||||||
CHECK_EQ(mi.num_files, 1);
|
CHECK_EQ(mi.num_files, 1);
|
||||||
CHECK_EQ(mi.num_trackers, 2);
|
CHECK_EQ(mi.num_trackers, 2);
|
||||||
|
CHECK(mi.tracker_tiers != NULL);
|
||||||
|
CHECK_EQ(mi.tracker_tiers[0], 0);
|
||||||
|
CHECK_EQ(mi.tracker_tiers[1], 1);
|
||||||
char hex[41]; naut_infohash_v1_hex(&mi, hex);
|
char hex[41]; naut_infohash_v1_hex(&mi, hex);
|
||||||
CHECK(strcmp(hex, "7f2555dfd18ba1c4a024264e939d7a5f8b25311b") == 0);
|
CHECK(strcmp(hex, "7f2555dfd18ba1c4a024264e939d7a5f8b25311b") == 0);
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
|
|
@ -134,6 +137,7 @@ int main(void) {
|
||||||
"7f2555dfd18ba1c4a024264e939d7a5f8b25311b", 20));
|
"7f2555dfd18ba1c4a024264e939d7a5f8b25311b", 20));
|
||||||
CHECK(mi.num_trackers == 1 &&
|
CHECK(mi.num_trackers == 1 &&
|
||||||
strcmp(mi.trackers[0], trackers[0]) == 0);
|
strcmp(mi.trackers[0], trackers[0]) == 0);
|
||||||
|
CHECK(mi.tracker_tiers && mi.tracker_tiers[0] == 0);
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
naut_bc_free(doc);
|
naut_bc_free(doc);
|
||||||
free(torrent);
|
free(torrent);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -90,6 +90,36 @@ int main(void) {
|
||||||
|
|
||||||
naut_download_destroy(d);
|
naut_download_destroy(d);
|
||||||
|
|
||||||
|
/* The picker should not open the entire torrent at once. A large swarm can
|
||||||
|
* keep many requests in flight, but new-piece fanout is bounded so the
|
||||||
|
* piece map does not show most pieces "downloading" while few verify. */
|
||||||
|
enum { CAP_NP = 80 };
|
||||||
|
uint64_t cap_total = (uint64_t)CAP_NP * NAUT_BLOCK;
|
||||||
|
uint8_t *cap_hashes = malloc(CAP_NP * NAUT_SHA1_LEN);
|
||||||
|
CHECK(cap_hashes != NULL);
|
||||||
|
for (int p = 0; p < CAP_NP; p++)
|
||||||
|
naut_sha1(data, NAUT_BLOCK, cap_hashes + p * NAUT_SHA1_LEN);
|
||||||
|
naut_file cap_file[1] = { { (char *)"cap.bin", (int64_t)cap_total } };
|
||||||
|
naut_metainfo cap_mi; memset(&cap_mi, 0, sizeof cap_mi);
|
||||||
|
cap_mi.has_v1 = true; cap_mi.num_pieces = CAP_NP;
|
||||||
|
cap_mi.piece_length = NAUT_BLOCK; cap_mi.total_length = cap_total;
|
||||||
|
cap_mi.piece_hashes = cap_hashes; cap_mi.files = cap_file;
|
||||||
|
cap_mi.num_files = 1; cap_mi.name = (char *)"cap";
|
||||||
|
d = naut_download_create(&cap_mi, st);
|
||||||
|
CHECK(d != NULL);
|
||||||
|
naut_bitfield all;
|
||||||
|
naut_bitfield_init(&all, CAP_NP);
|
||||||
|
for (int p = 0; p < CAP_NP; p++) naut_bitfield_set(&all, p);
|
||||||
|
naut_download_add_bitfield(d, &all);
|
||||||
|
int opened = 0;
|
||||||
|
while (naut_download_pick(d, &all, &idx, &begin, &len))
|
||||||
|
opened++;
|
||||||
|
CHECK(opened > 0);
|
||||||
|
CHECK(opened < CAP_NP);
|
||||||
|
naut_bitfield_free(&all);
|
||||||
|
naut_download_destroy(d);
|
||||||
|
free(cap_hashes);
|
||||||
|
|
||||||
/* full multi-peer download: alternate peers, all pieces verify */
|
/* full multi-peer download: alternate peers, all pieces verify */
|
||||||
d = naut_download_create(&mi, st);
|
d = naut_download_create(&mi, st);
|
||||||
naut_download_add_bitfield(d, &hb); /* one peer that has everything */
|
naut_download_add_bitfield(d, &hb); /* one peer that has everything */
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
|
@ -30,13 +31,29 @@ static naut_err capture_move(void *opaque, uint64_t torrent_id,
|
||||||
return NAUT_OK;
|
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) {
|
int main(void) {
|
||||||
pthread_t owner = pthread_self();
|
pthread_t owner = pthread_self();
|
||||||
move_capture capture = {0};
|
move_capture capture = {0};
|
||||||
naut_event_bus *events = naut_event_bus_create();
|
naut_event_bus *events = naut_event_bus_create();
|
||||||
naut_err error;
|
naut_err error;
|
||||||
|
naut_script_host host = {
|
||||||
|
.move_file = capture_move,
|
||||||
|
.labels = capture_labels,
|
||||||
|
.context = &capture,
|
||||||
|
};
|
||||||
naut_script *script = naut_script_create(
|
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);
|
CHECK(script && error == NAUT_OK);
|
||||||
|
|
||||||
naut_event event = {
|
naut_event event = {
|
||||||
|
|
@ -44,13 +61,13 @@ int main(void) {
|
||||||
.torrent_id = 42,
|
.torrent_id = 42,
|
||||||
};
|
};
|
||||||
naut_event_emit(events, &event);
|
naut_event_emit(events, &event);
|
||||||
for (unsigned i = 0; i < 100 && atomic_load(&capture.calls) == 0; i++)
|
for (unsigned i = 0; i < 100; i++) {
|
||||||
|
naut_script_stats pending;
|
||||||
|
naut_script_get_stats(script, &pending);
|
||||||
|
if (pending.handled == 1) break;
|
||||||
usleep(1000);
|
usleep(1000);
|
||||||
CHECK_EQ(atomic_load(&capture.calls), 1);
|
}
|
||||||
CHECK(!pthread_equal(owner, capture.caller));
|
CHECK_EQ(atomic_load(&capture.calls), 0);
|
||||||
CHECK_EQ(capture.torrent_id, 42);
|
|
||||||
CHECK_EQ(capture.file_index, 0);
|
|
||||||
CHECK(strcmp(capture.destination, "/tmp/naut-phase7-finished") == 0);
|
|
||||||
|
|
||||||
event = (naut_event) {
|
event = (naut_event) {
|
||||||
.type = NAUT_EVENT_FILE_COMPLETE,
|
.type = NAUT_EVENT_FILE_COMPLETE,
|
||||||
|
|
@ -59,18 +76,21 @@ int main(void) {
|
||||||
.path = "/tmp/completed-file",
|
.path = "/tmp/completed-file",
|
||||||
};
|
};
|
||||||
naut_event_emit(events, &event);
|
naut_event_emit(events, &event);
|
||||||
for (unsigned i = 0; i < 100 && atomic_load(&capture.calls) < 2; i++)
|
for (unsigned i = 0; i < 100 && atomic_load(&capture.calls) < 1; i++)
|
||||||
usleep(1000);
|
usleep(1000);
|
||||||
CHECK_EQ(atomic_load(&capture.calls), 2);
|
CHECK_EQ(atomic_load(&capture.calls), 1);
|
||||||
|
CHECK(!pthread_equal(owner, capture.caller));
|
||||||
|
CHECK_EQ(capture.torrent_id, 42);
|
||||||
CHECK_EQ(capture.file_index, 3);
|
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_stats stats;
|
||||||
naut_script_get_stats(script, &stats);
|
naut_script_get_stats(script, &stats);
|
||||||
CHECK_EQ(stats.queued, 2);
|
CHECK_EQ(stats.queued, 2);
|
||||||
CHECK_EQ(stats.handled, 2);
|
CHECK_EQ(stats.handled, 2);
|
||||||
CHECK_EQ(stats.errors, 0);
|
CHECK_EQ(stats.errors, 0);
|
||||||
CHECK_EQ(stats.move_requests, 2);
|
CHECK_EQ(stats.move_requests, 1);
|
||||||
|
|
||||||
naut_script_destroy(script);
|
naut_script_destroy(script);
|
||||||
naut_event_bus_destroy(events);
|
naut_event_bus_destroy(events);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue