mirror of
				https://github.com/EasyTier/EasyTier.git
				synced 2025-10-31 20:12:53 +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" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -109,8 +109,6 @@ enum SubCommand { | |||||||
|     Stats(StatsArgs), |     Stats(StatsArgs), | ||||||
|     #[command(about = "manage logger configuration")] |     #[command(about = "manage logger configuration")] | ||||||
|     Logger(LoggerArgs), |     Logger(LoggerArgs), | ||||||
|     #[command(about = "manage network instance configuration")] |  | ||||||
|     Config(ConfigArgs), |  | ||||||
|     #[command(about = t!("core_clap.generate_completions").to_string())] |     #[command(about = t!("core_clap.generate_completions").to_string())] | ||||||
|     GenAutocomplete { shell: Shell }, |     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)] | #[derive(Args, Debug)] | ||||||
| struct ServiceArgs { | struct ServiceArgs { | ||||||
|     #[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")] |     #[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")] | ||||||
| @@ -1305,68 +1286,6 @@ impl CommandHandler<'_> { | |||||||
|         } |         } | ||||||
|         Ok(ports) |         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)] | #[derive(Debug)] | ||||||
| @@ -2178,14 +2097,6 @@ async fn main() -> Result<(), Error> { | |||||||
|                 handler.handle_logger_set(level).await?; |                 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 } => { |         SubCommand::GenAutocomplete { shell } => { | ||||||
|             let mut cmd = Cli::command(); |             let mut cmd = Cli::command(); | ||||||
|             easytier::print_completions(shell, &mut cmd, "easytier-cli"); |             easytier::print_completions(shell, &mut cmd, "easytier-cli"); | ||||||
|   | |||||||
| @@ -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) | ||||||
|  | } | ||||||
| @@ -140,47 +140,6 @@ impl NetworkInstanceManager { | |||||||
|             .and_then(|instance| instance.value().get_running_info()) |             .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> { |     pub fn list_network_instance_ids(&self) -> Vec<uuid::Uuid> { | ||||||
|         self.instance_map.iter().map(|item| *item.key()).collect() |         self.instance_map.iter().map(|item| *item.key()).collect() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -460,10 +460,6 @@ impl NetworkInstance { | |||||||
|             None |             None | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn get_config(&self) -> TomlConfigLoader { |  | ||||||
|         self.config.clone() |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn add_proxy_network_to_config( | pub fn add_proxy_network_to_config( | ||||||
|   | |||||||
| @@ -178,25 +178,6 @@ message DeleteNetworkInstanceResponse { | |||||||
|   repeated common.UUID remain_inst_ids = 1; |   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 { | service WebClientService { | ||||||
|   rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse) {} |   rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse) {} | ||||||
|   rpc RunNetworkInstance(RunNetworkInstanceRequest) returns (RunNetworkInstanceResponse) {} |   rpc RunNetworkInstance(RunNetworkInstanceRequest) returns (RunNetworkInstanceResponse) {} | ||||||
| @@ -204,6 +185,4 @@ service WebClientService { | |||||||
|   rpc CollectNetworkInfo(CollectNetworkInfoRequest) returns (CollectNetworkInfoResponse) {} |   rpc CollectNetworkInfo(CollectNetworkInfoRequest) returns (CollectNetworkInfoResponse) {} | ||||||
|   rpc ListNetworkInstance(ListNetworkInstanceRequest) returns (ListNetworkInstanceResponse) {} |   rpc ListNetworkInstance(ListNetworkInstanceRequest) returns (ListNetworkInstanceResponse) {} | ||||||
|   rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest) returns (DeleteNetworkInstanceResponse) {} |   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 { |         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) | ||||||
|   | |||||||
| @@ -6,9 +6,8 @@ use crate::{ | |||||||
|         rpc_types::{self, controller::BaseController}, |         rpc_types::{self, controller::BaseController}, | ||||||
|         web::{ |         web::{ | ||||||
|             CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest, |             CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest, | ||||||
|             DeleteNetworkInstanceResponse, GetConfigRequest, GetConfigResponse, |             DeleteNetworkInstanceResponse, ListNetworkInstanceRequest, ListNetworkInstanceResponse, | ||||||
|             ListNetworkInstanceRequest, ListNetworkInstanceResponse, NetworkInstanceRunningInfoMap, |             NetworkInstanceRunningInfoMap, RetainNetworkInstanceRequest, | ||||||
|             ReplaceConfigRequest, ReplaceConfigResponse, RetainNetworkInstanceRequest, |  | ||||||
|             RetainNetworkInstanceResponse, RunNetworkInstanceRequest, RunNetworkInstanceResponse, |             RetainNetworkInstanceResponse, RunNetworkInstanceRequest, RunNetworkInstanceResponse, | ||||||
|             ValidateConfigRequest, ValidateConfigResponse, WebClientService, |             ValidateConfigRequest, ValidateConfigResponse, WebClientService, | ||||||
|         }, |         }, | ||||||
| @@ -154,79 +153,4 @@ impl WebClientService for Controller { | |||||||
|             remain_inst_ids: remain_inst_ids.into_iter().map(Into::into).collect(), |             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