Protocol Handlers
How Risuko handles different download protocols.
Risuko's engine includes specialized handlers for each supported protocol. Each handler is a separate module within risuko-engine.
HTTP/HTTPS
Module: engine/http.rs
The HTTP handler supports multi-threaded range-based downloads:
- Sends a
GETrequest withRange: bytes=0-0to probe file size and range support - Divides the file into 1 MiB pieces when it is larger than
min-split-size × split; spawns N worker tasks (configurable viasplit, like aria2's-s/-x) that pull pieces from a shared lock-free queue - Work-stealing is inherent: any idle worker claims the next free piece, so a slow connection never holds back the rest of the download
- Resume granularity is per-piece (≤1 MiB lost on
SIGKILL): each piece tracks anAtomicU32byte counter that the writer increments per flushedBytes, and a sidecar JSON snapshot is persisted every 2 s - Downloads use HTTP
Rangeheaders; workers are pinned to HTTP/1.1 to keep each piece on its own TCP connection - Each piece streams
bytes::Bytesdirectly into a dedicated writer task that issues positioned writes (pwriteon Unix,seek_writeon Windows) against a shared file handle, so workers never contend on a seek lock - Calls
fsynconce on completion before renaming the.partfile into place - Validates the
Content-Rangeheader on every 206 response and aborts on mismatch; sendsIf-Matchwhen anETagwas recorded for resume - Falls back to a single-connection download when the file is below
min-split-sizeor the server doesn't support ranges - Handles redirects, cookies, and custom headers
- Supports proxy connections via the
all-proxyoption (http://,socks5://,socks5h://;https://proxies are not supported) - Optional DNS over HTTPS: when
doh-enable+doh-urlare set, hostnames resolve via RFC 8484 wire-format queries to the endpoint instead of the system resolver. Applied process-wide to everyrisuko-httpclient through a pluggable resolver hook, with an in-memory TTL cache and optional fallback to system DNS (doh-fallback) - Stalled-transfer watchdog: aborts a worker when the EMA stays below
lowest-speed-limitforlowest-speed-limit-timeout(auto-retry restarts it when enabled) - Multi-URI tasks pick a mirror via
uri-selector(feedback/inorder/adaptive) - Optional
.netrclookup for HTTP Basic auth when the URL has no embedded credentials (disabled withno-netrc) - Cookies can be primed from a Netscape/Mozilla
cookies.txtviaload-cookies
Key Options
| Option | Default | Description |
|---|---|---|
split | 16 | Number of parallel worker tasks (one TCP connection each) |
min-split-size | 1M | Skip splitting for files smaller than this |
connect-timeout | 60 | Connect timeout in seconds |
lowest-speed-limit | 0 | Stalled-transfer floor in bytes/s (0 disables the watchdog) |
lowest-speed-limit-timeout | 30 | Seconds below the floor before the worker is aborted |
uri-selector | feedback | Mirror selection strategy: feedback, inorder, or adaptive |
file-allocation | falloc (none on Android) | Disk reservation strategy: falloc, trunc, or none |
user-agent | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 | Custom user agent |
header | — | Custom HTTP headers |
all-proxy | — | Proxy URL (http://, socks5://, or socks5h://) |
doh-enable | false | Resolve DNS over HTTPS (needs doh-url); global, not per-task |
doh-url | — | DoH endpoint URL (https:// only) |
doh-bootstrap | — | IPs for reaching the endpoint without system DNS |
doh-fallback | true | Fall back to system DNS when a DoH query fails |
load-cookies | — | Path to a Netscape/Mozilla cookies.txt |
netrc-path | ~/.netrc | Override the .netrc file location |
no-netrc | false | Skip .netrc lookup |
referer | — | HTTP referer |
max-download-limit | 0 | Per-task speed limit (bytes/s, 0 = unlimited) |
remote-time | false | Set the local file mtime from the server's Last-Modified header |
BitTorrent
Module: engine/torrent.rs
Built on risuko-bt:
- Magnet link resolution (session fast-path for already-managed torrents, then tracker + DHT). Accepts both
xt=urn:btih:(v1) andxt=urn:btmh:(v2 multihash) URNs; hybrid magnets carry both .torrentfile parsing via the bundled bencode decoder; supports v1, pure-v2, and hybrid metainfo (BEP 52meta version=2withfile treeandpiece layers)- BEP-5 DHT for peer discovery, dual-stack (IPv4 + IPv6 via BEP-32) when an IPv6 socket can be bound; per-torrent continuous
get_peerspolling andannounce_peerkeep the peer supply live throughout the download session. Pure-v2 swarms announce the leading 20 bytes of the SHA-256 info-hash on the v1 DHT/trackers per BEP 52 - BEP-14 Local Service Discovery (LSD) for peers on the same LAN
- BEP-10 extension protocol with
ut_metadata(BEP-9) and full BEP-11 PEX (ut_pex): received peer-exchange messages are parsed and gossiped peers are fed into the dial channel - BEP-55
ut_holepunchNAT traversal: advertised in the extended handshake. When a direct dial to a PEX-gossiped peer fails, Risuko asks the relay that gossiped it to perform a rendezvous; as a relay it forwardsconnectmessages so two NATed peers can simultaneously open a connection - BEP-29 µTP (Micro Transport Protocol): outbound dials attempt TCP first and fall back to µTP when TCP is refused; inbound µTP connections are accepted on the same UDP port as the TCP listener
- BEP-8 Message Stream Encryption (MSE/PE) with selectable
plaintext/prefer/requirepolicy - BEP-52 BitTorrent v2: SHA-256 Merkle piece-tree verification (16 KiB block leaves), reserved-bit
0x08in handshake byte 7 advertises v2 capability, and theHashRequest/Hashes/HashRejectwire messages exchange Merkle layer slices with sibling proofs - UPnP IGD port forwarding for the TCP listener only (the µTP UDP port and IPv6 are not mapped)
- Rarest-first piece selection with endgame mode; SHA-1 (v1) or SHA-256 Merkle root (v2) verification off the async runtime
- HTTP/HTTPS (BEP-23 compact) and UDP (BEP-15) trackers
- Configurable seeding (ratio, time, or keep-seeding), upload speed limiting
- Tunable per-peer request pipeline and per-torrent peer cap
Pure-v2 magnet links resolve by fetching the BEP 52 piece layers from peers over HASH_REQUEST after the info dict. If no peer serves those layers, resolution fails with piece layers unavailable. Hybrid magnets fall back to the v1 info dict when layers are missing but a btih hash is present.
Key Options
| Option | Description |
|---|---|
seed-ratio | Stop seeding after this upload/download ratio |
seed-time | Stop seeding after this many minutes |
keep-seeding | Seed until manually stopped (overrides seed-time / seed-ratio) |
bt-upload-rate-limit | Upload speed limit (bytes/s, 0 = unlimited); there is no BT download-rate limit |
bt-tracker | Additional tracker URLs |
bt-max-peers-per-torrent | Max concurrent peer connections per torrent (default 100) |
bt-max-outstanding-per-peer | Max pipelined chunk requests per peer (default 128) |
bt-enable-upnp | UPnP IGD port forwarding (default true) |
bt-upnp-lease | UPnP mapping lease in seconds (default 300) |
bt-enable-lsd | BEP-14 Local Service Discovery (default true) |
bt-encryption-policy | MSE/PE policy: plaintext, prefer, require (default prefer) |
bt-listen-v6 | Also bind an IPv6 TCP listener (default false) |
Seeding Behavior
After a torrent download completes, seeding behavior depends on the seed options:
onBtDownloadCompletefires when the download finishes- If
keep-seedingis true, orseed-time > 0, orseed-ratio > 0, the task stays active and seeding continues - When
keep-seedingis true,seed-time/seed-ratiolimits are ignored; seeding runs until the task is stopped manually - Otherwise seeding stops when
seed-timeelapses orseed-ratiois reached (whichever comes first) onDownloadCompletefires when seeding is done (or immediately if no seeding was requested)
ED2K
Module: engine/ed2k/
Downloads files by known hash using ed2k://|file|… links (server links are rejected). Sources come from OP_GET_SOURCES on the configured servers plus any sources, entries in the link; only high-ID sources are used. Download only — no keyword search, no sharing/upload, no Kad DHT, and no encryption. Files over 4 GiB are rejected (the 64-bit large-file extension is not implemented), and downloaded data is not hash-verified.
Key Options
| Option | Default | Description |
|---|---|---|
ed2k-server | 176.123.5.89:4725, 45.82.80.155:5687, 85.239.33.123:4232, 91.208.162.87:4232, 145.239.2.134:4661 | Comma-separated ip:port server list. Ships seeded with the five servers shown; falls back to a built-in list only if set empty |
ed2k-port | 4662 | Local client TCP port |
M3U8/HLS
Module: engine/m3u8/
Downloads HTTP Live Streaming content:
- Parses master and media M3U8 playlists (highest-bandwidth variant selected from a master playlist)
- Downloads segments concurrently (bounded by
split, default 5) with per-segment retry and a resume-progress sidecar - Decrypts AES-128-CBC segments (
EXT-X-KEY); SAMPLE-AES and other methods are written through undecrypted - Concatenates segments into a single
.ts, optionally remuxing to MP4 viaffmpegwhenm3u8-output-formatismp4 - VOD only — live playlists (no
#EXT-X-ENDLIST) are rejected
FTP/SFTP
Module: engine/ftp/
File Transfer Protocol handler:
- FTP with optional implicit-TLS encryption (FTPS)
- SFTP (SSH File Transfer Protocol) via
russh+russh-sftp - Server host keys pinned on first use (TOFU) via
engine/ssh_known_hosts.rs; subsequent fingerprint mismatches are refused. The same store is shared with the SFTP upload sink so trust state stays consistent across download and upload paths - Resume from an existing
.partvia FTPRESTor SFTP positional reads; single file per task (no directory listing or multi-file transfer) - FTPS certificate verification on by default (
check-certificate); SFTP auth accepts a password or a PEM private key (sftp-private-key)
Media (yt-dlp)
Module: engine/media.rs
Delegates extraction and download to an external yt-dlp process resolved from PATH (not bundled; check_yt_dlp_available runs yt-dlp --version first). Routing uses engine::media::is_media_uri, an allowlist of ~26 host suffixes (YouTube, Vimeo, Twitch, Bilibili, SoundCloud, TikTok, and others); the force-ytdlp option forces any URL through yt-dlp. The handler:
- Probes the URL with
yt-dlp --dump-json --no-playlist --flat-playlistto fetch a JSON info object (title, duration, formats), exposed via the Tauri commandget_media_info - Spawns
yt-dlpwith--newline --progress-templateand parses progress lines into the same atomic counters used by HTTP/BT, so the regular speed/progress UI works - Picks the format selector from the
media-formatoption (falling back toyoutube-format, thenyt-format); merges viaffmpegwhen it is onPATH - Watches yt-dlp's
before_dl/after_move/Destination:lines to learn the resolved output filename - Forwards proxy, user-agent, referer, and cookies (
all-proxy/proxy,user-agent,referer,cookiesfile orcookieheader) - For YouTube URLs adds
--extractor-args youtube:player_client=web,default
Routed automatically by add_uri whenever is_media_uri matches; the dedicated add_media Tauri command and risuko.addMedia RPC method bypass detection.
ADC / Direct Connect (NMDC + ADC dialects)
Module: engine/adc/
Schemes: adc://, adcs:// (parsed as a TLS flag), dchub://, nmdc://. Default port 411 (ADC), 412 (ADCS). Direct file URIs of the form dchub://hub.host[:port]/?TTH=<base32>&xl=<bytes>&dn=<name> carry the TTH hash, file size and display name.
The transfer implementation (NMDC $Lock/$Key/$ADCGET, the ADC framed protocol, and TTH/Tiger hashing) was removed. The handler now only parses the URI: it validates the scheme, requires a TTH and non-zero xl, stores the size, then returns no source has the requested file without any network I/O. Risuko runs passive-only with no listening socket, so $ConnectToMe / ADC CTM can never complete. adcs:// TLS is not implemented.
ADC / Direct Connect (both NMDC and ADC dialects) parses URIs but cannot download. No handshake, search, transfer, or encryption exists in the current build.
Gnutella 0.6
Module: engine/gnutella/
URI scheme: gnutella://host[:port]/uri-res/N2R?urn:sha1:<base32> (also gnet://). Default port 6346. The Gnutella wire protocol itself is not implemented — there is no GNUTELLA CONNECT/0.6 handshake, no GWebCache discovery, and no PING/QUERY/QUERYHIT. The handler:
- Connects directly to the host in the URI and issues an HTTP/1.1
GET /uri-res/N2R?<urn>with anX-Gnutella-Content-URNheader (User-Agent: Risuko/0.1) - Accepts only
200or206, requires aContent-Length(no chunked encoding), and checks it is within 4 KiB of thexl=size hint - Streams the single source to disk with no range resume and no SHA-1 verification (the URN is only a request key)
Gnutella2 (G2)
Module: engine/g2/
URI scheme: g2://host[:port]/sha1/<base32>?xl=<bytes>&dn=<name>. Default port 6346. No G2 wire protocol is implemented; run_g2_download reuses the Gnutella fetch_by_urn path with a hardcoded /uri-res/N2R route since both networks serve content over the same peer-to-peer HTTP/1.1 endpoint. Same single-source fetch, length tolerance, and lack of hash verification as Gnutella.
giFT IPC bridge
Module: engine/gift/
URI scheme: gift://<inner-uri>. Off by default. When gift-enabled is true, the handler connects to a locally-running giftd daemon (default 127.0.0.1:1213, override with gift-host / gift-port) over its line-oriented IPC socket and forwards the inner URI verbatim. Status is parsed from TRANSFER STATUS id=… total=… done=… state=active|complete|... lines so progress bars work like any other transfer. Cancelling the task issues a TRANSFER REMOVE id=… to giftd.
The legacy P2P stacks (ADC/Direct Connect, Gnutella, G2, giFT) are best-effort and depend on the public network still being reachable. They share the engine's progress/cancel/auto-retry plumbing but bypass the multi-threaded HTTP downloader. ADC/Direct Connect currently only parses URIs and cannot transfer; giFT is off unless a local giftd is running and gift-enabled is set.
RSS
Module: engine/rss/
Automatic feed subscription and download:
- Add RSS/Atom feed URLs
- Periodic polling for new items (configurable interval)
- Automatic download of new items matching configured rules
- Persistent feed state via
StorageBackend
The RSS manager uses tokio::spawn for async polling and requires a tokio runtime context.
Cloud Upload Sinks
Module: engine/upload/
Push completed downloads to a remote destination. The upload subsystem mirrors
the RssManager pattern: a single UploadSinkManager owns sinks, routing
rules, and a bounded job queue. Each protocol implements an UploadSink
trait (upload + test) and is dispatched from manager::build_sink_runtime.
| File | Role |
|---|---|
sink.rs | UploadSink trait, SinkConfig discriminated union, UploadFile / UploadControl / UploadProgress |
manager.rs | UploadSinkManager: CRUD, persistence, semaphore-bounded job queue, event emission |
rules.rs | UploadRule matcher (categories, extensions, size bounds, task kinds) |
webdav.rs | WebDAV: single PUT, MKCOL for missing parents, optional Basic auth, optional self-signed-cert mode |
s3.rs | S3-compatible: SigV4 with UNSIGNED-PAYLOAD. Single PUT up to 5 GiB, then 3-step multipart pipeline (POST ?uploads= → PUT ?partNumber&uploadId → POST ?uploadId) with a 64 MiB default part size and best-effort DELETE ?uploadId to abort orphaned uploads. Virtual-host or path-style URLs |
sftp.rs | SFTP via russh + russh-sftp, password or PEM private-key auth. Server host keys are pinned on first use (TOFU) via the shared ssh_known_hosts store and refused on mismatch |
ftp.rs | FTP / implicit-TLS FTPS via suppaftp, optional self-signed-cert mode (insecure) |
Sink selection is per-task override → first matching rule → default sink id.
The manager runs uploads on a tokio::Semaphore (default permits = 2,
clamped to 1..=16) and emits engine:upload-{queued,start,complete,error,cancelled}
events. Persisted state lives under the upload-sinks key via StorageBackend.
See the Cloud Upload Sinks guide for user-facing configuration.
Protocol Routing
The Tauri add_uri command and the desktop Add Task modal sniff the URI scheme to dispatch transparently. The raw JSON-RPC layer keeps protocol-specific methods so that aria2-style clients still know exactly what they're calling:
| Protocol | URI scheme(s) | RPC Method | Node.js Function | CLI |
|---|---|---|---|---|
| HTTP/HTTPS | http(s):// | risuko.addUri | addUri | risuko download <url> |
| BitTorrent (.torrent) | — (file path) | risuko.addTorrent | addTorrent | risuko download <file.torrent> (auto-detected) |
| BitTorrent (magnet) | magnet: | risuko.addUri (sniffed) | addMagnet | risuko download <magnet> |
| ED2K | ed2k:// | risuko.addEd2k | addEd2k | — |
| M3U8/HLS | *.m3u8, *.m3u | risuko.addUri (sniffed) | addM3u8 | — |
| FTP/SFTP | ftp(s)://, sftp:// | risuko.addUri (sniffed) | addFtp | — |
| Media (yt-dlp) | media host allowlist (~26 sites) | risuko.addMedia | — | risuko download <url> |
| ADC / Direct Connect | adc(s)://, dchub://, nmdc:// | risuko.addUri (sniffed) | — | — |
| Gnutella 0.6 | gnutella://, gnet:// | risuko.addUri (sniffed) | — | — |
| Gnutella2 (G2) | g2:// | risuko.addUri (sniffed) | — | — |
| giFT IPC | gift:// | risuko.addUri (sniffed) | — | — |
Magnet links are sniffed inside add_http_task, the shared target of risuko.addUri, so the CLI and aria2-style clients add magnets too — a magnet: URI is routed to the torrent handler wherever it enters. Scheme sniffing for M3U8, FTP, and the legacy P2P protocols still lives only in the Tauri add_uri command, so a .m3u8, ftp://, or gnutella:// URL sent over plain JSON-RPC is enqueued as an HTTP task. The CLI's download command distinguishes .torrent files (path ends with .torrent and exists on disk → risuko.addTorrent) and media URLs (is_media_uri or --ytdlp → risuko.addMedia); everything else, magnets included, is passed to risuko.addUri. The Node.js API also offers protocol-specific functions (addTorrent, addMagnet, addEd2k, addM3u8, addFtp) that call the engine directly without scheme sniffing.