mirror of
https://github.com/EasyTier/EasyTier.git
synced 2025-11-01 04:22:55 +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-sys 0.52.0",
|
||||
"winreg 0.52.0",
|
||||
"xmltree",
|
||||
"zerocopy",
|
||||
"zip",
|
||||
"zstd",
|
||||
@@ -11141,6 +11142,15 @@ version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
|
||||
@@ -217,6 +217,9 @@ version-compare = "0.2.0"
|
||||
hmac = "0.12.1"
|
||||
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]
|
||||
machine-uid = "0.5.3"
|
||||
|
||||
|
||||
@@ -109,8 +109,6 @@ enum SubCommand {
|
||||
Stats(StatsArgs),
|
||||
#[command(about = "manage logger configuration")]
|
||||
Logger(LoggerArgs),
|
||||
#[command(about = "manage network instance configuration")]
|
||||
Config(ConfigArgs),
|
||||
#[command(about = t!("core_clap.generate_completions").to_string())]
|
||||
GenAutocomplete { shell: Shell },
|
||||
}
|
||||
@@ -295,23 +293,6 @@ enum LoggerSubCommand {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct ConfigArgs {
|
||||
#[command(subcommand)]
|
||||
sub_command: Option<ConfigSubCommand>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum ConfigSubCommand {
|
||||
/// List network instances and their configurations
|
||||
List,
|
||||
/// Get configuration for a specific instance
|
||||
Get {
|
||||
#[arg(help = "Instance ID")]
|
||||
inst_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct ServiceArgs {
|
||||
#[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")]
|
||||
@@ -1305,68 +1286,6 @@ impl CommandHandler<'_> {
|
||||
}
|
||||
Ok(ports)
|
||||
}
|
||||
|
||||
async fn handle_config_list(&self) -> Result<(), Error> {
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let node_info = client
|
||||
.show_node_info(BaseController::default(), ShowNodeInfoRequest::default())
|
||||
.await?
|
||||
.node_info
|
||||
.ok_or(anyhow::anyhow!("node info not found"))?;
|
||||
|
||||
if self.verbose || *self.output_format == OutputFormat::Json {
|
||||
println!("{}", serde_json::to_string_pretty(&node_info)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[derive(tabled::Tabled, serde::Serialize)]
|
||||
struct ConfigTableItem {
|
||||
#[tabled(rename = "Instance ID")]
|
||||
inst_id: String,
|
||||
#[tabled(rename = "Virtual IP")]
|
||||
ipv4: String,
|
||||
#[tabled(rename = "Hostname")]
|
||||
hostname: String,
|
||||
#[tabled(rename = "Network Name")]
|
||||
network_name: String,
|
||||
}
|
||||
|
||||
let items = vec![ConfigTableItem {
|
||||
inst_id: node_info.peer_id.to_string(),
|
||||
ipv4: node_info.ipv4_addr,
|
||||
hostname: node_info.hostname,
|
||||
network_name: "".to_string(), // NodeInfo doesn't have network_name field
|
||||
}];
|
||||
|
||||
print_output(&items, self.output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_config_get(&self, inst_id: &str) -> Result<(), Error> {
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let node_info = client
|
||||
.show_node_info(BaseController::default(), ShowNodeInfoRequest::default())
|
||||
.await?
|
||||
.node_info
|
||||
.ok_or(anyhow::anyhow!("node info not found"))?;
|
||||
|
||||
// Check if the requested instance ID matches the current node
|
||||
if node_info.peer_id.to_string() != inst_id {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Instance ID {} not found. Current instance ID is {}",
|
||||
inst_id,
|
||||
node_info.peer_id
|
||||
));
|
||||
}
|
||||
|
||||
if self.verbose || *self.output_format == OutputFormat::Json {
|
||||
println!("{}", serde_json::to_string_pretty(&node_info)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{}", node_info.config);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -2178,14 +2097,6 @@ async fn main() -> Result<(), Error> {
|
||||
handler.handle_logger_set(level).await?;
|
||||
}
|
||||
},
|
||||
SubCommand::Config(config_args) => match &config_args.sub_command {
|
||||
Some(ConfigSubCommand::List) | None => {
|
||||
handler.handle_config_list().await?;
|
||||
}
|
||||
Some(ConfigSubCommand::Get { inst_id }) => {
|
||||
handler.handle_config_get(inst_id).await?;
|
||||
}
|
||||
},
|
||||
SubCommand::GenAutocomplete { shell } => {
|
||||
let mut cmd = Cli::command();
|
||||
easytier::print_completions(shell, &mut cmd, "easytier-cli");
|
||||
|
||||
@@ -968,8 +968,17 @@ impl NetworkOptions {
|
||||
old_udp_whitelist.extend(self.udp_whitelist.clone());
|
||||
cfg.set_udp_whitelist(old_udp_whitelist);
|
||||
|
||||
cfg.set_stun_servers(self.stun_servers.clone());
|
||||
cfg.set_stun_servers_v6(self.stun_servers_v6.clone());
|
||||
if let Some(stun_servers) = &self.stun_servers {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,5 @@ pub mod listeners;
|
||||
pub mod virtual_nic;
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -140,47 +140,6 @@ impl NetworkInstanceManager {
|
||||
.and_then(|instance| instance.value().get_running_info())
|
||||
}
|
||||
|
||||
pub fn get_network_config(&self, instance_id: &uuid::Uuid) -> Option<TomlConfigLoader> {
|
||||
self.instance_map
|
||||
.get(instance_id)
|
||||
.map(|instance| instance.value().get_config())
|
||||
}
|
||||
|
||||
pub fn replace_network_config(
|
||||
&self,
|
||||
instance_id: &uuid::Uuid,
|
||||
new_config: TomlConfigLoader,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut instance = self
|
||||
.instance_map
|
||||
.get_mut(instance_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("instance {} not found", instance_id))?;
|
||||
|
||||
// Stop the current instance if it's running
|
||||
if instance.is_easytier_running() {
|
||||
// Get the config source before stopping
|
||||
let config_source = instance.get_config_source();
|
||||
|
||||
// Create a new instance with the new config
|
||||
let mut new_instance = NetworkInstance::new(new_config, config_source);
|
||||
|
||||
// Start the new instance
|
||||
new_instance.start()?;
|
||||
|
||||
// Replace the old instance with the new one
|
||||
*instance = new_instance;
|
||||
|
||||
// Restart the instance task if needed
|
||||
self.start_instance_task(*instance_id)?;
|
||||
} else {
|
||||
// If the instance is not running, just replace the config
|
||||
let config_source = instance.get_config_source();
|
||||
*instance = NetworkInstance::new(new_config, config_source);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_network_instance_ids(&self) -> Vec<uuid::Uuid> {
|
||||
self.instance_map.iter().map(|item| *item.key()).collect()
|
||||
}
|
||||
|
||||
@@ -460,10 +460,6 @@ impl NetworkInstance {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config(&self) -> TomlConfigLoader {
|
||||
self.config.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_proxy_network_to_config(
|
||||
|
||||
@@ -178,25 +178,6 @@ message DeleteNetworkInstanceResponse {
|
||||
repeated common.UUID remain_inst_ids = 1;
|
||||
}
|
||||
|
||||
message GetConfigRequest {
|
||||
common.UUID inst_id = 1;
|
||||
}
|
||||
|
||||
message GetConfigResponse {
|
||||
NetworkConfig config = 1;
|
||||
string toml_config = 2;
|
||||
}
|
||||
|
||||
message ReplaceConfigRequest {
|
||||
common.UUID inst_id = 1;
|
||||
NetworkConfig config = 2;
|
||||
}
|
||||
|
||||
message ReplaceConfigResponse {
|
||||
bool success = 1;
|
||||
optional string error_msg = 2;
|
||||
}
|
||||
|
||||
service WebClientService {
|
||||
rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse) {}
|
||||
rpc RunNetworkInstance(RunNetworkInstanceRequest) returns (RunNetworkInstanceResponse) {}
|
||||
@@ -204,6 +185,4 @@ service WebClientService {
|
||||
rpc CollectNetworkInfo(CollectNetworkInfoRequest) returns (CollectNetworkInfoResponse) {}
|
||||
rpc ListNetworkInstance(ListNetworkInstanceRequest) returns (ListNetworkInstanceResponse) {}
|
||||
rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest) returns (DeleteNetworkInstanceResponse) {}
|
||||
rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {}
|
||||
rpc ReplaceConfig(ReplaceConfigRequest) returns (ReplaceConfigResponse) {}
|
||||
}
|
||||
|
||||
@@ -384,7 +384,11 @@ pub(crate) fn setup_sokcet2_ext(
|
||||
unsafe {
|
||||
let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 file_appender = builder
|
||||
.filename(file_config.file.unwrap_or("easytier.log".to_string()))
|
||||
.filename(path_str)
|
||||
.condition_daily()
|
||||
.max_filecount(file_config.count.unwrap_or(10))
|
||||
.condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024)
|
||||
|
||||
@@ -6,9 +6,8 @@ use crate::{
|
||||
rpc_types::{self, controller::BaseController},
|
||||
web::{
|
||||
CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest,
|
||||
DeleteNetworkInstanceResponse, GetConfigRequest, GetConfigResponse,
|
||||
ListNetworkInstanceRequest, ListNetworkInstanceResponse, NetworkInstanceRunningInfoMap,
|
||||
ReplaceConfigRequest, ReplaceConfigResponse, RetainNetworkInstanceRequest,
|
||||
DeleteNetworkInstanceResponse, ListNetworkInstanceRequest, ListNetworkInstanceResponse,
|
||||
NetworkInstanceRunningInfoMap, RetainNetworkInstanceRequest,
|
||||
RetainNetworkInstanceResponse, RunNetworkInstanceRequest, RunNetworkInstanceResponse,
|
||||
ValidateConfigRequest, ValidateConfigResponse, WebClientService,
|
||||
},
|
||||
@@ -154,79 +153,4 @@ impl WebClientService for Controller {
|
||||
remain_inst_ids: remain_inst_ids.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
// rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {}
|
||||
async fn get_config(
|
||||
&self,
|
||||
_: BaseController,
|
||||
req: GetConfigRequest,
|
||||
) -> Result<GetConfigResponse, rpc_types::error::Error> {
|
||||
let inst_id = req.inst_id.ok_or_else(|| {
|
||||
rpc_types::error::Error::ExecutionError(
|
||||
anyhow::anyhow!("instance_id is required").into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let config = self
|
||||
.manager
|
||||
.get_network_config(&inst_id.into())
|
||||
.ok_or_else(|| {
|
||||
rpc_types::error::Error::ExecutionError(
|
||||
anyhow::anyhow!("instance {} not found", inst_id).into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Get the NetworkConfig from the instance
|
||||
let network_config = crate::launcher::NetworkConfig::new_from_config(&config)?;
|
||||
|
||||
// Get the TOML config string
|
||||
let toml_config = config.dump();
|
||||
|
||||
Ok(GetConfigResponse {
|
||||
config: Some(network_config),
|
||||
toml_config,
|
||||
})
|
||||
}
|
||||
|
||||
// rpc ReplaceConfig(ReplaceConfigRequest) returns (ReplaceConfigResponse) {}
|
||||
async fn replace_config(
|
||||
&self,
|
||||
_: BaseController,
|
||||
req: ReplaceConfigRequest,
|
||||
) -> Result<ReplaceConfigResponse, rpc_types::error::Error> {
|
||||
let inst_id = req.inst_id.ok_or_else(|| {
|
||||
rpc_types::error::Error::ExecutionError(
|
||||
anyhow::anyhow!("instance_id is required").into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let new_config = req.config.ok_or_else(|| {
|
||||
rpc_types::error::Error::ExecutionError(anyhow::anyhow!("config is required").into())
|
||||
})?;
|
||||
|
||||
// Generate the TomlConfigLoader from NetworkConfig
|
||||
let new_toml_config = new_config.gen_config()?;
|
||||
|
||||
// Replace the configuration
|
||||
match self
|
||||
.manager
|
||||
.replace_network_config(&inst_id.into(), new_toml_config)
|
||||
{
|
||||
Ok(()) => {
|
||||
println!("instance {} config replaced successfully", inst_id);
|
||||
Ok(ReplaceConfigResponse {
|
||||
success: true,
|
||||
error_msg: None,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to replace config for instance {}: {}", inst_id, e);
|
||||
eprintln!("{}", error_msg);
|
||||
Ok(ReplaceConfigResponse {
|
||||
success: false,
|
||||
error_msg: Some(error_msg),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user