igd_next/
gateway.rs

1use attohttpc::{Method, RequestBuilder};
2use std::collections::HashMap;
3use std::fmt;
4use std::net::{IpAddr, SocketAddr};
5
6use crate::common::{self, messages, parsing, parsing::RequestResult};
7use crate::errors::{self, AddAnyPortError, AddPortError, GetExternalIpError, RemovePortError, RequestError};
8use crate::PortMappingProtocol;
9use crate::RequestError::AttoHttpError;
10
11/// This structure represents a gateway found by the search functions.
12#[derive(Clone, Debug)]
13pub struct Gateway {
14    /// Socket address of the gateway
15    pub addr: SocketAddr,
16    /// Root url of the device
17    pub root_url: String,
18    /// Control url of the device
19    pub control_url: String,
20    /// Url to get schema data from
21    pub control_schema_url: String,
22    /// Control schema for all actions
23    pub control_schema: HashMap<String, Vec<String>>,
24}
25
26impl Gateway {
27    fn perform_request(&self, header: &str, body: &str, ok: &str) -> RequestResult {
28        let url = format!("http://{}{}", self.addr, self.control_url);
29
30        let response = match RequestBuilder::try_new(Method::POST, url) {
31            Ok(request_builder) => request_builder
32                .header("SOAPAction", header)
33                .header("Content-Type", "text/xml")
34                .text(body)
35                .send()?,
36            Err(e) => return Err(AttoHttpError(e)),
37        };
38
39        parsing::parse_response(response.text()?, ok)
40    }
41
42    /// Get the external IP address of the gateway.
43    pub fn get_external_ip(&self) -> Result<IpAddr, GetExternalIpError> {
44        parsing::parse_get_external_ip_response(self.perform_request(
45            messages::GET_EXTERNAL_IP_HEADER,
46            &messages::format_get_external_ip_message(),
47            "GetExternalIPAddressResponse",
48        ))
49    }
50
51    /// Get an external socket address with our external ip and any port. This is a convenience
52    /// function that calls `get_external_ip` followed by `add_any_port`
53    ///
54    /// The local_addr is the address where the traffic is sent to.
55    /// The lease_duration parameter is in seconds. A value of 0 is infinite.
56    ///
57    /// # Returns
58    ///
59    /// The external address that was mapped on success. Otherwise an error.
60    pub fn get_any_address(
61        &self,
62        protocol: PortMappingProtocol,
63        local_addr: SocketAddr,
64        lease_duration: u32,
65        description: &str,
66    ) -> Result<SocketAddr, AddAnyPortError> {
67        let ip = self.get_external_ip()?;
68        let port = self.add_any_port(protocol, local_addr, lease_duration, description)?;
69        Ok(SocketAddr::new(ip, port))
70    }
71
72    /// Add a port mapping.with any external port.
73    ///
74    /// The local_addr is the address where the traffic is sent to.
75    /// The lease_duration parameter is in seconds. A value of 0 is infinite.
76    ///
77    /// # Returns
78    ///
79    /// The external port that was mapped on success. Otherwise an error.
80    pub fn add_any_port(
81        &self,
82        protocol: PortMappingProtocol,
83        local_addr: SocketAddr,
84        lease_duration: u32,
85        description: &str,
86    ) -> Result<u16, AddAnyPortError> {
87        // This function first attempts to call AddAnyPortMapping on the IGD with a random port
88        // number. If that fails due to the method being unknown it attempts to call AddPortMapping
89        // instead with a random port number. If that fails due to ConflictInMappingEntry it retrys
90        // with another port up to a maximum of 20 times. If it fails due to SamePortValuesRequired
91        // it retrys once with the same port values.
92
93        if local_addr.port() == 0 {
94            return Err(AddAnyPortError::InternalPortZeroInvalid);
95        }
96
97        let schema = self.control_schema.get("AddAnyPortMapping");
98        if let Some(schema) = schema {
99            let external_port = common::random_port();
100
101            parsing::parse_add_any_port_mapping_response(self.perform_request(
102                messages::ADD_ANY_PORT_MAPPING_HEADER,
103                &messages::format_add_any_port_mapping_message(
104                    schema,
105                    protocol,
106                    external_port,
107                    local_addr,
108                    lease_duration,
109                    description,
110                ),
111                "AddAnyPortMappingResponse",
112            ))
113        } else {
114            self.retry_add_random_port_mapping(protocol, local_addr, lease_duration, description)
115        }
116    }
117
118    fn retry_add_random_port_mapping(
119        &self,
120        protocol: PortMappingProtocol,
121        local_addr: SocketAddr,
122        lease_duration: u32,
123        description: &str,
124    ) -> Result<u16, AddAnyPortError> {
125        const ATTEMPTS: usize = 20;
126
127        for _ in 0..ATTEMPTS {
128            if let Ok(port) = self.add_random_port_mapping(protocol, local_addr, lease_duration, description) {
129                return Ok(port);
130            }
131        }
132
133        Err(AddAnyPortError::NoPortsAvailable)
134    }
135
136    fn add_random_port_mapping(
137        &self,
138        protocol: PortMappingProtocol,
139        local_addr: SocketAddr,
140        lease_duration: u32,
141        description: &str,
142    ) -> Result<u16, AddAnyPortError> {
143        let external_port = common::random_port();
144
145        if let Err(err) = self.add_port_mapping(protocol, external_port, local_addr, lease_duration, description) {
146            match parsing::convert_add_random_port_mapping_error(err) {
147                Some(err) => return Err(err),
148                None => return self.add_same_port_mapping(protocol, local_addr, lease_duration, description),
149            }
150        }
151
152        Ok(external_port)
153    }
154
155    fn add_same_port_mapping(
156        &self,
157        protocol: PortMappingProtocol,
158        local_addr: SocketAddr,
159        lease_duration: u32,
160        description: &str,
161    ) -> Result<u16, AddAnyPortError> {
162        match self.add_port_mapping(protocol, local_addr.port(), local_addr, lease_duration, description) {
163            Ok(_) => Ok(local_addr.port()),
164            Err(e) => Err(parsing::convert_add_same_port_mapping_error(e)),
165        }
166    }
167
168    fn add_port_mapping(
169        &self,
170        protocol: PortMappingProtocol,
171        external_port: u16,
172        local_addr: SocketAddr,
173        lease_duration: u32,
174        description: &str,
175    ) -> Result<(), RequestError> {
176        self.perform_request(
177            messages::ADD_PORT_MAPPING_HEADER,
178            &messages::format_add_port_mapping_message(
179                self.control_schema
180                    .get("AddPortMapping")
181                    .ok_or_else(|| RequestError::UnsupportedAction("AddPortMapping".to_string()))?,
182                protocol,
183                external_port,
184                local_addr,
185                lease_duration,
186                description,
187            ),
188            "AddPortMappingResponse",
189        )?;
190
191        Ok(())
192    }
193
194    /// Add a port mapping.
195    ///
196    /// The local_addr is the address where the traffic is sent to.
197    /// The lease_duration parameter is in seconds. A value of 0 is infinite.
198    pub fn add_port(
199        &self,
200        protocol: PortMappingProtocol,
201        external_port: u16,
202        local_addr: SocketAddr,
203        lease_duration: u32,
204        description: &str,
205    ) -> Result<(), AddPortError> {
206        if external_port == 0 {
207            return Err(AddPortError::ExternalPortZeroInvalid);
208        }
209        if local_addr.port() == 0 {
210            return Err(AddPortError::InternalPortZeroInvalid);
211        }
212
213        self.add_port_mapping(protocol, external_port, local_addr, lease_duration, description)
214            .map_err(parsing::convert_add_port_error)
215    }
216
217    /// Remove a port mapping.
218    pub fn remove_port(&self, protocol: PortMappingProtocol, external_port: u16) -> Result<(), RemovePortError> {
219        parsing::parse_delete_port_mapping_response(self.perform_request(
220            messages::DELETE_PORT_MAPPING_HEADER,
221            &messages::format_delete_port_message(
222                self.control_schema.get("DeletePortMapping").ok_or_else(|| {
223                    RemovePortError::RequestError(RequestError::UnsupportedAction("DeletePortMapping".to_string()))
224                })?,
225                protocol,
226                external_port,
227            ),
228            "DeletePortMappingResponse",
229        ))
230    }
231
232    /// Get one port mapping entry
233    ///
234    /// Gets one port mapping entry by its index.
235    /// Not all existing port mappings might be visible to this client.
236    /// If the index is out of bound, GetGenericPortMappingEntryError::SpecifiedArrayIndexInvalid will be returned
237    pub fn get_generic_port_mapping_entry(
238        &self,
239        index: u32,
240    ) -> Result<parsing::PortMappingEntry, errors::GetGenericPortMappingEntryError> {
241        parsing::parse_get_generic_port_mapping_entry(self.perform_request(
242            messages::GET_GENERIC_PORT_MAPPING_ENTRY,
243            &messages::formate_get_generic_port_mapping_entry_message(index),
244            "GetGenericPortMappingEntryResponse",
245        ))
246    }
247}
248
249impl fmt::Display for Gateway {
250    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
251        write!(f, "http://{}{}", self.addr, self.control_url)
252    }
253}