diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 34c030b..759664e 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -21,8 +21,9 @@ default = ["games", "services", "game_defs"] games = [] services = [] game_defs = ["dep:phf", "games"] -serde = ["dep:serde", "serde/derive"] +serde = ["dep:serde", "serde/derive", "ureq/json"] clap = ["dep:clap"] +tls = ["ureq/tls"] [dependencies] byteorder = "1.5" @@ -31,7 +32,8 @@ crc32fast = "1.3" serde_json = "1.0" serde_derive = "1.0" encoding_rs = "0.8" -reqwest = { version = "0.11", features = ["blocking", "json"] } +ureq = { version = "2.8", default-features = false, features = ["gzip"] } +url = "2" serde = { version = "1.0", optional = true } diff --git a/crates/lib/src/games/eco/protocol.rs b/crates/lib/src/games/eco/protocol.rs index 9bb107f..99fb4f7 100644 --- a/crates/lib/src/games/eco/protocol.rs +++ b/crates/lib/src/games/eco/protocol.rs @@ -1,13 +1,29 @@ use crate::eco::{Response, Root}; -use crate::http::HttpClient; +use crate::http::{HTTPSettings, HttpClient}; use crate::{GDResult, TimeoutSettings}; use std::net::{IpAddr, SocketAddr}; -pub fn query(address: &IpAddr, port: Option) -> GDResult { - let address = &SocketAddr::new(*address, port.unwrap_or(3001)); - let mut client = HttpClient::new(address, &Some(TimeoutSettings::default()))?; +/// Query a eco server. +#[inline] +pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, &None) } - let response = client.request::("/frontpage")?; +/// Query a eco server. +pub fn query_with_timeout( + address: &IpAddr, + port: Option, + timeout_settings: &Option, +) -> GDResult { + let address = &SocketAddr::new(*address, port.unwrap_or(3001)); + let mut client = HttpClient::new( + address, + timeout_settings, + HTTPSettings { + protocol: crate::http::Protocol::HTTP, + hostname: None, + }, + )?; + + let response = client.get_json::("/frontpage")?; Ok(response.into()) } diff --git a/crates/lib/src/games/mod.rs b/crates/lib/src/games/mod.rs index ea063fc..3e3e032 100644 --- a/crates/lib/src/games/mod.rs +++ b/crates/lib/src/games/mod.rs @@ -13,6 +13,7 @@ pub use valve::*; /// Battalion 1944 pub mod battalion1944; /// Eco +#[cfg(feature = "serde")] pub mod eco; /// Frontlines: Fuel of War pub mod ffow; diff --git a/crates/lib/src/http.rs b/crates/lib/src/http.rs index 9071e58..eb34cfe 100644 --- a/crates/lib/src/http.rs +++ b/crates/lib/src/http.rs @@ -1,37 +1,156 @@ -use crate::GDErrorKind::{PacketSend, ProtocolFormat, SocketConnect}; +use crate::GDErrorKind::{InvalidInput, PacketSend, ProtocolFormat}; use crate::{GDResult, TimeoutSettings}; -use reqwest::blocking::*; -use serde::de::DeserializeOwned; + use std::net::SocketAddr; +use ureq::{Agent, AgentBuilder}; +use url::Url; + +#[cfg(feature = "serde")] +use serde::{de::DeserializeOwned, Serialize}; + +/// HTTP request client. Define parameters host parameters on new, then re-use +/// for each request. pub struct HttpClient { - client: Client, - address: String, + client: Agent, + address: Url, +} + +/// HTTP Protocols. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] +pub enum Protocol { + #[default] + HTTP, + #[cfg(feature = "tls")] + HTTPS, +} + +impl Protocol { + /// Convert [Protocol] to a static str for use in a [Url]. + /// e.g. "http:" + pub const fn as_str(&self) -> &'static str { + use Protocol::*; + match self { + HTTP => "http:", + #[cfg(feature = "tls")] + HTTPS => "https:", + } + } +} + +/// Additional settings for HTTPClients. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct HTTPSettings { + /// Choose whether to use HTTP or HTTPS. + pub protocol: Protocol, + /// Choose a hostname override (used to set the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header) and for TLS. + pub hostname: Option, } impl HttpClient { - pub fn new(address: &SocketAddr, timeout_settings: &Option) -> GDResult - where Self: Sized { - let client = Client::builder() - .connect_timeout(TimeoutSettings::get_connect_or_default(timeout_settings)) - .timeout(TimeoutSettings::get_connect_or_default(timeout_settings)) - .build() - .map_err(|e| SocketConnect.context(e))?; + /// Creates a new HTTPClient that can be used to send requests. + /// + /// # Parameters + /// - [address](SocketAddr): The IP and port the HTTP request will connect + /// to. + /// - [timeout_settings](TimeoutSettings): Used to set the connect and + /// socket timeouts for the requests. + /// - [http_settings](HttpSettings): Additional settings for the HTTPClient. + pub fn new( + address: &SocketAddr, + timeout_settings: &Option, + http_settings: HTTPSettings, + ) -> GDResult + where + Self: Sized, + { + let mut client_builder = AgentBuilder::new(); + + // Set timeout settings + let (read_timeout, write_timeout) = TimeoutSettings::get_read_and_write_or_defaults(timeout_settings); + + if let Some(read_timeout) = read_timeout { + client_builder = client_builder.timeout_read(read_timeout); + } + + if let Some(write_timeout) = write_timeout { + client_builder = client_builder.timeout_write(write_timeout); + } + + if let Some(connect_timeout) = TimeoutSettings::get_connect_or_default(timeout_settings) { + client_builder = client_builder.timeout_connect(connect_timeout); + } + + // Every request sent from this client will connect to the address set + { + let address = *address; + client_builder = client_builder.resolver(move |_: &str| Ok(vec![address])); + } + + // Set a friendly user-agent string + client_builder = client_builder.user_agent(concat!( + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION") + )); + + let client = client_builder.build(); + + let host = http_settings.hostname.unwrap_or(address.ip().to_string()); Ok(Self { client, - address: format!("http://{}:{}", address.ip(), address.port()), + // TODO: Use Url from_parts if it gets added + address: Url::parse(&format!( + "{}//{}:{}", + http_settings.protocol.as_str(), + host, + address.port() + )) + .map_err(|e| InvalidInput.context(e))?, }) } - pub fn concat_path(&self, path: &str) -> String { format!("{}{}", self.address, path) } + /// Send a HTTP GET request and parse the JSON resonse. + #[cfg(feature = "serde")] + pub fn get_json(&mut self, path: &str) -> GDResult { self.request_json("GET", path) } - pub fn request(&mut self, path: &str) -> GDResult { + /// Send a HTTP Post request with JSON data and parse a JSON response. + #[cfg(feature = "serde")] + pub fn post_json(&mut self, path: &str, data: S) -> GDResult { + self.request_with_json_data("POST", path, data) + } + + // NOTE: More methods can be added here as required + + /// Send a HTTP request without any data and parse the JSON response. + #[inline] + #[cfg(feature = "serde")] + fn request_json(&mut self, method: &str, path: &str) -> GDResult { + self.address.set_path(path); self.client - .get(self.concat_path(path)) - .send() + .request_url(method, &self.address) + .call() .map_err(|e| PacketSend.context(e))? - .json::() + .into_json::() + .map_err(|e| ProtocolFormat.context(e)) + } + + /// Send a HTTP request with JSON data and parse the JSON response. + #[inline] + #[cfg(feature = "serde")] + fn request_with_json_data( + &mut self, + method: &str, + path: &str, + data: S, + ) -> GDResult { + self.address.set_path(path); + self.client + .request_url(method, &self.address) + .send_json(data) + .map_err(|e| PacketSend.context(e))? + .into_json::() .map_err(|e| ProtocolFormat.context(e)) } }