Skip to content

Commit

Permalink
Test both sync and async gateway search
Browse files Browse the repository at this point in the history
Gateway searching tests extracted out of async only code into a common
test module.
  • Loading branch information
tomaszklak committed Dec 5, 2024
1 parent 826721e commit bbb320d
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 224 deletions.
185 changes: 2 additions & 183 deletions src/aio/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand All @@ -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#"<?xml version="1.0" ?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<device>
<deviceList>
<device>
<deviceList>
<device>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<controlURL>/igdupnp/control/WANIPConn1</controlURL>
<SCPDURL>:[email protected]/exec_cmd?cmd=touch%20%2ftmp%2frce</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
</device>
</root>
"#;
const RESP2: &'static str = r#"<?xml version="1.0" ?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<device>
<deviceList>
<device>
<deviceList>
<device>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<controlURL>:[email protected]/exec_cmd?cmd=touch%20%2ftmp%2frce</controlURL>
<SCPDURL>/igdupnp/control/WANIPConn1</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
</device>
</root>
"#;
const CONTROL_SCHEMA: &'static str = r#"<?xml version="1.0" ?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<actionList>
<action>
</action>
</actionList>
</root>
"#;

async fn start_http_server(responses: Vec<String>) -> u16 {
let addr = SocketAddr::from(([0, 0, 0, 0], 0));

// We create a TcpListener and bind it to 0.0.0.0:0
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<hyper::body::Incoming>| -> Result<Response<Full<Bytes>>, 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:?}");
}
}
}
2 changes: 2 additions & 0 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub mod messages;
pub mod options;
pub mod parsing;
#[cfg(test)]
mod tests;

pub use self::options::SearchOptions;

Expand Down
Loading

0 comments on commit bbb320d

Please sign in to comment.