diff --git a/src/aio/search.rs b/src/aio/search.rs
index a5107471..29843264 100644
--- a/src/aio/search.rs
+++ b/src/aio/search.rs
@@ -100,6 +100,8 @@ fn handle_broadcast_resp(from: &SocketAddr, data: &[u8]) -> Result<(SocketAddr,
async fn get_control_urls(addr: &SocketAddr, path: &str) -> Result<(String, String), SearchError> {
let url: reqwest::Url = format!("http://{}{}", addr, path).parse()?;
+ validate_url(addr.ip(), &url)?;
+
debug!("requesting control url from: {:?}", url);
let client = reqwest::Client::new();
let resp = client.get(url).send().await?;
@@ -126,186 +128,3 @@ async fn get_control_schemas(
let body = resp.bytes().await?;
parsing::parse_schemas(body.as_ref())
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use http_body_util::Full;
- use hyper::{body::Bytes, service::service_fn, Request, Response};
- use hyper_util::rt::TokioIo;
- use std::convert::Infallible;
- use std::{
- net::{Ipv4Addr, SocketAddrV4},
- time::Duration,
- };
- use test_log::test;
- use tokio::net::TcpListener;
-
- async fn start_broadcast_reply_sender(location: String) -> u16 {
- let port = {
- // Not 100% reliable way to find a free port number, but should be good enough
- let sock = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).await.unwrap();
- sock.local_addr().unwrap().port()
- };
-
- tokio::spawn(async move {
- tokio::time::sleep(Duration::from_secs(1)).await;
-
- let sock = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
-
- sock.send_to(format!("location: {location}").as_bytes(), (Ipv4Addr::LOCALHOST, port))
- .await
- .unwrap();
- });
- port
- }
-
- fn default_options_with_using_free_port(port: u16) -> SearchOptions {
- SearchOptions {
- bind_addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)),
- timeout: Some(Duration::from_secs(5)),
- http_timeout: Some(Duration::from_secs(1)),
- ..Default::default()
- }
- }
-
- #[test(tokio::test)]
- async fn ip_spoofing_in_broadcast_response() {
- let port = start_broadcast_reply_sender("http://1.2.3.4:5".to_owned()).await;
-
- let options = default_options_with_using_free_port(port);
-
- let result = search_gateway(options).await;
- if let Err(SearchError::SpoofedIp { src_ip, url_ip }) = result {
- assert_eq!(src_ip, Ipv4Addr::LOCALHOST);
- assert_eq!(url_ip, Ipv4Addr::new(1, 2, 3, 4));
- } else {
- panic!("Unexpected result: {result:?}");
- }
- }
-
- const RESP: &'static str = r#"
-
-
-
-
-
-
-
-
- urn:schemas-upnp-org:service:WANIPConnection:1
- /igdupnp/control/WANIPConn1
- :aaa@example.com/exec_cmd?cmd=touch%20%2ftmp%2frce
-
-
-
-
-
-
-
-
- "#;
- const RESP2: &'static str = r#"
-
-
-
-
-
-
-
-
- urn:schemas-upnp-org:service:WANIPConnection:1
- :aaa@example.com/exec_cmd?cmd=touch%20%2ftmp%2frce
- /igdupnp/control/WANIPConn1
-
-
-
-
-
-
-
-
- "#;
- const CONTROL_SCHEMA: &'static str = r#"
-
-
-
-
-
-
- "#;
-
- async fn start_http_server(responses: Vec) -> u16 {
- let addr = SocketAddr::from(([0, 0, 0, 0], 0));
-
- // We create a TcpListener and bind it to 127.0.0.1:3000
- let listener = TcpListener::bind(addr).await.unwrap();
-
- let ret = listener.local_addr().unwrap().port();
-
- tokio::task::spawn(async move {
- for resp in responses {
- let (stream, _) = listener.accept().await.unwrap();
-
- // Use an adapter to access something implementing `tokio::io` traits as if they implement
- // `hyper::rt` IO traits.
- let io = TokioIo::new(stream);
-
- let hello_fn = move |r: Request| -> Result>, Infallible> {
- println!("Request: {r:?}");
- Ok(Response::new(Full::new(Bytes::from(resp.clone()))))
- };
-
- // Finally, we bind the incoming connection to our `hello` service
- if let Err(err) = hyper::server::conn::http1::Builder::new()
- // `service_fn` converts our function in a `Service`
- .serve_connection(io, service_fn(|r| async { hello_fn(r) }))
- .await
- {
- eprintln!("Error serving connection: {:?}", err);
- }
- }
- });
-
- ret
- }
-
- #[test(tokio::test)]
- async fn ip_spoofing_in_getxml_body() {
- let http_port = start_http_server(vec![RESP.to_owned()]).await;
-
- let port = start_broadcast_reply_sender(format!("http://127.0.0.1:{http_port}")).await;
-
- println!("http server port: {http_port}, udp port: {port}");
-
- let options = default_options_with_using_free_port(port);
-
- let result = search_gateway(options).await;
- if let Err(SearchError::SpoofedUrl { src_ip, url_host }) = result {
- assert_eq!(src_ip, Ipv4Addr::LOCALHOST);
- assert_eq!(url_host, "example.com");
- } else {
- panic!("Unexpected result: {result:?}");
- }
- }
-
- #[test(tokio::test)]
- async fn ip_spoofing_in_getxml_body_control_url() {
- let http_port = start_http_server(vec![RESP2.to_owned(), CONTROL_SCHEMA.to_owned()]).await;
-
- let port = start_broadcast_reply_sender(format!("http://127.0.0.1:{http_port}")).await;
-
- println!("http server port: {http_port}, udp port: {port}");
-
- let options = default_options_with_using_free_port(port);
-
- let result = search_gateway(options).await;
-
- if let Err(SearchError::SpoofedUrl { src_ip, url_host }) = result {
- assert_eq!(src_ip, Ipv4Addr::LOCALHOST);
- assert_eq!(url_host, "example.com");
- } else {
- panic!("Unexpected result: {result:?}");
- }
- }
-}
diff --git a/src/common/mod.rs b/src/common/mod.rs
index 8417752a..7cfac75b 100644
--- a/src/common/mod.rs
+++ b/src/common/mod.rs
@@ -1,6 +1,8 @@
pub mod messages;
pub mod options;
pub mod parsing;
+#[cfg(test)]
+mod tests;
pub use self::options::SearchOptions;
diff --git a/src/common/tests.rs b/src/common/tests.rs
new file mode 100644
index 00000000..8563a51c
--- /dev/null
+++ b/src/common/tests.rs
@@ -0,0 +1,227 @@
+use crate::{search_gateway, SearchError, SearchOptions};
+use http_body_util::Full;
+use hyper::{body::Bytes, service::service_fn, Request, Response};
+use hyper_util::rt::TokioIo;
+use std::{
+ convert::Infallible,
+ future::Future,
+ net::{Ipv4Addr, SocketAddr, SocketAddrV4},
+ time::Duration,
+};
+use test_log::test;
+use tokio::net::{TcpListener, UdpSocket};
+
+async fn start_broadcast_reply_sender(location: String) -> u16 {
+ let port = {
+ // Not 100% reliable way to find a free port number, but should be good enough
+ let sock = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).await.unwrap();
+ sock.local_addr().unwrap().port()
+ };
+
+ tokio::spawn(async move {
+ tokio::time::sleep(Duration::from_secs(1)).await;
+
+ let sock = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
+
+ sock.send_to(format!("location: {location}").as_bytes(), (Ipv4Addr::LOCALHOST, port))
+ .await
+ .unwrap();
+ });
+ port
+}
+
+fn default_options_with_using_free_port(port: u16) -> SearchOptions {
+ SearchOptions {
+ bind_addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)),
+ timeout: Some(Duration::from_secs(5)),
+ http_timeout: Some(Duration::from_secs(1)),
+ ..Default::default()
+ }
+}
+
+const RESP_SPOOFED_SCPDURL: &'static str = r#"
+
+
+
+
+
+
+
+
+ urn:schemas-upnp-org:service:WANIPConnection:1
+ /igdupnp/control/WANIPConn1
+ :aaa@example.com/exec_cmd?cmd=touch%20%2ftmp%2frce
+
+
+
+
+
+
+
+
+ "#;
+
+const RESP_SPOOFED_CONTROL_URL: &'static str = r#"
+
+
+
+
+
+
+
+
+ urn:schemas-upnp-org:service:WANIPConnection:1
+ :aaa@example2.com/exec_cmd?cmd=touch%20%2ftmp%2frce
+ /igdupnp/control/WANIPConn1
+
+
+
+
+
+
+
+
+ "#;
+
+const RESP_CONTROL_SCHEMA: &'static str = r#"
+
+
+
+
+
+
+ "#;
+
+async fn start_http_server(responses: Vec) -> u16 {
+ let addr = SocketAddr::from(([0, 0, 0, 0], 0));
+
+ // We create a TcpListener and bind it to 127.0.0.1:3000
+ let listener = TcpListener::bind(addr).await.unwrap();
+
+ let ret = listener.local_addr().unwrap().port();
+
+ tokio::task::spawn(async move {
+ for resp in responses {
+ let (stream, _) = listener.accept().await.unwrap();
+
+ // Use an adapter to access something implementing `tokio::io` traits as if they implement
+ // `hyper::rt` IO traits.
+ let io = TokioIo::new(stream);
+
+ let handler = move |r: Request| -> Result>, Infallible> {
+ println!("Request: {r:?}");
+ Ok(Response::new(Full::new(Bytes::from(resp.clone()))))
+ };
+
+ if let Err(err) = hyper::server::conn::http1::Builder::new()
+ .serve_connection(io, service_fn(|r| async { handler(r) }))
+ .await
+ {
+ eprintln!("Error serving connection: {:?}", err);
+ }
+ }
+ });
+
+ ret
+}
+
+#[test(tokio::test)]
+async fn ip_spoofing_in_broadcast_response() {
+ async fn aux(search_gateway: F)
+ where
+ Fut: Future