From 8fb446f6c05c5e7b196bda9cc7fdf87f1a460081 Mon Sep 17 00:00:00 2001 From: Tomasz Klak Date: Wed, 4 Dec 2024 16:28:41 +0100 Subject: [PATCH] Test both sync and async gateway search Gateway searching tests extracted out of async only code into a common test module. --- src/aio/search.rs | 185 +----------------------------------- src/common/mod.rs | 2 + src/common/tests.rs | 227 ++++++++++++++++++++++++++++++++++++++++++++ src/search.rs | 82 ++++++++-------- 4 files changed, 272 insertions(+), 224 deletions(-) create mode 100644 src/common/tests.rs 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>, + F: Fn(SearchOptions) -> Fut, + { + 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:?}"); + } + } + + aux(|opt| async { + tokio::task::spawn_blocking(|| search_gateway(opt).map(|_| ())) + .await + .unwrap() + }) + .await; + #[cfg(feature = "aio")] + aux(|opt| async { crate::aio::search_gateway(opt).await.map(|_| ()) }).await; +} + +#[test(tokio::test)] +async fn ip_spoofing_in_getxml_body() { + async fn aux(search_gateway: F) + where + Fut: Future>, + F: Fn(SearchOptions) -> Fut, + { + let http_port = start_http_server(vec![RESP_SPOOFED_SCPDURL.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:?}"); + } + } + aux(|opt| async { + tokio::task::spawn_blocking(|| search_gateway(opt).map(|_| ())) + .await + .unwrap() + }) + .await; + #[cfg(feature = "aio")] + aux(|opt| async { crate::aio::search_gateway(opt).await.map(|_| ()) }).await; +} + +#[test(tokio::test)] +async fn ip_spoofing_in_getxml_body_control_url() { + async fn aux(search_gateway: F) + where + Fut: Future>, + F: Fn(SearchOptions) -> Fut, + { + let http_port = start_http_server(vec![ + RESP_SPOOFED_CONTROL_URL.to_owned(), + RESP_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, "example2.com"); + } else { + panic!("Unexpected result: {result:?}"); + } + } + aux(|opt| async { + tokio::task::spawn_blocking(|| search_gateway(opt).map(|_| ())) + .await + .unwrap() + }) + .await; + #[cfg(feature = "aio")] + aux(|opt| async { crate::aio::search_gateway(opt).await.map(|_| ()) }).await; +} diff --git a/src/search.rs b/src/search.rs index ccdcb8de..6d4685dd 100644 --- a/src/search.rs +++ b/src/search.rs @@ -29,56 +29,56 @@ pub fn search_gateway(options: SearchOptions) -> Result { socket.send_to(messages::SEARCH_REQUEST.as_bytes(), options.broadcast_address)?; - loop { - let mut buf = [0u8; 1500]; - let (read, from) = socket.recv_from(&mut buf)?; - let text = std::str::from_utf8(&buf[..read])?; - - let (addr, root_url) = parsing::parse_search_result(text)?; - - check_is_ip_spoofed(&from, &addr.into())?; - - let (control_schema_url, control_url) = match get_control_urls(&addr, &root_url) { - Ok(o) => o, - Err(e) => { - debug!( - "Error has occurred while getting control urls. error: {}, addr: {}, root_url: {}", - e, addr, root_url - ); - continue; - } - }; - - let control_schema = match get_control_schemas(&addr, &control_schema_url) { - Ok(o) => o, - Err(e) => { - debug!( - "Error has occurred while getting schemas. error: {}, addr: {}, control_schema_url: {}", - e, addr, control_schema_url - ); - continue; - } - }; + let mut buf = [0u8; 1500]; + let (read, from) = socket.recv_from(&mut buf)?; + let text = std::str::from_utf8(&buf[..read])?; + + let (addr, root_url) = parsing::parse_search_result(text)?; + + check_is_ip_spoofed(&from, &addr.into())?; + + let (control_schema_url, control_url) = match get_control_urls(&addr, &root_url) { + Ok(o) => o, + Err(e) => { + debug!( + "Error has occurred while getting control urls. error: {}, addr: {}, root_url: {}", + e, addr, root_url + ); + return Err(e); + } + }; + + let control_schema = match get_control_schemas(&addr, &control_schema_url) { + Ok(o) => o, + Err(e) => { + debug!( + "Error has occurred while getting schemas. error: {}, addr: {}, control_schema_url: {}", + e, addr, control_schema_url + ); + return Err(e); + } + }; - let gateway = Gateway { - addr, - root_url, - control_url, - control_schema_url, - control_schema, - }; + let gateway = Gateway { + addr, + root_url, + control_url, + control_schema_url, + control_schema, + }; - let gateway_url = reqwest::Url::from_str(&format!("{gateway}"))?; + let gateway_url = reqwest::Url::from_str(&format!("{gateway}"))?; - validate_url((*addr.ip()).into(), &gateway_url)?; + validate_url((*addr.ip()).into(), &gateway_url)?; - return Ok(gateway); - } + return Ok(gateway); } fn get_control_urls(addr: &SocketAddrV4, path: &str) -> Result<(String, String), SearchError> { let url: reqwest::Url = format!("http://{}{}", addr, path).parse()?; + validate_url((*addr.ip()).into(), &url)?; + debug!("requesting control url from: {:?}", url); let client = reqwest::blocking::Client::new(); let resp = client.get(url).send()?;