diff --git a/Cargo.toml b/Cargo.toml index 93bf35b1..d9abc1c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,32 +15,24 @@ version = "0.12.1" all-features = true [dependencies] -attohttpc = {version = "0.16", default-features = false} -bytes = {version = "1", optional = true} -futures = {version = "0.3", optional = true} -http = {version = "0.2", optional = true} log = "0.4" rand = "0.8" +reqwest = { version = "0.12.9", features = ["blocking"] } +thiserror = "2.0.4" tokio = {version = "1", optional = true, features = ["net"]} url = "2" -xmltree = "0.10" - -[dependencies.hyper] -default-features = false -features = ["client", "http1", "http2", "runtime"] -optional = true -version = "0.14" +xmltree = "0.11" [dev-dependencies] -http-body-util = "0.1.2" -hyper-new = { package = "hyper", version = "1", features = ["server", "http1"] } -hyper-util = { version = "0.1.10", features = ["tokio"] } +http-body-util = "0.1" +hyper = { package = "hyper", version = "1", features = ["server", "http1"] } +hyper-util = { version = "0.1", features = ["tokio"] } simplelog = "0.9" test-log = "0.2" tokio = {version = "1", features = ["full"]} [features] -aio = ["futures", "tokio", "hyper", "bytes", "http"] +aio = ["tokio"] default = [] [[example]] diff --git a/src/aio/search.rs b/src/aio/search.rs index 3b781239..5f6fa63a 100644 --- a/src/aio/search.rs +++ b/src/aio/search.rs @@ -1,16 +1,15 @@ use std::collections::HashMap; -use std::net::{IpAddr, SocketAddr}; +use std::future::Future; +use std::net::SocketAddr; use std::str::FromStr; use std::time::Duration; - -use futures::prelude::*; -use hyper::client::Client; use tokio::net::UdpSocket; use tokio::time::timeout; use crate::aio::Gateway; use crate::common::{messages, parsing, SearchOptions}; use crate::errors::SearchError; +use crate::search::validate_url; const MAX_RESPONSE_SIZE: usize = 1500; @@ -78,7 +77,7 @@ pub async fn search_gateway(options: SearchOptions) -> Result Result addr, socket.local_addr() ); - socket + Ok(socket .send_to(messages::SEARCH_REQUEST.as_bytes(), &addr) - .map_ok(|_| ()) - .map_err(SearchError::from) .await + .map(|_| ())?) } async fn receive_search_response(socket: &mut UdpSocket) -> Result<(Vec, SocketAddr), SearchError> { let mut buff = [0u8; MAX_RESPONSE_SIZE]; - let (n, from) = socket.recv_from(&mut buff).map_err(SearchError::from).await?; + let (n, from) = socket.recv_from(&mut buff).await?; debug!("received broadcast response from: {}", from); Ok((buff[..n].to_vec(), from)) } @@ -130,60 +128,40 @@ fn handle_broadcast_resp(from: &SocketAddr, data: &[u8]) -> Result<(SocketAddr, } async fn get_control_urls(addr: &SocketAddr, path: &str) -> Result<(String, String), SearchError> { - let uri = match format!("http://{}{}", addr, path).parse() { - Ok(uri) => uri, - Err(err) => return Err(SearchError::from(err)), - }; + let url: reqwest::Url = format!("http://{}{}", addr, path).parse()?; - debug!("requesting control url from: {}", uri); - let client = Client::new(); - let resp = hyper::body::to_bytes(client.get(uri).await?.into_body()) - .map_err(SearchError::from) - .await?; + debug!("requesting control url from: {:?}", url); + let client = reqwest::Client::new(); + let resp = client.get(url).send().await?; debug!("handling control response from: {}", addr); - let c = std::io::Cursor::new(&resp); - parsing::parse_control_urls(c) + let body = resp.bytes().await?; + parsing::parse_control_urls(body.as_ref()) } async fn get_control_schemas( addr: &SocketAddr, control_schema_url: &str, ) -> Result>, SearchError> { - let uri: hyper::Uri = match format!("http://{}{}", addr, control_schema_url).parse() { - Ok(uri) => uri, - Err(err) => return Err(SearchError::from(err)), - }; + let url: reqwest::Url = format!("http://{}{}", addr, control_schema_url).parse()?; - validate_url(addr.ip(), &uri)?; + validate_url(addr.ip(), &url)?; - debug!("requesting control schema from: {}", uri); - let client = Client::new(); - let resp = hyper::body::to_bytes(client.get(uri).await?.into_body()) - .map_err(SearchError::from) - .await?; + debug!("requesting control schema from: {}", url); + let client = reqwest::Client::new(); + let resp = client.get(url).send().await?; debug!("handling schema response from: {}", addr); - let c = std::io::Cursor::new(&resp); - parsing::parse_schemas(c) -} -fn validate_url(src_ip: IpAddr, url: &hyper::Uri) -> Result<(), SearchError> { - match url.host() { - Some(url_host) if url_host != src_ip.to_string() => Err(SearchError::SpoofedUrl { - src_ip, - url_host: url_host.to_owned(), - }), - None => Err(SearchError::UriMissingHost(url.clone())), - _ => Ok(()), - } + let body = resp.bytes().await?; + parsing::parse_schemas(body.as_ref()) } #[cfg(test)] mod tests { use super::*; use http_body_util::Full; - use hyper_new::{body::Bytes, service::service_fn, Request, Response}; + use hyper::{body::Bytes, service::service_fn, Request, Response}; use hyper_util::rt::TokioIo; use std::convert::Infallible; use std::{ @@ -303,14 +281,13 @@ mod tests { // `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())))) - }; + 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_new::server::conn::http1::Builder::new() + 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 diff --git a/src/aio/soap.rs b/src/aio/soap.rs index 797f5ee2..e6705da7 100644 --- a/src/aio/soap.rs +++ b/src/aio/soap.rs @@ -1,9 +1,5 @@ -use hyper::{ - header::{CONTENT_LENGTH, CONTENT_TYPE}, - Body, Client, Request, -}; - use crate::errors::RequestError; +use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE}; #[derive(Clone, Debug)] pub struct Action(String); @@ -17,18 +13,16 @@ impl Action { const HEADER_NAME: &str = "SOAPAction"; pub async fn send_async(url: &str, action: Action, body: &str) -> Result { - let client = Client::new(); + let client = reqwest::Client::new(); - let req = Request::builder() - .uri(url) - .method("POST") + let resp = client + .post(url) .header(HEADER_NAME, action.0) .header(CONTENT_TYPE, "text/xml") .header(CONTENT_LENGTH, body.len() as u64) - .body(Body::from(body.to_string()))?; + .body(body.to_owned()) + .send() + .await?; - let resp = client.request(req).await?; - let body = hyper::body::to_bytes(resp.into_body()).await?; - let string = String::from_utf8(body.to_vec())?; - Ok(string) + Ok(resp.text().await?) } diff --git a/src/errors.rs b/src/errors.rs index f7989c46..867f5af5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -10,62 +10,27 @@ use std::string::FromUtf8Error; use tokio::time::error::Elapsed; /// Errors that can occur when sending the request to the gateway. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum RequestError { - /// attohttp error - AttoHttpError(attohttpc::Error), - /// IO Error - IoError(io::Error), - /// The response from the gateway could not be parsed. + #[error("IO Error: {0}")] + /// I/O error + IoError(#[from] io::Error), + #[error("The response from the gateway could not be parsed: '{0}'")] + /// Invalid response InvalidResponse(String), - /// The gateway returned an unhandled error code and description. + #[error("The gateway returned an unhandled error code {0} and description: {1}")] + /// Unhandled error code ErrorCode(u16, String), - /// Action is not supported by the gateway + #[error("Action is not supported by the gateway: {0}")] + /// Unsupported action UnsupportedAction(String), - /// When using the aio feature. - #[cfg(feature = "aio")] - HyperError(hyper::Error), - - #[cfg(feature = "aio")] - /// http crate error type - HttpError(http::Error), - + #[error("Reqwest error: {0}")] + /// Reqwest error + ReqwestError(#[from] reqwest::Error), #[cfg(feature = "aio")] - /// Error parsing HTTP body - Utf8Error(FromUtf8Error), -} - -impl From for RequestError { - fn from(err: attohttpc::Error) -> RequestError { - RequestError::AttoHttpError(err) - } -} - -impl From for RequestError { - fn from(err: io::Error) -> RequestError { - RequestError::IoError(err) - } -} - -#[cfg(feature = "aio")] -impl From for RequestError { - fn from(err: http::Error) -> RequestError { - RequestError::HttpError(err) - } -} - -#[cfg(feature = "aio")] -impl From for RequestError { - fn from(err: hyper::Error) -> RequestError { - RequestError::HyperError(err) - } -} - -#[cfg(feature = "aio")] -impl From for RequestError { - fn from(err: FromUtf8Error) -> RequestError { - RequestError::Utf8Error(err) - } + #[error("Error parsing HTTP body as utf-8: {0}")] + /// Utf-8 parsning error + Utf8Error(#[from] FromUtf8Error), } #[cfg(feature = "aio")] @@ -75,42 +40,6 @@ impl From for RequestError { } } -impl fmt::Display for RequestError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - RequestError::AttoHttpError(ref e) => write!(f, "HTTP error {}", e), - RequestError::InvalidResponse(ref e) => write!(f, "Invalid response from gateway: {}", e), - RequestError::IoError(ref e) => write!(f, "IO error. {}", e), - RequestError::ErrorCode(n, ref e) => write!(f, "Gateway response error {}: {}", n, e), - RequestError::UnsupportedAction(ref e) => write!(f, "Gateway does not support action: {}", e), - #[cfg(feature = "aio")] - RequestError::HyperError(ref e) => write!(f, "Hyper Error: {}", e), - #[cfg(feature = "aio")] - RequestError::HttpError(ref e) => write!(f, "Http Error: {}", e), - #[cfg(feature = "aio")] - RequestError::Utf8Error(ref e) => write!(f, "Utf8Error Error: {}", e), - } - } -} - -impl std::error::Error for RequestError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match *self { - RequestError::AttoHttpError(ref e) => Some(e), - RequestError::InvalidResponse(..) => None, - RequestError::IoError(ref e) => Some(e), - RequestError::ErrorCode(..) => None, - RequestError::UnsupportedAction(..) => None, - #[cfg(feature = "aio")] - RequestError::HyperError(ref e) => Some(e), - #[cfg(feature = "aio")] - RequestError::HttpError(ref e) => Some(e), - #[cfg(feature = "aio")] - RequestError::Utf8Error(ref e) => Some(e), - } - } -} - /// Errors returned by `Gateway::get_external_ip` #[derive(Debug)] pub enum GetExternalIpError { @@ -296,28 +225,31 @@ impl std::error::Error for AddPortError { } /// Errors than can occur while trying to find the gateway. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum SearchError { - /// Http/Hyper error - HttpError(attohttpc::Error), - /// Unable to process the response + #[error("Unable to process the response")] + /// Invalid response InvalidResponse, - /// IO Error - IoError(io::Error), - /// UTF-8 decoding error - Utf8Error(str::Utf8Error), - /// XML processing error - XmlError(xmltree::ParseError), - /// When using the aio feature. - #[cfg(feature = "aio")] - HyperError(hyper::Error), - /// Error parsing URI - #[cfg(feature = "aio")] - InvalidUri(hyper::http::uri::InvalidUri), - #[cfg(feature = "aio")] - /// The uri is missing the host - UriMissingHost(hyper::Uri), - /// Ip spoofing detected error + #[error("IO Error: {0}")] + /// I/O error + IoError(#[from] io::Error), + #[error("UTF-8 decoding error: {0}")] + /// Utf-8 parsing error + Utf8Error(#[from] str::Utf8Error), + #[error("XML processing error: {0}")] + /// Xml parsing error + XmlError(#[from] xmltree::ParseError), + #[error("Reqwest error: {0}")] + /// Reqwest error + ReqwestError(#[from] reqwest::Error), + #[error("Error parsing URI: {0}")] + /// Invalid uri + InvalidUri(#[from] url::ParseError), + #[error("The uri is missing the host: {0}")] + /// Uri is missing host + UrlMissingHost(reqwest::Url), + #[error("Ip {src_ip} spoofed as {url_ip}")] + /// IP spoofing error SpoofedIp { /// The IP from which packet was actually received src_ip: IpAddr, @@ -325,6 +257,8 @@ pub enum SearchError { url_ip: IpAddr, }, /// Uri spoofing detected error + #[error("Ip {src_ip} spoofed in url as {url_host}")] + /// Url host spoofing error SpoofedUrl { /// The IP from which packet was actually received src_ip: IpAddr, @@ -333,43 +267,6 @@ pub enum SearchError { }, } -impl From for SearchError { - fn from(err: attohttpc::Error) -> SearchError { - SearchError::HttpError(err) - } -} - -impl From for SearchError { - fn from(err: io::Error) -> SearchError { - SearchError::IoError(err) - } -} - -impl From for SearchError { - fn from(err: str::Utf8Error) -> SearchError { - SearchError::Utf8Error(err) - } -} - -impl From for SearchError { - fn from(err: xmltree::ParseError) -> SearchError { - SearchError::XmlError(err) - } -} - -#[cfg(feature = "aio")] -impl From for SearchError { - fn from(err: hyper::Error) -> SearchError { - SearchError::HyperError(err) - } -} - -#[cfg(feature = "aio")] -impl From for SearchError { - fn from(err: hyper::http::uri::InvalidUri) -> SearchError { - SearchError::InvalidUri(err) - } -} #[cfg(feature = "aio")] impl From for SearchError { fn from(_err: Elapsed) -> SearchError { @@ -377,47 +274,6 @@ impl From for SearchError { } } -impl fmt::Display for SearchError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - SearchError::HttpError(ref e) => write!(f, "HTTP error {}", e), - SearchError::InvalidResponse => write!(f, "Invalid response"), - SearchError::IoError(ref e) => write!(f, "IO error: {}", e), - SearchError::Utf8Error(ref e) => write!(f, "UTF-8 error: {}", e), - SearchError::XmlError(ref e) => write!(f, "XML error: {}", e), - #[cfg(feature = "aio")] - SearchError::HyperError(ref e) => write!(f, "Hyper Error: {}", e), - #[cfg(feature = "aio")] - SearchError::InvalidUri(ref e) => write!(f, "InvalidUri Error: {}", e), - SearchError::SpoofedIp { src_ip, url_ip } => write!(f, "Spoofed IP from {src_ip} as {url_ip}"), - SearchError::UriMissingHost(ref uri) => write!(f, "Host part of '{uri} is missing"), - SearchError::SpoofedUrl { - ref src_ip, - ref url_host, - } => write!(f, "Spoofed IP from {src_ip} as {url_host}"), - } - } -} - -impl error::Error for SearchError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match *self { - SearchError::HttpError(ref e) => Some(e), - SearchError::InvalidResponse => None, - SearchError::IoError(ref e) => Some(e), - SearchError::Utf8Error(ref e) => Some(e), - SearchError::XmlError(ref e) => Some(e), - #[cfg(feature = "aio")] - SearchError::HyperError(ref e) => Some(e), - #[cfg(feature = "aio")] - SearchError::InvalidUri(ref e) => Some(e), - SearchError::SpoofedIp { .. } => None, - SearchError::UriMissingHost { .. } => None, - SearchError::SpoofedUrl { .. } => None, - } - } -} - /// Errors than can occur while getting a port mapping #[derive(Debug)] pub enum GetGenericPortMappingEntryError { diff --git a/src/gateway.rs b/src/gateway.rs index 8931c7c5..efd43844 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -1,3 +1,4 @@ +use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE}; use std::collections::HashMap; use std::fmt; use std::net::{Ipv4Addr, SocketAddrV4}; @@ -25,10 +26,14 @@ impl Gateway { fn perform_request(&self, header: &str, body: &str, ok: &str) -> RequestResult { let url = format!("http://{}{}", self.addr, self.control_url); - let response = attohttpc::post(url) + let client = reqwest::blocking::Client::new(); + + let response = client + .post(url) .header("SOAPAction", header) - .header("Content-Type", "text/xml") - .text(body) + .header(CONTENT_TYPE, "text/xml") + .header(CONTENT_LENGTH, body.len() as u64) + .body(body.to_owned()) .send()?; parsing::parse_response(response.text()?, ok) diff --git a/src/lib.rs b/src/lib.rs index 6363af62..fe397f41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,23 +4,13 @@ //! Use one of the `search_gateway` functions to obtain a `Gateway` object. //! You can then communicate with the device via this object. -extern crate attohttpc; #[macro_use] extern crate log; -#[cfg(feature = "aio")] -extern crate bytes; extern crate rand; extern crate url; extern crate xmltree; -#[cfg(feature = "aio")] -extern crate futures; -#[cfg(feature = "aio")] -extern crate http; -#[cfg(feature = "aio")] -extern crate tokio; - // data structures pub use self::common::parsing::PortMappingEntry; pub use self::common::SearchOptions; @@ -36,6 +26,7 @@ pub use self::search::search_gateway; #[cfg(feature = "aio")] pub mod aio; + mod common; mod errors; mod gateway; diff --git a/src/search.rs b/src/search.rs index 69fe8bf0..142f3e1b 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,6 +1,5 @@ -use attohttpc::Method; -use attohttpc::RequestBuilder; use std::collections::HashMap; +use std::net::IpAddr; use std::net::{SocketAddrV4, UdpSocket}; use std::str; @@ -48,7 +47,7 @@ pub fn search_gateway(options: SearchOptions) -> Result { } }; - let control_schema = match get_schemas(&addr, &control_schema_url) { + let control_schema = match get_control_schemas(&addr, &control_schema_url) { Ok(o) => o, Err(e) => { debug!( @@ -69,26 +68,43 @@ pub fn search_gateway(options: SearchOptions) -> Result { } } -fn get_control_urls(addr: &SocketAddrV4, root_url: &str) -> Result<(String, String), SearchError> { - let url = format!("http://{}:{}{}", addr.ip(), addr.port(), root_url); +fn get_control_urls(addr: &SocketAddrV4, path: &str) -> Result<(String, String), SearchError> { + let url: reqwest::Url = format!("http://{}{}", addr, path).parse()?; - match RequestBuilder::try_new(Method::GET, url) { - Ok(request_builder) => { - let response = request_builder.send()?; - parsing::parse_control_urls(&response.bytes()?[..]) - } - Err(error) => Err(SearchError::HttpError(error)), - } + debug!("requesting control url from: {:?}", url); + let client = reqwest::blocking::Client::new(); + let resp = client.get(url).send()?; + + debug!("handling control response from: {}", addr); + let body = resp.bytes()?; + parsing::parse_control_urls(body.as_ref()) } -fn get_schemas(addr: &SocketAddrV4, control_schema_url: &str) -> Result>, SearchError> { - let url = format!("http://{}:{}{}", addr.ip(), addr.port(), control_schema_url); +fn get_control_schemas( + addr: &SocketAddrV4, + control_schema_url: &str, +) -> Result>, SearchError> { + let url: reqwest::Url = format!("http://{}{}", addr, control_schema_url).parse()?; + + validate_url((*addr.ip()).into(), &url)?; + + debug!("requesting control schema from: {}", url); + let client = reqwest::blocking::Client::new(); + let resp = client.get(url).send()?; + + debug!("handling schema response from: {}", addr); + + let body = resp.bytes()?; + parsing::parse_schemas(body.as_ref()) +} - match RequestBuilder::try_new(Method::GET, url) { - Ok(request_builder) => { - let response = request_builder.send()?; - parsing::parse_schemas(&response.bytes()?[..]) - } - Err(error) => Err(SearchError::HttpError(error)), +pub fn validate_url(src_ip: IpAddr, url: &reqwest::Url) -> Result<(), SearchError> { + match url.host_str() { + Some(url_host) if url_host != src_ip.to_string() => Err(SearchError::SpoofedUrl { + src_ip, + url_host: url_host.to_owned(), + }), + None => Err(SearchError::UrlMissingHost(url.clone())), + _ => Ok(()), } }