From 9f340d92e5f341a967eeb4e1f610dd5f045cab61 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Sat, 24 Aug 2024 00:34:57 +0100 Subject: [PATCH] UPnP server configurable from UI --- desktop/src-tauri/Cargo.lock | 58 ++++++++++++++++++++++++++++++++- desktop/src-tauri/Cargo.toml | 1 + desktop/src-tauri/src/config.rs | 35 ++++++++++++++++++-- desktop/src-tauri/src/main.rs | 36 ++++++++++++++++++-- desktop/src/configuration.tsx | 4 +++ desktop/src/configure.tsx | 40 +++++++++++++++++++++-- 6 files changed, 166 insertions(+), 8 deletions(-) diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 920c35c..0061b89 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -317,6 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" dependencies = [ "memchr", + "regex-automata 0.4.7", "serde", ] @@ -1182,6 +1183,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" +dependencies = [ + "rustix", + "windows-targets 0.52.6", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1855,6 +1866,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "upnp-serve", "url", "urlencoding", "uuid", @@ -2670,7 +2682,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", "indexmap 2.4.0", - "quick-xml", + "quick-xml 0.32.0", "serde", "time", ] @@ -2767,6 +2779,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.36" @@ -3315,6 +3336,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3886,7 +3916,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -4186,6 +4218,30 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "upnp-serve" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "bstr", + "gethostname", + "http 1.1.0", + "httparse", + "librqbit-core", + "librqbit-sha1-wrapper", + "librqbit-upnp", + "mime_guess", + "parking_lot", + "quick-xml 0.36.1", + "reqwest", + "tokio", + "tokio-util", + "tracing", + "url", + "uuid", +] + [[package]] name = "url" version = "2.5.2" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index c76eea1..cfc64af 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ librqbit = { path = "../../crates/librqbit", features = [ "tracing-subscriber-utils", "http-api", "webui", + "upnp-serve-adapter", ] } tokio = { version = "1.34.0", features = ["rt-multi-thread"] } anyhow = "1.0.75" diff --git a/desktop/src-tauri/src/config.rs b/desktop/src-tauri/src/config.rs index 9caae75..72308f4 100644 --- a/desktop/src-tauri/src/config.rs +++ b/desktop/src-tauri/src/config.rs @@ -125,10 +125,21 @@ impl Default for RqbitDesktopConfigHttpApi { } } -#[derive(Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] #[serde(default)] pub struct RqbitDesktopConfigUpnp { - pub disable: bool, + // rename for backwards compat + #[serde(rename = "disable")] + pub disable_tcp_port_forward: bool, + + #[serde(default)] + pub enable_server: bool, + + #[serde(default)] + pub server_hostname: Option, + + #[serde(default)] + pub server_friendly_name: Option, } #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -162,3 +173,23 @@ impl Default for RqbitDesktopConfig { } } } + +impl RqbitDesktopConfig { + pub fn validate(&self) -> anyhow::Result<()> { + if self.upnp.enable_server { + if self.http_api.disable { + anyhow::bail!("if UPnP server is enabled, you need to enable the HTTP API also.") + } + if self.http_api.listen_addr.ip().is_loopback() { + anyhow::bail!("if UPnP server is enabled, you need to set HTTP API IP to 0.0.0.0 or at least non-localhost address.") + } + match self.upnp.server_hostname.as_ref().map(|s| s.trim()) { + Some("") | None => { + anyhow::bail!("UPnP hostname must be set to non-empty string") + } + Some(_) => {} + } + } + Ok(()) + } +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 6a49429..3979182 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -69,6 +69,9 @@ async fn api_from_config( init_logging: &InitLoggingResult, config: &RqbitDesktopConfig, ) -> anyhow::Result { + config + .validate() + .context("error validating configuration")?; let persistence = if config.persistence.disable { None } else { @@ -100,7 +103,7 @@ async fn api_from_config( } else { None }, - enable_upnp_port_forwarding: !config.upnp.disable, + enable_upnp_port_forwarding: !config.upnp.disable_tcp_port_forward, fastresume: config.persistence.fastresume, ..Default::default() }, @@ -118,6 +121,35 @@ async fn api_from_config( let listen_addr = config.http_api.listen_addr; let api = api.clone(); let read_only = config.http_api.read_only; + let upnp_router = if config.upnp.enable_server { + let hostname = config + .upnp + .server_hostname + .as_ref() + .map(|h| h.trim()) + .context("empty UPNP hostname")? + .to_owned(); + let friendly_name = config + .upnp + .server_friendly_name + .as_ref() + .map(|f| f.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_owned()) + .unwrap_or_else(|| format!("rqbit@{hostname}")); + + let mut upnp_adapter = session + .make_upnp_adapter(friendly_name, hostname, config.http_api.listen_addr.port()) + .await + .context("error starting UPnP server")?; + let router = upnp_adapter.take_router()?; + session.spawn(error_span!("ssdp"), async move { + upnp_adapter.run_ssdp_forever().await + }); + Some(router) + } else { + None + }; let http_api_task = async move { let listener = tokio::net::TcpListener::bind(listen_addr) .await @@ -126,7 +158,7 @@ async fn api_from_config( api.clone(), Some(librqbit::http_api::HttpApiOptions { read_only }), ) - .make_http_api_and_run(listener, None) + .make_http_api_and_run(listener, upnp_router) .await }; diff --git a/desktop/src/configuration.tsx b/desktop/src/configuration.tsx index 0428039..7373d67 100644 --- a/desktop/src/configuration.tsx +++ b/desktop/src/configuration.tsx @@ -34,6 +34,10 @@ interface RqbitDesktopConfigHttpApi { interface RqbitDesktopConfigUpnp { disable: boolean; + + enable_server: boolean; + server_hostname: string; + server_friendly_name: string; } export interface RqbitDesktopConfig { diff --git a/desktop/src/configure.tsx b/desktop/src/configure.tsx index 9b3dddd..c2fb649 100644 --- a/desktop/src/configure.tsx +++ b/desktop/src/configure.tsx @@ -59,7 +59,8 @@ type TAB = | "Session" | "Peer options" | "HTTP API" - | "TCP Listen"; + | "TCP Listen" + | "UPnP Server"; const TABS: readonly TAB[] = [ "Home", @@ -68,6 +69,7 @@ const TABS: readonly TAB[] = [ "TCP Listen", "Peer options", "HTTP API", + "UPnP Server", ] as const; const Tab: React.FC<{ @@ -252,11 +254,11 @@ export const ConfigModal: React.FC<{ /> + +
+ + + + + +
+
+