├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── add_any_port.rs ├── add_port.rs ├── add_remove.rs ├── aio.rs ├── external_ip.rs └── remove_port.rs ├── rustfmt.toml └── src ├── aio ├── gateway.rs ├── mod.rs ├── search.rs └── soap.rs ├── common ├── messages.rs ├── mod.rs ├── options.rs └── parsing.rs ├── errors.rs ├── gateway.rs ├── lib.rs └── search.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test & Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | - run: cargo test 18 | shell: bash 19 | - run: cargo test --features aio 20 | shell: bash 21 | 22 | clippy: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: stable 29 | - run: rustup component add clippy 30 | - uses: actions-rs/clippy-check@v1 31 | with: 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | args: --all-features --all-targets 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | # Executables 7 | *.exe 8 | # Generated by Cargo 9 | /target/ 10 | Cargo.lock 11 | # Visual Studio Code files 12 | .vscode/ 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Simon Bernier St-Pierre "] 3 | description = "Internet Gateway Protocol client" 4 | documentation = "https://docs.rs/igd/" 5 | edition = "2018" 6 | homepage = "https://github.com/sbstp/rust-igd" 7 | keywords = ["igd", "upnp"] 8 | license = "MIT" 9 | name = "igd" 10 | readme = "README.md" 11 | repository = "https://github.com/sbstp/rust-igd" 12 | version = "0.12.1" 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | 17 | [dependencies] 18 | attohttpc = {version = "0.16", default-features = false} 19 | bytes = {version = "1", optional = true} 20 | futures = {version = "0.3", optional = true} 21 | http = {version = "0.2", optional = true} 22 | log = "0.4" 23 | rand = "0.8" 24 | tokio = {version = "1", optional = true, features = ["net"]} 25 | url = "2" 26 | xmltree = "0.10" 27 | 28 | [dependencies.hyper] 29 | default-features = false 30 | features = ["client", "http1", "http2", "runtime"] 31 | optional = true 32 | version = "0.14" 33 | 34 | [dev-dependencies] 35 | simplelog = "0.9" 36 | tokio = {version = "1", features = ["full"]} 37 | 38 | [features] 39 | aio = ["futures", "tokio", "hyper", "bytes", "http"] 40 | default = [] 41 | 42 | [[example]] 43 | name = "add_any_port" 44 | 45 | [[example]] 46 | name = "add_port" 47 | 48 | [[example]] 49 | name = "add_remove" 50 | 51 | [[example]] 52 | name = "aio" 53 | required-features = ["aio"] 54 | 55 | [[example]] 56 | name = "external_ip" 57 | 58 | [[example]] 59 | name = "remove_port" 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Simon Bernier St-Pierre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Internet Gateway Device client 2 | 3 | This is a simple library that communicates with an UPNP enabled gateway device (a router). Contributions and feedback are welcome. 4 | At the moment, you can search for the gateway, request the gateway's external address and, add/remove port mappings. See the `examples/` folder for a demo. 5 | 6 | Contributions are welcome! This is pretty delicate to test, please submit an issue if you have trouble using this. 7 | 8 | * [Documentation](https://docs.rs/igd/) 9 | * [Repository](https://github.com/sbstp/rust-igd) 10 | * [Crates.io](https://crates.io/crates/igd) 11 | 12 | ## License 13 | MIT 14 | -------------------------------------------------------------------------------- /examples/add_any_port.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, SocketAddrV4}; 2 | 3 | extern crate igd; 4 | 5 | fn main() { 6 | match igd::search_gateway(Default::default()) { 7 | Err(ref err) => println!("Error: {}", err), 8 | Ok(gateway) => { 9 | let local_addr = match std::env::args().nth(1) { 10 | Some(local_addr) => local_addr, 11 | None => panic!("Expected IP address (cargo run --example add_any_port )"), 12 | }; 13 | let local_addr = local_addr.parse::().unwrap(); 14 | let local_addr = SocketAddrV4::new(local_addr, 8080u16); 15 | 16 | match gateway.add_any_port(igd::PortMappingProtocol::TCP, local_addr, 60, "add_port example") { 17 | Err(ref err) => { 18 | println!("There was an error! {}", err); 19 | } 20 | Ok(port) => { 21 | println!("It worked! Got port {}", port); 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/add_port.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, SocketAddrV4}; 2 | 3 | extern crate igd; 4 | 5 | fn main() { 6 | match igd::search_gateway(Default::default()) { 7 | Err(ref err) => println!("Error: {}", err), 8 | Ok(gateway) => { 9 | let local_addr = match std::env::args().nth(1) { 10 | Some(local_addr) => local_addr, 11 | None => panic!("Expected IP address (cargo run --example add_port )"), 12 | }; 13 | let local_addr = local_addr.parse::().unwrap(); 14 | let local_addr = SocketAddrV4::new(local_addr, 8080u16); 15 | 16 | match gateway.add_port(igd::PortMappingProtocol::TCP, 80, local_addr, 60, "add_port example") { 17 | Err(ref err) => { 18 | println!("There was an error! {}", err); 19 | } 20 | Ok(()) => { 21 | println!("It worked"); 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/add_remove.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::net::SocketAddrV4; 3 | 4 | extern crate igd; 5 | 6 | fn main() { 7 | match igd::search_gateway(Default::default()) { 8 | Err(ref err) => match *err { 9 | igd::SearchError::IoError(ref ioe) => println!("IoError: {}", ioe), 10 | _ => println!("{:?}", err), 11 | }, 12 | Ok(gateway) => { 13 | let args: Vec<_> = env::args().collect(); 14 | if args.len() != 4 { 15 | println!("Usage: add_remove "); 16 | return; 17 | } 18 | let local_ip = args[1].parse().expect("Invalid IP address"); 19 | let local_port = args[2].parse().expect("Invalid local port"); 20 | let remote_port = args[3].parse().expect("Invalid remote port"); 21 | 22 | let local_addr = SocketAddrV4::new(local_ip, local_port); 23 | 24 | match gateway.add_port(igd::PortMappingProtocol::TCP, remote_port, local_addr, 60, "crust") { 25 | Err(ref err) => println!("{:?}", err), 26 | Ok(()) => { 27 | println!("AddPortMapping successful."); 28 | match gateway.remove_port(igd::PortMappingProtocol::TCP, remote_port) { 29 | Err(ref err) => println!("Error removing: {:?}", err), 30 | Ok(_) => println!("DeletePortMapping successful."), 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/aio.rs: -------------------------------------------------------------------------------- 1 | //! IGD async API example. 2 | //! 3 | //! It demonstrates how to: 4 | //! * get external IP 5 | //! * add port mappings 6 | //! * remove port mappings 7 | //! 8 | //! If everything works fine, 2 port mappings are added, 1 removed and we're left with single 9 | //! port mapping: External 1234 ---> 4321 Internal 10 | 11 | use std::env; 12 | use std::net::SocketAddrV4; 13 | 14 | use igd::aio::search_gateway; 15 | use igd::PortMappingProtocol; 16 | use simplelog::{Config as LogConfig, LevelFilter, SimpleLogger}; 17 | 18 | #[tokio::main] 19 | async fn main() { 20 | let ip = match env::args().nth(1) { 21 | Some(ip) => ip, 22 | None => { 23 | println!("Local socket address is missing!"); 24 | println!("This example requires a socket address representing the local machine and the port to bind to as an argument"); 25 | println!("Example: target/debug/examples/io 192.168.0.198:4321"); 26 | println!("Example: cargo run --features aio --example aio -- 192.168.0.198:4321"); 27 | return; 28 | } 29 | }; 30 | let ip: SocketAddrV4 = ip.parse().expect("Invalid socket address"); 31 | 32 | let _ = SimpleLogger::init(LevelFilter::Debug, LogConfig::default()); 33 | 34 | let gateway = match search_gateway(Default::default()).await { 35 | Ok(g) => g, 36 | Err(err) => return println!("Faild to find IGD: {}", err), 37 | }; 38 | let pub_ip = match gateway.get_external_ip().await { 39 | Ok(ip) => ip, 40 | Err(err) => return println!("Failed to get external IP: {}", err), 41 | }; 42 | println!("Our public IP is {}", pub_ip); 43 | if let Err(e) = gateway 44 | .add_port(PortMappingProtocol::TCP, 1234, ip, 120, "rust-igd-async-example") 45 | .await 46 | { 47 | println!("Failed to add port mapping: {}", e); 48 | } 49 | println!("New port mapping was successfully added."); 50 | 51 | if let Err(e) = gateway 52 | .add_port(PortMappingProtocol::TCP, 2345, ip, 120, "rust-igd-async-example") 53 | .await 54 | { 55 | println!("Failed to add port mapping: {}", e); 56 | } 57 | println!("New port mapping was successfully added."); 58 | 59 | if gateway.remove_port(PortMappingProtocol::TCP, 2345).await.is_err() { 60 | println!("Port mapping was not successfully removed"); 61 | } else { 62 | println!("Port was removed."); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/external_ip.rs: -------------------------------------------------------------------------------- 1 | extern crate igd; 2 | 3 | fn main() { 4 | match igd::search_gateway(Default::default()) { 5 | Err(ref err) => println!("Error: {}", err), 6 | Ok(gateway) => match gateway.get_external_ip() { 7 | Err(ref err) => { 8 | println!("There was an error! {}", err); 9 | } 10 | Ok(ext_addr) => { 11 | println!("Local gateway: {}, External ip address: {}", gateway, ext_addr); 12 | } 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/remove_port.rs: -------------------------------------------------------------------------------- 1 | extern crate igd; 2 | 3 | fn main() { 4 | match igd::search_gateway(Default::default()) { 5 | Err(ref err) => println!("Error: {}", err), 6 | Ok(gateway) => match gateway.remove_port(igd::PortMappingProtocol::TCP, 80) { 7 | Err(ref err) => { 8 | println!("There was an error! {}", err); 9 | } 10 | Ok(()) => { 11 | println!("It worked"); 12 | } 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | -------------------------------------------------------------------------------- /src/aio/gateway.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt; 3 | use std::hash::{Hash, Hasher}; 4 | use std::net::{Ipv4Addr, SocketAddrV4}; 5 | 6 | use super::soap; 7 | use crate::errors::{self, AddAnyPortError, AddPortError, GetExternalIpError, RemovePortError, RequestError}; 8 | 9 | use crate::common::{self, messages, parsing, parsing::RequestReponse}; 10 | use crate::PortMappingProtocol; 11 | 12 | /// This structure represents a gateway found by the search functions. 13 | #[derive(Clone, Debug)] 14 | pub struct Gateway { 15 | /// Socket address of the gateway 16 | pub addr: SocketAddrV4, 17 | /// Root url of the device 18 | pub root_url: String, 19 | /// Control url of the device 20 | pub control_url: String, 21 | /// Url to get schema data from 22 | pub control_schema_url: String, 23 | /// Control schema for all actions 24 | pub control_schema: HashMap>, 25 | } 26 | 27 | impl Gateway { 28 | async fn perform_request(&self, header: &str, body: &str, ok: &str) -> Result { 29 | let url = format!("{}", self); 30 | let text = soap::send_async(&url, soap::Action::new(header), body).await?; 31 | parsing::parse_response(text, ok) 32 | } 33 | 34 | /// Get the external IP address of the gateway in a tokio compatible way 35 | pub async fn get_external_ip(&self) -> Result { 36 | let result = self 37 | .perform_request( 38 | messages::GET_EXTERNAL_IP_HEADER, 39 | &messages::format_get_external_ip_message(), 40 | "GetExternalIPAddressResponse", 41 | ) 42 | .await; 43 | parsing::parse_get_external_ip_response(result) 44 | } 45 | 46 | /// Get an external socket address with our external ip and any port. This is a convenience 47 | /// function that calls `get_external_ip` followed by `add_any_port` 48 | /// 49 | /// The local_addr is the address where the traffic is sent to. 50 | /// The lease_duration parameter is in seconds. A value of 0 is infinite. 51 | /// 52 | /// # Returns 53 | /// 54 | /// The external address that was mapped on success. Otherwise an error. 55 | pub async fn get_any_address( 56 | &self, 57 | protocol: PortMappingProtocol, 58 | local_addr: SocketAddrV4, 59 | lease_duration: u32, 60 | description: &str, 61 | ) -> Result { 62 | let description = description.to_owned(); 63 | let ip = self.get_external_ip().await?; 64 | let port = self 65 | .add_any_port(protocol, local_addr, lease_duration, &description) 66 | .await?; 67 | Ok(SocketAddrV4::new(ip, port)) 68 | } 69 | 70 | /// Add a port mapping.with any external port. 71 | /// 72 | /// The local_addr is the address where the traffic is sent to. 73 | /// The lease_duration parameter is in seconds. A value of 0 is infinite. 74 | /// 75 | /// # Returns 76 | /// 77 | /// The external port that was mapped on success. Otherwise an error. 78 | pub async fn add_any_port( 79 | &self, 80 | protocol: PortMappingProtocol, 81 | local_addr: SocketAddrV4, 82 | lease_duration: u32, 83 | description: &str, 84 | ) -> Result { 85 | // This function first attempts to call AddAnyPortMapping on the IGD with a random port 86 | // number. If that fails due to the method being unknown it attempts to call AddPortMapping 87 | // instead with a random port number. If that fails due to ConflictInMappingEntry it retrys 88 | // with another port up to a maximum of 20 times. If it fails due to SamePortValuesRequired 89 | // it retrys once with the same port values. 90 | 91 | if local_addr.port() == 0 { 92 | return Err(AddAnyPortError::InternalPortZeroInvalid); 93 | } 94 | 95 | let schema = self.control_schema.get("AddAnyPortMapping"); 96 | if let Some(schema) = schema { 97 | let external_port = common::random_port(); 98 | 99 | let description = description.to_owned(); 100 | 101 | let resp = self 102 | .perform_request( 103 | messages::ADD_ANY_PORT_MAPPING_HEADER, 104 | &messages::format_add_any_port_mapping_message( 105 | schema, 106 | protocol, 107 | external_port, 108 | local_addr, 109 | lease_duration, 110 | &description, 111 | ), 112 | "AddAnyPortMappingResponse", 113 | ) 114 | .await; 115 | parsing::parse_add_any_port_mapping_response(resp) 116 | } else { 117 | // The router does not have the AddAnyPortMapping method. 118 | // Fall back to using AddPortMapping with a random port. 119 | let gateway = self.clone(); 120 | gateway 121 | .retry_add_random_port_mapping(protocol, local_addr, lease_duration, &description) 122 | .await 123 | } 124 | } 125 | 126 | async fn retry_add_random_port_mapping( 127 | &self, 128 | protocol: PortMappingProtocol, 129 | local_addr: SocketAddrV4, 130 | lease_duration: u32, 131 | description: &str, 132 | ) -> Result { 133 | for _ in 0u8..20u8 { 134 | match self 135 | .add_random_port_mapping(protocol, local_addr, lease_duration, &description) 136 | .await 137 | { 138 | Ok(port) => return Ok(port), 139 | Err(AddAnyPortError::NoPortsAvailable) => continue, 140 | e => return e, 141 | } 142 | } 143 | Err(AddAnyPortError::NoPortsAvailable) 144 | } 145 | 146 | async fn add_random_port_mapping( 147 | &self, 148 | protocol: PortMappingProtocol, 149 | local_addr: SocketAddrV4, 150 | lease_duration: u32, 151 | description: &str, 152 | ) -> Result { 153 | let description = description.to_owned(); 154 | let gateway = self.clone(); 155 | 156 | let external_port = common::random_port(); 157 | let res = self 158 | .add_port_mapping(protocol, external_port, local_addr, lease_duration, &description) 159 | .await; 160 | 161 | match res { 162 | Ok(_) => Ok(external_port), 163 | Err(err) => match parsing::convert_add_random_port_mapping_error(err) { 164 | Some(err) => Err(err), 165 | None => { 166 | gateway 167 | .add_same_port_mapping(protocol, local_addr, lease_duration, &description) 168 | .await 169 | } 170 | }, 171 | } 172 | } 173 | 174 | async fn add_same_port_mapping( 175 | &self, 176 | protocol: PortMappingProtocol, 177 | local_addr: SocketAddrV4, 178 | lease_duration: u32, 179 | description: &str, 180 | ) -> Result { 181 | let res = self 182 | .add_port_mapping(protocol, local_addr.port(), local_addr, lease_duration, description) 183 | .await; 184 | match res { 185 | Ok(_) => Ok(local_addr.port()), 186 | Err(err) => Err(parsing::convert_add_same_port_mapping_error(err)), 187 | } 188 | } 189 | 190 | async fn add_port_mapping( 191 | &self, 192 | protocol: PortMappingProtocol, 193 | external_port: u16, 194 | local_addr: SocketAddrV4, 195 | lease_duration: u32, 196 | description: &str, 197 | ) -> Result<(), RequestError> { 198 | self.perform_request( 199 | messages::ADD_PORT_MAPPING_HEADER, 200 | &messages::format_add_port_mapping_message( 201 | self.control_schema 202 | .get("AddPortMapping") 203 | .ok_or_else(|| RequestError::UnsupportedAction("AddPortMapping".to_string()))?, 204 | protocol, 205 | external_port, 206 | local_addr, 207 | lease_duration, 208 | description, 209 | ), 210 | "AddPortMappingResponse", 211 | ) 212 | .await?; 213 | Ok(()) 214 | } 215 | 216 | /// Add a port mapping. 217 | /// 218 | /// The local_addr is the address where the traffic is sent to. 219 | /// The lease_duration parameter is in seconds. A value of 0 is infinite. 220 | pub async fn add_port( 221 | &self, 222 | protocol: PortMappingProtocol, 223 | external_port: u16, 224 | local_addr: SocketAddrV4, 225 | lease_duration: u32, 226 | description: &str, 227 | ) -> Result<(), AddPortError> { 228 | if external_port == 0 { 229 | return Err(AddPortError::ExternalPortZeroInvalid); 230 | } 231 | if local_addr.port() == 0 { 232 | return Err(AddPortError::InternalPortZeroInvalid); 233 | } 234 | 235 | let res = self 236 | .add_port_mapping(protocol, external_port, local_addr, lease_duration, description) 237 | .await; 238 | if let Err(err) = res { 239 | return Err(parsing::convert_add_port_error(err)); 240 | }; 241 | Ok(()) 242 | } 243 | 244 | /// Remove a port mapping. 245 | pub async fn remove_port(&self, protocol: PortMappingProtocol, external_port: u16) -> Result<(), RemovePortError> { 246 | let res = self 247 | .perform_request( 248 | messages::DELETE_PORT_MAPPING_HEADER, 249 | &messages::format_delete_port_message( 250 | self.control_schema 251 | .get("DeletePortMapping") 252 | .ok_or_else(|| RemovePortError::RequestError(RequestError::UnsupportedAction( 253 | "DeletePortMapping".to_string(), 254 | )))?, 255 | protocol, 256 | external_port, 257 | ), 258 | "DeletePortMappingResponse", 259 | ) 260 | .await; 261 | parsing::parse_delete_port_mapping_response(res) 262 | } 263 | 264 | /// Get one port mapping entry 265 | /// 266 | /// Gets one port mapping entry by its index. 267 | /// Not all existing port mappings might be visible to this client. 268 | /// If the index is out of bound, GetGenericPortMappingEntryError::SpecifiedArrayIndexInvalid will be returned 269 | pub async fn get_generic_port_mapping_entry( 270 | &self, 271 | index: u32, 272 | ) -> Result { 273 | let result = self 274 | .perform_request( 275 | messages::GET_GENERIC_PORT_MAPPING_ENTRY, 276 | &messages::formate_get_generic_port_mapping_entry_message(index), 277 | "GetGenericPortMappingEntryResponse", 278 | ) 279 | .await; 280 | parsing::parse_get_generic_port_mapping_entry(result) 281 | } 282 | } 283 | 284 | impl fmt::Display for Gateway { 285 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 286 | write!(f, "http://{}{}", self.addr, self.control_url) 287 | } 288 | } 289 | 290 | impl PartialEq for Gateway { 291 | fn eq(&self, other: &Gateway) -> bool { 292 | self.addr == other.addr && self.control_url == other.control_url 293 | } 294 | } 295 | 296 | impl Eq for Gateway {} 297 | 298 | impl Hash for Gateway { 299 | fn hash(&self, state: &mut H) { 300 | self.addr.hash(state); 301 | self.control_url.hash(state); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/aio/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module implements the same features as the main crate, but using async io. 2 | 3 | mod gateway; 4 | mod search; 5 | mod soap; 6 | 7 | pub use self::gateway::Gateway; 8 | pub use self::search::search_gateway; 9 | -------------------------------------------------------------------------------- /src/aio/search.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::net::SocketAddr; 3 | 4 | use futures::prelude::*; 5 | use hyper::Client; 6 | use tokio::net::UdpSocket; 7 | use tokio::time::timeout; 8 | 9 | use crate::aio::Gateway; 10 | use crate::common::{messages, parsing, SearchOptions}; 11 | use crate::errors::SearchError; 12 | 13 | const MAX_RESPONSE_SIZE: usize = 1500; 14 | 15 | /// Search for a gateway with the provided options 16 | pub async fn search_gateway(options: SearchOptions) -> Result { 17 | // Create socket for future calls 18 | let mut socket = UdpSocket::bind(&options.bind_addr).await?; 19 | 20 | send_search_request(&mut socket, options.broadcast_address).await?; 21 | 22 | let search_response = receive_search_response(&mut socket); 23 | 24 | // Receive search response, optionally with a timeout 25 | let (response_body, from) = match options.timeout { 26 | Some(t) => timeout(t, search_response).await?, 27 | None => search_response.await, 28 | }?; 29 | 30 | let (addr, root_url) = handle_broadcast_resp(&from, &response_body)?; 31 | 32 | let (control_schema_url, control_url) = get_control_urls(&addr, &root_url).await?; 33 | let control_schema = get_control_schemas(&addr, &control_schema_url).await?; 34 | 35 | let addr = match addr { 36 | SocketAddr::V4(a) => Ok(a), 37 | _ => { 38 | warn!("unsupported IPv6 gateway response from addr: {}", addr); 39 | Err(SearchError::InvalidResponse) 40 | } 41 | }?; 42 | 43 | Ok(Gateway { 44 | addr, 45 | root_url, 46 | control_url, 47 | control_schema_url, 48 | control_schema, 49 | }) 50 | } 51 | 52 | // Create a new search 53 | async fn send_search_request(socket: &mut UdpSocket, addr: SocketAddr) -> Result<(), SearchError> { 54 | debug!( 55 | "sending broadcast request to: {} on interface: {:?}", 56 | addr, 57 | socket.local_addr() 58 | ); 59 | socket 60 | .send_to(messages::SEARCH_REQUEST.as_bytes(), &addr) 61 | .map_ok(|_| ()) 62 | .map_err(SearchError::from) 63 | .await 64 | } 65 | 66 | async fn receive_search_response(socket: &mut UdpSocket) -> Result<(Vec, SocketAddr), SearchError> { 67 | let mut buff = [0u8; MAX_RESPONSE_SIZE]; 68 | let (n, from) = socket.recv_from(&mut buff).map_err(SearchError::from).await?; 69 | debug!("received broadcast response from: {}", from); 70 | Ok((buff[..n].to_vec(), from)) 71 | } 72 | 73 | // Handle a UDP response message 74 | fn handle_broadcast_resp(from: &SocketAddr, data: &[u8]) -> Result<(SocketAddr, String), SearchError> { 75 | debug!("handling broadcast response from: {}", from); 76 | 77 | // Convert response to text 78 | let text = std::str::from_utf8(&data).map_err(SearchError::from)?; 79 | 80 | // Parse socket address and path 81 | let (addr, root_url) = parsing::parse_search_result(text)?; 82 | 83 | Ok((SocketAddr::V4(addr), root_url)) 84 | } 85 | 86 | async fn get_control_urls(addr: &SocketAddr, path: &str) -> Result<(String, String), SearchError> { 87 | let uri = match format!("http://{}{}", addr, path).parse() { 88 | Ok(uri) => uri, 89 | Err(err) => return Err(SearchError::from(err)), 90 | }; 91 | 92 | debug!("requesting control url from: {}", uri); 93 | let client = Client::new(); 94 | let resp = hyper::body::to_bytes(client.get(uri).await?.into_body()) 95 | .map_err(SearchError::from) 96 | .await?; 97 | 98 | debug!("handling control response from: {}", addr); 99 | let c = std::io::Cursor::new(&resp); 100 | parsing::parse_control_urls(c) 101 | } 102 | 103 | async fn get_control_schemas( 104 | addr: &SocketAddr, 105 | control_schema_url: &str, 106 | ) -> Result>, SearchError> { 107 | let uri = match format!("http://{}{}", addr, control_schema_url).parse() { 108 | Ok(uri) => uri, 109 | Err(err) => return Err(SearchError::from(err)), 110 | }; 111 | 112 | debug!("requesting control schema from: {}", uri); 113 | let client = Client::new(); 114 | let resp = hyper::body::to_bytes(client.get(uri).await?.into_body()) 115 | .map_err(SearchError::from) 116 | .await?; 117 | 118 | debug!("handling schema response from: {}", addr); 119 | let c = std::io::Cursor::new(&resp); 120 | parsing::parse_schemas(c) 121 | } 122 | -------------------------------------------------------------------------------- /src/aio/soap.rs: -------------------------------------------------------------------------------- 1 | use hyper::{ 2 | header::{CONTENT_LENGTH, CONTENT_TYPE}, 3 | Body, Client, Request, 4 | }; 5 | 6 | use crate::errors::RequestError; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct Action(String); 10 | 11 | impl Action { 12 | pub fn new(action: &str) -> Action { 13 | Action(action.into()) 14 | } 15 | } 16 | 17 | const HEADER_NAME: &str = "SOAPAction"; 18 | 19 | pub async fn send_async(url: &str, action: Action, body: &str) -> Result { 20 | let client = Client::new(); 21 | 22 | let req = Request::builder() 23 | .uri(url) 24 | .method("POST") 25 | .header(HEADER_NAME, action.0) 26 | .header(CONTENT_TYPE, "text/xml") 27 | .header(CONTENT_LENGTH, body.len() as u64) 28 | .body(Body::from(body.to_string()))?; 29 | 30 | let resp = client.request(req).await?; 31 | let body = hyper::body::to_bytes(resp.into_body()).await?; 32 | let string = String::from_utf8(body.to_vec())?; 33 | Ok(string) 34 | } 35 | -------------------------------------------------------------------------------- /src/common/messages.rs: -------------------------------------------------------------------------------- 1 | use crate::PortMappingProtocol; 2 | use std::net::SocketAddrV4; 3 | 4 | // Content of the request. 5 | pub const SEARCH_REQUEST: &str = "M-SEARCH * HTTP/1.1\r 6 | Host:239.255.255.250:1900\r 7 | ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r 8 | Man:\"ssdp:discover\"\r 9 | MX:3\r\n\r\n"; 10 | 11 | pub const GET_EXTERNAL_IP_HEADER: &str = r#""urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress""#; 12 | 13 | pub const ADD_ANY_PORT_MAPPING_HEADER: &str = r#""urn:schemas-upnp-org:service:WANIPConnection:1#AddAnyPortMapping""#; 14 | 15 | pub const ADD_PORT_MAPPING_HEADER: &str = r#""urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping""#; 16 | 17 | pub const DELETE_PORT_MAPPING_HEADER: &str = r#""urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping""#; 18 | 19 | pub const GET_GENERIC_PORT_MAPPING_ENTRY: &str = 20 | r#""urn:schemas-upnp-org:service:WANIPConnection:1#GetGenericPortMappingEntry""#; 21 | 22 | const MESSAGE_HEAD: &str = r#" 23 | 24 | "#; 25 | 26 | const MESSAGE_TAIL: &str = r#" 27 | "#; 28 | 29 | fn format_message(body: String) -> String { 30 | format!("{}{}{}", MESSAGE_HEAD, body, MESSAGE_TAIL) 31 | } 32 | 33 | pub fn format_get_external_ip_message() -> String { 34 | r#" 35 | 36 | 37 | 38 | 39 | 40 | "# 41 | .into() 42 | } 43 | 44 | pub fn format_add_any_port_mapping_message( 45 | schema: &[String], 46 | protocol: PortMappingProtocol, 47 | external_port: u16, 48 | local_addr: SocketAddrV4, 49 | lease_duration: u32, 50 | description: &str, 51 | ) -> String { 52 | let args = schema 53 | .iter() 54 | .filter_map(|argument| { 55 | let value = match argument.as_str() { 56 | "NewEnabled" => 1.to_string(), 57 | "NewExternalPort" => external_port.to_string(), 58 | "NewInternalClient" => local_addr.ip().to_string(), 59 | "NewInternalPort" => local_addr.port().to_string(), 60 | "NewLeaseDuration" => lease_duration.to_string(), 61 | "NewPortMappingDescription" => description.to_string(), 62 | "NewProtocol" => protocol.to_string(), 63 | "NewRemoteHost" => "".to_string(), 64 | unknown => { 65 | warn!("Unknown argument: {}", unknown); 66 | return None; 67 | } 68 | }; 69 | Some(format!( 70 | "<{argument}>{value}", 71 | argument = argument, 72 | value = value 73 | )) 74 | }) 75 | .collect::>() 76 | .join("\n"); 77 | 78 | format_message(format!( 79 | r#" 80 | {} 81 | "#, 82 | args, 83 | )) 84 | } 85 | 86 | pub fn format_add_port_mapping_message( 87 | schema: &[String], 88 | protocol: PortMappingProtocol, 89 | external_port: u16, 90 | local_addr: SocketAddrV4, 91 | lease_duration: u32, 92 | description: &str, 93 | ) -> String { 94 | let args = schema 95 | .iter() 96 | .filter_map(|argument| { 97 | let value = match argument.as_str() { 98 | "NewEnabled" => 1.to_string(), 99 | "NewExternalPort" => external_port.to_string(), 100 | "NewInternalClient" => local_addr.ip().to_string(), 101 | "NewInternalPort" => local_addr.port().to_string(), 102 | "NewLeaseDuration" => lease_duration.to_string(), 103 | "NewPortMappingDescription" => description.to_string(), 104 | "NewProtocol" => protocol.to_string(), 105 | "NewRemoteHost" => "".to_string(), 106 | unknown => { 107 | warn!("Unknown argument: {}", unknown); 108 | return None; 109 | } 110 | }; 111 | Some(format!( 112 | "<{argument}>{value}", 113 | argument = argument, 114 | value = value 115 | )) 116 | }) 117 | .collect::>() 118 | .join("\n"); 119 | 120 | format_message(format!( 121 | r#" 122 | {} 123 | "#, 124 | args, 125 | )) 126 | } 127 | 128 | pub fn format_delete_port_message(schema: &[String], protocol: PortMappingProtocol, external_port: u16) -> String { 129 | let args = schema 130 | .iter() 131 | .filter_map(|argument| { 132 | let value = match argument.as_str() { 133 | "NewExternalPort" => external_port.to_string(), 134 | "NewProtocol" => protocol.to_string(), 135 | "NewRemoteHost" => "".to_string(), 136 | unknown => { 137 | warn!("Unknown argument: {}", unknown); 138 | return None; 139 | } 140 | }; 141 | Some(format!( 142 | "<{argument}>{value}", 143 | argument = argument, 144 | value = value 145 | )) 146 | }) 147 | .collect::>() 148 | .join("\n"); 149 | 150 | format_message(format!( 151 | r#" 152 | {} 153 | "#, 154 | args, 155 | )) 156 | } 157 | 158 | pub fn formate_get_generic_port_mapping_entry_message(port_mapping_index: u32) -> String { 159 | format_message(format!( 160 | r#" 161 | {} 162 | "#, 163 | port_mapping_index 164 | )) 165 | } 166 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod messages; 2 | pub mod options; 3 | pub mod parsing; 4 | 5 | pub use self::options::SearchOptions; 6 | 7 | use rand::{self, Rng}; 8 | 9 | pub fn random_port() -> u16 { 10 | rand::thread_rng().gen_range(32_768_u16..65_535_u16) 11 | } 12 | -------------------------------------------------------------------------------- /src/common/options.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; 2 | use std::time::Duration; 3 | 4 | /// Gateway search configuration 5 | /// 6 | /// SearchOptions::default() should suffice for most situations. 7 | /// 8 | /// # Example 9 | /// To customize only a few options you can use `Default::default()` or `SearchOptions::default()` and the 10 | /// [struct update syntax](https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax). 11 | /// ``` 12 | /// # use std::time::Duration; 13 | /// # use igd::SearchOptions; 14 | /// let opts = SearchOptions { 15 | /// timeout: Some(Duration::from_secs(60)), 16 | /// ..Default::default() 17 | /// }; 18 | /// ``` 19 | pub struct SearchOptions { 20 | /// Bind address for UDP socket (defaults to all `0.0.0.0`) 21 | pub bind_addr: SocketAddr, 22 | /// Broadcast address for discovery packets (defaults to `239.255.255.250:1900`) 23 | pub broadcast_address: SocketAddr, 24 | /// Timeout for a search iteration (defaults to 10s) 25 | pub timeout: Option, 26 | } 27 | 28 | impl Default for SearchOptions { 29 | fn default() -> Self { 30 | Self { 31 | bind_addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0)), 32 | broadcast_address: "239.255.255.250:1900".parse().unwrap(), 33 | timeout: Some(Duration::from_secs(10)), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/common/parsing.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io; 3 | use std::net::{Ipv4Addr, SocketAddrV4}; 4 | 5 | use url::Url; 6 | use xmltree::{self, Element}; 7 | 8 | use crate::errors::{ 9 | AddAnyPortError, AddPortError, GetExternalIpError, GetGenericPortMappingEntryError, RemovePortError, RequestError, 10 | SearchError, 11 | }; 12 | use crate::PortMappingProtocol; 13 | 14 | // Parse the result. 15 | pub fn parse_search_result(text: &str) -> Result<(SocketAddrV4, String), SearchError> { 16 | use SearchError::InvalidResponse; 17 | 18 | for line in text.lines() { 19 | let line = line.trim(); 20 | if line.to_ascii_lowercase().starts_with("location:") { 21 | if let Some(colon) = line.find(':') { 22 | let url_text = &line[colon + 1..].trim(); 23 | let url = Url::parse(url_text).map_err(|_| InvalidResponse)?; 24 | let addr: Ipv4Addr = url 25 | .host_str() 26 | .ok_or(InvalidResponse) 27 | .and_then(|s| s.parse().map_err(|_| InvalidResponse))?; 28 | let port: u16 = url.port_or_known_default().ok_or(InvalidResponse)?; 29 | 30 | return Ok((SocketAddrV4::new(addr, port), url.path().to_string())); 31 | } 32 | } 33 | } 34 | Err(InvalidResponse) 35 | } 36 | 37 | pub fn parse_control_urls(resp: R) -> Result<(String, String), SearchError> 38 | where 39 | R: io::Read, 40 | { 41 | let root = Element::parse(resp)?; 42 | 43 | let mut urls = root.children.iter().filter_map(|child| { 44 | let child = child.as_element()?; 45 | if child.name == "device" { 46 | Some(parse_device(child)?) 47 | } else { 48 | None 49 | } 50 | }); 51 | 52 | urls.next().ok_or(SearchError::InvalidResponse) 53 | } 54 | 55 | fn parse_device(device: &Element) -> Option<(String, String)> { 56 | let services = device 57 | .get_child("serviceList") 58 | .map(|service_list| { 59 | service_list 60 | .children 61 | .iter() 62 | .filter_map(|child| { 63 | let child = child.as_element()?; 64 | if child.name == "service" { 65 | parse_service(child) 66 | } else { 67 | None 68 | } 69 | }) 70 | .next() 71 | }) 72 | .flatten(); 73 | let devices = device.get_child("deviceList").map(parse_device_list).flatten(); 74 | services.or(devices) 75 | } 76 | 77 | fn parse_device_list(device_list: &Element) -> Option<(String, String)> { 78 | device_list 79 | .children 80 | .iter() 81 | .filter_map(|child| { 82 | let child = child.as_element()?; 83 | if child.name == "device" { 84 | parse_device(child) 85 | } else { 86 | None 87 | } 88 | }) 89 | .next() 90 | } 91 | 92 | fn parse_service(service: &Element) -> Option<(String, String)> { 93 | let service_type = service.get_child("serviceType")?; 94 | let service_type = service_type 95 | .get_text() 96 | .map(|s| s.into_owned()) 97 | .unwrap_or_else(|| "".into()); 98 | if [ 99 | "urn:schemas-upnp-org:service:WANPPPConnection:1", 100 | "urn:schemas-upnp-org:service:WANIPConnection:1", 101 | "urn:schemas-upnp-org:service:WANIPConnection:2", 102 | ] 103 | .contains(&service_type.as_str()) 104 | { 105 | let scpd_url = service.get_child("SCPDURL"); 106 | let control_url = service.get_child("controlURL"); 107 | if let (Some(scpd_url), Some(control_url)) = (scpd_url, control_url) { 108 | Some(( 109 | scpd_url.get_text().map(|s| s.into_owned()).unwrap_or_else(|| "".into()), 110 | control_url 111 | .get_text() 112 | .map(|s| s.into_owned()) 113 | .unwrap_or_else(|| "".into()), 114 | )) 115 | } else { 116 | None 117 | } 118 | } else { 119 | None 120 | } 121 | } 122 | 123 | pub fn parse_schemas(resp: R) -> Result>, SearchError> 124 | where 125 | R: io::Read, 126 | { 127 | let root = Element::parse(resp)?; 128 | 129 | let mut schema = root.children.iter().filter_map(|child| { 130 | let child = child.as_element()?; 131 | if child.name == "actionList" { 132 | parse_action_list(child) 133 | } else { 134 | None 135 | } 136 | }); 137 | 138 | schema.next().ok_or(SearchError::InvalidResponse) 139 | } 140 | 141 | fn parse_action_list(action_list: &Element) -> Option>> { 142 | Some( 143 | action_list 144 | .children 145 | .iter() 146 | .filter_map(|child| { 147 | let child = child.as_element()?; 148 | if child.name == "action" { 149 | parse_action(child) 150 | } else { 151 | None 152 | } 153 | }) 154 | .collect(), 155 | ) 156 | } 157 | 158 | fn parse_action(action: &Element) -> Option<(String, Vec)> { 159 | Some(( 160 | action.get_child("name")?.get_text()?.into_owned(), 161 | parse_argument_list(action.get_child("argumentList")?)?, 162 | )) 163 | } 164 | 165 | fn parse_argument_list(argument_list: &Element) -> Option> { 166 | Some( 167 | argument_list 168 | .children 169 | .iter() 170 | .filter_map(|child| { 171 | let child = child.as_element()?; 172 | if child.name == "argument" { 173 | parse_argument(child) 174 | } else { 175 | None 176 | } 177 | }) 178 | .collect(), 179 | ) 180 | } 181 | 182 | fn parse_argument(action: &Element) -> Option { 183 | if action.get_child("direction")?.get_text()?.into_owned().as_str() == "in" { 184 | Some(action.get_child("name")?.get_text()?.into_owned()) 185 | } else { 186 | None 187 | } 188 | } 189 | 190 | pub struct RequestReponse { 191 | text: String, 192 | xml: xmltree::Element, 193 | } 194 | 195 | pub type RequestResult = Result; 196 | 197 | pub fn parse_response(text: String, ok: &str) -> RequestResult { 198 | let mut xml = match xmltree::Element::parse(text.as_bytes()) { 199 | Ok(xml) => xml, 200 | Err(..) => return Err(RequestError::InvalidResponse(text)), 201 | }; 202 | let body = match xml.get_mut_child("Body") { 203 | Some(body) => body, 204 | None => return Err(RequestError::InvalidResponse(text)), 205 | }; 206 | if let Some(ok) = body.take_child(ok) { 207 | return Ok(RequestReponse { text, xml: ok }); 208 | } 209 | let upnp_error = match body 210 | .get_child("Fault") 211 | .and_then(|e| e.get_child("detail")) 212 | .and_then(|e| e.get_child("UPnPError")) 213 | { 214 | Some(upnp_error) => upnp_error, 215 | None => return Err(RequestError::InvalidResponse(text)), 216 | }; 217 | 218 | match ( 219 | upnp_error.get_child("errorCode"), 220 | upnp_error.get_child("errorDescription"), 221 | ) { 222 | (Some(e), Some(d)) => match (e.get_text().as_ref(), d.get_text().as_ref()) { 223 | (Some(et), Some(dt)) => match et.parse::() { 224 | Ok(en) => Err(RequestError::ErrorCode(en, From::from(&dt[..]))), 225 | Err(..) => Err(RequestError::InvalidResponse(text)), 226 | }, 227 | _ => Err(RequestError::InvalidResponse(text)), 228 | }, 229 | _ => Err(RequestError::InvalidResponse(text)), 230 | } 231 | } 232 | 233 | pub fn parse_get_external_ip_response(result: RequestResult) -> Result { 234 | match result { 235 | Ok(resp) => match resp 236 | .xml 237 | .get_child("NewExternalIPAddress") 238 | .and_then(|e| e.get_text()) 239 | .and_then(|t| t.parse::().ok()) 240 | { 241 | Some(ipv4_addr) => Ok(ipv4_addr), 242 | None => Err(GetExternalIpError::RequestError(RequestError::InvalidResponse( 243 | resp.text, 244 | ))), 245 | }, 246 | Err(RequestError::ErrorCode(606, _)) => Err(GetExternalIpError::ActionNotAuthorized), 247 | Err(e) => Err(GetExternalIpError::RequestError(e)), 248 | } 249 | } 250 | 251 | pub fn parse_add_any_port_mapping_response(result: RequestResult) -> Result { 252 | match result { 253 | Ok(resp) => { 254 | match resp 255 | .xml 256 | .get_child("NewReservedPort") 257 | .and_then(|e| e.get_text()) 258 | .and_then(|t| t.parse::().ok()) 259 | { 260 | Some(port) => Ok(port), 261 | None => Err(AddAnyPortError::RequestError(RequestError::InvalidResponse(resp.text))), 262 | } 263 | } 264 | Err(err) => Err(match err { 265 | RequestError::ErrorCode(605, _) => AddAnyPortError::DescriptionTooLong, 266 | RequestError::ErrorCode(606, _) => AddAnyPortError::ActionNotAuthorized, 267 | RequestError::ErrorCode(728, _) => AddAnyPortError::NoPortsAvailable, 268 | e => AddAnyPortError::RequestError(e), 269 | }), 270 | } 271 | } 272 | 273 | pub fn convert_add_random_port_mapping_error(error: RequestError) -> Option { 274 | match error { 275 | RequestError::ErrorCode(724, _) => None, 276 | RequestError::ErrorCode(605, _) => Some(AddAnyPortError::DescriptionTooLong), 277 | RequestError::ErrorCode(606, _) => Some(AddAnyPortError::ActionNotAuthorized), 278 | RequestError::ErrorCode(718, _) => Some(AddAnyPortError::NoPortsAvailable), 279 | RequestError::ErrorCode(725, _) => Some(AddAnyPortError::OnlyPermanentLeasesSupported), 280 | e => Some(AddAnyPortError::RequestError(e)), 281 | } 282 | } 283 | 284 | pub fn convert_add_same_port_mapping_error(error: RequestError) -> AddAnyPortError { 285 | match error { 286 | RequestError::ErrorCode(606, _) => AddAnyPortError::ActionNotAuthorized, 287 | RequestError::ErrorCode(718, _) => AddAnyPortError::ExternalPortInUse, 288 | RequestError::ErrorCode(725, _) => AddAnyPortError::OnlyPermanentLeasesSupported, 289 | e => AddAnyPortError::RequestError(e), 290 | } 291 | } 292 | 293 | pub fn convert_add_port_error(err: RequestError) -> AddPortError { 294 | match err { 295 | RequestError::ErrorCode(605, _) => AddPortError::DescriptionTooLong, 296 | RequestError::ErrorCode(606, _) => AddPortError::ActionNotAuthorized, 297 | RequestError::ErrorCode(718, _) => AddPortError::PortInUse, 298 | RequestError::ErrorCode(724, _) => AddPortError::SamePortValuesRequired, 299 | RequestError::ErrorCode(725, _) => AddPortError::OnlyPermanentLeasesSupported, 300 | e => AddPortError::RequestError(e), 301 | } 302 | } 303 | 304 | pub fn parse_delete_port_mapping_response(result: RequestResult) -> Result<(), RemovePortError> { 305 | match result { 306 | Ok(_) => Ok(()), 307 | Err(err) => Err(match err { 308 | RequestError::ErrorCode(606, _) => RemovePortError::ActionNotAuthorized, 309 | RequestError::ErrorCode(714, _) => RemovePortError::NoSuchPortMapping, 310 | e => RemovePortError::RequestError(e), 311 | }), 312 | } 313 | } 314 | 315 | /// One port mapping entry as returned by GetGenericPortMappingEntry 316 | pub struct PortMappingEntry { 317 | /// The remote host for which the mapping is valid 318 | /// Can be an IP address or a host name 319 | pub remote_host: String, 320 | /// The external port of the mapping 321 | pub external_port: u16, 322 | /// The protocol of the mapping 323 | pub protocol: PortMappingProtocol, 324 | /// The internal (local) port 325 | pub internal_port: u16, 326 | /// The internal client of the port mapping 327 | /// Can be an IP address or a host name 328 | pub internal_client: String, 329 | /// A flag whether this port mapping is enabled 330 | pub enabled: bool, 331 | /// A description for this port mapping 332 | pub port_mapping_description: String, 333 | /// The lease duration of this port mapping in seconds 334 | pub lease_duration: u32, 335 | } 336 | 337 | pub fn parse_get_generic_port_mapping_entry( 338 | result: RequestResult, 339 | ) -> Result { 340 | let response = result?; 341 | let xml = response.xml; 342 | let make_err = |msg: String| || GetGenericPortMappingEntryError::RequestError(RequestError::InvalidResponse(msg)); 343 | let extract_field = |field: &str| { 344 | xml.get_child(field) 345 | .ok_or_else(make_err(format!("{} is missing", field))) 346 | }; 347 | let remote_host = extract_field("NewRemoteHost")? 348 | .get_text() 349 | .map(|c| c.into_owned()) 350 | .unwrap_or_else(|| "".into()); 351 | let external_port = extract_field("NewExternalPort")? 352 | .get_text() 353 | .and_then(|t| t.parse::().ok()) 354 | .ok_or_else(make_err("Field NewExternalPort is invalid".into()))?; 355 | let protocol = match extract_field("NewProtocol")?.get_text() { 356 | Some(std::borrow::Cow::Borrowed("UDP")) => PortMappingProtocol::UDP, 357 | Some(std::borrow::Cow::Borrowed("TCP")) => PortMappingProtocol::TCP, 358 | _ => { 359 | return Err(GetGenericPortMappingEntryError::RequestError( 360 | RequestError::InvalidResponse("Field NewProtocol is invalid".into()), 361 | )) 362 | } 363 | }; 364 | let internal_port = extract_field("NewInternalPort")? 365 | .get_text() 366 | .and_then(|t| t.parse::().ok()) 367 | .ok_or_else(make_err("Field NewInternalPort is invalid".into()))?; 368 | let internal_client = extract_field("NewInternalClient")? 369 | .get_text() 370 | .map(|c| c.into_owned()) 371 | .ok_or_else(make_err("Field NewInternalClient is empty".into()))?; 372 | let enabled = match extract_field("NewEnabled")? 373 | .get_text() 374 | .and_then(|t| t.parse::().ok()) 375 | .ok_or_else(make_err("Field Enabled is invalid".into()))? 376 | { 377 | 0 => false, 378 | 1 => true, 379 | _ => { 380 | return Err(GetGenericPortMappingEntryError::RequestError( 381 | RequestError::InvalidResponse("Field NewEnabled is invalid".into()), 382 | )) 383 | } 384 | }; 385 | let port_mapping_description = extract_field("NewPortMappingDescription")? 386 | .get_text() 387 | .map(|c| c.into_owned()) 388 | .unwrap_or_else(|| "".into()); 389 | let lease_duration = extract_field("NewLeaseDuration")? 390 | .get_text() 391 | .and_then(|t| t.parse::().ok()) 392 | .ok_or_else(make_err("Field NewLeaseDuration is invalid".into()))?; 393 | Ok(PortMappingEntry { 394 | remote_host, 395 | external_port, 396 | protocol, 397 | internal_port, 398 | internal_client, 399 | enabled, 400 | port_mapping_description, 401 | lease_duration, 402 | }) 403 | } 404 | 405 | #[test] 406 | fn test_parse_search_result_case_insensitivity() { 407 | assert!(parse_search_result("location:http://0.0.0.0:0/control_url").is_ok()); 408 | assert!(parse_search_result("LOCATION:http://0.0.0.0:0/control_url").is_ok()); 409 | } 410 | 411 | #[test] 412 | fn test_parse_search_result_ok() { 413 | let result = parse_search_result("location:http://0.0.0.0:0/control_url").unwrap(); 414 | assert_eq!(result.0.ip(), &Ipv4Addr::new(0, 0, 0, 0)); 415 | assert_eq!(result.0.port(), 0); 416 | assert_eq!(&result.1[..], "/control_url"); 417 | } 418 | 419 | #[test] 420 | fn test_parse_search_result_fail() { 421 | assert!(parse_search_result("content-type:http://0.0.0.0:0/control_url").is_err()); 422 | } 423 | 424 | #[test] 425 | fn test_parse_device1() { 426 | let text = r#" 427 | 428 | 429 | 1 430 | 0 431 | 432 | 433 | urn:schemas-upnp-org:device:InternetGatewayDevice:1 434 | 435 | 436 | 437 | 438 | 439 | 1 440 | 00000000 441 | 442 | 443 | 444 | urn:schemas-upnp-org:service:Layer3Forwarding:1 445 | urn:upnp-org:serviceId:Layer3Forwarding1 446 | /ctl/L3F 447 | /evt/L3F 448 | /L3F.xml 449 | 450 | 451 | 452 | 453 | urn:schemas-upnp-org:device:WANDevice:1 454 | WANDevice 455 | MiniUPnP 456 | http://miniupnp.free.fr/ 457 | WAN Device 458 | WAN Device 459 | 20180615 460 | http://miniupnp.free.fr/ 461 | 00000000 462 | uuid:804e2e56-7bfe-4733-bae0-04bf6d569692 463 | MINIUPNPD 464 | 465 | 466 | urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 467 | urn:upnp-org:serviceId:WANCommonIFC1 468 | /ctl/CmnIfCfg 469 | /evt/CmnIfCfg 470 | /WANCfg.xml 471 | 472 | 473 | 474 | 475 | urn:schemas-upnp-org:device:WANConnectionDevice:1 476 | WANConnectionDevice 477 | MiniUPnP 478 | http://miniupnp.free.fr/ 479 | MiniUPnP daemon 480 | MiniUPnPd 481 | 20180615 482 | http://miniupnp.free.fr/ 483 | 00000000 484 | uuid:804e2e56-7bfe-4733-bae0-04bf6d569692 485 | MINIUPNPD 486 | 487 | 488 | urn:schemas-upnp-org:service:WANIPConnection:1 489 | urn:upnp-org:serviceId:WANIPConn1 490 | /ctl/IPConn 491 | /evt/IPConn 492 | /WANIPCn.xml 493 | 494 | 495 | 496 | 497 | 498 | 499 | http://192.168.0.1/ 500 | 501 | "#; 502 | 503 | let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap(); 504 | assert_eq!(control_url, "/ctl/IPConn"); 505 | assert_eq!(control_schema_url, "/WANIPCn.xml"); 506 | } 507 | 508 | #[test] 509 | fn test_parse_device2() { 510 | let text = r#" 511 | 512 | 513 | 514 | 1 515 | 0 516 | 517 | 518 | urn:schemas-upnp-org:device:InternetGatewayDevice:1 519 | FRITZ!Box 7430 520 | AVM Berlin 521 | http://www.avm.de 522 | FRITZ!Box 7430 523 | FRITZ!Box 7430 524 | avm 525 | http://www.avm.de 526 | uuid:00000000-0000-0000-0000-000000000000 527 | 528 | 529 | image/gif 530 | 118 531 | 119 532 | 8 533 | /ligd.gif 534 | 535 | 536 | 537 | 538 | urn:schemas-any-com:service:Any:1 539 | urn:any-com:serviceId:any1 540 | /igdupnp/control/any 541 | /igdupnp/control/any 542 | /any.xml 543 | 544 | 545 | 546 | 547 | urn:schemas-upnp-org:device:WANDevice:1 548 | WANDevice - FRITZ!Box 7430 549 | AVM Berlin 550 | www.avm.de 551 | WANDevice - FRITZ!Box 7430 552 | WANDevice - FRITZ!Box 7430 553 | avm 554 | www.avm.de 555 | uuid:00000000-0000-0000-0000-000000000000 556 | AVM IGD 557 | 558 | 559 | urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 560 | urn:upnp-org:serviceId:WANCommonIFC1 561 | /igdupnp/control/WANCommonIFC1 562 | /igdupnp/control/WANCommonIFC1 563 | /igdicfgSCPD.xml 564 | 565 | 566 | 567 | 568 | urn:schemas-upnp-org:device:WANConnectionDevice:1 569 | WANConnectionDevice - FRITZ!Box 7430 570 | AVM Berlin 571 | www.avm.de 572 | WANConnectionDevice - FRITZ!Box 7430 573 | WANConnectionDevice - FRITZ!Box 7430 574 | avm 575 | www.avm.de 576 | uuid:00000000-0000-0000-0000-000000000000 577 | AVM IGD 578 | 579 | 580 | urn:schemas-upnp-org:service:WANDSLLinkConfig:1 581 | urn:upnp-org:serviceId:WANDSLLinkC1 582 | /igdupnp/control/WANDSLLinkC1 583 | /igdupnp/control/WANDSLLinkC1 584 | /igddslSCPD.xml 585 | 586 | 587 | urn:schemas-upnp-org:service:WANIPConnection:1 588 | urn:upnp-org:serviceId:WANIPConn1 589 | /igdupnp/control/WANIPConn1 590 | /igdupnp/control/WANIPConn1 591 | /igdconnSCPD.xml 592 | 593 | 594 | urn:schemas-upnp-org:service:WANIPv6FirewallControl:1 595 | urn:upnp-org:serviceId:WANIPv6Firewall1 596 | /igd2upnp/control/WANIPv6Firewall1 597 | /igd2upnp/control/WANIPv6Firewall1 598 | /igd2ipv6fwcSCPD.xml 599 | 600 | 601 | 602 | 603 | 604 | 605 | http://fritz.box 606 | 607 | 608 | "#; 609 | let result = parse_control_urls(text.as_bytes()); 610 | assert!(result.is_ok()); 611 | let (control_schema_url, control_url) = result.unwrap(); 612 | assert_eq!(control_url, "/igdupnp/control/WANIPConn1"); 613 | assert_eq!(control_schema_url, "/igdconnSCPD.xml"); 614 | } 615 | 616 | #[test] 617 | fn test_parse_device3() { 618 | let text = r#" 619 | 620 | 621 | 1 622 | 0 623 | 624 | 625 | urn:schemas-upnp-org:device:InternetGatewayDevice:1 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | http://192.168.1.1 634 | uuid:00000000-0000-0000-0000-000000000000 635 | 999999999001 636 | 637 | 638 | image/png 639 | 16 640 | 16 641 | 8 642 | /ligd.png 643 | 644 | 645 | 646 | 647 | urn:schemas-upnp-org:device:WANDevice:1 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | http://192.168.1.254 657 | uuid:00000000-0000-0000-0000-000000000000 658 | 999999999001 659 | 660 | 661 | urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 662 | urn:upnp-org:serviceId:WANCommonIFC1 663 | /upnp/control/WANCommonIFC1 664 | /upnp/control/WANCommonIFC1 665 | /332b484d/wancomicfgSCPD.xml 666 | 667 | 668 | 669 | 670 | urn:schemas-upnp-org:device:WANConnectionDevice:1 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | http://192.168.1.254 680 | uuid:00000000-0000-0000-0000-000000000000 681 | 999999999001 682 | 683 | 684 | urn:schemas-upnp-org:service:WANIPConnection:1 685 | urn:upnp-org:serviceId:WANIPConn1 686 | /upnp/control/WANIPConn1 687 | /upnp/control/WANIPConn1 688 | /332b484d/wanipconnSCPD.xml 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | "#; 697 | 698 | let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap(); 699 | assert_eq!(control_url, "/upnp/control/WANIPConn1"); 700 | assert_eq!(control_schema_url, "/332b484d/wanipconnSCPD.xml"); 701 | } 702 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::fmt; 3 | use std::io; 4 | use std::str; 5 | #[cfg(feature = "aio")] 6 | use std::string::FromUtf8Error; 7 | 8 | #[cfg(feature = "aio")] 9 | use tokio::time::error::Elapsed; 10 | 11 | /// Errors that can occur when sending the request to the gateway. 12 | #[derive(Debug)] 13 | pub enum RequestError { 14 | /// attohttp error 15 | AttoHttpError(attohttpc::Error), 16 | /// IO Error 17 | IoError(io::Error), 18 | /// The response from the gateway could not be parsed. 19 | InvalidResponse(String), 20 | /// The gateway returned an unhandled error code and description. 21 | ErrorCode(u16, String), 22 | /// Action is not supported by the gateway 23 | UnsupportedAction(String), 24 | /// When using the aio feature. 25 | #[cfg(feature = "aio")] 26 | HyperError(hyper::Error), 27 | 28 | #[cfg(feature = "aio")] 29 | /// http crate error type 30 | HttpError(http::Error), 31 | 32 | #[cfg(feature = "aio")] 33 | /// Error parsing HTTP body 34 | Utf8Error(FromUtf8Error), 35 | } 36 | 37 | impl From for RequestError { 38 | fn from(err: attohttpc::Error) -> RequestError { 39 | RequestError::AttoHttpError(err) 40 | } 41 | } 42 | 43 | impl From for RequestError { 44 | fn from(err: io::Error) -> RequestError { 45 | RequestError::IoError(err) 46 | } 47 | } 48 | 49 | #[cfg(feature = "aio")] 50 | impl From for RequestError { 51 | fn from(err: http::Error) -> RequestError { 52 | RequestError::HttpError(err) 53 | } 54 | } 55 | 56 | #[cfg(feature = "aio")] 57 | impl From for RequestError { 58 | fn from(err: hyper::Error) -> RequestError { 59 | RequestError::HyperError(err) 60 | } 61 | } 62 | 63 | #[cfg(feature = "aio")] 64 | impl From for RequestError { 65 | fn from(err: FromUtf8Error) -> RequestError { 66 | RequestError::Utf8Error(err) 67 | } 68 | } 69 | 70 | #[cfg(feature = "aio")] 71 | impl From for RequestError { 72 | fn from(_err: Elapsed) -> RequestError { 73 | RequestError::IoError(io::Error::new(io::ErrorKind::TimedOut, "timer failed")) 74 | } 75 | } 76 | 77 | impl fmt::Display for RequestError { 78 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 79 | match *self { 80 | RequestError::AttoHttpError(ref e) => write!(f, "HTTP error {}", e), 81 | RequestError::InvalidResponse(ref e) => write!(f, "Invalid response from gateway: {}", e), 82 | RequestError::IoError(ref e) => write!(f, "IO error. {}", e), 83 | RequestError::ErrorCode(n, ref e) => write!(f, "Gateway response error {}: {}", n, e), 84 | RequestError::UnsupportedAction(ref e) => write!(f, "Gateway does not support action: {}", e), 85 | #[cfg(feature = "aio")] 86 | RequestError::HyperError(ref e) => write!(f, "Hyper Error: {}", e), 87 | #[cfg(feature = "aio")] 88 | RequestError::HttpError(ref e) => write!(f, "Http Error: {}", e), 89 | #[cfg(feature = "aio")] 90 | RequestError::Utf8Error(ref e) => write!(f, "Utf8Error Error: {}", e), 91 | } 92 | } 93 | } 94 | 95 | impl std::error::Error for RequestError { 96 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 97 | match *self { 98 | RequestError::AttoHttpError(ref e) => Some(e), 99 | RequestError::InvalidResponse(..) => None, 100 | RequestError::IoError(ref e) => Some(e), 101 | RequestError::ErrorCode(..) => None, 102 | RequestError::UnsupportedAction(..) => None, 103 | #[cfg(feature = "aio")] 104 | RequestError::HyperError(ref e) => Some(e), 105 | #[cfg(feature = "aio")] 106 | RequestError::HttpError(ref e) => Some(e), 107 | #[cfg(feature = "aio")] 108 | RequestError::Utf8Error(ref e) => Some(e), 109 | } 110 | } 111 | } 112 | 113 | /// Errors returned by `Gateway::get_external_ip` 114 | #[derive(Debug)] 115 | pub enum GetExternalIpError { 116 | /// The client is not authorized to perform the operation. 117 | ActionNotAuthorized, 118 | /// Some other error occured performing the request. 119 | RequestError(RequestError), 120 | } 121 | 122 | /// Errors returned by `Gateway::remove_port` 123 | #[derive(Debug)] 124 | pub enum RemovePortError { 125 | /// The client is not authorized to perform the operation. 126 | ActionNotAuthorized, 127 | /// No such port mapping. 128 | NoSuchPortMapping, 129 | /// Some other error occured performing the request. 130 | RequestError(RequestError), 131 | } 132 | 133 | /// Errors returned by `Gateway::add_any_port` and `Gateway::get_any_address` 134 | #[derive(Debug)] 135 | pub enum AddAnyPortError { 136 | /// The client is not authorized to perform the operation. 137 | ActionNotAuthorized, 138 | /// Can not add a mapping for local port 0. 139 | InternalPortZeroInvalid, 140 | /// The gateway does not have any free ports. 141 | NoPortsAvailable, 142 | /// The gateway can only map internal ports to same-numbered external ports 143 | /// and this external port is in use. 144 | ExternalPortInUse, 145 | /// The gateway only supports permanent leases (ie. a `lease_duration` of 0). 146 | OnlyPermanentLeasesSupported, 147 | /// The description was too long for the gateway to handle. 148 | DescriptionTooLong, 149 | /// Some other error occured performing the request. 150 | RequestError(RequestError), 151 | } 152 | 153 | impl From for AddAnyPortError { 154 | fn from(err: RequestError) -> AddAnyPortError { 155 | AddAnyPortError::RequestError(err) 156 | } 157 | } 158 | 159 | impl From for AddAnyPortError { 160 | fn from(err: GetExternalIpError) -> AddAnyPortError { 161 | match err { 162 | GetExternalIpError::ActionNotAuthorized => AddAnyPortError::ActionNotAuthorized, 163 | GetExternalIpError::RequestError(e) => AddAnyPortError::RequestError(e), 164 | } 165 | } 166 | } 167 | 168 | /// Errors returned by `Gateway::add_port` 169 | #[derive(Debug)] 170 | pub enum AddPortError { 171 | /// The client is not authorized to perform the operation. 172 | ActionNotAuthorized, 173 | /// Can not add a mapping for local port 0. 174 | InternalPortZeroInvalid, 175 | /// External port number 0 (any port) is considered invalid by the gateway. 176 | ExternalPortZeroInvalid, 177 | /// The requested mapping conflicts with a mapping assigned to another client. 178 | PortInUse, 179 | /// The gateway requires that the requested internal and external ports are the same. 180 | SamePortValuesRequired, 181 | /// The gateway only supports permanent leases (ie. a `lease_duration` of 0). 182 | OnlyPermanentLeasesSupported, 183 | /// The description was too long for the gateway to handle. 184 | DescriptionTooLong, 185 | /// Some other error occured performing the request. 186 | RequestError(RequestError), 187 | } 188 | 189 | impl fmt::Display for GetExternalIpError { 190 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 191 | match *self { 192 | GetExternalIpError::ActionNotAuthorized => write!(f, "The client is not authorized to remove the port"), 193 | GetExternalIpError::RequestError(ref e) => write!(f, "Request Error. {}", e), 194 | } 195 | } 196 | } 197 | 198 | impl From for GetExternalIpError { 199 | fn from(err: io::Error) -> GetExternalIpError { 200 | GetExternalIpError::RequestError(RequestError::from(err)) 201 | } 202 | } 203 | 204 | impl std::error::Error for GetExternalIpError { 205 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 206 | None 207 | } 208 | } 209 | 210 | impl fmt::Display for RemovePortError { 211 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 212 | match *self { 213 | RemovePortError::ActionNotAuthorized => write!(f, "The client is not authorized to remove the port"), 214 | RemovePortError::NoSuchPortMapping => write!(f, "The port was not mapped"), 215 | RemovePortError::RequestError(ref e) => write!(f, "Request error. {}", e), 216 | } 217 | } 218 | } 219 | 220 | impl std::error::Error for RemovePortError { 221 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 222 | None 223 | } 224 | } 225 | 226 | impl fmt::Display for AddAnyPortError { 227 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 228 | match *self { 229 | AddAnyPortError::ActionNotAuthorized => { 230 | write!(f, "The client is not authorized to remove the port") 231 | } 232 | AddAnyPortError::InternalPortZeroInvalid => { 233 | write!(f, "Can not add a mapping for local port 0") 234 | } 235 | AddAnyPortError::NoPortsAvailable => { 236 | write!(f, "The gateway does not have any free ports") 237 | } 238 | AddAnyPortError::OnlyPermanentLeasesSupported => { 239 | write!( 240 | f, 241 | "The gateway only supports permanent leases (ie. a `lease_duration` of 0)," 242 | ) 243 | } 244 | AddAnyPortError::ExternalPortInUse => { 245 | write!( 246 | f, 247 | "The gateway can only map internal ports to same-numbered external ports and this external port is in use." 248 | ) 249 | } 250 | AddAnyPortError::DescriptionTooLong => { 251 | write!(f, "The description was too long for the gateway to handle.") 252 | } 253 | AddAnyPortError::RequestError(ref e) => write!(f, "Request error. {}", e), 254 | } 255 | } 256 | } 257 | 258 | impl std::error::Error for AddAnyPortError { 259 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 260 | None 261 | } 262 | } 263 | 264 | impl fmt::Display for AddPortError { 265 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 266 | match *self { 267 | AddPortError::ActionNotAuthorized => write!(f, "The client is not authorized to map this port."), 268 | AddPortError::InternalPortZeroInvalid => write!(f, "Can not add a mapping for local port 0"), 269 | AddPortError::ExternalPortZeroInvalid => write!( 270 | f, 271 | "External port number 0 (any port) is considered invalid by the gateway." 272 | ), 273 | AddPortError::PortInUse => write!( 274 | f, 275 | "The requested mapping conflicts with a mapping assigned to another client." 276 | ), 277 | AddPortError::SamePortValuesRequired => write!( 278 | f, 279 | "The gateway requires that the requested internal and external ports are the same." 280 | ), 281 | AddPortError::OnlyPermanentLeasesSupported => write!( 282 | f, 283 | "The gateway only supports permanent leases (ie. a `lease_duration` of 0)," 284 | ), 285 | AddPortError::DescriptionTooLong => write!(f, "The description was too long for the gateway to handle."), 286 | AddPortError::RequestError(ref e) => write!(f, "Request error. {}", e), 287 | } 288 | } 289 | } 290 | 291 | impl std::error::Error for AddPortError { 292 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 293 | None 294 | } 295 | } 296 | 297 | /// Errors than can occur while trying to find the gateway. 298 | #[derive(Debug)] 299 | pub enum SearchError { 300 | /// Http/Hyper error 301 | HttpError(attohttpc::Error), 302 | /// Unable to process the response 303 | InvalidResponse, 304 | /// IO Error 305 | IoError(io::Error), 306 | /// UTF-8 decoding error 307 | Utf8Error(str::Utf8Error), 308 | /// XML processing error 309 | XmlError(xmltree::ParseError), 310 | /// When using the aio feature. 311 | #[cfg(feature = "aio")] 312 | HyperError(hyper::Error), 313 | /// Error parsing URI 314 | #[cfg(feature = "aio")] 315 | InvalidUri(hyper::http::uri::InvalidUri), 316 | } 317 | 318 | impl From for SearchError { 319 | fn from(err: attohttpc::Error) -> SearchError { 320 | SearchError::HttpError(err) 321 | } 322 | } 323 | 324 | impl From for SearchError { 325 | fn from(err: io::Error) -> SearchError { 326 | SearchError::IoError(err) 327 | } 328 | } 329 | 330 | impl From for SearchError { 331 | fn from(err: str::Utf8Error) -> SearchError { 332 | SearchError::Utf8Error(err) 333 | } 334 | } 335 | 336 | impl From for SearchError { 337 | fn from(err: xmltree::ParseError) -> SearchError { 338 | SearchError::XmlError(err) 339 | } 340 | } 341 | 342 | #[cfg(feature = "aio")] 343 | impl From for SearchError { 344 | fn from(err: hyper::Error) -> SearchError { 345 | SearchError::HyperError(err) 346 | } 347 | } 348 | 349 | #[cfg(feature = "aio")] 350 | impl From for SearchError { 351 | fn from(err: hyper::http::uri::InvalidUri) -> SearchError { 352 | SearchError::InvalidUri(err) 353 | } 354 | } 355 | #[cfg(feature = "aio")] 356 | impl From for SearchError { 357 | fn from(_err: Elapsed) -> SearchError { 358 | SearchError::IoError(io::Error::new(io::ErrorKind::TimedOut, "search timed out")) 359 | } 360 | } 361 | 362 | impl fmt::Display for SearchError { 363 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 364 | match *self { 365 | SearchError::HttpError(ref e) => write!(f, "HTTP error {}", e), 366 | SearchError::InvalidResponse => write!(f, "Invalid response"), 367 | SearchError::IoError(ref e) => write!(f, "IO error: {}", e), 368 | SearchError::Utf8Error(ref e) => write!(f, "UTF-8 error: {}", e), 369 | SearchError::XmlError(ref e) => write!(f, "XML error: {}", e), 370 | #[cfg(feature = "aio")] 371 | SearchError::HyperError(ref e) => write!(f, "Hyper Error: {}", e), 372 | #[cfg(feature = "aio")] 373 | SearchError::InvalidUri(ref e) => write!(f, "InvalidUri Error: {}", e), 374 | } 375 | } 376 | } 377 | 378 | impl error::Error for SearchError { 379 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 380 | match *self { 381 | SearchError::HttpError(ref e) => Some(e), 382 | SearchError::InvalidResponse => None, 383 | SearchError::IoError(ref e) => Some(e), 384 | SearchError::Utf8Error(ref e) => Some(e), 385 | SearchError::XmlError(ref e) => Some(e), 386 | #[cfg(feature = "aio")] 387 | SearchError::HyperError(ref e) => Some(e), 388 | #[cfg(feature = "aio")] 389 | SearchError::InvalidUri(ref e) => Some(e), 390 | } 391 | } 392 | } 393 | 394 | /// Errors than can occur while getting a port mapping 395 | #[derive(Debug)] 396 | pub enum GetGenericPortMappingEntryError { 397 | /// The client is not authorized to perform the operation. 398 | ActionNotAuthorized, 399 | /// The specified array index is out of bounds. 400 | SpecifiedArrayIndexInvalid, 401 | /// Some other error occured performing the request. 402 | RequestError(RequestError), 403 | } 404 | 405 | impl From for GetGenericPortMappingEntryError { 406 | fn from(err: RequestError) -> GetGenericPortMappingEntryError { 407 | match err { 408 | RequestError::ErrorCode(code, _) if code == 606 => GetGenericPortMappingEntryError::ActionNotAuthorized, 409 | RequestError::ErrorCode(code, _) if code == 713 => { 410 | GetGenericPortMappingEntryError::SpecifiedArrayIndexInvalid 411 | } 412 | other => GetGenericPortMappingEntryError::RequestError(other), 413 | } 414 | } 415 | } 416 | 417 | impl fmt::Display for GetGenericPortMappingEntryError { 418 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 419 | match *self { 420 | GetGenericPortMappingEntryError::ActionNotAuthorized => { 421 | write!(f, "The client is not authorized to look up port mappings.") 422 | } 423 | GetGenericPortMappingEntryError::SpecifiedArrayIndexInvalid => { 424 | write!(f, "The provided index into the port mapping list is invalid.") 425 | } 426 | GetGenericPortMappingEntryError::RequestError(ref e) => e.fmt(f), 427 | } 428 | } 429 | } 430 | 431 | impl std::error::Error for GetGenericPortMappingEntryError {} 432 | 433 | /// An error type that emcompasses all possible errors. 434 | #[derive(Debug)] 435 | pub enum Error { 436 | /// `AddAnyPortError` 437 | AddAnyPortError(AddAnyPortError), 438 | /// `AddPortError` 439 | AddPortError(AddPortError), 440 | /// `GetExternalIpError` 441 | GetExternalIpError(GetExternalIpError), 442 | /// `RemovePortError` 443 | RemovePortError(RemovePortError), 444 | /// `RequestError` 445 | RequestError(RequestError), 446 | /// `SearchError` 447 | SearchError(SearchError), 448 | } 449 | 450 | /// A result type where the error is `igd::Error`. 451 | pub type Result = std::result::Result; 452 | 453 | impl fmt::Display for Error { 454 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 455 | match *self { 456 | Error::AddAnyPortError(ref e) => e.fmt(f), 457 | Error::AddPortError(ref e) => e.fmt(f), 458 | Error::GetExternalIpError(ref e) => e.fmt(f), 459 | Error::RemovePortError(ref e) => e.fmt(f), 460 | Error::RequestError(ref e) => e.fmt(f), 461 | Error::SearchError(ref e) => e.fmt(f), 462 | } 463 | } 464 | } 465 | 466 | impl error::Error for Error { 467 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 468 | match *self { 469 | Error::AddAnyPortError(ref e) => Some(e), 470 | Error::AddPortError(ref e) => Some(e), 471 | Error::GetExternalIpError(ref e) => Some(e), 472 | Error::RemovePortError(ref e) => Some(e), 473 | Error::RequestError(ref e) => Some(e), 474 | Error::SearchError(ref e) => Some(e), 475 | } 476 | } 477 | } 478 | 479 | impl From for Error { 480 | fn from(err: AddAnyPortError) -> Error { 481 | Error::AddAnyPortError(err) 482 | } 483 | } 484 | 485 | impl From for Error { 486 | fn from(err: AddPortError) -> Error { 487 | Error::AddPortError(err) 488 | } 489 | } 490 | 491 | impl From for Error { 492 | fn from(err: GetExternalIpError) -> Error { 493 | Error::GetExternalIpError(err) 494 | } 495 | } 496 | 497 | impl From for Error { 498 | fn from(err: RemovePortError) -> Error { 499 | Error::RemovePortError(err) 500 | } 501 | } 502 | 503 | impl From for Error { 504 | fn from(err: RequestError) -> Error { 505 | Error::RequestError(err) 506 | } 507 | } 508 | 509 | impl From for Error { 510 | fn from(err: SearchError) -> Error { 511 | Error::SearchError(err) 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /src/gateway.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt; 3 | use std::net::{Ipv4Addr, SocketAddrV4}; 4 | 5 | use crate::common::{self, messages, parsing, parsing::RequestResult}; 6 | use crate::errors::{self, AddAnyPortError, AddPortError, GetExternalIpError, RemovePortError, RequestError}; 7 | use crate::PortMappingProtocol; 8 | 9 | /// This structure represents a gateway found by the search functions. 10 | #[derive(Clone, Debug)] 11 | pub struct Gateway { 12 | /// Socket address of the gateway 13 | pub addr: SocketAddrV4, 14 | /// Root url of the device 15 | pub root_url: String, 16 | /// Control url of the device 17 | pub control_url: String, 18 | /// Url to get schema data from 19 | pub control_schema_url: String, 20 | /// Control schema for all actions 21 | pub control_schema: HashMap>, 22 | } 23 | 24 | impl Gateway { 25 | fn perform_request(&self, header: &str, body: &str, ok: &str) -> RequestResult { 26 | let url = format!("http://{}{}", self.addr, self.control_url); 27 | 28 | let response = attohttpc::post(&url) 29 | .header("SOAPAction", header) 30 | .header("Content-Type", "text/xml") 31 | .text(body) 32 | .send()?; 33 | 34 | parsing::parse_response(response.text()?, ok) 35 | } 36 | 37 | /// Get the external IP address of the gateway. 38 | pub fn get_external_ip(&self) -> Result { 39 | parsing::parse_get_external_ip_response(self.perform_request( 40 | messages::GET_EXTERNAL_IP_HEADER, 41 | &messages::format_get_external_ip_message(), 42 | "GetExternalIPAddressResponse", 43 | )) 44 | } 45 | 46 | /// Get an external socket address with our external ip and any port. This is a convenience 47 | /// function that calls `get_external_ip` followed by `add_any_port` 48 | /// 49 | /// The local_addr is the address where the traffic is sent to. 50 | /// The lease_duration parameter is in seconds. A value of 0 is infinite. 51 | /// 52 | /// # Returns 53 | /// 54 | /// The external address that was mapped on success. Otherwise an error. 55 | pub fn get_any_address( 56 | &self, 57 | protocol: PortMappingProtocol, 58 | local_addr: SocketAddrV4, 59 | lease_duration: u32, 60 | description: &str, 61 | ) -> Result { 62 | let ip = self.get_external_ip()?; 63 | let port = self.add_any_port(protocol, local_addr, lease_duration, description)?; 64 | Ok(SocketAddrV4::new(ip, port)) 65 | } 66 | 67 | /// Add a port mapping.with any external port. 68 | /// 69 | /// The local_addr is the address where the traffic is sent to. 70 | /// The lease_duration parameter is in seconds. A value of 0 is infinite. 71 | /// 72 | /// # Returns 73 | /// 74 | /// The external port that was mapped on success. Otherwise an error. 75 | pub fn add_any_port( 76 | &self, 77 | protocol: PortMappingProtocol, 78 | local_addr: SocketAddrV4, 79 | lease_duration: u32, 80 | description: &str, 81 | ) -> Result { 82 | // This function first attempts to call AddAnyPortMapping on the IGD with a random port 83 | // number. If that fails due to the method being unknown it attempts to call AddPortMapping 84 | // instead with a random port number. If that fails due to ConflictInMappingEntry it retrys 85 | // with another port up to a maximum of 20 times. If it fails due to SamePortValuesRequired 86 | // it retrys once with the same port values. 87 | 88 | if local_addr.port() == 0 { 89 | return Err(AddAnyPortError::InternalPortZeroInvalid); 90 | } 91 | 92 | let schema = self.control_schema.get("AddAnyPortMapping"); 93 | if let Some(schema) = schema { 94 | let external_port = common::random_port(); 95 | 96 | parsing::parse_add_any_port_mapping_response(self.perform_request( 97 | messages::ADD_ANY_PORT_MAPPING_HEADER, 98 | &messages::format_add_any_port_mapping_message( 99 | schema, 100 | protocol, 101 | external_port, 102 | local_addr, 103 | lease_duration, 104 | description, 105 | ), 106 | "AddAnyPortMappingResponse", 107 | )) 108 | } else { 109 | self.retry_add_random_port_mapping(protocol, local_addr, lease_duration, description) 110 | } 111 | } 112 | 113 | fn retry_add_random_port_mapping( 114 | &self, 115 | protocol: PortMappingProtocol, 116 | local_addr: SocketAddrV4, 117 | lease_duration: u32, 118 | description: &str, 119 | ) -> Result { 120 | const ATTEMPTS: usize = 20; 121 | 122 | for _ in 0..ATTEMPTS { 123 | if let Ok(port) = self.add_random_port_mapping(protocol, local_addr, lease_duration, &description) { 124 | return Ok(port); 125 | } 126 | } 127 | 128 | Err(AddAnyPortError::NoPortsAvailable) 129 | } 130 | 131 | fn add_random_port_mapping( 132 | &self, 133 | protocol: PortMappingProtocol, 134 | local_addr: SocketAddrV4, 135 | lease_duration: u32, 136 | description: &str, 137 | ) -> Result { 138 | let external_port = common::random_port(); 139 | 140 | if let Err(err) = self.add_port_mapping(protocol, external_port, local_addr, lease_duration, &description) { 141 | match parsing::convert_add_random_port_mapping_error(err) { 142 | Some(err) => return Err(err), 143 | None => return self.add_same_port_mapping(protocol, local_addr, lease_duration, description), 144 | } 145 | } 146 | 147 | Ok(external_port) 148 | } 149 | 150 | fn add_same_port_mapping( 151 | &self, 152 | protocol: PortMappingProtocol, 153 | local_addr: SocketAddrV4, 154 | lease_duration: u32, 155 | description: &str, 156 | ) -> Result { 157 | match self.add_port_mapping(protocol, local_addr.port(), local_addr, lease_duration, description) { 158 | Ok(_) => Ok(local_addr.port()), 159 | Err(e) => Err(parsing::convert_add_same_port_mapping_error(e)), 160 | } 161 | } 162 | 163 | fn add_port_mapping( 164 | &self, 165 | protocol: PortMappingProtocol, 166 | external_port: u16, 167 | local_addr: SocketAddrV4, 168 | lease_duration: u32, 169 | description: &str, 170 | ) -> Result<(), RequestError> { 171 | self.perform_request( 172 | messages::ADD_PORT_MAPPING_HEADER, 173 | &messages::format_add_port_mapping_message( 174 | self.control_schema 175 | .get("AddPortMapping") 176 | .ok_or_else(|| RequestError::UnsupportedAction("AddPortMapping".to_string()))?, 177 | protocol, 178 | external_port, 179 | local_addr, 180 | lease_duration, 181 | description, 182 | ), 183 | "AddPortMappingResponse", 184 | )?; 185 | 186 | Ok(()) 187 | } 188 | 189 | /// Add a port mapping. 190 | /// 191 | /// The local_addr is the address where the traffic is sent to. 192 | /// The lease_duration parameter is in seconds. A value of 0 is infinite. 193 | pub fn add_port( 194 | &self, 195 | protocol: PortMappingProtocol, 196 | external_port: u16, 197 | local_addr: SocketAddrV4, 198 | lease_duration: u32, 199 | description: &str, 200 | ) -> Result<(), AddPortError> { 201 | if external_port == 0 { 202 | return Err(AddPortError::ExternalPortZeroInvalid); 203 | } 204 | if local_addr.port() == 0 { 205 | return Err(AddPortError::InternalPortZeroInvalid); 206 | } 207 | 208 | self.add_port_mapping(protocol, external_port, local_addr, lease_duration, description) 209 | .map_err(parsing::convert_add_port_error) 210 | } 211 | 212 | /// Remove a port mapping. 213 | pub fn remove_port(&self, protocol: PortMappingProtocol, external_port: u16) -> Result<(), RemovePortError> { 214 | parsing::parse_delete_port_mapping_response(self.perform_request( 215 | messages::DELETE_PORT_MAPPING_HEADER, 216 | &messages::format_delete_port_message( 217 | self.control_schema.get("DeletePortMapping").ok_or_else(|| { 218 | RemovePortError::RequestError(RequestError::UnsupportedAction("DeletePortMapping".to_string())) 219 | })?, 220 | protocol, 221 | external_port, 222 | ), 223 | "DeletePortMappingResponse", 224 | )) 225 | } 226 | 227 | /// Get one port mapping entry 228 | /// 229 | /// Gets one port mapping entry by its index. 230 | /// Not all existing port mappings might be visible to this client. 231 | /// If the index is out of bound, GetGenericPortMappingEntryError::SpecifiedArrayIndexInvalid will be returned 232 | pub fn get_generic_port_mapping_entry( 233 | &self, 234 | index: u32, 235 | ) -> Result { 236 | parsing::parse_get_generic_port_mapping_entry(self.perform_request( 237 | messages::GET_GENERIC_PORT_MAPPING_ENTRY, 238 | &messages::formate_get_generic_port_mapping_entry_message(index), 239 | "GetGenericPortMappingEntryResponse", 240 | )) 241 | } 242 | } 243 | 244 | impl fmt::Display for Gateway { 245 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 246 | write!(f, "http://{}{}", self.addr, self.control_url) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | //! This library allows you to communicate with an IGD enabled device. 4 | //! Use one of the `search_gateway` functions to obtain a `Gateway` object. 5 | //! You can then communicate with the device via this object. 6 | 7 | extern crate attohttpc; 8 | #[macro_use] 9 | extern crate log; 10 | #[cfg(feature = "aio")] 11 | extern crate bytes; 12 | 13 | extern crate rand; 14 | extern crate url; 15 | extern crate xmltree; 16 | 17 | #[cfg(feature = "aio")] 18 | extern crate futures; 19 | #[cfg(feature = "aio")] 20 | extern crate http; 21 | #[cfg(feature = "aio")] 22 | extern crate hyper; 23 | #[cfg(feature = "aio")] 24 | extern crate tokio; 25 | 26 | // data structures 27 | pub use self::common::parsing::PortMappingEntry; 28 | pub use self::common::SearchOptions; 29 | pub use self::errors::{ 30 | AddAnyPortError, AddPortError, GetExternalIpError, GetGenericPortMappingEntryError, RemovePortError, RequestError, 31 | SearchError, 32 | }; 33 | pub use self::errors::{Error, Result}; 34 | pub use self::gateway::Gateway; 35 | 36 | // search of gateway 37 | pub use self::search::search_gateway; 38 | 39 | #[cfg(feature = "aio")] 40 | pub mod aio; 41 | mod common; 42 | mod errors; 43 | mod gateway; 44 | mod search; 45 | 46 | use std::fmt; 47 | 48 | /// Represents the protocols available for port mapping. 49 | #[derive(Debug, Clone, Copy, PartialEq)] 50 | pub enum PortMappingProtocol { 51 | /// TCP protocol 52 | TCP, 53 | /// UDP protocol 54 | UDP, 55 | } 56 | 57 | impl fmt::Display for PortMappingProtocol { 58 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 59 | write!( 60 | f, 61 | "{}", 62 | match *self { 63 | PortMappingProtocol::TCP => "TCP", 64 | PortMappingProtocol::UDP => "UDP", 65 | } 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use attohttpc::Method; 2 | use attohttpc::RequestBuilder; 3 | use std::collections::HashMap; 4 | use std::net::{SocketAddrV4, UdpSocket}; 5 | use std::str; 6 | 7 | use crate::common::{messages, parsing, SearchOptions}; 8 | use crate::errors::SearchError; 9 | use crate::gateway::Gateway; 10 | 11 | /// Search gateway, using the given `SearchOptions`. 12 | /// 13 | /// The default `SearchOptions` should suffice in most cases. 14 | /// It can be created with `Default::default()` or `SearchOptions::default()`. 15 | /// 16 | /// # Example 17 | /// ```no_run 18 | /// use igd::{search_gateway, SearchOptions, Result}; 19 | /// 20 | /// fn main() -> Result { 21 | /// let gateway = search_gateway(Default::default())?; 22 | /// let ip = gateway.get_external_ip()?; 23 | /// println!("External IP address: {}", ip); 24 | /// Ok(()) 25 | /// } 26 | /// ``` 27 | pub fn search_gateway(options: SearchOptions) -> Result { 28 | let socket = UdpSocket::bind(options.bind_addr)?; 29 | socket.set_read_timeout(options.timeout)?; 30 | 31 | socket.send_to(messages::SEARCH_REQUEST.as_bytes(), options.broadcast_address)?; 32 | 33 | loop { 34 | let mut buf = [0u8; 1500]; 35 | let (read, _) = socket.recv_from(&mut buf)?; 36 | let text = str::from_utf8(&buf[..read])?; 37 | 38 | let (addr, root_url) = parsing::parse_search_result(text)?; 39 | 40 | let (control_schema_url, control_url) = match get_control_urls(&addr, &root_url) { 41 | Ok(o) => o, 42 | Err(e) => { 43 | debug!( 44 | "Error has occurred while getting control urls. error: {}, addr: {}, root_url: {}", 45 | e, addr, root_url 46 | ); 47 | continue; 48 | } 49 | }; 50 | 51 | let control_schema = match get_schemas(&addr, &control_schema_url) { 52 | Ok(o) => o, 53 | Err(e) => { 54 | debug!( 55 | "Error has occurred while getting schemas. error: {}, addr: {}, control_schema_url: {}", 56 | e, addr, control_schema_url 57 | ); 58 | continue; 59 | } 60 | }; 61 | 62 | return Ok(Gateway { 63 | addr, 64 | root_url, 65 | control_url, 66 | control_schema_url, 67 | control_schema, 68 | }); 69 | } 70 | } 71 | 72 | fn get_control_urls(addr: &SocketAddrV4, root_url: &str) -> Result<(String, String), SearchError> { 73 | let url = format!("http://{}:{}{}", addr.ip(), addr.port(), root_url); 74 | 75 | match RequestBuilder::try_new(Method::GET, &url) { 76 | Ok(request_builder) => { 77 | let response = request_builder.send()?; 78 | parsing::parse_control_urls(&response.bytes()?[..]) 79 | } 80 | Err(error) => Err(SearchError::HttpError(error)), 81 | } 82 | } 83 | 84 | fn get_schemas(addr: &SocketAddrV4, control_schema_url: &str) -> Result>, SearchError> { 85 | let url = format!("http://{}:{}{}", addr.ip(), addr.port(), control_schema_url); 86 | 87 | match RequestBuilder::try_new(Method::GET, &url) { 88 | Ok(request_builder) => { 89 | let response = request_builder.send()?; 90 | parsing::parse_schemas(&response.bytes()?[..]) 91 | } 92 | Err(error) => Err(SearchError::HttpError(error)), 93 | } 94 | } 95 | --------------------------------------------------------------------------------