Merge pull request #222 from ikatson/upnp-server-autoip
[Feature] UPNP MediaServer: send notifies on all interfaces, no need to specify hostname
This commit is contained in:
commit
5f7bf174bf
25 changed files with 210 additions and 99 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
|
@ -120,6 +120,15 @@ version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
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]]
|
[[package]]
|
||||||
name = "async-backtrace"
|
name = "async-backtrace"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
|
|
@ -531,6 +540,12 @@ version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const_panic"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7782af8f90fe69a4bb41e460abe1727d493403d8b2cc43201a3a3e906b24379f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
@ -1517,6 +1532,7 @@ name = "librqbit-core"
|
||||||
version = "4.0.1"
|
version = "4.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"assert_cfg",
|
||||||
"bytes",
|
"bytes",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"directories",
|
"directories",
|
||||||
|
|
@ -1583,6 +1599,7 @@ dependencies = [
|
||||||
name = "librqbit-sha1-wrapper"
|
name = "librqbit-sha1-wrapper"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"assert_cfg",
|
||||||
"crypto-hash",
|
"crypto-hash",
|
||||||
"ring",
|
"ring",
|
||||||
]
|
]
|
||||||
|
|
@ -1639,6 +1656,7 @@ dependencies = [
|
||||||
"librqbit-sha1-wrapper",
|
"librqbit-sha1-wrapper",
|
||||||
"librqbit-upnp",
|
"librqbit-upnp",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
"network-interface",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|
@ -2475,6 +2493,7 @@ dependencies = [
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"console-subscriber",
|
"console-subscriber",
|
||||||
"futures",
|
"futures",
|
||||||
|
"gethostname",
|
||||||
"libc",
|
"libc",
|
||||||
"librqbit",
|
"librqbit",
|
||||||
"librqbit-upnp-serve",
|
"librqbit-upnp-serve",
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -16,7 +16,7 @@ webui-build: webui-deps
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# NOTE: on LG TV using hostname is unstable for some reason, use IP address.
|
# 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_UPNP_SERVER_FRIENDLY_NAME ?= rqbit-dev
|
||||||
export RQBIT_HTTP_API_LISTEN_ADDR ?= 0.0.0.0:3030
|
export RQBIT_HTTP_API_LISTEN_ADDR ?= 0.0.0.0:3030
|
||||||
RQBIT_OUTPUT_FOLDER ?= /tmp/scratch
|
RQBIT_OUTPUT_FOLDER ?= /tmp/scratch
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,19 @@ http-api = ["axum", "tower-http"]
|
||||||
upnp-serve-adapter = ["upnp-serve"]
|
upnp-serve-adapter = ["upnp-serve"]
|
||||||
webui = []
|
webui = []
|
||||||
timed_existence = []
|
timed_existence = []
|
||||||
default-tls = ["reqwest/default-tls", "sha1w/sha1-crypto-hash"]
|
default-tls = [
|
||||||
rust-tls = ["reqwest/rustls-tls", "sha1w/sha1-ring"]
|
"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_middleware = ["lru"]
|
||||||
storage_examples = []
|
storage_examples = []
|
||||||
tracing-subscriber-utils = ["tracing-subscriber"]
|
tracing-subscriber-utils = ["tracing-subscriber"]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ use crate::{session::TorrentId, ManagedTorrent, Session};
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UpnpServerSessionAdapter {
|
pub struct UpnpServerSessionAdapter {
|
||||||
session: Arc<Session>,
|
session: Arc<Session>,
|
||||||
hostname: String,
|
|
||||||
port: u16,
|
port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +53,7 @@ impl TorrentFileTreeNode {
|
||||||
fn as_item_or_container(
|
fn as_item_or_container(
|
||||||
&self,
|
&self,
|
||||||
id: usize,
|
id: usize,
|
||||||
|
http_host: &str,
|
||||||
torrent: &ManagedTorrent,
|
torrent: &ManagedTorrent,
|
||||||
adapter: &UpnpServerSessionAdapter,
|
adapter: &UpnpServerSessionAdapter,
|
||||||
) -> ItemOrContainer {
|
) -> ItemOrContainer {
|
||||||
|
|
@ -79,7 +79,7 @@ impl TorrentFileTreeNode {
|
||||||
mime_type: mime_guess::from_path(filename).first(),
|
mime_type: mime_guess::from_path(filename).first(),
|
||||||
url: format!(
|
url: format!(
|
||||||
"http://{}:{}/torrents/{}/stream/{}/{}",
|
"http://{}:{}/torrents/{}/stream/{}/{}",
|
||||||
adapter.hostname,
|
http_host,
|
||||||
adapter.port,
|
adapter.port,
|
||||||
torrent.id(),
|
torrent.id(),
|
||||||
fid,
|
fid,
|
||||||
|
|
@ -197,7 +197,7 @@ impl TorrentFileTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpnpServerSessionAdapter {
|
impl UpnpServerSessionAdapter {
|
||||||
fn build_root(&self) -> Vec<ItemOrContainer> {
|
fn build_root(&self, hostname: &str) -> Vec<ItemOrContainer> {
|
||||||
let mut all = self
|
let mut all = self
|
||||||
.session
|
.session
|
||||||
.with_torrents(|torrents| torrents.map(|(_, t)| t.clone()).collect_vec());
|
.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 mime_type = mime_guess::from_path(rf).first();
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"http://{}:{}/torrents/{real_id}/stream/0/{title}",
|
"http://{}:{}/torrents/{real_id}/stream/0/{title}",
|
||||||
self.hostname, self.port
|
hostname, self.port
|
||||||
);
|
);
|
||||||
Some(ItemOrContainer::Item(Item {
|
Some(ItemOrContainer::Item(Item {
|
||||||
id: upnp_id,
|
id: upnp_id,
|
||||||
|
|
@ -249,9 +249,13 @@ impl UpnpServerSessionAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter {
|
impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter {
|
||||||
fn browse_direct_children(&self, parent_id: usize) -> Vec<ItemOrContainer> {
|
fn browse_direct_children(
|
||||||
|
&self,
|
||||||
|
parent_id: usize,
|
||||||
|
http_hostname: &str,
|
||||||
|
) -> Vec<ItemOrContainer> {
|
||||||
if parent_id == 0 {
|
if parent_id == 0 {
|
||||||
return self.build_root();
|
return self.build_root(http_hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (node_id, torrent_id) = match decode_id(parent_id) {
|
let (node_id, torrent_id) = match decode_id(parent_id) {
|
||||||
|
|
@ -292,14 +296,19 @@ impl ContentDirectoryBrowseProvider for UpnpServerSessionAdapter {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
if node.real_torrent_file_id.is_some() {
|
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 {
|
} else {
|
||||||
for (child_node_id, child_node) in node
|
for (child_node_id, child_node) in node
|
||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|id| Some((*id, tree.nodes.get(*id)?)))
|
.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(
|
pub async fn make_upnp_adapter(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
friendly_name: String,
|
friendly_name: String,
|
||||||
http_hostname: String,
|
|
||||||
http_listen_port: u16,
|
http_listen_port: u16,
|
||||||
) -> anyhow::Result<UpnpServer> {
|
) -> anyhow::Result<UpnpServer> {
|
||||||
UpnpServer::new(UpnpServerOptions {
|
UpnpServer::new(UpnpServerOptions {
|
||||||
friendly_name,
|
friendly_name,
|
||||||
http_hostname: http_hostname.clone(),
|
|
||||||
http_listen_port,
|
http_listen_port,
|
||||||
http_prefix: "/upnp".to_owned(),
|
http_prefix: "/upnp".to_owned(),
|
||||||
browse_provider: Box::new(UpnpServerSessionAdapter {
|
browse_provider: Box::new(UpnpServerSessionAdapter {
|
||||||
session: self.clone(),
|
session: self.clone(),
|
||||||
hostname: http_hostname,
|
|
||||||
port: http_listen_port,
|
port: http_listen_port,
|
||||||
}),
|
}),
|
||||||
cancellation_token: self.cancellation_token().child_token(),
|
cancellation_token: self.cancellation_token().child_token(),
|
||||||
|
|
@ -536,12 +542,11 @@ mod tests {
|
||||||
|
|
||||||
let adapter = UpnpServerSessionAdapter {
|
let adapter = UpnpServerSessionAdapter {
|
||||||
session,
|
session,
|
||||||
hostname: "127.0.0.1".into(),
|
|
||||||
port: 9005,
|
port: 9005,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
adapter.browse_direct_children(0),
|
adapter.browse_direct_children(0, "127.0.0.1"),
|
||||||
vec![
|
vec![
|
||||||
ItemOrContainer::Item(Item {
|
ItemOrContainer::Item(Item {
|
||||||
id: encode_id(0, 0),
|
id: encode_id(0, 0),
|
||||||
|
|
@ -560,7 +565,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
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 {
|
vec![ItemOrContainer::Container(Container {
|
||||||
id: encode_id(1, 1),
|
id: encode_id(1, 1),
|
||||||
parent_id: Some(encode_id(0, 1)),
|
parent_id: Some(encode_id(0, 1)),
|
||||||
|
|
@ -570,7 +575,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
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 {
|
vec![ItemOrContainer::Item(Item {
|
||||||
id: encode_id(2, 1),
|
id: encode_id(2, 1),
|
||||||
parent_id: Some(encode_id(1, 1)),
|
parent_id: Some(encode_id(1, 1)),
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ directories = "5"
|
||||||
tokio-util = "0.7.10"
|
tokio-util = "0.7.10"
|
||||||
data-encoding = "2.6.0"
|
data-encoding = "2.6.0"
|
||||||
bytes = "1.7.1"
|
bytes = "1.7.1"
|
||||||
|
assert_cfg = "0.1.0"
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,8 @@ pub mod speed_estimator;
|
||||||
pub mod torrent_metainfo;
|
pub mod torrent_metainfo;
|
||||||
|
|
||||||
pub use hash_id::Id20;
|
pub use hash_id::Id20;
|
||||||
|
|
||||||
|
assert_cfg::exactly_one! {
|
||||||
|
feature = "sha1-crypto-hash",
|
||||||
|
feature = "sha1-ring",
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 mut t = TorrentMetaV1::deserialize(&mut de)?;
|
||||||
let (digest, info_bytes) = match (de.torrent_info_digest, de.torrent_info_bytes) {
|
let (digest, info_bytes) = match (de.torrent_info_digest, de.torrent_info_bytes) {
|
||||||
(Some(digest), Some(info_bytes)) => (digest, 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);
|
t.info_hash = Id20::new(digest);
|
||||||
Ok(ParsedTorrent {
|
Ok(ParsedTorrent {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ upnp-serve = { path = "../upnp-serve", default-features = false, version = "0.1.
|
||||||
libc = "0.2.158"
|
libc = "0.2.158"
|
||||||
signal-hook = "0.3.17"
|
signal-hook = "0.3.17"
|
||||||
tokio-util = "0.7.11"
|
tokio-util = "0.7.11"
|
||||||
|
gethostname = "0.5.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
futures = { version = "0.3" }
|
futures = { version = "0.3" }
|
||||||
|
|
|
||||||
|
|
@ -139,13 +139,16 @@ struct Opts {
|
||||||
tcp_listen_max_port: u16,
|
tcp_listen_max_port: u16,
|
||||||
|
|
||||||
/// If set, will try to publish the chosen port through upnp on your router.
|
/// If set, will try to publish the chosen port through upnp on your router.
|
||||||
#[arg(long = "disable-upnp", env = "RQBIT_UPNP_DISABLE_PORT_FORWARD")]
|
#[arg(
|
||||||
disable_upnp: bool,
|
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.
|
/// 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.
|
/// Should be set to your hostname/IP as seen by your LAN neighbors.
|
||||||
#[arg(long = "upnp-server-hostname", env = "RQBIT_UPNP_SERVER_HOSTNAME")]
|
#[arg(long = "enable-upnp-server", env = "RQBIT_UPNP_SERVER_ENABLE")]
|
||||||
upnp_server_hostname: Option<String>,
|
enable_upnp_server: bool,
|
||||||
|
|
||||||
/// UPNP server name that would be displayed on devices in your network.
|
/// UPNP server name that would be displayed on devices in your network.
|
||||||
#[arg(
|
#[arg(
|
||||||
|
|
@ -425,7 +428,7 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
|
||||||
} else {
|
} else {
|
||||||
None
|
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,
|
defer_writes_up_to: opts.defer_writes_up_to,
|
||||||
default_storage_factory: Some({
|
default_storage_factory: Some({
|
||||||
fn wrap<S: StorageFactory + Clone>(s: S) -> impl StorageFactory {
|
fn wrap<S: StorageFactory + Clone>(s: S) -> impl StorageFactory {
|
||||||
|
|
@ -544,23 +547,26 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut upnp_server = {
|
let mut upnp_server = {
|
||||||
match opts.upnp_server_hostname {
|
match opts.enable_upnp_server {
|
||||||
Some(hn) => {
|
true => {
|
||||||
if opts.http_api_listen_addr.ip().is_loopback() {
|
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");
|
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
|
let server = session
|
||||||
.make_upnp_adapter(
|
.make_upnp_adapter(
|
||||||
opts.upnp_server_friendly_name
|
opts.upnp_server_friendly_name.unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| format!("rqbit at {hn}")),
|
format!(
|
||||||
hn,
|
"rqbit@{}",
|
||||||
|
gethostname::gethostname().to_string_lossy()
|
||||||
|
)
|
||||||
|
}),
|
||||||
opts.http_api_listen_addr.port(),
|
opts.http_api_listen_addr.port(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context("error starting UPNP server")?;
|
.context("error starting UPNP server")?;
|
||||||
Some(server)
|
Some(server)
|
||||||
}
|
}
|
||||||
None => None,
|
false => None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,6 @@ sha1-crypto-hash = ["crypto-hash"]
|
||||||
sha1-ring = ["ring"]
|
sha1-ring = ["ring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
assert_cfg = "0.1.0"
|
||||||
crypto-hash = { version = "0.3", optional = true }
|
crypto-hash = { version = "0.3", optional = true }
|
||||||
ring = { version = "0.17", optional = true }
|
ring = { version = "0.17", optional = true }
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ pub trait ISha1 {
|
||||||
fn finish(self) -> [u8; 20];
|
fn finish(self) -> [u8; 20];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_cfg::exactly_one! {
|
||||||
|
feature = "sha1-crypto-hash",
|
||||||
|
feature = "sha1-ring",
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sha1-crypto-hash")]
|
#[cfg(feature = "sha1-crypto-hash")]
|
||||||
mod crypto_hash_impl {
|
mod crypto_hash_impl {
|
||||||
use super::ISha1;
|
use super::ISha1;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ tokio-util = "0.7.11"
|
||||||
reqwest = { version = "0.12.7", default-features = false }
|
reqwest = { version = "0.12.7", default-features = false }
|
||||||
socket2 = "0.5.7"
|
socket2 = "0.5.7"
|
||||||
quick-xml = { version = "0.36.1", features = ["serialize"] }
|
quick-xml = { version = "0.36.1", features = ["serialize"] }
|
||||||
|
network-interface = "2.0.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,6 @@ async fn main() -> anyhow::Result<()> {
|
||||||
info!("Creating UpnpServer");
|
info!("Creating UpnpServer");
|
||||||
let mut server = UpnpServer::new(UpnpServerOptions {
|
let mut server = UpnpServer::new(UpnpServerOptions {
|
||||||
friendly_name: "demo upnp server".to_owned(),
|
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_listen_port: HTTP_PORT,
|
||||||
http_prefix: HTTP_PREFIX.to_owned(),
|
http_prefix: HTTP_PREFIX.to_owned(),
|
||||||
browse_provider: Box::new(items),
|
browse_provider: Box::new(items),
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,15 @@ async fn generate_content_directory_control_response(
|
||||||
};
|
};
|
||||||
match action.as_ref() {
|
match action.as_ref() {
|
||||||
SOAP_ACTION_CONTENT_DIRECTORY_BROWSE => {
|
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) {
|
let body = match std::str::from_utf8(body) {
|
||||||
Ok(body) => body,
|
Ok(body) => body,
|
||||||
Err(_) => return (StatusCode::BAD_REQUEST, "cannot parse request").into_response(),
|
Err(_) => return (StatusCode::BAD_REQUEST, "cannot parse request").into_response(),
|
||||||
|
|
@ -71,7 +80,9 @@ async fn generate_content_directory_control_response(
|
||||||
BrowseFlag::BrowseDirectChildren => (
|
BrowseFlag::BrowseDirectChildren => (
|
||||||
[(CONTENT_TYPE, CONTENT_TYPE_XML_UTF8)],
|
[(CONTENT_TYPE, CONTENT_TYPE_XML_UTF8)],
|
||||||
render_content_directory_browse(
|
render_content_directory_browse(
|
||||||
state.provider.browse_direct_children(request.object_id),
|
state
|
||||||
|
.provider
|
||||||
|
.browse_direct_children(request.object_id, http_hostname),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ pub mod upnp_types;
|
||||||
|
|
||||||
pub struct UpnpServerOptions {
|
pub struct UpnpServerOptions {
|
||||||
pub friendly_name: String,
|
pub friendly_name: String,
|
||||||
pub http_hostname: String,
|
|
||||||
pub http_listen_port: u16,
|
pub http_listen_port: u16,
|
||||||
pub http_prefix: String,
|
pub http_prefix: String,
|
||||||
pub browse_provider: Box<dyn ContentDirectoryBrowseProvider>,
|
pub browse_provider: Box<dyn ContentDirectoryBrowseProvider>,
|
||||||
|
|
@ -57,14 +56,16 @@ impl UpnpServer {
|
||||||
let usn = create_usn(&opts).context("error generating USN")?;
|
let usn = create_usn(&opts).context("error generating USN")?;
|
||||||
|
|
||||||
let description_http_location = {
|
let description_http_location = {
|
||||||
let hostname = &opts.http_hostname;
|
|
||||||
let port = opts.http_listen_port;
|
let port = opts.http_listen_port;
|
||||||
let http_prefix = &opts.http_prefix;
|
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!(
|
info!(
|
||||||
location = description_http_location,
|
location = %description_http_location,
|
||||||
"starting UPnP/SSDP announcer for MediaServer"
|
"starting UPnP/SSDP announcer for MediaServer"
|
||||||
);
|
);
|
||||||
let ssdp_runner = crate::ssdp::SsdpRunner::new(ssdp::SsdpRunnerOptions {
|
let ssdp_runner = crate::ssdp::SsdpRunner::new(ssdp::SsdpRunnerOptions {
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ pub fn try_parse_ssdp<'a, 'h>(
|
||||||
|
|
||||||
pub struct SsdpRunnerOptions {
|
pub struct SsdpRunnerOptions {
|
||||||
pub usn: String,
|
pub usn: String,
|
||||||
pub description_http_location: String,
|
pub description_http_location: url::Url,
|
||||||
pub server_string: String,
|
pub server_string: String,
|
||||||
pub notify_interval: Duration,
|
pub notify_interval: Duration,
|
||||||
pub shutdown: CancellationToken,
|
pub shutdown: CancellationToken,
|
||||||
|
|
@ -133,16 +133,15 @@ impl SsdpRunner {
|
||||||
Ok(Self { opts, socket })
|
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 usn: &str = &self.opts.usn;
|
||||||
let description_http_location = &self.opts.description_http_location;
|
|
||||||
let server: &str = &self.opts.server_string;
|
let server: &str = &self.opts.server_string;
|
||||||
let bcast_addr = UPNP_BROADCAST_ADDR;
|
let bcast_addr = UPNP_BROADCAST_ADDR;
|
||||||
format!(
|
format!(
|
||||||
"NOTIFY * HTTP/1.1\r
|
"NOTIFY * HTTP/1.1\r
|
||||||
Host: {bcast_addr}\r
|
Host: {bcast_addr}\r
|
||||||
Cache-Control: max-age=75\r
|
Cache-Control: max-age=75\r
|
||||||
Location: {description_http_location}\r
|
Location: {location}\r
|
||||||
NT: {kind}\r
|
NT: {kind}\r
|
||||||
NTS: {nts}\r
|
NTS: {nts}\r
|
||||||
Server: {server}\r
|
Server: {server}\r
|
||||||
|
|
@ -152,11 +151,20 @@ USN: {usn}::{kind}\r
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_ssdp_discover_response(&self, st: &str) -> String {
|
fn generate_ssdp_discover_response(
|
||||||
let location = &self.opts.description_http_location;
|
&self,
|
||||||
|
st: &str,
|
||||||
|
addr: SocketAddr,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
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 usn = &self.opts.usn;
|
||||||
let server = &self.opts.server_string;
|
let server = &self.opts.server_string;
|
||||||
format!(
|
Ok(format!(
|
||||||
"HTTP/1.1 200 OK\r
|
"HTTP/1.1 200 OK\r
|
||||||
Cache-Control: max-age=75\r
|
Cache-Control: max-age=75\r
|
||||||
Ext: \r
|
Ext: \r
|
||||||
|
|
@ -165,21 +173,47 @@ Server: {server}\r
|
||||||
St: {st}\r
|
St: {st}\r
|
||||||
Usn: {usn}::{st}\r
|
Usn: {usn}::{st}\r
|
||||||
Content-Length: 0\r\n\r\n"
|
Content-Length: 0\r\n\r\n"
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_send_notifies(&self, nts: &str) {
|
async fn try_send_notifies(&self, nts: &str) {
|
||||||
for kind in [UPNP_KIND_ROOT_DEVICE, UPNP_KIND_MEDIASERVER] {
|
use network_interface::NetworkInterfaceConfig;
|
||||||
let msg = self.generate_notify_message(kind, nts);
|
let interfaces = network_interface::NetworkInterface::show();
|
||||||
trace!(content=?msg, addr=?UPNP_BROADCAST_ADDR, "sending SSDP NOTIFY");
|
let interfaces = match interfaces {
|
||||||
if let Err(e) = self
|
Ok(interfaces) => interfaces,
|
||||||
.socket
|
Err(e) => {
|
||||||
.send_to(msg.as_bytes(), UPNP_BROADCAST_ADDR)
|
warn!(error=?e, "error determining network interfaces");
|
||||||
.await
|
return;
|
||||||
{
|
}
|
||||||
warn!(error=?e, "error sending SSDP NOTIFY")
|
};
|
||||||
} else {
|
|
||||||
debug!(kind, nts, "sent SSDP NOTIFY")
|
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,
|
||||||
|
Err(e) => {
|
||||||
|
debug!(%addr, error=?e, "error binding UDP to send NOTIFY");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
debug!(sock_addr=%addr, error=%e, kind, nts, "error sending SSDP NOTIFY")
|
||||||
|
} else {
|
||||||
|
debug!(kind, nts, %location, "sent SSDP NOTIFY")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +247,7 @@ Content-Length: 0\r\n\r\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(st) = std::str::from_utf8(msg.st) {
|
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");
|
trace!(content = response, ?addr, "sending SSDP discover response");
|
||||||
self.socket
|
self.socket
|
||||||
.send_to(response.as_bytes(), addr)
|
.send_to(response.as_bytes(), addr)
|
||||||
|
|
|
||||||
|
|
@ -70,11 +70,19 @@ pub mod content_directory {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ContentDirectoryBrowseProvider: Send + Sync {
|
pub trait ContentDirectoryBrowseProvider: Send + Sync {
|
||||||
fn browse_direct_children(&self, parent_id: usize) -> Vec<ItemOrContainer>;
|
fn browse_direct_children(
|
||||||
|
&self,
|
||||||
|
parent_id: usize,
|
||||||
|
http_hostname: &str,
|
||||||
|
) -> Vec<ItemOrContainer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentDirectoryBrowseProvider for Vec<ItemOrContainer> {
|
impl ContentDirectoryBrowseProvider for Vec<ItemOrContainer> {
|
||||||
fn browse_direct_children(&self, _parent_id: usize) -> Vec<ItemOrContainer> {
|
fn browse_direct_children(
|
||||||
|
&self,
|
||||||
|
_parent_id: usize,
|
||||||
|
_http_host: &str,
|
||||||
|
) -> Vec<ItemOrContainer> {
|
||||||
self.clone()
|
self.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
|
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\
|
ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n\
|
||||||
\r\n";
|
\r\n";
|
||||||
|
|
||||||
fn get_local_ip_relative_to(local_dest: Ipv4Addr) -> anyhow::Result<Ipv4Addr> {
|
pub fn get_local_ip_relative_to(local_dest: IpAddr) -> anyhow::Result<Ipv4Addr> {
|
||||||
|
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.
|
// Ipv4Addr.to_bits() is only there in nightly rust, so copying here for now.
|
||||||
fn ip_bits(addr: Ipv4Addr) -> u32 {
|
fn ip_bits(addr: Ipv4Addr) -> u32 {
|
||||||
u32::from_be_bytes(addr.octets())
|
u32::from_be_bytes(addr.octets())
|
||||||
|
|
@ -214,14 +221,9 @@ impl UpnpEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn my_local_ip(&self) -> anyhow::Result<Ipv4Addr> {
|
fn my_local_ip(&self) -> anyhow::Result<Ipv4Addr> {
|
||||||
let dest_ipv4 = match self.discover_response.received_from {
|
let dest_ip = self.discover_response.received_from.ip();
|
||||||
SocketAddr::V4(v4) => *v4.ip(),
|
let local_ip = get_local_ip_relative_to(dest_ip)
|
||||||
SocketAddr::V6(v6) => {
|
.with_context(|| format!("can't determine local IP relative to {dest_ip}"))?;
|
||||||
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}"))?;
|
|
||||||
Ok(local_ip)
|
Ok(local_ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
19
desktop/src-tauri/Cargo.lock
generated
19
desktop/src-tauri/Cargo.lock
generated
|
|
@ -68,6 +68,15 @@ version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
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]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
|
|
@ -498,6 +507,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const_panic"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7782af8f90fe69a4bb41e460abe1727d493403d8b2cc43201a3a3e906b24379f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -1916,6 +1931,7 @@ name = "librqbit-core"
|
||||||
version = "4.0.1"
|
version = "4.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"assert_cfg",
|
||||||
"bytes",
|
"bytes",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"directories",
|
"directories",
|
||||||
|
|
@ -1980,6 +1996,7 @@ dependencies = [
|
||||||
name = "librqbit-sha1-wrapper"
|
name = "librqbit-sha1-wrapper"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"assert_cfg",
|
||||||
"crypto-hash",
|
"crypto-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2034,6 +2051,7 @@ dependencies = [
|
||||||
"librqbit-sha1-wrapper",
|
"librqbit-sha1-wrapper",
|
||||||
"librqbit-upnp",
|
"librqbit-upnp",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
"network-interface",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"quick-xml 0.36.1",
|
"quick-xml 0.36.1",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|
@ -3051,6 +3069,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"directories",
|
"directories",
|
||||||
|
"gethostname",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"librqbit",
|
"librqbit",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
serde_with = "3.4.0"
|
serde_with = "3.4.0"
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
|
gethostname = "0.5.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
|
|
|
||||||
|
|
@ -135,9 +135,6 @@ pub struct RqbitDesktopConfigUpnp {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub enable_server: bool,
|
pub enable_server: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub server_hostname: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub server_friendly_name: Option<String>,
|
pub server_friendly_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -183,12 +180,6 @@ impl RqbitDesktopConfig {
|
||||||
if self.http_api.listen_addr.ip().is_loopback() {
|
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.")
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,13 +122,6 @@ async fn api_from_config(
|
||||||
let api = api.clone();
|
let api = api.clone();
|
||||||
let read_only = config.http_api.read_only;
|
let read_only = config.http_api.read_only;
|
||||||
let upnp_router = if config.upnp.enable_server {
|
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
|
let friendly_name = config
|
||||||
.upnp
|
.upnp
|
||||||
.server_friendly_name
|
.server_friendly_name
|
||||||
|
|
@ -136,10 +129,15 @@ async fn api_from_config(
|
||||||
.map(|f| f.trim())
|
.map(|f| f.trim())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.map(|s| s.to_owned())
|
.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
|
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
|
.await
|
||||||
.context("error starting UPnP server")?;
|
.context("error starting UPnP server")?;
|
||||||
let router = upnp_adapter.take_router()?;
|
let router = upnp_adapter.take_router()?;
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ interface RqbitDesktopConfigUpnp {
|
||||||
disable: boolean;
|
disable: boolean;
|
||||||
|
|
||||||
enable_server: boolean;
|
enable_server: boolean;
|
||||||
server_hostname: string;
|
|
||||||
server_friendly_name: string;
|
server_friendly_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,16 +293,6 @@ export const ConfigModal: React.FC<{
|
||||||
help="If enabled, rqbit will advertise the media to supported LAN devices, e.g. TVs."
|
help="If enabled, rqbit will advertise the media to supported LAN devices, e.g. TVs."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormInput
|
|
||||||
inputType="text"
|
|
||||||
label="[Required] Hostname"
|
|
||||||
name="upnp.server_hostname"
|
|
||||||
value={config.upnp.server_hostname}
|
|
||||||
disabled={!config.upnp.enable_server}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
help="Set this to your LAN IP or hostname resolvable from LAN."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormInput
|
<FormInput
|
||||||
inputType="text"
|
inputType="text"
|
||||||
label="Friendly name"
|
label="Friendly name"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ services:
|
||||||
- 4240:4240 # TCP BitTorrent port
|
- 4240:4240 # TCP BitTorrent port
|
||||||
environment:
|
environment:
|
||||||
# Replace this with your LAN hostname or IP, resolvable from other devices in your LAN
|
# 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
|
RQBIT_UPNP_SERVER_FRIENDLY_NAME: rqbit-docker
|
||||||
|
|
||||||
# Replace this if you want to change the HTTP/Web UI port
|
# Replace this if you want to change the HTTP/Web UI port
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue