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