mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-05-06 07:17:27 +00:00
http: Replace reqwest with ureq and add HTTPS support
ureq markets itself as a lightweight blocking HTTP client which might be a good choice for rust-gamedig at the moment. However the main reason for changing to ureq is that it allows setting a "resolver" function which overrides the IP address to connect to. This is useful because it allows us to pass a URL with the desired hostname without the HTTP library doing an extra DNS lookup (this allows HTTPS to work when we specify the exact IP and port to connect to external to the URL). Other changes in this commit are: - Feature gated things that depend on serde: this means that the eco game won't be available if the library is compiled without serde - Added the TLS feature to enable TLS support in the HTTP library - Added HTTPSettings to set the protocol (HTTP/HTTPS) and the hostname - Setting a user-agent string on HTTP requests (allows the server to see what program is being used to query them) - Store the address as a parsed Url so we don't re-parse it on every request - Add a method to POST JSON data and parse response - Renamed the request() method to get_json() in anticipation of a future method that will send a GET request and handle the raw bytes instead of using serde - Improved documentation
This commit is contained in:
parent
285bd7fe6e
commit
723f2f5a06
4 changed files with 163 additions and 25 deletions
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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<u16>) -> GDResult<Response> {
|
||||
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<u16>) -> GDResult<Response> { query_with_timeout(address, port, &None) }
|
||||
|
||||
let response = client.request::<Root>("/frontpage")?;
|
||||
/// Query a eco server.
|
||||
pub fn query_with_timeout(
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: &Option<TimeoutSettings>,
|
||||
) -> GDResult<Response> {
|
||||
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::<Root>("/frontpage")?;
|
||||
|
||||
Ok(response.into())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
pub fn new(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<Self>
|
||||
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<TimeoutSettings>,
|
||||
http_settings: HTTPSettings,
|
||||
) -> GDResult<Self>
|
||||
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<T: DeserializeOwned>(&mut self, path: &str) -> GDResult<T> { self.request_json("GET", path) }
|
||||
|
||||
pub fn request<T: DeserializeOwned>(&mut self, path: &str) -> GDResult<T> {
|
||||
/// Send a HTTP Post request with JSON data and parse a JSON response.
|
||||
#[cfg(feature = "serde")]
|
||||
pub fn post_json<T: DeserializeOwned, S: Serialize>(&mut self, path: &str, data: S) -> GDResult<T> {
|
||||
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<T: DeserializeOwned>(&mut self, method: &str, path: &str) -> GDResult<T> {
|
||||
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::<T>()
|
||||
.into_json::<T>()
|
||||
.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<T: DeserializeOwned, S: Serialize>(
|
||||
&mut self,
|
||||
method: &str,
|
||||
path: &str,
|
||||
data: S,
|
||||
) -> GDResult<T> {
|
||||
self.address.set_path(path);
|
||||
self.client
|
||||
.request_url(method, &self.address)
|
||||
.send_json(data)
|
||||
.map_err(|e| PacketSend.context(e))?
|
||||
.into_json::<T>()
|
||||
.map_err(|e| ProtocolFormat.context(e))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue