From 9eed5aeb0793427d979e886155f478a1363c6d97 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 28 Aug 2024 12:33:50 +0100 Subject: [PATCH 1/5] sending notifies to all interfaces --- Cargo.lock | 1 + crates/upnp-serve/Cargo.toml | 1 + crates/upnp-serve/src/ssdp.rs | 59 ++++++++++++++++++++++++++--------- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fe9901..775bb9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1639,6 +1639,7 @@ dependencies = [ "librqbit-sha1-wrapper", "librqbit-upnp", "mime_guess", + "network-interface", "parking_lot", "quick-xml", "reqwest", diff --git a/crates/upnp-serve/Cargo.toml b/crates/upnp-serve/Cargo.toml index 411327b..1b3c829 100644 --- a/crates/upnp-serve/Cargo.toml +++ b/crates/upnp-serve/Cargo.toml @@ -34,6 +34,7 @@ tokio-util = "0.7.11" reqwest = { version = "0.12.7", default-features = false } socket2 = "0.5.7" quick-xml = { version = "0.36.1", features = ["serialize"] } +network-interface = "2.0.0" [dev-dependencies] tracing-subscriber = "0.3.18" diff --git a/crates/upnp-serve/src/ssdp.rs b/crates/upnp-serve/src/ssdp.rs index 95197af..cc4d37f 100644 --- a/crates/upnp-serve/src/ssdp.rs +++ b/crates/upnp-serve/src/ssdp.rs @@ -133,16 +133,15 @@ impl SsdpRunner { Ok(Self { opts, socket }) } - fn generate_notify_message(&self, kind: &str, nts: &str) -> String { + fn generate_notify_message(&self, kind: &str, nts: &str, location: &str) -> String { let usn: &str = &self.opts.usn; - let description_http_location = &self.opts.description_http_location; let server: &str = &self.opts.server_string; let bcast_addr = UPNP_BROADCAST_ADDR; format!( "NOTIFY * HTTP/1.1\r Host: {bcast_addr}\r Cache-Control: max-age=75\r -Location: {description_http_location}\r +Location: {location}\r NT: {kind}\r NTS: {nts}\r Server: {server}\r @@ -169,17 +168,49 @@ Content-Length: 0\r\n\r\n" } async fn try_send_notifies(&self, nts: &str) { - for kind in [UPNP_KIND_ROOT_DEVICE, UPNP_KIND_MEDIASERVER] { - let msg = self.generate_notify_message(kind, nts); - trace!(content=?msg, addr=?UPNP_BROADCAST_ADDR, "sending SSDP NOTIFY"); - if let Err(e) = self - .socket - .send_to(msg.as_bytes(), UPNP_BROADCAST_ADDR) - .await - { - warn!(error=?e, "error sending SSDP NOTIFY") - } else { - debug!(kind, nts, "sent SSDP NOTIFY") + use network_interface::NetworkInterfaceConfig; + let interfaces = network_interface::NetworkInterface::show(); + let interfaces = match interfaces { + Ok(interfaces) => interfaces, + Err(e) => { + warn!(error=?e, "error determining network interfaces"); + return; + } + }; + + let location = match url::Url::parse(&self.opts.description_http_location) { + Ok(u) => u, + // TODO: rewrite this + Err(e) => { + warn!(error=?e, "error parsing description_http_location"); + return; + } + }; + + for ni in interfaces { + for niaddr in ni.addr { + let ip = niaddr.ip(); + let addr = SocketAddr::new(ip, 0); + let sock = match tokio::net::UdpSocket::bind(addr).await { + Ok(sock) => sock, + Err(e) => { + debug!(%addr, error=?e, "error binding UDP to send NOTIFY"); + continue; + } + }; + + let mut location = location.clone(); + location.set_host(Some(&format!("{ip}"))).unwrap(); + + for kind in [UPNP_KIND_ROOT_DEVICE, UPNP_KIND_MEDIASERVER] { + let msg = self.generate_notify_message(kind, nts, &format!("{location}")); + trace!(content=?msg, addr=?UPNP_BROADCAST_ADDR, "sending SSDP NOTIFY"); + if let Err(e) = sock.send_to(msg.as_bytes(), UPNP_BROADCAST_ADDR).await { + warn!(error=?e, "error sending SSDP NOTIFY") + } else { + debug!(kind, nts, "sent SSDP NOTIFY") + } + } } } } From 02148171228e80dcdc5ea28c8661fb42d4fa7191 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 28 Aug 2024 13:32:42 +0100 Subject: [PATCH 2/5] Fix root crate compilation dependencies to force sha1* --- Cargo.lock | 17 +++++++++++++++++ crates/librqbit/Cargo.toml | 15 +++++++++++++-- crates/librqbit_core/Cargo.toml | 1 + crates/librqbit_core/src/lib.rs | 5 +++++ crates/librqbit_core/src/torrent_metainfo.rs | 6 +++++- crates/sha1w/Cargo.toml | 1 + crates/sha1w/src/lib.rs | 5 +++++ 7 files changed, 47 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 775bb9b..d9ec68a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,15 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "assert_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e2651f366b7ee3f97729fded1441539b49d5f39eeb05b842689e11e84501b2" +dependencies = [ + "const_panic", +] + [[package]] name = "async-backtrace" version = "0.2.7" @@ -531,6 +540,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_panic" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7782af8f90fe69a4bb41e460abe1727d493403d8b2cc43201a3a3e906b24379f" + [[package]] name = "core-foundation" version = "0.9.4" @@ -1517,6 +1532,7 @@ name = "librqbit-core" version = "4.0.1" dependencies = [ "anyhow", + "assert_cfg", "bytes", "data-encoding", "directories", @@ -1583,6 +1599,7 @@ dependencies = [ name = "librqbit-sha1-wrapper" version = "4.0.0" dependencies = [ + "assert_cfg", "crypto-hash", "ring", ] diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index 840771c..7cc9b25 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -17,8 +17,19 @@ http-api = ["axum", "tower-http"] upnp-serve-adapter = ["upnp-serve"] webui = [] timed_existence = [] -default-tls = ["reqwest/default-tls", "sha1w/sha1-crypto-hash"] -rust-tls = ["reqwest/rustls-tls", "sha1w/sha1-ring"] +default-tls = [ + "reqwest/default-tls", + "sha1w/sha1-crypto-hash", + "bencode/sha1-crypto-hash", + "librqbit-core/sha1-crypto-hash", +] +rust-tls = [ + "reqwest/rustls-tls", + "sha1w/sha1-ring", + "sha1w/sha1-ring", + "bencode/sha1-ring", + "librqbit-core/sha1-ring", +] storage_middleware = ["lru"] storage_examples = [] tracing-subscriber-utils = ["tracing-subscriber"] diff --git a/crates/librqbit_core/Cargo.toml b/crates/librqbit_core/Cargo.toml index 552012b..403b0ce 100644 --- a/crates/librqbit_core/Cargo.toml +++ b/crates/librqbit_core/Cargo.toml @@ -31,6 +31,7 @@ directories = "5" tokio-util = "0.7.10" data-encoding = "2.6.0" bytes = "1.7.1" +assert_cfg = "0.1.0" [dev-dependencies] diff --git a/crates/librqbit_core/src/lib.rs b/crates/librqbit_core/src/lib.rs index 63577d6..eb6d654 100644 --- a/crates/librqbit_core/src/lib.rs +++ b/crates/librqbit_core/src/lib.rs @@ -9,3 +9,8 @@ pub mod speed_estimator; pub mod torrent_metainfo; pub use hash_id::Id20; + +assert_cfg::exactly_one! { + feature = "sha1-crypto-hash", + feature = "sha1-ring", +} diff --git a/crates/librqbit_core/src/torrent_metainfo.rs b/crates/librqbit_core/src/torrent_metainfo.rs index b1caf55..ee61bd5 100644 --- a/crates/librqbit_core/src/torrent_metainfo.rs +++ b/crates/librqbit_core/src/torrent_metainfo.rs @@ -29,7 +29,11 @@ pub fn torrent_from_bytes_ext<'de, BufType: Deserialize<'de> + From<&'de [u8]>>( let mut t = TorrentMetaV1::deserialize(&mut de)?; let (digest, info_bytes) = match (de.torrent_info_digest, de.torrent_info_bytes) { (Some(digest), Some(info_bytes)) => (digest, info_bytes), - _ => anyhow::bail!("programming error"), + (o1, o2) => anyhow::bail!( + "programming error: digest.is_some()={}, info_bytes.is_some()={}. Probably one of bencode/sha1* features isn't enabled.", + o1.is_some(), + o2.is_some() + ), }; t.info_hash = Id20::new(digest); Ok(ParsedTorrent { diff --git a/crates/sha1w/Cargo.toml b/crates/sha1w/Cargo.toml index 486c3b9..bd2fd17 100644 --- a/crates/sha1w/Cargo.toml +++ b/crates/sha1w/Cargo.toml @@ -16,5 +16,6 @@ sha1-crypto-hash = ["crypto-hash"] sha1-ring = ["ring"] [dependencies] +assert_cfg = "0.1.0" crypto-hash = { version = "0.3", optional = true } ring = { version = "0.17", optional = true } diff --git a/crates/sha1w/src/lib.rs b/crates/sha1w/src/lib.rs index 1982a07..6a7ec22 100644 --- a/crates/sha1w/src/lib.rs +++ b/crates/sha1w/src/lib.rs @@ -11,6 +11,11 @@ pub trait ISha1 { fn finish(self) -> [u8; 20]; } +assert_cfg::exactly_one! { + feature = "sha1-crypto-hash", + feature = "sha1-ring", +} + #[cfg(feature = "sha1-crypto-hash")] mod crypto_hash_impl { use super::ISha1; From b174afaa1276cc0cd046354b5b3479e008c4a943 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 28 Aug 2024 13:38:11 +0100 Subject: [PATCH 3/5] UPNP: send notifies on all interfaces, no need to specify hostname anymore --- Cargo.lock | 1 + Makefile | 2 +- crates/librqbit/src/upnp_server_adapter.rs | 35 ++++++++++------- crates/rqbit/Cargo.toml | 1 + crates/rqbit/src/main.rs | 28 +++++++------ .../upnp-serve/examples/upnp-stub-server.rs | 2 - crates/upnp-serve/src/http_handlers.rs | 13 ++++++- crates/upnp-serve/src/lib.rs | 9 +++-- crates/upnp-serve/src/ssdp.rs | 39 ++++++++++--------- crates/upnp-serve/src/upnp_types.rs | 12 +++++- crates/upnp/src/lib.rs | 22 ++++++----- docker/compose-examples/server.yaml | 2 +- 12 files changed, 101 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9ec68a..7d70a1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2493,6 +2493,7 @@ dependencies = [ "clap_complete", "console-subscriber", "futures", + "gethostname", "libc", "librqbit", "librqbit-upnp-serve", diff --git a/Makefile b/Makefile index a9f4aad..d99359b 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ webui-build: webui-deps npm run build # NOTE: on LG TV using hostname is unstable for some reason, use IP address. -export RQBIT_UPNP_SERVER_HOSTNAME ?= $(shell hostname) +export RQBIT_UPNP_SERVER_ENABLE ?= true export RQBIT_UPNP_SERVER_FRIENDLY_NAME ?= rqbit-dev export RQBIT_HTTP_API_LISTEN_ADDR ?= 0.0.0.0:3030 RQBIT_OUTPUT_FOLDER ?= /tmp/scratch diff --git a/crates/librqbit/src/upnp_server_adapter.rs b/crates/librqbit/src/upnp_server_adapter.rs index 52e6f1f..2000e47 100644 --- a/crates/librqbit/src/upnp_server_adapter.rs +++ b/crates/librqbit/src/upnp_server_adapter.rs @@ -11,7 +11,6 @@ use crate::{session::TorrentId, ManagedTorrent, Session}; #[derive(Clone)] pub struct UpnpServerSessionAdapter { session: Arc, - hostname: String, port: u16, } @@ -54,6 +53,7 @@ impl TorrentFileTreeNode { fn as_item_or_container( &self, id: usize, + http_host: &str, torrent: &ManagedTorrent, adapter: &UpnpServerSessionAdapter, ) -> ItemOrContainer { @@ -79,7 +79,7 @@ impl TorrentFileTreeNode { mime_type: mime_guess::from_path(filename).first(), url: format!( "http://{}:{}/torrents/{}/stream/{}/{}", - adapter.hostname, + http_host, adapter.port, torrent.id(), fid, @@ -197,7 +197,7 @@ impl TorrentFileTree { } impl UpnpServerSessionAdapter { - fn build_root(&self) -> Vec { + fn build_root(&self, hostname: &str) -> Vec { let mut all = self .session .with_torrents(|torrents| torrents.map(|(_, t)| t.clone()).collect_vec()); @@ -216,7 +216,7 @@ impl UpnpServerSessionAdapter { let mime_type = mime_guess::from_path(rf).first(); let url = format!( "http://{}:{}/torrents/{real_id}/stream/0/{title}", - self.hostname, self.port + hostname, self.port ); Some(ItemOrContainer::Item(Item { id: upnp_id, @@ -249,9 +249,13 @@ impl UpnpServerSessionAdapter { } impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter { - fn browse_direct_children(&self, parent_id: usize) -> Vec { + fn browse_direct_children( + &self, + parent_id: usize, + http_hostname: &str, + ) -> Vec { if parent_id == 0 { - return self.build_root(); + return self.build_root(http_hostname); } let (node_id, torrent_id) = match decode_id(parent_id) { @@ -292,14 +296,19 @@ impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter { let mut result = Vec::new(); if node.real_torrent_file_id.is_some() { - result.push(node.as_item_or_container(node_id, &torrent, self)) + result.push(node.as_item_or_container(node_id, http_hostname, &torrent, self)) } else { for (child_node_id, child_node) in node .children .iter() .filter_map(|id| Some((*id, tree.nodes.get(*id)?))) { - result.push(child_node.as_item_or_container(child_node_id, &torrent, self)); + result.push(child_node.as_item_or_container( + child_node_id, + http_hostname, + &torrent, + self, + )); } }; @@ -311,17 +320,14 @@ impl Session { pub async fn make_upnp_adapter( self: &Arc, friendly_name: String, - http_hostname: String, http_listen_port: u16, ) -> anyhow::Result { UpnpServer::new(UpnpServerOptions { friendly_name, - http_hostname: http_hostname.clone(), http_listen_port, http_prefix: "/upnp".to_owned(), browse_provider: Box::new(UpnpServerSessionAdapter { session: self.clone(), - hostname: http_hostname, port: http_listen_port, }), cancellation_token: self.cancellation_token().child_token(), @@ -536,12 +542,11 @@ mod tests { let adapter = UpnpServerSessionAdapter { session, - hostname: "127.0.0.1".into(), port: 9005, }; assert_eq!( - adapter.browse_direct_children(0), + adapter.browse_direct_children(0, "127.0.0.1"), vec![ ItemOrContainer::Item(Item { id: encode_id(0, 0), @@ -560,7 +565,7 @@ mod tests { ); assert_eq!( - adapter.browse_direct_children(encode_id(0, 1)), + adapter.browse_direct_children(encode_id(0, 1), "127.0.0.1"), vec![ItemOrContainer::Container(Container { id: encode_id(1, 1), parent_id: Some(encode_id(0, 1)), @@ -570,7 +575,7 @@ mod tests { ); assert_eq!( - adapter.browse_direct_children(encode_id(1, 1)), + adapter.browse_direct_children(encode_id(1, 1), "127.0.0.1"), vec![ItemOrContainer::Item(Item { id: encode_id(2, 1), parent_id: Some(encode_id(1, 1)), diff --git a/crates/rqbit/Cargo.toml b/crates/rqbit/Cargo.toml index 0a086be..217aa5c 100644 --- a/crates/rqbit/Cargo.toml +++ b/crates/rqbit/Cargo.toml @@ -48,6 +48,7 @@ upnp-serve = { path = "../upnp-serve", default-features = false, version = "0.1. libc = "0.2.158" signal-hook = "0.3.17" tokio-util = "0.7.11" +gethostname = "0.5.0" [dev-dependencies] futures = { version = "0.3" } diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 619b90d..ea89913 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -139,13 +139,16 @@ struct Opts { tcp_listen_max_port: u16, /// If set, will try to publish the chosen port through upnp on your router. - #[arg(long = "disable-upnp", env = "RQBIT_UPNP_DISABLE_PORT_FORWARD")] - disable_upnp: bool, + #[arg( + long = "disable-upnp-port-forward", + env = "RQBIT_UPNP_PORT_FORWARD_DISABLE" + )] + disable_upnp_port_forward: bool, /// If set, will run a UPNP Media server and stream all the torrents through it. /// Should be set to your hostname/IP as seen by your LAN neighbors. - #[arg(long = "upnp-server-hostname", env = "RQBIT_UPNP_SERVER_HOSTNAME")] - upnp_server_hostname: Option, + #[arg(long = "enable-upnp-server", env = "RQBIT_UPNP_SERVER_ENABLE")] + enable_upnp_server: bool, /// UPNP server name that would be displayed on devices in your network. #[arg( @@ -425,7 +428,7 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()> } else { None }, - enable_upnp_port_forwarding: !opts.disable_upnp, + enable_upnp_port_forwarding: !opts.disable_upnp_port_forward, defer_writes_up_to: opts.defer_writes_up_to, default_storage_factory: Some({ fn wrap(s: S) -> impl StorageFactory { @@ -544,23 +547,26 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()> ); let mut upnp_server = { - match opts.upnp_server_hostname { - Some(hn) => { + match opts.enable_upnp_server { + true => { if opts.http_api_listen_addr.ip().is_loopback() { bail!("cannot enable UPNP server as HTTP API listen addr is localhost. Change --http-api-listen-addr to start with 0.0.0.0"); } let server = session .make_upnp_adapter( - opts.upnp_server_friendly_name - .unwrap_or_else(|| format!("rqbit at {hn}")), - hn, + opts.upnp_server_friendly_name.unwrap_or_else(|| { + format!( + "rqbit@{}", + gethostname::gethostname().to_string_lossy() + ) + }), opts.http_api_listen_addr.port(), ) .await .context("error starting UPNP server")?; Some(server) } - None => None, + false => None, } }; diff --git a/crates/upnp-serve/examples/upnp-stub-server.rs b/crates/upnp-serve/examples/upnp-stub-server.rs index 52ccd68..4fb2f77 100644 --- a/crates/upnp-serve/examples/upnp-stub-server.rs +++ b/crates/upnp-serve/examples/upnp-stub-server.rs @@ -34,8 +34,6 @@ async fn main() -> anyhow::Result<()> { info!("Creating UpnpServer"); let mut server = UpnpServer::new(UpnpServerOptions { friendly_name: "demo upnp server".to_owned(), - http_hostname: std::env::var("UPNP_HOSTNAME") - .context("you need to set UPNP_HOSTNAME to your IP visible from LAN")?, http_listen_port: HTTP_PORT, http_prefix: HTTP_PREFIX.to_owned(), browse_provider: Box::new(items), diff --git a/crates/upnp-serve/src/http_handlers.rs b/crates/upnp-serve/src/http_handlers.rs index 07ad381..59c8851 100644 --- a/crates/upnp-serve/src/http_handlers.rs +++ b/crates/upnp-serve/src/http_handlers.rs @@ -52,6 +52,15 @@ async fn generate_content_directory_control_response( }; match action.as_ref() { SOAP_ACTION_CONTENT_DIRECTORY_BROWSE => { + let http_hostname = headers + .get("host") + .and_then(|h| std::str::from_utf8(h.as_bytes()).ok()) + .and_then(|h| h.split(':').next()); + let http_hostname = match http_hostname { + Some(h) => h, + None => return StatusCode::BAD_REQUEST.into_response(), + }; + let body = match std::str::from_utf8(body) { Ok(body) => body, Err(_) => return (StatusCode::BAD_REQUEST, "cannot parse request").into_response(), @@ -71,7 +80,9 @@ async fn generate_content_directory_control_response( BrowseFlag::BrowseDirectChildren => ( [(CONTENT_TYPE, CONTENT_TYPE_XML_UTF8)], render_content_directory_browse( - state.provider.browse_direct_children(request.object_id), + state + .provider + .browse_direct_children(request.object_id, http_hostname), ), ) .into_response(), diff --git a/crates/upnp-serve/src/lib.rs b/crates/upnp-serve/src/lib.rs index eb781b6..00aaa1a 100644 --- a/crates/upnp-serve/src/lib.rs +++ b/crates/upnp-serve/src/lib.rs @@ -20,7 +20,6 @@ pub mod upnp_types; pub struct UpnpServerOptions { pub friendly_name: String, - pub http_hostname: String, pub http_listen_port: u16, pub http_prefix: String, pub browse_provider: Box, @@ -57,14 +56,16 @@ impl UpnpServer { let usn = create_usn(&opts).context("error generating USN")?; let description_http_location = { - let hostname = &opts.http_hostname; let port = opts.http_listen_port; let http_prefix = &opts.http_prefix; - format!("http://{hostname}:{port}{http_prefix}/description.xml") + let surl = format!("http://0.0.0.0:{port}{http_prefix}/description.xml"); + url::Url::parse(&surl) + .context(surl) + .context("error parsing url")? }; info!( - location = description_http_location, + location = %description_http_location, "starting UPnP/SSDP announcer for MediaServer" ); let ssdp_runner = crate::ssdp::SsdpRunner::new(ssdp::SsdpRunnerOptions { diff --git a/crates/upnp-serve/src/ssdp.rs b/crates/upnp-serve/src/ssdp.rs index cc4d37f..6027ae2 100644 --- a/crates/upnp-serve/src/ssdp.rs +++ b/crates/upnp-serve/src/ssdp.rs @@ -94,7 +94,7 @@ pub fn try_parse_ssdp<'a, 'h>( pub struct SsdpRunnerOptions { pub usn: String, - pub description_http_location: String, + pub description_http_location: url::Url, pub server_string: String, pub notify_interval: Duration, pub shutdown: CancellationToken, @@ -151,11 +151,20 @@ USN: {usn}::{kind}\r ) } - fn generate_ssdp_discover_response(&self, st: &str) -> String { - let location = &self.opts.description_http_location; + fn generate_ssdp_discover_response( + &self, + st: &str, + addr: SocketAddr, + ) -> anyhow::Result { + let local_ip = ::librqbit_upnp::get_local_ip_relative_to(addr.ip())?; + let location = { + let mut loc = self.opts.description_http_location.clone(); + loc.set_host(Some(&format!("{local_ip}")))?; + loc + }; let usn = &self.opts.usn; let server = &self.opts.server_string; - format!( + Ok(format!( "HTTP/1.1 200 OK\r Cache-Control: max-age=75\r Ext: \r @@ -164,7 +173,7 @@ Server: {server}\r St: {st}\r Usn: {usn}::{st}\r Content-Length: 0\r\n\r\n" - ) + )) } async fn try_send_notifies(&self, nts: &str) { @@ -178,18 +187,12 @@ Content-Length: 0\r\n\r\n" } }; - let location = match url::Url::parse(&self.opts.description_http_location) { - Ok(u) => u, - // TODO: rewrite this - Err(e) => { - warn!(error=?e, "error parsing description_http_location"); - return; - } - }; - for ni in interfaces { for niaddr in ni.addr { let ip = niaddr.ip(); + if ip.is_ipv6() || ip.is_loopback() { + continue; + } let addr = SocketAddr::new(ip, 0); let sock = match tokio::net::UdpSocket::bind(addr).await { Ok(sock) => sock, @@ -199,16 +202,16 @@ Content-Length: 0\r\n\r\n" } }; - let mut location = location.clone(); + let mut location = self.opts.description_http_location.clone(); location.set_host(Some(&format!("{ip}"))).unwrap(); for kind in [UPNP_KIND_ROOT_DEVICE, UPNP_KIND_MEDIASERVER] { let msg = self.generate_notify_message(kind, nts, &format!("{location}")); trace!(content=?msg, addr=?UPNP_BROADCAST_ADDR, "sending SSDP NOTIFY"); if let Err(e) = sock.send_to(msg.as_bytes(), UPNP_BROADCAST_ADDR).await { - warn!(error=?e, "error sending SSDP NOTIFY") + warn!(sock_addr=%addr, error=%e, "error sending SSDP NOTIFY") } else { - debug!(kind, nts, "sent SSDP NOTIFY") + debug!(kind, nts, %location, "sent SSDP NOTIFY") } } } @@ -244,7 +247,7 @@ Content-Length: 0\r\n\r\n" } if let Ok(st) = std::str::from_utf8(msg.st) { - let response = self.generate_ssdp_discover_response(st); + let response = self.generate_ssdp_discover_response(st, addr)?; trace!(content = response, ?addr, "sending SSDP discover response"); self.socket .send_to(response.as_bytes(), addr) diff --git a/crates/upnp-serve/src/upnp_types.rs b/crates/upnp-serve/src/upnp_types.rs index 28489f0..0cf2afe 100644 --- a/crates/upnp-serve/src/upnp_types.rs +++ b/crates/upnp-serve/src/upnp_types.rs @@ -70,11 +70,19 @@ pub mod content_directory { } pub trait ContentDirectoryBrowseProvider: Send + Sync { - fn browse_direct_children(&self, parent_id: usize) -> Vec; + fn browse_direct_children( + &self, + parent_id: usize, + http_hostname: &str, + ) -> Vec; } impl ContentDirectoryBrowseProvider for Vec { - fn browse_direct_children(&self, _parent_id: usize) -> Vec { + fn browse_direct_children( + &self, + _parent_id: usize, + _http_host: &str, + ) -> Vec { self.clone() } } diff --git a/crates/upnp/src/lib.rs b/crates/upnp/src/lib.rs index ac68c92..11aad56 100644 --- a/crates/upnp/src/lib.rs +++ b/crates/upnp/src/lib.rs @@ -6,7 +6,7 @@ use reqwest::Client; use serde::Deserialize; use std::{ collections::HashSet, - net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, time::Duration, }; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; @@ -23,7 +23,14 @@ const SSDP_SEARCH_REQUEST: &str = "M-SEARCH * HTTP/1.1\r\n\ ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n\ \r\n"; -fn get_local_ip_relative_to(local_dest: Ipv4Addr) -> anyhow::Result { +pub fn get_local_ip_relative_to(local_dest: IpAddr) -> anyhow::Result { + let local_dest = match local_dest { + IpAddr::V4(v4) => v4, + IpAddr::V6(v6) => { + anyhow::bail!("get_local_ip_relative_to not implemented for IPv6; addr={v6}") + } + }; + // Ipv4Addr.to_bits() is only there in nightly rust, so copying here for now. fn ip_bits(addr: Ipv4Addr) -> u32 { u32::from_be_bytes(addr.octets()) @@ -214,14 +221,9 @@ impl UpnpEndpoint { } fn my_local_ip(&self) -> anyhow::Result { - let dest_ipv4 = match self.discover_response.received_from { - SocketAddr::V4(v4) => *v4.ip(), - SocketAddr::V6(v6) => { - bail!("don't support IPv6, but remote ip is {}", v6.ip()) - } - }; - let local_ip = get_local_ip_relative_to(dest_ipv4) - .with_context(|| format!("can't determine local IP relative to {dest_ipv4}"))?; + let dest_ip = self.discover_response.received_from.ip(); + let local_ip = get_local_ip_relative_to(dest_ip) + .with_context(|| format!("can't determine local IP relative to {dest_ip}"))?; Ok(local_ip) } diff --git a/docker/compose-examples/server.yaml b/docker/compose-examples/server.yaml index 3b27b2a..ece9823 100644 --- a/docker/compose-examples/server.yaml +++ b/docker/compose-examples/server.yaml @@ -11,7 +11,7 @@ services: - 4240:4240 # TCP BitTorrent port environment: # Replace this with your LAN hostname or IP, resolvable from other devices in your LAN - RQBIT_UPNP_SERVER_HOSTNAME: 192.168.0.112 + RQBIT_UPNP_SERVER_ENABLE: "true" RQBIT_UPNP_SERVER_FRIENDLY_NAME: rqbit-docker # Replace this if you want to change the HTTP/Web UI port From d90c4dabe713e4e4263d530668a6e738ce4069d5 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 28 Aug 2024 13:45:05 +0100 Subject: [PATCH 4/5] Downgrade an SSDP message log level to debug --- crates/upnp-serve/src/ssdp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/upnp-serve/src/ssdp.rs b/crates/upnp-serve/src/ssdp.rs index 6027ae2..b9e4342 100644 --- a/crates/upnp-serve/src/ssdp.rs +++ b/crates/upnp-serve/src/ssdp.rs @@ -209,7 +209,7 @@ Content-Length: 0\r\n\r\n" let msg = self.generate_notify_message(kind, nts, &format!("{location}")); trace!(content=?msg, addr=?UPNP_BROADCAST_ADDR, "sending SSDP NOTIFY"); if let Err(e) = sock.send_to(msg.as_bytes(), UPNP_BROADCAST_ADDR).await { - warn!(sock_addr=%addr, error=%e, "error sending SSDP NOTIFY") + debug!(sock_addr=%addr, error=%e, kind, nts, "error sending SSDP NOTIFY") } else { debug!(kind, nts, %location, "sent SSDP NOTIFY") } From fef068d8098ed7becd8f63f2498d7a8ecad4db86 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 28 Aug 2024 13:56:12 +0100 Subject: [PATCH 5/5] Fix desktop to support this --- desktop/src-tauri/Cargo.lock | 19 +++++++++++++++++++ desktop/src-tauri/Cargo.toml | 1 + desktop/src-tauri/src/config.rs | 9 --------- desktop/src-tauri/src/main.rs | 16 +++++++--------- desktop/src/configuration.tsx | 1 - desktop/src/configure.tsx | 10 ---------- 6 files changed, 27 insertions(+), 29 deletions(-) diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 4855d4e..5afb096 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -68,6 +68,15 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "assert_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e2651f366b7ee3f97729fded1441539b49d5f39eeb05b842689e11e84501b2" +dependencies = [ + "const_panic", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -498,6 +507,12 @@ dependencies = [ "libc", ] +[[package]] +name = "const_panic" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7782af8f90fe69a4bb41e460abe1727d493403d8b2cc43201a3a3e906b24379f" + [[package]] name = "convert_case" version = "0.4.0" @@ -1916,6 +1931,7 @@ name = "librqbit-core" version = "4.0.1" dependencies = [ "anyhow", + "assert_cfg", "bytes", "data-encoding", "directories", @@ -1980,6 +1996,7 @@ dependencies = [ name = "librqbit-sha1-wrapper" version = "4.0.0" dependencies = [ + "assert_cfg", "crypto-hash", ] @@ -2034,6 +2051,7 @@ dependencies = [ "librqbit-sha1-wrapper", "librqbit-upnp", "mime_guess", + "network-interface", "parking_lot", "quick-xml 0.36.1", "reqwest", @@ -3051,6 +3069,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "directories", + "gethostname", "http 1.1.0", "librqbit", "parking_lot", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 4efd073..6378cd4 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -31,6 +31,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] } tracing = "0.1" serde_with = "3.4.0" parking_lot = "0.12.1" +gethostname = "0.5.0" [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/desktop/src-tauri/src/config.rs b/desktop/src-tauri/src/config.rs index 72308f4..bb6dc90 100644 --- a/desktop/src-tauri/src/config.rs +++ b/desktop/src-tauri/src/config.rs @@ -135,9 +135,6 @@ pub struct RqbitDesktopConfigUpnp { #[serde(default)] pub enable_server: bool, - #[serde(default)] - pub server_hostname: Option, - #[serde(default)] pub server_friendly_name: Option, } @@ -183,12 +180,6 @@ impl RqbitDesktopConfig { 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 3979182..eecce76 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -122,13 +122,6 @@ async fn api_from_config( 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 @@ -136,10 +129,15 @@ async fn api_from_config( .map(|f| f.trim()) .filter(|s| !s.is_empty()) .map(|s| s.to_owned()) - .unwrap_or_else(|| format!("rqbit@{hostname}")); + .unwrap_or_else(|| { + format!( + "rqbit-desktop@{}", + gethostname::gethostname().to_string_lossy() + ) + }); let mut upnp_adapter = session - .make_upnp_adapter(friendly_name, hostname, config.http_api.listen_addr.port()) + .make_upnp_adapter(friendly_name, config.http_api.listen_addr.port()) .await .context("error starting UPnP server")?; let router = upnp_adapter.take_router()?; diff --git a/desktop/src/configuration.tsx b/desktop/src/configuration.tsx index 7373d67..80f7677 100644 --- a/desktop/src/configuration.tsx +++ b/desktop/src/configuration.tsx @@ -36,7 +36,6 @@ interface RqbitDesktopConfigUpnp { disable: boolean; enable_server: boolean; - server_hostname: string; server_friendly_name: string; } diff --git a/desktop/src/configure.tsx b/desktop/src/configure.tsx index c2fb649..bfe68de 100644 --- a/desktop/src/configure.tsx +++ b/desktop/src/configure.tsx @@ -293,16 +293,6 @@ export const ConfigModal: React.FC<{ help="If enabled, rqbit will advertise the media to supported LAN devices, e.g. TVs." /> - -