From 36f6f6600e2c9b1b3299a0a3fa9666b12e1276dc Mon Sep 17 00:00:00 2001 From: jannikac Date: Sat, 4 Feb 2023 14:37:07 +0100 Subject: [PATCH] Replaced die_with with ClientError enum derived with thiserror. This enables prettier and more structured error handling than anyhow --- src/ip_source/icanhazip.rs | 8 +++-- src/ip_source/ip_source.rs | 6 ++-- src/ip_source/ipify.rs | 8 +++-- src/ip_source/seeip.rs | 8 +++-- src/main.rs | 71 ++++++++++++++++++++++++++++++-------- 5 files changed, 76 insertions(+), 25 deletions(-) diff --git a/src/ip_source/icanhazip.rs b/src/ip_source/icanhazip.rs index 77394c3..9d26aac 100644 --- a/src/ip_source/icanhazip.rs +++ b/src/ip_source/icanhazip.rs @@ -1,10 +1,12 @@ use async_trait::async_trait; +use crate::ClientError; + use super::ip_source::IPSource; pub(crate) struct IPSourceIcanhazip; -async fn get_ip(api_url: &str) -> anyhow::Result { +async fn get_ip(api_url: &str) -> Result { let response = reqwest::get(api_url).await?; let text = response.text().await?; Ok(text) @@ -12,14 +14,14 @@ async fn get_ip(api_url: &str) -> anyhow::Result { #[async_trait] impl IPSource for IPSourceIcanhazip { - async fn get_ipv4(&self) -> anyhow::Result { + async fn get_ipv4(&self) -> Result { Ok(get_ip("https://ipv4.icanhazip.com") .await? // icanazip puts a newline at the end .trim() .to_string()) } - async fn get_ipv6(&self) -> anyhow::Result { + async fn get_ipv6(&self) -> Result { Ok(get_ip("https://ipv6.icanhazip.com") .await? // icanazip puts a newline at the end diff --git a/src/ip_source/ip_source.rs b/src/ip_source/ip_source.rs index e3e49fc..60d6c96 100644 --- a/src/ip_source/ip_source.rs +++ b/src/ip_source/ip_source.rs @@ -1,7 +1,9 @@ use async_trait::async_trait; +use crate::ClientError; + #[async_trait] pub trait IPSource { - async fn get_ipv4(&self) -> anyhow::Result; - async fn get_ipv6(&self) -> anyhow::Result; + async fn get_ipv4(&self) -> Result; + async fn get_ipv6(&self) -> Result; } diff --git a/src/ip_source/ipify.rs b/src/ip_source/ipify.rs index 19fb8eb..2a0eb96 100644 --- a/src/ip_source/ipify.rs +++ b/src/ip_source/ipify.rs @@ -1,10 +1,12 @@ use async_trait::async_trait; +use crate::ClientError; + use super::ip_source::IPSource; pub(crate) struct IPSourceIpify; -async fn get_ip(api_url: &str) -> anyhow::Result { +async fn get_ip(api_url: &str) -> Result { let response = reqwest::get(api_url).await?; let text = response.text().await?; Ok(text) @@ -12,10 +14,10 @@ async fn get_ip(api_url: &str) -> anyhow::Result { #[async_trait] impl IPSource for IPSourceIpify { - async fn get_ipv4(&self) -> anyhow::Result { + async fn get_ipv4(&self) -> Result { get_ip("https://api.ipify.org").await } - async fn get_ipv6(&self) -> anyhow::Result { + async fn get_ipv6(&self) -> Result { get_ip("https://api6.ipify.org").await } } diff --git a/src/ip_source/seeip.rs b/src/ip_source/seeip.rs index 9945e96..d87c526 100644 --- a/src/ip_source/seeip.rs +++ b/src/ip_source/seeip.rs @@ -1,10 +1,12 @@ use async_trait::async_trait; +use crate::ClientError; + use super::ip_source::IPSource; pub(crate) struct IPSourceSeeIP; -async fn get_ip(api_url: &str) -> anyhow::Result { +async fn get_ip(api_url: &str) -> Result { let response = reqwest::get(api_url).await?; let text = response.text().await?; Ok(text) @@ -12,10 +14,10 @@ async fn get_ip(api_url: &str) -> anyhow::Result { #[async_trait] impl IPSource for IPSourceSeeIP { - async fn get_ipv4(&self) -> anyhow::Result { + async fn get_ipv4(&self) -> Result { get_ip("https://ip4.seeip.org").await } - async fn get_ipv6(&self) -> anyhow::Result { + async fn get_ipv6(&self) -> Result { get_ip("https://ip6.seeip.org").await } } diff --git a/src/main.rs b/src/main.rs index 4ff6e08..29c0b07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,11 @@ use crate::config::Config; use crate::gandi::GandiAPI; use crate::ip_source::{ip_source::IPSource, ipify::IPSourceIpify}; use clap::Parser; -use config::IPSourceName; +use config::{ConfigError, IPSourceName}; use ip_source::icanhazip::IPSourceIcanhazip; use ip_source::seeip::IPSourceSeeIP; use opts::Opts; +use reqwest::header::InvalidHeaderValue; use reqwest::{header, Client, ClientBuilder, StatusCode}; use serde::Serialize; use std::{num::NonZeroU32, sync::Arc, time::Duration}; @@ -16,13 +17,44 @@ mod gandi; mod ip_source; mod opts; use die_exit::*; +use thiserror::Error; /// 30 requests per minute, see https://api.gandi.net/docs/reference/ const GANDI_RATE_LIMIT: u32 = 30; /// If we hit the rate limit, wait up to this many seconds before next attempt const GANDI_DELAY_JITTER: u64 = 20; -fn api_client(api_key: &str) -> anyhow::Result { +#[derive(Error, Debug)] +pub enum ClientError { + #[error("Error occured while reading config: {0}")] + Config(#[from] ConfigError), + #[error("Error while accessing the Gandi API: {0}")] + Api(#[from] ApiError), + #[error("Error while converting the API key to a header: {0}")] + InvalidHeader(#[from] InvalidHeaderValue), + #[error("Error while sending request: {0}")] + Request(#[from] reqwest::Error), + #[error("Error while joining async tasks: {0}")] + TaskJoin(#[from] tokio::task::JoinError), + #[error("Unexpected type in config: {0}")] + BadEntry(String), + #[error("Entry '{0}' includes type A which requires an IPv4 adress but no IPv4 adress could be determined because: {1}")] + Ipv4missing(String, String), + #[error("Entry '{0}' includes type AAAA which requires an IPv6 adress but no IPv6 adress could be determined because: {1}")] + Ipv6missing(String, String), +} + +#[derive(Error, Debug)] +pub enum ApiError { + #[error("API returned 403 - Forbidden. Message: {message:?}")] + Forbidden { message: String }, + #[error("API returned 403 - Unauthorized. Provided API key is possibly incorrect")] + Unauthorized(), + #[error("API returned {0} - {0}")] + Unknown(StatusCode, String), +} + +fn api_client(api_key: &str) -> Result { let client_builder = ClientBuilder::new(); let key = format!("Apikey {}", api_key); @@ -47,7 +79,7 @@ async fn run( ip_source: &Box, conf: &Config, opts: &Opts, -) -> anyhow::Result<()> { +) -> Result<(), ClientError> { let mut last_ipv4: Option = None; let mut last_ipv6: Option = None; @@ -100,10 +132,22 @@ async fn run( } .url(); let ip = match entry_type { - "A" => ipv4.die_with(|error| format!("Needed IPv4 for {fqdn}: {error}")), - "AAAA" => ipv6.die_with(|error| format!("Needed IPv6 for {fqdn}: {error}")), - bad_entry_type => die!("Unexpected type in config: {}", bad_entry_type), - }; + "A" => match ipv4 { + Ok(ref value) => Ok(value), + Err(ref err) => Err(ClientError::Ipv4missing( + entry.name.clone(), + err.to_string(), + )), + }, + "AAAA" => match ipv6 { + Ok(ref value) => Ok(value), + Err(ref err) => Err(ClientError::Ipv6missing( + entry.name.clone(), + err.to_string(), + )), + }, + &_ => Err(ClientError::BadEntry(entry_type.to_string())), + }?; let payload = APIPayload { rrset_values: vec![ip.to_string()], rrset_ttl: Config::ttl(entry, conf), @@ -151,17 +195,16 @@ async fn run( } #[tokio::main(flavor = "current_thread")] -async fn main() -> anyhow::Result<()> { +async fn main() -> Result<(), ClientError> { let opts = opts::Opts::parse(); - let conf = config::load_config(&opts) - .die_with(|error| format!("Failed to read config file: {}", error)); + let conf = config::load_config(&opts)?; let ip_source: Box = match conf.ip_source { IPSourceName::Ipify => Box::new(IPSourceIpify), IPSourceName::Icanhazip => Box::new(IPSourceIcanhazip), IPSourceName::SeeIP => Box::new(IPSourceSeeIP), }; - config::validate_config(&conf).die_with(|error| format!("Invalid config: {}", error)); + config::validate_config(&conf)?; run("https://api.gandi.net", &ip_source, &conf, &opts).await?; Ok(()) } @@ -170,7 +213,7 @@ async fn main() -> anyhow::Result<()> { mod tests { use std::{env::temp_dir, time::Duration}; - use crate::{config, ip_source::ip_source::IPSource, opts::Opts, run}; + use crate::{config, ip_source::ip_source::IPSource, opts::Opts, run, ClientError}; use async_trait::async_trait; use httpmock::MockServer; use tokio::{fs, task::LocalSet, time::sleep}; @@ -179,10 +222,10 @@ mod tests { #[async_trait] impl IPSource for IPSourceMock { - async fn get_ipv4(&self) -> anyhow::Result { + async fn get_ipv4(&self) -> Result { Ok("192.168.0.0".to_string()) } - async fn get_ipv6(&self) -> anyhow::Result { + async fn get_ipv6(&self) -> Result { Ok("fe80:0000:0208:74ff:feda:625c".to_string()) } }