mirror of
https://github.com/EasyTier/EasyTier.git
synced 2025-11-01 20:42:46 +08:00
Compare commits
3 Commits
manage-con
...
upnp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f272a1775e | ||
|
|
a102a8bfc7 | ||
|
|
c9e8c35e77 |
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -2226,6 +2226,7 @@ dependencies = [
|
|||||||
"windows-service",
|
"windows-service",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
"winreg 0.52.0",
|
"winreg 0.52.0",
|
||||||
|
"xmltree",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
"zip",
|
"zip",
|
||||||
"zstd",
|
"zstd",
|
||||||
@@ -11141,6 +11142,15 @@ version = "0.8.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
|
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xmltree"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6"
|
||||||
|
dependencies = [
|
||||||
|
"xml-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yansi"
|
name = "yansi"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ version-compare = "0.2.0"
|
|||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
|
|
||||||
|
# upnp igd
|
||||||
|
xmltree = "0.11.0"
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
||||||
machine-uid = "0.5.3"
|
machine-uid = "0.5.3"
|
||||||
|
|
||||||
|
|||||||
@@ -968,8 +968,17 @@ impl NetworkOptions {
|
|||||||
old_udp_whitelist.extend(self.udp_whitelist.clone());
|
old_udp_whitelist.extend(self.udp_whitelist.clone());
|
||||||
cfg.set_udp_whitelist(old_udp_whitelist);
|
cfg.set_udp_whitelist(old_udp_whitelist);
|
||||||
|
|
||||||
cfg.set_stun_servers(self.stun_servers.clone());
|
if let Some(stun_servers) = &self.stun_servers {
|
||||||
cfg.set_stun_servers_v6(self.stun_servers_v6.clone());
|
let mut old_stun_servers = cfg.get_stun_servers().unwrap_or_default();
|
||||||
|
old_stun_servers.extend(stun_servers.iter().cloned());
|
||||||
|
cfg.set_stun_servers(Some(old_stun_servers));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(stun_servers_v6) = &self.stun_servers_v6 {
|
||||||
|
let mut old_stun_servers_v6 = cfg.get_stun_servers_v6().unwrap_or_default();
|
||||||
|
old_stun_servers_v6.extend(stun_servers_v6.iter().cloned());
|
||||||
|
cfg.set_stun_servers_v6(Some(old_stun_servers_v6));
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ pub mod listeners;
|
|||||||
pub mod virtual_nic;
|
pub mod virtual_nic;
|
||||||
|
|
||||||
pub mod logger_rpc_service;
|
pub mod logger_rpc_service;
|
||||||
|
|
||||||
|
pub mod upnp_igd;
|
||||||
|
|||||||
157
easytier/src/instance/upnp_igd/common/messages.rs
Normal file
157
easytier/src/instance/upnp_igd/common/messages.rs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
use super::super::PortMappingProtocol;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
// Content of the request.
|
||||||
|
pub const SEARCH_REQUEST: &str = "M-SEARCH * HTTP/1.1\r
|
||||||
|
Host:239.255.255.250:1900\r
|
||||||
|
ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r
|
||||||
|
Man:\"ssdp:discover\"\r
|
||||||
|
MX:3\r\n\r\n";
|
||||||
|
|
||||||
|
pub const GET_EXTERNAL_IP_HEADER: &str =
|
||||||
|
r#""urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress""#;
|
||||||
|
|
||||||
|
pub const ADD_ANY_PORT_MAPPING_HEADER: &str =
|
||||||
|
r#""urn:schemas-upnp-org:service:WANIPConnection:1#AddAnyPortMapping""#;
|
||||||
|
|
||||||
|
pub const ADD_PORT_MAPPING_HEADER: &str =
|
||||||
|
r#""urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping""#;
|
||||||
|
|
||||||
|
pub const DELETE_PORT_MAPPING_HEADER: &str =
|
||||||
|
r#""urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping""#;
|
||||||
|
|
||||||
|
pub const GET_GENERIC_PORT_MAPPING_ENTRY: &str =
|
||||||
|
r#""urn:schemas-upnp-org:service:WANIPConnection:1#GetGenericPortMappingEntry""#;
|
||||||
|
|
||||||
|
const MESSAGE_HEAD: &str = r#"<?xml version="1.0"?>
|
||||||
|
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
|
<s:Body>"#;
|
||||||
|
|
||||||
|
const MESSAGE_TAIL: &str = r#"</s:Body>
|
||||||
|
</s:Envelope>"#;
|
||||||
|
|
||||||
|
fn format_message(body: String) -> String {
|
||||||
|
format!("{MESSAGE_HEAD}{body}{MESSAGE_TAIL}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_get_external_ip_message() -> String {
|
||||||
|
r#"<?xml version="1.0"?>
|
||||||
|
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
|
<s:Body>
|
||||||
|
<m:GetExternalIPAddress xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">
|
||||||
|
</m:GetExternalIPAddress>
|
||||||
|
</s:Body>
|
||||||
|
</s:Envelope>"#
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_add_any_port_mapping_message(
|
||||||
|
schema: &[String],
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
external_port: u16,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
lease_duration: u32,
|
||||||
|
description: &str,
|
||||||
|
) -> String {
|
||||||
|
let args = schema
|
||||||
|
.iter()
|
||||||
|
.filter_map(|argument| {
|
||||||
|
let value = match argument.as_str() {
|
||||||
|
"NewEnabled" => 1.to_string(),
|
||||||
|
"NewExternalPort" => external_port.to_string(),
|
||||||
|
"NewInternalClient" => local_addr.ip().to_string(),
|
||||||
|
"NewInternalPort" => local_addr.port().to_string(),
|
||||||
|
"NewLeaseDuration" => lease_duration.to_string(),
|
||||||
|
"NewPortMappingDescription" => description.to_string(),
|
||||||
|
"NewProtocol" => protocol.to_string(),
|
||||||
|
"NewRemoteHost" => "".to_string(),
|
||||||
|
unknown => {
|
||||||
|
tracing::warn!("Unknown argument: {}", unknown);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(format!("<{argument}>{value}</{argument}>"))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
format_message(format!(
|
||||||
|
r#"<u:AddAnyPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
||||||
|
{args}
|
||||||
|
</u:AddAnyPortMapping>"#,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_add_port_mapping_message(
|
||||||
|
schema: &[String],
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
external_port: u16,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
lease_duration: u32,
|
||||||
|
description: &str,
|
||||||
|
) -> String {
|
||||||
|
let args = schema
|
||||||
|
.iter()
|
||||||
|
.filter_map(|argument| {
|
||||||
|
let value = match argument.as_str() {
|
||||||
|
"NewEnabled" => 1.to_string(),
|
||||||
|
"NewExternalPort" => external_port.to_string(),
|
||||||
|
"NewInternalClient" => local_addr.ip().to_string(),
|
||||||
|
"NewInternalPort" => local_addr.port().to_string(),
|
||||||
|
"NewLeaseDuration" => lease_duration.to_string(),
|
||||||
|
"NewPortMappingDescription" => description.to_string(),
|
||||||
|
"NewProtocol" => protocol.to_string(),
|
||||||
|
"NewRemoteHost" => "".to_string(),
|
||||||
|
unknown => {
|
||||||
|
tracing::warn!("Unknown argument: {}", unknown);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(format!("<{argument}>{value}</{argument}>",))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
format_message(format!(
|
||||||
|
r#"<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
||||||
|
{args}
|
||||||
|
</u:AddPortMapping>"#
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_delete_port_message(
|
||||||
|
schema: &[String],
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
external_port: u16,
|
||||||
|
) -> String {
|
||||||
|
let args = schema
|
||||||
|
.iter()
|
||||||
|
.filter_map(|argument| {
|
||||||
|
let value = match argument.as_str() {
|
||||||
|
"NewExternalPort" => external_port.to_string(),
|
||||||
|
"NewProtocol" => protocol.to_string(),
|
||||||
|
"NewRemoteHost" => "".to_string(),
|
||||||
|
unknown => {
|
||||||
|
tracing::warn!("Unknown argument: {}", unknown);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(format!("<{argument}>{value}</{argument}>",))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
format_message(format!(
|
||||||
|
r#"<u:DeletePortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
||||||
|
{args}
|
||||||
|
</u:DeletePortMapping>"#
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn formate_get_generic_port_mapping_entry_message(port_mapping_index: u32) -> String {
|
||||||
|
format_message(format!(
|
||||||
|
r#"<u:GetGenericPortMappingEntry xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
||||||
|
<NewPortMappingIndex>{port_mapping_index}</NewPortMappingIndex>
|
||||||
|
</u:GetGenericPortMappingEntry>"#
|
||||||
|
))
|
||||||
|
}
|
||||||
11
easytier/src/instance/upnp_igd/common/mod.rs
Normal file
11
easytier/src/instance/upnp_igd/common/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
pub mod messages;
|
||||||
|
pub mod options;
|
||||||
|
pub mod parsing;
|
||||||
|
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
pub use self::options::SearchOptions;
|
||||||
|
|
||||||
|
pub fn random_port() -> u16 {
|
||||||
|
rand::thread_rng().gen_range(32_768_u16..65_535_u16)
|
||||||
|
}
|
||||||
45
easytier/src/instance/upnp_igd/common/options.rs
Normal file
45
easytier/src/instance/upnp_igd/common/options.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Default timeout for a gateway search.
|
||||||
|
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
/// Timeout for each broadcast response during a gateway search.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// Gateway search configuration
|
||||||
|
///
|
||||||
|
/// SearchOptions::default() should suffice for most situations.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// To customize only a few options you can use `Default::default()` or `SearchOptions::default()` and the
|
||||||
|
/// [struct update syntax](https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax).
|
||||||
|
/// ```
|
||||||
|
/// # use std::time::Duration;
|
||||||
|
/// # use igd_next::SearchOptions;
|
||||||
|
/// let opts = SearchOptions {
|
||||||
|
/// timeout: Some(Duration::from_secs(60)),
|
||||||
|
/// ..Default::default()
|
||||||
|
/// };
|
||||||
|
/// ```
|
||||||
|
pub struct SearchOptions {
|
||||||
|
/// Bind address for UDP socket (defaults to all `0.0.0.0`)
|
||||||
|
pub bind_addr: SocketAddr,
|
||||||
|
/// Broadcast address for discovery packets (defaults to `239.255.255.250:1900`)
|
||||||
|
pub broadcast_address: SocketAddr,
|
||||||
|
/// Timeout for a search iteration (defaults to 10s)
|
||||||
|
pub timeout: Option<Duration>,
|
||||||
|
/// Timeout for a single search response (defaults to 5s)
|
||||||
|
pub single_search_timeout: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SearchOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
bind_addr: (IpAddr::from([0, 0, 0, 0]), 0).into(),
|
||||||
|
broadcast_address: "239.255.255.250:1900".parse().unwrap(),
|
||||||
|
timeout: Some(DEFAULT_TIMEOUT),
|
||||||
|
single_search_timeout: Some(RESPONSE_TIMEOUT),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
756
easytier/src/instance/upnp_igd/common/parsing.rs
Normal file
756
easytier/src/instance/upnp_igd/common/parsing.rs
Normal file
@@ -0,0 +1,756 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io;
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use url::Url;
|
||||||
|
use xmltree::{self, Element};
|
||||||
|
|
||||||
|
use super::super::PortMappingProtocol;
|
||||||
|
|
||||||
|
// Parse the result.
|
||||||
|
pub fn parse_search_result(text: &str) -> anyhow::Result<(SocketAddr, String)> {
|
||||||
|
for line in text.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.to_ascii_lowercase().starts_with("location:") {
|
||||||
|
if let Some(colon) = line.find(':') {
|
||||||
|
let url_text = &line[colon + 1..].trim();
|
||||||
|
let url = Url::parse(url_text).map_err(|_| anyhow::anyhow!("Invalid response"))?;
|
||||||
|
let addr: IpAddr = url
|
||||||
|
.host_str()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid response"))
|
||||||
|
.and_then(|s| s.parse().map_err(|_| anyhow::anyhow!("Invalid response")))?;
|
||||||
|
let port: u16 = url
|
||||||
|
.port_or_known_default()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid response"))?;
|
||||||
|
|
||||||
|
return Ok((SocketAddr::new(addr, port), url.path().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::anyhow!("Invalid response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_control_urls<R>(resp: R) -> anyhow::Result<(String, String)>
|
||||||
|
where
|
||||||
|
R: io::Read,
|
||||||
|
{
|
||||||
|
let root = Element::parse(resp)?;
|
||||||
|
|
||||||
|
let mut urls = root.children.iter().filter_map(|child| {
|
||||||
|
let child = child.as_element()?;
|
||||||
|
if child.name == "device" {
|
||||||
|
Some(parse_device(child)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
urls.next()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_device(device: &Element) -> Option<(String, String)> {
|
||||||
|
let services = device.get_child("serviceList").and_then(|service_list| {
|
||||||
|
service_list
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.filter_map(|child| {
|
||||||
|
let child = child.as_element()?;
|
||||||
|
if child.name == "service" {
|
||||||
|
parse_service(child)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
});
|
||||||
|
let devices = device.get_child("deviceList").and_then(parse_device_list);
|
||||||
|
services.or(devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_device_list(device_list: &Element) -> Option<(String, String)> {
|
||||||
|
device_list
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.filter_map(|child| {
|
||||||
|
let child = child.as_element()?;
|
||||||
|
if child.name == "device" {
|
||||||
|
parse_device(child)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_service(service: &Element) -> Option<(String, String)> {
|
||||||
|
let service_type = service.get_child("serviceType")?;
|
||||||
|
let service_type = service_type
|
||||||
|
.get_text()
|
||||||
|
.map(|s| s.into_owned())
|
||||||
|
.unwrap_or_else(|| "".into());
|
||||||
|
if [
|
||||||
|
"urn:schemas-upnp-org:service:WANPPPConnection:1",
|
||||||
|
"urn:schemas-upnp-org:service:WANIPConnection:1",
|
||||||
|
"urn:schemas-upnp-org:service:WANIPConnection:2",
|
||||||
|
]
|
||||||
|
.contains(&service_type.as_str())
|
||||||
|
{
|
||||||
|
let scpd_url = service.get_child("SCPDURL");
|
||||||
|
let control_url = service.get_child("controlURL");
|
||||||
|
if let (Some(scpd_url), Some(control_url)) = (scpd_url, control_url) {
|
||||||
|
Some((
|
||||||
|
scpd_url
|
||||||
|
.get_text()
|
||||||
|
.map(|s| s.into_owned())
|
||||||
|
.unwrap_or_else(|| "".into()),
|
||||||
|
control_url
|
||||||
|
.get_text()
|
||||||
|
.map(|s| s.into_owned())
|
||||||
|
.unwrap_or_else(|| "".into()),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_schemas<R>(resp: R) -> anyhow::Result<HashMap<String, Vec<String>>>
|
||||||
|
where
|
||||||
|
R: io::Read,
|
||||||
|
{
|
||||||
|
let root = Element::parse(resp)?;
|
||||||
|
|
||||||
|
let mut schema = root.children.iter().filter_map(|child| {
|
||||||
|
let child = child.as_element()?;
|
||||||
|
if child.name == "actionList" {
|
||||||
|
parse_action_list(child)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
schema
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_action_list(action_list: &Element) -> Option<HashMap<String, Vec<String>>> {
|
||||||
|
Some(
|
||||||
|
action_list
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.filter_map(|child| {
|
||||||
|
let child = child.as_element()?;
|
||||||
|
if child.name == "action" {
|
||||||
|
parse_action(child)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_action(action: &Element) -> Option<(String, Vec<String>)> {
|
||||||
|
Some((
|
||||||
|
action.get_child("name")?.get_text()?.into_owned(),
|
||||||
|
parse_argument_list(action.get_child("argumentList")?)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_argument_list(argument_list: &Element) -> Option<Vec<String>> {
|
||||||
|
Some(
|
||||||
|
argument_list
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.filter_map(|child| {
|
||||||
|
let child = child.as_element()?;
|
||||||
|
if child.name == "argument" {
|
||||||
|
parse_argument(child)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_argument(action: &Element) -> Option<String> {
|
||||||
|
if action
|
||||||
|
.get_child("direction")?
|
||||||
|
.get_text()?
|
||||||
|
.into_owned()
|
||||||
|
.as_str()
|
||||||
|
== "in"
|
||||||
|
{
|
||||||
|
Some(action.get_child("name")?.get_text()?.into_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RequestReponse {
|
||||||
|
text: String,
|
||||||
|
xml: xmltree::Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RequestResult = anyhow::Result<RequestReponse>;
|
||||||
|
|
||||||
|
pub fn parse_response(text: String, ok: &str) -> RequestResult {
|
||||||
|
let mut xml = match xmltree::Element::parse(text.as_bytes()) {
|
||||||
|
Ok(xml) => xml,
|
||||||
|
Err(..) => return Err(anyhow::anyhow!("Invalid response: {}", text)),
|
||||||
|
};
|
||||||
|
let body = match xml.get_mut_child("Body") {
|
||||||
|
Some(body) => body,
|
||||||
|
None => return Err(anyhow::anyhow!("Invalid response: {}", text)),
|
||||||
|
};
|
||||||
|
if let Some(ok) = body.take_child(ok) {
|
||||||
|
return Ok(RequestReponse { text, xml: ok });
|
||||||
|
}
|
||||||
|
let upnp_error = match body
|
||||||
|
.get_child("Fault")
|
||||||
|
.and_then(|e| e.get_child("detail"))
|
||||||
|
.and_then(|e| e.get_child("UPnPError"))
|
||||||
|
{
|
||||||
|
Some(upnp_error) => upnp_error,
|
||||||
|
None => return Err(anyhow::anyhow!("Invalid response: {}", text)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match (
|
||||||
|
upnp_error.get_child("errorCode"),
|
||||||
|
upnp_error.get_child("errorDescription"),
|
||||||
|
) {
|
||||||
|
(Some(e), Some(d)) => match (e.get_text().as_ref(), d.get_text().as_ref()) {
|
||||||
|
(Some(et), Some(dt)) => match et.parse::<u16>() {
|
||||||
|
Ok(en) => Err(anyhow::anyhow!("Error code {}: {}", en, dt)),
|
||||||
|
Err(..) => Err(anyhow::anyhow!("Invalid response: {}", text)),
|
||||||
|
},
|
||||||
|
_ => Err(anyhow::anyhow!("Invalid response: {}", text)),
|
||||||
|
},
|
||||||
|
_ => Err(anyhow::anyhow!("Invalid response: {}", text)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_get_external_ip_response(result: RequestResult) -> anyhow::Result<Option<IpAddr>> {
|
||||||
|
if let Ok(resp) = &result {
|
||||||
|
let child = resp.xml.get_child("NewExternalIPAddress");
|
||||||
|
if let Some(child) = child {
|
||||||
|
let text = child.get_text();
|
||||||
|
println!("text {:?}", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child_empty = resp.xml.get_child("NewExternalIPAddressFuck");
|
||||||
|
println!("child_empty {:?}", child_empty);
|
||||||
|
}
|
||||||
|
match result {
|
||||||
|
Ok(resp) => {
|
||||||
|
let child = resp.xml.get_child("NewExternalIPAddress");
|
||||||
|
if let Some(child) = child {
|
||||||
|
match child.get_text() {
|
||||||
|
Some(text) => {
|
||||||
|
Ok(Some(text.parse::<IpAddr>().with_context(|| {
|
||||||
|
format!("Invalid IP address: {}", text)
|
||||||
|
})?))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Invalid response: {}", resp.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let error_msg = e.to_string();
|
||||||
|
if error_msg.contains("Error code 606") {
|
||||||
|
Err(anyhow::anyhow!("Action not authorized"))
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_add_any_port_mapping_response(result: RequestResult) -> anyhow::Result<u16> {
|
||||||
|
match result {
|
||||||
|
Ok(resp) => {
|
||||||
|
match resp
|
||||||
|
.xml
|
||||||
|
.get_child("NewReservedPort")
|
||||||
|
.and_then(|e| e.get_text())
|
||||||
|
.and_then(|t| t.parse::<u16>().ok())
|
||||||
|
{
|
||||||
|
Some(port) => Ok(port),
|
||||||
|
None => Err(anyhow::anyhow!("Invalid response: {}", resp.text)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = err.to_string();
|
||||||
|
if error_msg.contains("Error code 605") {
|
||||||
|
Err(anyhow::anyhow!("Description too long"))
|
||||||
|
} else if error_msg.contains("Error code 606") {
|
||||||
|
Err(anyhow::anyhow!("Action not authorized"))
|
||||||
|
} else if error_msg.contains("Error code 728") {
|
||||||
|
Err(anyhow::anyhow!("No ports available"))
|
||||||
|
} else {
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_add_random_port_mapping_error(error: anyhow::Error) -> Option<anyhow::Error> {
|
||||||
|
let error_msg = error.to_string();
|
||||||
|
if error_msg.contains("Error code 724") {
|
||||||
|
None
|
||||||
|
} else if error_msg.contains("Error code 605") {
|
||||||
|
Some(anyhow::anyhow!("Description too long"))
|
||||||
|
} else if error_msg.contains("Error code 606") {
|
||||||
|
Some(anyhow::anyhow!("Action not authorized"))
|
||||||
|
} else if error_msg.contains("Error code 718") {
|
||||||
|
Some(anyhow::anyhow!("No ports available"))
|
||||||
|
} else if error_msg.contains("Error code 725") {
|
||||||
|
Some(anyhow::anyhow!("Only permanent leases supported"))
|
||||||
|
} else {
|
||||||
|
Some(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_add_same_port_mapping_error(error: anyhow::Error) -> anyhow::Error {
|
||||||
|
let error_msg = error.to_string();
|
||||||
|
if error_msg.contains("Error code 606") {
|
||||||
|
anyhow::anyhow!("Action not authorized")
|
||||||
|
} else if error_msg.contains("Error code 718") {
|
||||||
|
anyhow::anyhow!("External port in use")
|
||||||
|
} else if error_msg.contains("Error code 725") {
|
||||||
|
anyhow::anyhow!("Only permanent leases supported")
|
||||||
|
} else {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_add_port_error(err: anyhow::Error) -> anyhow::Error {
|
||||||
|
let error_msg = err.to_string();
|
||||||
|
if error_msg.contains("Error code 605") {
|
||||||
|
anyhow::anyhow!("Description too long")
|
||||||
|
} else if error_msg.contains("Error code 606") {
|
||||||
|
anyhow::anyhow!("Action not authorized")
|
||||||
|
} else if error_msg.contains("Error code 718") {
|
||||||
|
anyhow::anyhow!("Port in use")
|
||||||
|
} else if error_msg.contains("Error code 724") {
|
||||||
|
anyhow::anyhow!("Same port values required")
|
||||||
|
} else if error_msg.contains("Error code 725") {
|
||||||
|
anyhow::anyhow!("Only permanent leases supported")
|
||||||
|
} else {
|
||||||
|
err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_delete_port_mapping_response(result: RequestResult) -> anyhow::Result<()> {
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = err.to_string();
|
||||||
|
if error_msg.contains("Error code 606") {
|
||||||
|
Err(anyhow::anyhow!("Action not authorized"))
|
||||||
|
} else if error_msg.contains("Error code 714") {
|
||||||
|
Err(anyhow::anyhow!("No such port mapping"))
|
||||||
|
} else {
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One port mapping entry as returned by GetGenericPortMappingEntry
|
||||||
|
pub struct PortMappingEntry {
|
||||||
|
/// The remote host for which the mapping is valid
|
||||||
|
/// Can be an IP address or a host name
|
||||||
|
pub remote_host: String,
|
||||||
|
/// The external port of the mapping
|
||||||
|
pub external_port: u16,
|
||||||
|
/// The protocol of the mapping
|
||||||
|
pub protocol: PortMappingProtocol,
|
||||||
|
/// The internal (local) port
|
||||||
|
pub internal_port: u16,
|
||||||
|
/// The internal client of the port mapping
|
||||||
|
/// Can be an IP address or a host name
|
||||||
|
pub internal_client: String,
|
||||||
|
/// A flag whether this port mapping is enabled
|
||||||
|
pub enabled: bool,
|
||||||
|
/// A description for this port mapping
|
||||||
|
pub port_mapping_description: String,
|
||||||
|
/// The lease duration of this port mapping in seconds
|
||||||
|
pub lease_duration: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_get_generic_port_mapping_entry(
|
||||||
|
result: RequestResult,
|
||||||
|
) -> anyhow::Result<PortMappingEntry> {
|
||||||
|
let response = result?;
|
||||||
|
let xml = response.xml;
|
||||||
|
let make_err = |msg: String| move || anyhow::anyhow!("Invalid response: {}", msg);
|
||||||
|
let extract_field = |field: &str| {
|
||||||
|
xml.get_child(field)
|
||||||
|
.ok_or_else(make_err(format!("{field} is missing")))
|
||||||
|
};
|
||||||
|
let remote_host = extract_field("NewRemoteHost")?
|
||||||
|
.get_text()
|
||||||
|
.map(|c| c.into_owned())
|
||||||
|
.unwrap_or_else(|| "".into());
|
||||||
|
let external_port = extract_field("NewExternalPort")?
|
||||||
|
.get_text()
|
||||||
|
.and_then(|t| t.parse::<u16>().ok())
|
||||||
|
.ok_or_else(make_err("Field NewExternalPort is invalid".into()))?;
|
||||||
|
let protocol = match extract_field("NewProtocol")?.get_text() {
|
||||||
|
Some(std::borrow::Cow::Borrowed("UDP")) => PortMappingProtocol::Udp,
|
||||||
|
Some(std::borrow::Cow::Borrowed("TCP")) => PortMappingProtocol::Tcp,
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Invalid response: Field NewProtocol is invalid"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let internal_port = extract_field("NewInternalPort")?
|
||||||
|
.get_text()
|
||||||
|
.and_then(|t| t.parse::<u16>().ok())
|
||||||
|
.ok_or_else(make_err("Field NewInternalPort is invalid".into()))?;
|
||||||
|
let internal_client = extract_field("NewInternalClient")?
|
||||||
|
.get_text()
|
||||||
|
.map(|c| c.into_owned())
|
||||||
|
.ok_or_else(make_err("Field NewInternalClient is empty".into()))?;
|
||||||
|
let enabled = match extract_field("NewEnabled")?
|
||||||
|
.get_text()
|
||||||
|
.and_then(|t| t.parse::<u16>().ok())
|
||||||
|
.ok_or_else(make_err("Field Enabled is invalid".into()))?
|
||||||
|
{
|
||||||
|
0 => false,
|
||||||
|
1 => true,
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Invalid response: Field NewEnabled is invalid"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let port_mapping_description = extract_field("NewPortMappingDescription")?
|
||||||
|
.get_text()
|
||||||
|
.map(|c| c.into_owned())
|
||||||
|
.unwrap_or_else(|| "".into());
|
||||||
|
let lease_duration = extract_field("NewLeaseDuration")?
|
||||||
|
.get_text()
|
||||||
|
.and_then(|t| t.parse::<u32>().ok())
|
||||||
|
.ok_or_else(make_err("Field NewLeaseDuration is invalid".into()))?;
|
||||||
|
Ok(PortMappingEntry {
|
||||||
|
remote_host,
|
||||||
|
external_port,
|
||||||
|
protocol,
|
||||||
|
internal_port,
|
||||||
|
internal_client,
|
||||||
|
enabled,
|
||||||
|
port_mapping_description,
|
||||||
|
lease_duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_search_result_case_insensitivity() {
|
||||||
|
assert!(parse_search_result("location:http://0.0.0.0:0/control_url").is_ok());
|
||||||
|
assert!(parse_search_result("LOCATION:http://0.0.0.0:0/control_url").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_search_result_ok() {
|
||||||
|
let result = parse_search_result("location:http://0.0.0.0:0/control_url").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result.0.ip(),
|
||||||
|
IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0))
|
||||||
|
);
|
||||||
|
assert_eq!(result.0.port(), 0);
|
||||||
|
assert_eq!(&result.1[..], "/control_url");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_search_result_fail() {
|
||||||
|
assert!(parse_search_result("content-type:http://0.0.0.0:0/control_url").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_device1() {
|
||||||
|
let text = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||||
|
<specVersion>
|
||||||
|
<major>1</major>
|
||||||
|
<minor>0</minor>
|
||||||
|
</specVersion>
|
||||||
|
<device>
|
||||||
|
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
|
||||||
|
<friendlyName></friendlyName>
|
||||||
|
<manufacturer></manufacturer>
|
||||||
|
<manufacturerURL></manufacturerURL>
|
||||||
|
<modelDescription></modelDescription>
|
||||||
|
<modelName></modelName>
|
||||||
|
<modelNumber>1</modelNumber>
|
||||||
|
<serialNumber>00000000</serialNumber>
|
||||||
|
<UDN></UDN>
|
||||||
|
<serviceList>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
|
||||||
|
<serviceId>urn:upnp-org:serviceId:Layer3Forwarding1</serviceId>
|
||||||
|
<controlURL>/ctl/L3F</controlURL>
|
||||||
|
<eventSubURL>/evt/L3F</eventSubURL>
|
||||||
|
<SCPDURL>/L3F.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
</serviceList>
|
||||||
|
<deviceList>
|
||||||
|
<device>
|
||||||
|
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
|
||||||
|
<friendlyName>WANDevice</friendlyName>
|
||||||
|
<manufacturer>MiniUPnP</manufacturer>
|
||||||
|
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
|
||||||
|
<modelDescription>WAN Device</modelDescription>
|
||||||
|
<modelName>WAN Device</modelName>
|
||||||
|
<modelNumber>20180615</modelNumber>
|
||||||
|
<modelURL>http://miniupnp.free.fr/</modelURL>
|
||||||
|
<serialNumber>00000000</serialNumber>
|
||||||
|
<UDN>uuid:804e2e56-7bfe-4733-bae0-04bf6d569692</UDN>
|
||||||
|
<UPC>MINIUPNPD</UPC>
|
||||||
|
<serviceList>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
|
||||||
|
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
|
||||||
|
<controlURL>/ctl/CmnIfCfg</controlURL>
|
||||||
|
<eventSubURL>/evt/CmnIfCfg</eventSubURL>
|
||||||
|
<SCPDURL>/WANCfg.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
</serviceList>
|
||||||
|
<deviceList>
|
||||||
|
<device>
|
||||||
|
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
|
||||||
|
<friendlyName>WANConnectionDevice</friendlyName>
|
||||||
|
<manufacturer>MiniUPnP</manufacturer>
|
||||||
|
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
|
||||||
|
<modelDescription>MiniUPnP daemon</modelDescription>
|
||||||
|
<modelName>MiniUPnPd</modelName>
|
||||||
|
<modelNumber>20180615</modelNumber>
|
||||||
|
<modelURL>http://miniupnp.free.fr/</modelURL>
|
||||||
|
<serialNumber>00000000</serialNumber>
|
||||||
|
<UDN>uuid:804e2e56-7bfe-4733-bae0-04bf6d569692</UDN>
|
||||||
|
<UPC>MINIUPNPD</UPC>
|
||||||
|
<serviceList>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
|
||||||
|
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
|
||||||
|
<controlURL>/ctl/IPConn</controlURL>
|
||||||
|
<eventSubURL>/evt/IPConn</eventSubURL>
|
||||||
|
<SCPDURL>/WANIPCn.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
</serviceList>
|
||||||
|
</device>
|
||||||
|
</deviceList>
|
||||||
|
</device>
|
||||||
|
</deviceList>
|
||||||
|
<presentationURL>http://192.168.0.1/</presentationURL>
|
||||||
|
</device>
|
||||||
|
</root>"#;
|
||||||
|
|
||||||
|
let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap();
|
||||||
|
assert_eq!(control_url, "/ctl/IPConn");
|
||||||
|
assert_eq!(control_schema_url, "/WANIPCn.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_device2() {
|
||||||
|
let text = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||||
|
<specVersion>
|
||||||
|
<major>1</major>
|
||||||
|
<minor>0</minor>
|
||||||
|
</specVersion>
|
||||||
|
<device>
|
||||||
|
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
|
||||||
|
<friendlyName>FRITZ!Box 7430</friendlyName>
|
||||||
|
<manufacturer>AVM Berlin</manufacturer>
|
||||||
|
<manufacturerURL>http://www.avm.de</manufacturerURL>
|
||||||
|
<modelDescription>FRITZ!Box 7430</modelDescription>
|
||||||
|
<modelName>FRITZ!Box 7430</modelName>
|
||||||
|
<modelNumber>avm</modelNumber>
|
||||||
|
<modelURL>http://www.avm.de</modelURL>
|
||||||
|
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
|
||||||
|
<iconList>
|
||||||
|
<icon>
|
||||||
|
<mimetype>image/gif</mimetype>
|
||||||
|
<width>118</width>
|
||||||
|
<height>119</height>
|
||||||
|
<depth>8</depth>
|
||||||
|
<url>/ligd.gif</url>
|
||||||
|
</icon>
|
||||||
|
</iconList>
|
||||||
|
<serviceList>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-any-com:service:Any:1</serviceType>
|
||||||
|
<serviceId>urn:any-com:serviceId:any1</serviceId>
|
||||||
|
<controlURL>/igdupnp/control/any</controlURL>
|
||||||
|
<eventSubURL>/igdupnp/control/any</eventSubURL>
|
||||||
|
<SCPDURL>/any.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
</serviceList>
|
||||||
|
<deviceList>
|
||||||
|
<device>
|
||||||
|
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
|
||||||
|
<friendlyName>WANDevice - FRITZ!Box 7430</friendlyName>
|
||||||
|
<manufacturer>AVM Berlin</manufacturer>
|
||||||
|
<manufacturerURL>www.avm.de</manufacturerURL>
|
||||||
|
<modelDescription>WANDevice - FRITZ!Box 7430</modelDescription>
|
||||||
|
<modelName>WANDevice - FRITZ!Box 7430</modelName>
|
||||||
|
<modelNumber>avm</modelNumber>
|
||||||
|
<modelURL>www.avm.de</modelURL>
|
||||||
|
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
|
||||||
|
<UPC>AVM IGD</UPC>
|
||||||
|
<serviceList>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
|
||||||
|
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
|
||||||
|
<controlURL>/igdupnp/control/WANCommonIFC1</controlURL>
|
||||||
|
<eventSubURL>/igdupnp/control/WANCommonIFC1</eventSubURL>
|
||||||
|
<SCPDURL>/igdicfgSCPD.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
</serviceList>
|
||||||
|
<deviceList>
|
||||||
|
<device>
|
||||||
|
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
|
||||||
|
<friendlyName>WANConnectionDevice - FRITZ!Box 7430</friendlyName>
|
||||||
|
<manufacturer>AVM Berlin</manufacturer>
|
||||||
|
<manufacturerURL>www.avm.de</manufacturerURL>
|
||||||
|
<modelDescription>WANConnectionDevice - FRITZ!Box 7430</modelDescription>
|
||||||
|
<modelName>WANConnectionDevice - FRITZ!Box 7430</modelName>
|
||||||
|
<modelNumber>avm</modelNumber>
|
||||||
|
<modelURL>www.avm.de</modelURL>
|
||||||
|
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
|
||||||
|
<UPC>AVM IGD</UPC>
|
||||||
|
<serviceList>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-upnp-org:service:WANDSLLinkConfig:1</serviceType>
|
||||||
|
<serviceId>urn:upnp-org:serviceId:WANDSLLinkC1</serviceId>
|
||||||
|
<controlURL>/igdupnp/control/WANDSLLinkC1</controlURL>
|
||||||
|
<eventSubURL>/igdupnp/control/WANDSLLinkC1</eventSubURL>
|
||||||
|
<SCPDURL>/igddslSCPD.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
|
||||||
|
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
|
||||||
|
<controlURL>/igdupnp/control/WANIPConn1</controlURL>
|
||||||
|
<eventSubURL>/igdupnp/control/WANIPConn1</eventSubURL>
|
||||||
|
<SCPDURL>/igdconnSCPD.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-upnp-org:service:WANIPv6FirewallControl:1</serviceType>
|
||||||
|
<serviceId>urn:upnp-org:serviceId:WANIPv6Firewall1</serviceId>
|
||||||
|
<controlURL>/igd2upnp/control/WANIPv6Firewall1</controlURL>
|
||||||
|
<eventSubURL>/igd2upnp/control/WANIPv6Firewall1</eventSubURL>
|
||||||
|
<SCPDURL>/igd2ipv6fwcSCPD.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
</serviceList>
|
||||||
|
</device>
|
||||||
|
</deviceList>
|
||||||
|
</device>
|
||||||
|
</deviceList>
|
||||||
|
<presentationURL>http://fritz.box</presentationURL>
|
||||||
|
</device>
|
||||||
|
</root>
|
||||||
|
"#;
|
||||||
|
let result = parse_control_urls(text.as_bytes());
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let (control_schema_url, control_url) = result.unwrap();
|
||||||
|
assert_eq!(control_url, "/igdupnp/control/WANIPConn1");
|
||||||
|
assert_eq!(control_schema_url, "/igdconnSCPD.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_device3() {
|
||||||
|
let text = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||||
|
<specVersion>
|
||||||
|
<major>1</major>
|
||||||
|
<minor>0</minor>
|
||||||
|
</specVersion>
|
||||||
|
<device xmlns="urn:schemas-upnp-org:device-1-0">
|
||||||
|
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
|
||||||
|
<friendlyName></friendlyName>
|
||||||
|
<manufacturer></manufacturer>
|
||||||
|
<manufacturerURL></manufacturerURL>
|
||||||
|
<modelDescription></modelDescription>
|
||||||
|
<modelName></modelName>
|
||||||
|
<modelNumber></modelNumber>
|
||||||
|
<serialNumber></serialNumber>
|
||||||
|
<presentationURL>http://192.168.1.1</presentationURL>
|
||||||
|
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
|
||||||
|
<UPC>999999999001</UPC>
|
||||||
|
<iconList>
|
||||||
|
<icon>
|
||||||
|
<mimetype>image/png</mimetype>
|
||||||
|
<width>16</width>
|
||||||
|
<height>16</height>
|
||||||
|
<depth>8</depth>
|
||||||
|
<url>/ligd.png</url>
|
||||||
|
</icon>
|
||||||
|
</iconList>
|
||||||
|
<deviceList>
|
||||||
|
<device>
|
||||||
|
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
|
||||||
|
<friendlyName></friendlyName>
|
||||||
|
<manufacturer></manufacturer>
|
||||||
|
<manufacturerURL></manufacturerURL>
|
||||||
|
<modelDescription></modelDescription>
|
||||||
|
<modelName></modelName>
|
||||||
|
<modelNumber></modelNumber>
|
||||||
|
<modelURL></modelURL>
|
||||||
|
<serialNumber></serialNumber>
|
||||||
|
<presentationURL>http://192.168.1.254</presentationURL>
|
||||||
|
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
|
||||||
|
<UPC>999999999001</UPC>
|
||||||
|
<serviceList>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
|
||||||
|
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
|
||||||
|
<controlURL>/upnp/control/WANCommonIFC1</controlURL>
|
||||||
|
<eventSubURL>/upnp/control/WANCommonIFC1</eventSubURL>
|
||||||
|
<SCPDURL>/332b484d/wancomicfgSCPD.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
</serviceList>
|
||||||
|
<deviceList>
|
||||||
|
<device>
|
||||||
|
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
|
||||||
|
<friendlyName></friendlyName>
|
||||||
|
<manufacturer></manufacturer>
|
||||||
|
<manufacturerURL></manufacturerURL>
|
||||||
|
<modelDescription></modelDescription>
|
||||||
|
<modelName></modelName>
|
||||||
|
<modelNumber></modelNumber>
|
||||||
|
<modelURL></modelURL>
|
||||||
|
<serialNumber></serialNumber>
|
||||||
|
<presentationURL>http://192.168.1.254</presentationURL>
|
||||||
|
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
|
||||||
|
<UPC>999999999001</UPC>
|
||||||
|
<serviceList>
|
||||||
|
<service>
|
||||||
|
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
|
||||||
|
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
|
||||||
|
<controlURL>/upnp/control/WANIPConn1</controlURL>
|
||||||
|
<eventSubURL>/upnp/control/WANIPConn1</eventSubURL>
|
||||||
|
<SCPDURL>/332b484d/wanipconnSCPD.xml</SCPDURL>
|
||||||
|
</service>
|
||||||
|
</serviceList>
|
||||||
|
</device>
|
||||||
|
</deviceList>
|
||||||
|
</device>
|
||||||
|
</deviceList>
|
||||||
|
</device>
|
||||||
|
</root>"#;
|
||||||
|
|
||||||
|
let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap();
|
||||||
|
assert_eq!(control_url, "/upnp/control/WANIPConn1");
|
||||||
|
assert_eq!(control_schema_url, "/332b484d/wanipconnSCPD.xml");
|
||||||
|
}
|
||||||
323
easytier/src/instance/upnp_igd/gateway.rs
Normal file
323
easytier/src/instance/upnp_igd/gateway.rs
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
|
||||||
|
use super::Provider;
|
||||||
|
|
||||||
|
use super::common::{self, messages, parsing, parsing::RequestReponse};
|
||||||
|
use super::PortMappingProtocol;
|
||||||
|
|
||||||
|
/// This structure represents a gateway found by the search functions.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Gateway<P> {
|
||||||
|
/// Socket address of the gateway
|
||||||
|
pub addr: SocketAddr,
|
||||||
|
/// Root url of the device
|
||||||
|
pub root_url: String,
|
||||||
|
/// Control url of the device
|
||||||
|
pub control_url: String,
|
||||||
|
/// Url to get schema data from
|
||||||
|
pub control_schema_url: String,
|
||||||
|
/// Control schema for all actions
|
||||||
|
pub control_schema: HashMap<String, Vec<String>>,
|
||||||
|
/// Executor provider
|
||||||
|
pub provider: P,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Provider> Gateway<P> {
|
||||||
|
async fn perform_request(
|
||||||
|
&self,
|
||||||
|
header: &str,
|
||||||
|
body: &str,
|
||||||
|
ok: &str,
|
||||||
|
) -> anyhow::Result<RequestReponse> {
|
||||||
|
let url = format!("{self}");
|
||||||
|
let text = P::send_async(&url, header, body).await?;
|
||||||
|
parsing::parse_response(text, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the external IP address of the gateway in a tokio compatible way
|
||||||
|
pub async fn get_external_ip(&self) -> anyhow::Result<Option<IpAddr>> {
|
||||||
|
let result = self
|
||||||
|
.perform_request(
|
||||||
|
messages::GET_EXTERNAL_IP_HEADER,
|
||||||
|
&messages::format_get_external_ip_message(),
|
||||||
|
"GetExternalIPAddressResponse",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
parsing::parse_get_external_ip_response(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an external socket address with our external ip and any port. This is a convenience
|
||||||
|
/// function that calls `get_external_ip` followed by `add_any_port`
|
||||||
|
///
|
||||||
|
/// The local_addr is the address where the traffic is sent to.
|
||||||
|
/// The lease_duration parameter is in seconds. A value of 0 is infinite.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The external address that was mapped on success. Otherwise an error.
|
||||||
|
pub async fn get_any_address(
|
||||||
|
&self,
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
lease_duration: u32,
|
||||||
|
description: &str,
|
||||||
|
) -> anyhow::Result<SocketAddr> {
|
||||||
|
let description = description.to_owned();
|
||||||
|
let ip = self
|
||||||
|
.get_external_ip()
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Router does not have an external IP address"))?;
|
||||||
|
let port = self
|
||||||
|
.add_any_port(protocol, local_addr, lease_duration, &description)
|
||||||
|
.await?;
|
||||||
|
Ok(SocketAddr::new(ip, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a port mapping.with any external port.
|
||||||
|
///
|
||||||
|
/// The local_addr is the address where the traffic is sent to.
|
||||||
|
/// The lease_duration parameter is in seconds. A value of 0 is infinite.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The external port that was mapped on success. Otherwise an error.
|
||||||
|
pub async fn add_any_port(
|
||||||
|
&self,
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
lease_duration: u32,
|
||||||
|
description: &str,
|
||||||
|
) -> anyhow::Result<u16> {
|
||||||
|
// This function first attempts to call AddAnyPortMapping on the IGD with a random port
|
||||||
|
// number. If that fails due to the method being unknown it attempts to call AddPortMapping
|
||||||
|
// instead with a random port number. If that fails due to ConflictInMappingEntry it retrys
|
||||||
|
// with another port up to a maximum of 20 times. If it fails due to SamePortValuesRequired
|
||||||
|
// it retrys once with the same port values.
|
||||||
|
|
||||||
|
if local_addr.port() == 0 {
|
||||||
|
return Err(anyhow::anyhow!("Internal port zero is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let schema = self.control_schema.get("AddAnyPortMapping");
|
||||||
|
if let Some(schema) = schema {
|
||||||
|
let external_port = common::random_port();
|
||||||
|
|
||||||
|
let description = description.to_owned();
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.perform_request(
|
||||||
|
messages::ADD_ANY_PORT_MAPPING_HEADER,
|
||||||
|
&messages::format_add_any_port_mapping_message(
|
||||||
|
schema,
|
||||||
|
protocol,
|
||||||
|
external_port,
|
||||||
|
local_addr,
|
||||||
|
lease_duration,
|
||||||
|
&description,
|
||||||
|
),
|
||||||
|
"AddAnyPortMappingResponse",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
parsing::parse_add_any_port_mapping_response(resp)
|
||||||
|
} else {
|
||||||
|
// The router does not have the AddAnyPortMapping method.
|
||||||
|
// Fall back to using AddPortMapping with a random port.
|
||||||
|
self.retry_add_random_port_mapping(protocol, local_addr, lease_duration, description)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn retry_add_random_port_mapping(
|
||||||
|
&self,
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
lease_duration: u32,
|
||||||
|
description: &str,
|
||||||
|
) -> anyhow::Result<u16> {
|
||||||
|
for _ in 0u8..20u8 {
|
||||||
|
match self
|
||||||
|
.add_random_port_mapping(protocol, local_addr, lease_duration, description)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(port) => return Ok(port),
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::anyhow!("No ports available"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_random_port_mapping(
|
||||||
|
&self,
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
lease_duration: u32,
|
||||||
|
description: &str,
|
||||||
|
) -> anyhow::Result<u16> {
|
||||||
|
let description = description.to_owned();
|
||||||
|
let external_port = common::random_port();
|
||||||
|
let res = self
|
||||||
|
.add_port_mapping(
|
||||||
|
protocol,
|
||||||
|
external_port,
|
||||||
|
local_addr,
|
||||||
|
lease_duration,
|
||||||
|
&description,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => Ok(external_port),
|
||||||
|
Err(_) => {
|
||||||
|
self.add_same_port_mapping(protocol, local_addr, lease_duration, &description)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_same_port_mapping(
|
||||||
|
&self,
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
lease_duration: u32,
|
||||||
|
description: &str,
|
||||||
|
) -> anyhow::Result<u16> {
|
||||||
|
let res = self
|
||||||
|
.add_port_mapping(
|
||||||
|
protocol,
|
||||||
|
local_addr.port(),
|
||||||
|
local_addr,
|
||||||
|
lease_duration,
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Ok(_) => Ok(local_addr.port()),
|
||||||
|
Err(err) => Err(anyhow::anyhow!("Add same port mapping failed: {}", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_port_mapping(
|
||||||
|
&self,
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
external_port: u16,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
lease_duration: u32,
|
||||||
|
description: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
self.perform_request(
|
||||||
|
messages::ADD_PORT_MAPPING_HEADER,
|
||||||
|
&messages::format_add_port_mapping_message(
|
||||||
|
self.control_schema
|
||||||
|
.get("AddPortMapping")
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Unsupported action: AddPortMapping"))?,
|
||||||
|
protocol,
|
||||||
|
external_port,
|
||||||
|
local_addr,
|
||||||
|
lease_duration,
|
||||||
|
description,
|
||||||
|
),
|
||||||
|
"AddPortMappingResponse",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a port mapping.
|
||||||
|
///
|
||||||
|
/// The local_addr is the address where the traffic is sent to.
|
||||||
|
/// The lease_duration parameter is in seconds. A value of 0 is infinite.
|
||||||
|
pub async fn add_port(
|
||||||
|
&self,
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
external_port: u16,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
lease_duration: u32,
|
||||||
|
description: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if external_port == 0 {
|
||||||
|
return Err(anyhow::anyhow!("External port zero is invalid"));
|
||||||
|
}
|
||||||
|
if local_addr.port() == 0 {
|
||||||
|
return Err(anyhow::anyhow!("Internal port zero is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.add_port_mapping(
|
||||||
|
protocol,
|
||||||
|
external_port,
|
||||||
|
local_addr,
|
||||||
|
lease_duration,
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Err(err) = res {
|
||||||
|
return Err(anyhow::anyhow!("Add port mapping failed: {}", err));
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a port mapping.
|
||||||
|
pub async fn remove_port(
|
||||||
|
&self,
|
||||||
|
protocol: PortMappingProtocol,
|
||||||
|
external_port: u16,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let res = self
|
||||||
|
.perform_request(
|
||||||
|
messages::DELETE_PORT_MAPPING_HEADER,
|
||||||
|
&messages::format_delete_port_message(
|
||||||
|
self.control_schema
|
||||||
|
.get("DeletePortMapping")
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Unsupported action: DeletePortMapping"))?,
|
||||||
|
protocol,
|
||||||
|
external_port,
|
||||||
|
),
|
||||||
|
"DeletePortMappingResponse",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
parsing::parse_delete_port_mapping_response(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get one port mapping entry
|
||||||
|
///
|
||||||
|
/// Gets one port mapping entry by its index.
|
||||||
|
/// Not all existing port mappings might be visible to this client.
|
||||||
|
/// If the index is out of bound, GetGenericPortMappingEntryError::SpecifiedArrayIndexInvalid will be returned
|
||||||
|
pub async fn get_generic_port_mapping_entry(
|
||||||
|
&self,
|
||||||
|
index: u32,
|
||||||
|
) -> anyhow::Result<parsing::PortMappingEntry> {
|
||||||
|
let result = self
|
||||||
|
.perform_request(
|
||||||
|
messages::GET_GENERIC_PORT_MAPPING_ENTRY,
|
||||||
|
&messages::formate_get_generic_port_mapping_entry_message(index),
|
||||||
|
"GetGenericPortMappingEntryResponse",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
parsing::parse_get_generic_port_mapping_entry(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> fmt::Display for Gateway<P> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "http://{}{}", self.addr, self.control_url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> PartialEq for Gateway<P> {
|
||||||
|
fn eq(&self, other: &Gateway<P>) -> bool {
|
||||||
|
self.addr == other.addr && self.control_url == other.control_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Eq for Gateway<P> {}
|
||||||
|
|
||||||
|
impl<P> Hash for Gateway<P> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.addr.hash(state);
|
||||||
|
self.control_url.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
easytier/src/instance/upnp_igd/mod.rs
Normal file
64
easytier/src/instance/upnp_igd/mod.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
mod common;
|
||||||
|
mod gateway;
|
||||||
|
mod search;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub(crate) const MAX_RESPONSE_SIZE: usize = 1500;
|
||||||
|
pub(crate) const HEADER_NAME: &str = "SOAPAction";
|
||||||
|
|
||||||
|
/// Trait to allow abstracting over `tokio`.
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait Provider {
|
||||||
|
/// Send an async request over the executor.
|
||||||
|
async fn send_async(url: &str, action: &str, body: &str) -> anyhow::Result<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the protocols available for port mapping.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PortMappingProtocol {
|
||||||
|
/// TCP protocol
|
||||||
|
Tcp,
|
||||||
|
/// UDP protocol
|
||||||
|
Udp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PortMappingProtocol {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match *self {
|
||||||
|
PortMappingProtocol::Tcp => "TCP",
|
||||||
|
PortMappingProtocol::Udp => "UDP",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::instance::upnp_igd::{
|
||||||
|
common::SearchOptions, search::search_gateway, PortMappingProtocol,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_search_device() {
|
||||||
|
let ret = search_gateway(SearchOptions::default()).await.unwrap();
|
||||||
|
println!("{:?}", ret);
|
||||||
|
let external_ip = ret.get_external_ip().await.unwrap();
|
||||||
|
println!("{:?}", external_ip);
|
||||||
|
|
||||||
|
let add_port_ret = ret
|
||||||
|
.add_port(
|
||||||
|
PortMappingProtocol::Tcp,
|
||||||
|
51010,
|
||||||
|
"10.147.223.128:11010".parse().unwrap(),
|
||||||
|
1000,
|
||||||
|
"test",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
println!("{:?}", add_port_ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
250
easytier/src/instance/upnp_igd/search.rs
Normal file
250
easytier/src/instance/upnp_igd/search.rs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
//! Tokio abstraction for the aio [`Gateway`].
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use http_req::response::Headers;
|
||||||
|
use tokio::{net::UdpSocket, time::timeout};
|
||||||
|
|
||||||
|
use super::common::options::{DEFAULT_TIMEOUT, RESPONSE_TIMEOUT};
|
||||||
|
use super::common::{messages, parsing, SearchOptions};
|
||||||
|
use super::gateway::Gateway;
|
||||||
|
use super::{Provider, HEADER_NAME, MAX_RESPONSE_SIZE};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
/// Tokio provider for the [`Gateway`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Tokio;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Provider for Tokio {
|
||||||
|
async fn send_async(url: &str, action: &str, body: &str) -> anyhow::Result<String> {
|
||||||
|
use http_req::request;
|
||||||
|
|
||||||
|
// Run the blocking HTTP request in a separate thread to avoid blocking the async runtime
|
||||||
|
let url_owned = url.to_string();
|
||||||
|
let body_clone = body.to_string();
|
||||||
|
let action_clone = action.to_string();
|
||||||
|
|
||||||
|
let (response, response_body) = tokio::task::spawn_blocking(move || {
|
||||||
|
let uri = http_req::uri::Uri::try_from(url_owned.as_str())
|
||||||
|
.map_err(|e| anyhow::anyhow!("URI parse error: {}", e))?;
|
||||||
|
|
||||||
|
println!("body: {body_clone}, action: {action_clone}");
|
||||||
|
|
||||||
|
let mut response_body = Vec::new();
|
||||||
|
let response = request::Request::new(&uri)
|
||||||
|
.method(request::Method::POST)
|
||||||
|
.header(HEADER_NAME, &action_clone)
|
||||||
|
.header("Content-Type", "text/xml; charset=\"utf-8\"")
|
||||||
|
.body(body_clone.as_bytes())
|
||||||
|
.send(&mut response_body);
|
||||||
|
|
||||||
|
if response.is_err() {
|
||||||
|
if response_body.is_empty() {
|
||||||
|
anyhow::bail!("HTTP request error: {}", response.unwrap_err());
|
||||||
|
} else {
|
||||||
|
anyhow::bail!(
|
||||||
|
"HTTP request error: {} with response body: {}",
|
||||||
|
response.unwrap_err(),
|
||||||
|
String::from_utf8_lossy(&response_body)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = response.unwrap();
|
||||||
|
Ok::<_, anyhow::Error>((response, response_body))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))??;
|
||||||
|
|
||||||
|
if !response.status_code().is_success() {
|
||||||
|
if response_body.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"HTTP error with empty body: {}",
|
||||||
|
response.status_code()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let string = String::from_utf8(response_body)
|
||||||
|
.map_err(|e| anyhow::anyhow!("UTF-8 conversion error: {}", e))?;
|
||||||
|
Ok(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search for a gateway with the provided options.
|
||||||
|
pub async fn search_gateway(options: SearchOptions) -> anyhow::Result<Gateway<Tokio>> {
|
||||||
|
let search_timeout = options.timeout.unwrap_or(DEFAULT_TIMEOUT);
|
||||||
|
match timeout(search_timeout, search_gateway_inner(options)).await {
|
||||||
|
Ok(Ok(gateway)) => Ok(gateway),
|
||||||
|
Ok(Err(err)) => Err(err),
|
||||||
|
Err(_err) => {
|
||||||
|
// Timeout
|
||||||
|
Err(anyhow::anyhow!("No response within timeout"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search_gateway_inner(options: SearchOptions) -> anyhow::Result<Gateway<Tokio>> {
|
||||||
|
// Create socket for future calls
|
||||||
|
let mut socket = UdpSocket::bind(&options.bind_addr)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to bind socket: {}", e))?;
|
||||||
|
|
||||||
|
send_search_request(&mut socket, options.broadcast_address).await?;
|
||||||
|
let response_timeout = options.single_search_timeout.unwrap_or(RESPONSE_TIMEOUT);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let search_response = receive_search_response(&mut socket);
|
||||||
|
|
||||||
|
// Receive search response
|
||||||
|
let (response_body, from) = match timeout(response_timeout, search_response).await {
|
||||||
|
Ok(Ok(v)) => v,
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
debug!("error while receiving broadcast response: {err}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
debug!("timeout while receiving broadcast response");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (addr, root_url) = match handle_broadcast_resp(&from, &response_body) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("error handling broadcast response: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (control_schema_url, control_url) = match get_control_urls(&addr, &root_url).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("error getting control URLs: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let control_schema = match get_control_schemas(&addr, &control_schema_url).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("error getting control schemas: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(Gateway {
|
||||||
|
addr,
|
||||||
|
root_url,
|
||||||
|
control_url,
|
||||||
|
control_schema_url,
|
||||||
|
control_schema,
|
||||||
|
provider: Tokio,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new search.
|
||||||
|
async fn send_search_request(socket: &mut UdpSocket, addr: SocketAddr) -> anyhow::Result<()> {
|
||||||
|
debug!(
|
||||||
|
"sending broadcast request to: {} on interface: {:?}",
|
||||||
|
addr,
|
||||||
|
socket.local_addr()
|
||||||
|
);
|
||||||
|
socket
|
||||||
|
.send_to(messages::SEARCH_REQUEST.as_bytes(), &addr)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to send search request: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_search_response(socket: &mut UdpSocket) -> anyhow::Result<(Vec<u8>, SocketAddr)> {
|
||||||
|
let mut buff = [0u8; MAX_RESPONSE_SIZE];
|
||||||
|
let (n, from) = socket
|
||||||
|
.recv_from(&mut buff)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to receive response: {}", e))?;
|
||||||
|
debug!("received broadcast response from: {}", from);
|
||||||
|
Ok((buff[..n].to_vec(), from))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a UDP response message.
|
||||||
|
fn handle_broadcast_resp(from: &SocketAddr, data: &[u8]) -> anyhow::Result<(SocketAddr, String)> {
|
||||||
|
debug!("handling broadcast response from: {}", from);
|
||||||
|
|
||||||
|
// Convert response to text.
|
||||||
|
let text =
|
||||||
|
std::str::from_utf8(data).map_err(|e| anyhow::anyhow!("UTF-8 conversion error: {}", e))?;
|
||||||
|
|
||||||
|
// Parse socket address and path.
|
||||||
|
let (addr, root_url) = parsing::parse_search_result(text)?;
|
||||||
|
|
||||||
|
Ok((addr, root_url))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_control_urls(addr: &SocketAddr, path: &str) -> anyhow::Result<(String, String)> {
|
||||||
|
use http_req::request;
|
||||||
|
|
||||||
|
let url = format!("http://{addr}{path}");
|
||||||
|
debug!("requesting control url from: {}", url);
|
||||||
|
|
||||||
|
// Run the blocking HTTP request in a separate thread to avoid blocking the async runtime
|
||||||
|
let (response, response_body) = tokio::task::spawn_blocking(move || {
|
||||||
|
let uri = http_req::uri::Uri::try_from(url.as_str())
|
||||||
|
.map_err(|e| anyhow::anyhow!("URI parse error: {}", e))?;
|
||||||
|
|
||||||
|
let mut response_body = Vec::new();
|
||||||
|
let response = request::Request::new(&uri)
|
||||||
|
.method(request::Method::GET)
|
||||||
|
.send(&mut response_body)
|
||||||
|
.map_err(|e| anyhow::anyhow!("HTTP GET error: {}", e))?;
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>((response, response_body))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))??;
|
||||||
|
|
||||||
|
if !response.status_code().is_success() {
|
||||||
|
return Err(anyhow::anyhow!("HTTP error: {}", response.status_code()));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("handling control response from: {addr}");
|
||||||
|
let c = std::io::Cursor::new(&response_body);
|
||||||
|
parsing::parse_control_urls(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_control_schemas(
|
||||||
|
addr: &SocketAddr,
|
||||||
|
control_schema_url: &str,
|
||||||
|
) -> anyhow::Result<HashMap<String, Vec<String>>> {
|
||||||
|
use http_req::request;
|
||||||
|
|
||||||
|
let url = format!("http://{addr}{control_schema_url}");
|
||||||
|
debug!("requesting control schema from: {}", url);
|
||||||
|
|
||||||
|
// Run the blocking HTTP request in a separate thread to avoid blocking the async runtime
|
||||||
|
let (response, response_body) = tokio::task::spawn_blocking(move || {
|
||||||
|
let uri = http_req::uri::Uri::try_from(url.as_str())
|
||||||
|
.map_err(|e| anyhow::anyhow!("URI parse error: {}", e))?;
|
||||||
|
|
||||||
|
let mut response_body = Vec::new();
|
||||||
|
let response = request::Request::new(&uri)
|
||||||
|
.method(request::Method::GET)
|
||||||
|
.send(&mut response_body)
|
||||||
|
.map_err(|e| anyhow::anyhow!("HTTP GET error: {}", e))?;
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>((response, response_body))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))??;
|
||||||
|
|
||||||
|
if !response.status_code().is_success() {
|
||||||
|
return Err(anyhow::anyhow!("HTTP error: {}", response.status_code()));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("handling schema response from: {addr}");
|
||||||
|
let c = std::io::Cursor::new(&response_body);
|
||||||
|
parsing::parse_schemas(c)
|
||||||
|
}
|
||||||
@@ -384,7 +384,11 @@ pub(crate) fn setup_sokcet2_ext(
|
|||||||
unsafe {
|
unsafe {
|
||||||
let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8);
|
let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8);
|
||||||
tracing::warn!(?dev_idx, ?dev_name, "bind device");
|
tracing::warn!(?dev_idx, ?dev_name, "bind device");
|
||||||
socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?;
|
if bind_addr.is_ipv4() {
|
||||||
|
socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?;
|
||||||
|
} else {
|
||||||
|
socket2_socket.bind_device_by_index_v6(std::num::NonZeroU32::new(dev_idx))?;
|
||||||
|
}
|
||||||
tracing::warn!(?dev_idx, ?dev_name, "bind device doen");
|
tracing::warn!(?dev_idx, ?dev_name, "bind device doen");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,9 +81,14 @@ pub fn init_logger(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dir = file_config.dir.as_deref().unwrap_or(".");
|
||||||
|
let file = file_config.file.as_deref().unwrap_or("easytier.log");
|
||||||
|
let path = std::path::Path::new(dir).join(file);
|
||||||
|
let path_str = path.to_string_lossy().into_owned();
|
||||||
|
|
||||||
let builder = RollingFileAppenderBase::builder();
|
let builder = RollingFileAppenderBase::builder();
|
||||||
let file_appender = builder
|
let file_appender = builder
|
||||||
.filename(file_config.file.unwrap_or("easytier.log".to_string()))
|
.filename(path_str)
|
||||||
.condition_daily()
|
.condition_daily()
|
||||||
.max_filecount(file_config.count.unwrap_or(10))
|
.max_filecount(file_config.count.unwrap_or(10))
|
||||||
.condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024)
|
.condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024)
|
||||||
|
|||||||
Reference in New Issue
Block a user