igd_next/
search.rs

1use std::collections::HashMap;
2use std::net::{SocketAddr, UdpSocket};
3use std::str;
4use std::time::{Duration, Instant};
5
6use attohttpc::{Method, RequestBuilder};
7use log::debug;
8
9use crate::common::options::{DEFAULT_TIMEOUT, RESPONSE_TIMEOUT};
10use crate::common::{messages, parsing, SearchOptions};
11use crate::errors::SearchError;
12use crate::gateway::Gateway;
13
14/// Search gateway, using the given `SearchOptions`.
15///
16/// The default `SearchOptions` should suffice in most cases.
17/// It can be created with `Default::default()` or `SearchOptions::default()`.
18///
19/// # Example
20/// ```no_run
21/// use igd_next::{search_gateway, SearchOptions, Result};
22///
23/// fn main() -> Result {
24///     let gateway = search_gateway(Default::default())?;
25///     let ip = gateway.get_external_ip()?;
26///     println!("External IP address: {}", ip);
27///     Ok(())
28/// }
29/// ```
30pub fn search_gateway(options: SearchOptions) -> Result<Gateway, SearchError> {
31    let start = Instant::now();
32    let max_time = options.timeout.unwrap_or(DEFAULT_TIMEOUT);
33
34    let socket = UdpSocket::bind(options.bind_addr)?;
35
36    let read_timeout = options.single_search_timeout.unwrap_or(RESPONSE_TIMEOUT);
37    socket.set_read_timeout(Some(read_timeout))?;
38
39    socket.send_to(messages::SEARCH_REQUEST.as_bytes(), options.broadcast_address)?;
40
41    while start.elapsed() < max_time {
42        let mut buf = [0u8; 1500];
43
44        // limit read, to the remaining time available
45        socket.set_read_timeout(Some(max_time - start.elapsed()))?;
46        let (read, _) = socket.recv_from(&mut buf)?;
47        let text = str::from_utf8(&buf[..read])?;
48
49        let (addr, root_url) = parsing::parse_search_result(text)?;
50
51        let (control_schema_url, control_url) = match get_control_urls(&addr, &root_url, max_time - start.elapsed()) {
52            Ok(o) => o,
53            Err(e) => {
54                debug!(
55                    "Error has occurred while getting control urls. error: {}, addr: {}, root_url: {}",
56                    e, addr, root_url
57                );
58                continue;
59            }
60        };
61
62        let control_schema = match get_schemas(&addr, &control_schema_url, max_time - start.elapsed()) {
63            Ok(o) => o,
64            Err(e) => {
65                debug!(
66                    "Error has occurred while getting schemas. error: {}, addr: {}, control_schema_url: {}",
67                    e, addr, control_schema_url
68                );
69                continue;
70            }
71        };
72
73        return Ok(Gateway {
74            addr,
75            root_url,
76            control_url,
77            control_schema_url,
78            control_schema,
79        });
80    }
81
82    Err(SearchError::NoResponseWithinTimeout)
83}
84
85fn get_control_urls(addr: &SocketAddr, root_url: &str, timeout: Duration) -> Result<(String, String), SearchError> {
86    let url = format!("http://{}:{}{}", addr.ip(), addr.port(), root_url);
87    match RequestBuilder::try_new(Method::GET, url) {
88        Ok(request_builder) => {
89            let response = request_builder.timeout(timeout).send()?;
90            parsing::parse_control_urls(&response.bytes()?[..])
91        }
92        Err(error) => Err(SearchError::HttpError(error)),
93    }
94}
95
96fn get_schemas(
97    addr: &SocketAddr,
98    control_schema_url: &str,
99    timeout: Duration,
100) -> Result<HashMap<String, Vec<String>>, SearchError> {
101    let url = format!("http://{}:{}{}", addr.ip(), addr.port(), control_schema_url);
102    match RequestBuilder::try_new(Method::GET, url) {
103        Ok(request_builder) => {
104            let response = request_builder.timeout(timeout).send()?;
105            parsing::parse_schemas(&response.bytes()?[..])
106        }
107        Err(error) => Err(SearchError::HttpError(error)),
108    }
109}