aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGus Power <gus@infinitesidequests.com>2025-05-22 16:21:05 +0100
committerGus Power <gus@infinitesidequests.com>2025-05-22 16:21:05 +0100
commit3e5aa28345bb009c12b5a55f2e7174957bf4ed9a (patch)
treefa9044d9e72e213aed22d1761a2bf01a439a0550
parente417f3afd13fa770a3b64d604bb1686ed6a77203 (diff)
started on arbitrary http endpoint configuration
-rw-r--r--src/error.rs4
-rw-r--r--src/http.rs97
-rw-r--r--src/ip_service.rs11
-rw-r--r--src/main.rs1
-rw-r--r--src/test_macros.rs11
5 files changed, 122 insertions, 2 deletions
diff --git a/src/error.rs b/src/error.rs
index 429aa58..1ce9482 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -14,6 +14,7 @@ pub enum AppError {
IoError(io::Error),
RequestFailed { url: Url, source: ReqwestError },
InvalidResponse { url: Url, reason: String },
+ InvalidHttpHeader(String),
UnableToGetHomeDirectory(GetHomeError),
}
@@ -33,7 +34,8 @@ impl fmt::Display for AppError {
Self::RequestFailed { url, .. } => write!(f, "Request to {} failed", url),
Self::InvalidResponse { url, reason } => {
write!(f, "Invalid response from {}: {}", url, reason)
- }
+ },
+ Self::InvalidHttpHeader(message) => write!(f, "Invalid HTTP header configuration: {}", message),
Self::UnableToGetHomeDirectory(err) => {
write!(f, "Failed to get home directory: {}", err)
}
diff --git a/src/http.rs b/src/http.rs
new file mode 100644
index 0000000..db5e3a6
--- /dev/null
+++ b/src/http.rs
@@ -0,0 +1,97 @@
+use serde_with::DisplayFromStr;
+use http::{HeaderMap, HeaderName, HeaderValue, Method};
+use reqwest::{Client, Request, Url};
+use serde::{Deserialize, Serialize};
+use serde_with::serde_as;
+use crate::error::{AppError, AppResult};
+
+#[serde_as]
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+pub struct RequestConfig {
+ #[serde_as(as = "DisplayFromStr")]
+ #[serde(default = "RequestConfig::default_method")]
+ method: Method,
+ #[serde_as(as = "DisplayFromStr")]
+ url: Url,
+ #[serde(default)]
+ headers: Vec<(String, String)>,
+}
+
+impl RequestConfig {
+
+ pub fn new(method: Method, url: Url, headers: Vec<(String, String)>) -> Self {
+ Self { method, url, headers }
+ }
+
+ pub fn get(url: Url, headers: Vec<(String, String)>) -> Self {
+ Self::new(Method::GET, url, headers)
+ }
+
+ pub fn build(&self, client: &Client) -> AppResult<Request> {
+ let mut builder = client.request(self.method.clone(), self.url.clone());
+ if !self.headers.is_empty() {
+ builder = builder.headers(self.header_map()?);
+ }
+ Ok(builder.build()?)
+ }
+
+ fn default_method() -> Method {
+ Method::GET
+ }
+
+ fn header_map(&self) -> AppResult<HeaderMap> {
+ let mut header_map = HeaderMap::new();
+ for (name, value) in &self.headers {
+ let name: HeaderName = name.try_into().map_err(|e| {
+ AppError::InvalidHttpHeader(format!("[{}] is not a valid HTTP header name ({})", name, e))
+ })?;
+ let value: HeaderValue = value.parse().map_err(|e| {
+ AppError::InvalidHttpHeader(format!("[{}] is not a valid HTTP header value ({})", value, e))
+ })?;
+ header_map.insert(name, value);
+ }
+
+ Ok(header_map)
+ }
+
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::AppError;
+ use crate::{assert_error, assert_invalid_http_header, test};
+ use reqwest::ClientBuilder;
+ use super::*;
+
+ test! {
+ fn simple_get_request() {
+ let url = Url::parse("https://example.com")?;
+ let actual = build_get_request(&url, vec![])?;
+ assert_eq!(actual.method(), Method::GET);
+ assert_eq!(actual.url(), &url);
+ }
+ }
+
+ test! {
+ fn invalid_header_name() {
+ let url = Url::parse("https://example.com")?;
+ let invalid_headers = vec![("Content\x00Type".to_string(), "application/json".to_string())];
+ assert_invalid_http_header!(build_get_request(&url, invalid_headers), "[Content\x00Type] is not a valid HTTP header name");
+ }
+ }
+
+ test! {
+ fn invalid_header_value() {
+ let url = Url::parse("https://example.com")?;
+ let invalid_headers = vec![("Content-Type".to_string(), "application/json\x00".to_string())];
+ assert_invalid_http_header!(build_get_request(&url, invalid_headers), "[application/json\x00] is not a valid HTTP header value");
+ }
+ }
+
+ fn build_get_request(url: &Url, headers: Vec<(String, String)>) -> AppResult<Request> {
+ let config = RequestConfig::get(url.clone(), headers);
+ let client = ClientBuilder::new().build()?;
+ config.build(&client)
+ }
+
+}
diff --git a/src/ip_service.rs b/src/ip_service.rs
index 3d869db..abe75f1 100644
--- a/src/ip_service.rs
+++ b/src/ip_service.rs
@@ -1,5 +1,5 @@
use crate::error::{AppError, AppResult};
-use crate::ip_service::IpServiceProvider::{IdentMe, Noop};
+use crate::ip_service::IpServiceProvider::{Http, IdentMe, Noop};
use http::StatusCode;
use reqwest::{Client, Url};
use serde::{Deserialize, Serialize};
@@ -11,15 +11,21 @@ use std::str::FromStr;
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "type")]
pub enum IpServiceProvider {
+ #[serde(rename = "HTTP")]
+ Http(HttpConfig),
#[serde(rename = "IDENTME")]
IdentMe(IdentMeConfig),
#[serde(rename = "NOOP")]
Noop,
}
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub struct HttpConfig;
+
impl IpService for IpServiceProvider {
async fn resolve(&self, client: &Client) -> AppResult<IpAddr> {
match self {
+ Http(_http) => Ok(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
IdentMe(ident_me) => ident_me.resolve(client).await,
Noop => Ok(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
}
@@ -84,6 +90,9 @@ impl Default for IdentMeConfig {
}
}
+// http service
+// url - method - headers - response content-type (text, json) - response parser (text, json -> json path?)
+
impl IpService for IdentMeConfig {
async fn resolve(&self, client: &Client) -> AppResult<IpAddr> {
let response = client
diff --git a/src/main.rs b/src/main.rs
index 66fbdee..2843eb3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,6 +15,7 @@ mod ip_service;
#[cfg(test)]
mod test_macros;
+mod http;
#[derive(Parser, Debug)]
#[command(ignore_errors(true), version, about, long_about = None)]
diff --git a/src/test_macros.rs b/src/test_macros.rs
index e1998cd..7887647 100644
--- a/src/test_macros.rs
+++ b/src/test_macros.rs
@@ -84,3 +84,14 @@ macro_rules! assert_invalid_response {
});
};
}
+
+#[macro_export]
+macro_rules! assert_invalid_http_header {
+ ($result:expr, $reason_prefix:expr) => {
+ assert_error!($result, AppError::InvalidHttpHeader(reason) => {
+ assert!(reason.starts_with($reason_prefix),
+ "Expected reason to start with '{}', but got '{}'",
+ $reason_prefix, reason);
+ });
+ };
+}