UPnP server configurable from UI

This commit is contained in:
Igor Katson 2024-08-24 00:34:57 +01:00
parent 3110f68f36
commit 9f340d92e5
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
6 changed files with 166 additions and 8 deletions

View file

@ -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"

View file

@ -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"

View file

@ -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<String>,
#[serde(default)]
pub server_friendly_name: Option<String>,
}
#[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(())
}
}

View file

@ -69,6 +69,9 @@ async fn api_from_config(
init_logging: &InitLoggingResult,
config: &RqbitDesktopConfig,
) -> anyhow::Result<Api> {
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
};