use std::time::Duration;
use reqwest::Method;
use crate::client::{Client, ClientBuilder, Secret};
use crate::error::Result;
use crate::ratelimit::{RateLimits, Scope, ScopeSet};
pub const DEFAULT_UPDATE_URL: &str = "https://update.dedyn.io";
pub const IPV6_UPDATE_URL: &str = "https://update6.dedyn.io";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IpUpdate {
Set(Vec<String>),
Preserve,
Remove,
}
impl IpUpdate {
pub fn set(addresses: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self::Set(addresses.into_iter().map(Into::into).collect())
}
fn as_param(&self) -> String {
match self {
Self::Set(addresses) => addresses.join(","),
Self::Preserve => "preserve".to_owned(),
Self::Remove => String::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Family {
V4,
V6,
}
impl Family {
fn param(self) -> &'static str {
match self {
Self::V4 => "myipv4",
Self::V6 => "myipv6",
}
}
}
#[derive(Debug, Clone)]
pub struct DynDnsClient {
client: Client,
query_credentials: Option<(String, Secret)>,
}
impl DynDnsClient {
pub fn builder() -> DynDnsClientBuilder {
DynDnsClientBuilder::default()
}
pub fn update(&self, hostname: impl Into<String>) -> UpdateRequest<'_> {
UpdateRequest {
client: self,
hostnames: vec![hostname.into()],
ipv4: None,
ipv6: None,
overrides: Vec::new(),
rate_limit_key: None,
}
}
}
#[derive(Debug, Default)]
pub struct DynDnsClientBuilder {
inner: ClientBuilder,
base: Option<String>,
query_credentials: Option<(String, Secret)>,
}
impl DynDnsClientBuilder {
pub fn token(mut self, token: impl Into<Secret>) -> Self {
self.inner = self.inner.token(token);
self
}
pub fn basic_auth(mut self, username: impl Into<String>, token: impl Into<Secret>) -> Self {
self.inner = self.inner.basic_auth(username, token);
self
}
pub fn query_credentials(
mut self,
username: impl Into<String>,
token: impl Into<Secret>,
) -> Self {
self.query_credentials = Some((username.into(), token.into()));
self
}
pub fn base_url(mut self, base: impl Into<String>) -> Self {
self.base = Some(base.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.inner = self.inner.timeout(timeout);
self
}
pub fn rate_limits(mut self, limits: RateLimits) -> Self {
self.inner = self.inner.rate_limits(limits);
self
}
pub fn max_retries(mut self, retries: u32) -> Self {
self.inner = self.inner.max_retries(retries);
self
}
pub fn max_retry_delay(mut self, delay: Duration) -> Self {
self.inner = self.inner.max_retry_delay(delay);
self
}
pub fn max_rate_limit_wait(mut self, max_wait: Duration) -> Self {
self.inner = self.inner.max_rate_limit_wait(max_wait);
self
}
pub fn build(self) -> Result<DynDnsClient> {
let base = self.base.unwrap_or_else(|| DEFAULT_UPDATE_URL.to_owned());
Ok(DynDnsClient {
client: self.inner.base_url(base).build()?,
query_credentials: self.query_credentials,
})
}
}
#[derive(Debug)]
pub struct UpdateRequest<'a> {
client: &'a DynDnsClient,
hostnames: Vec<String>,
ipv4: Option<IpUpdate>,
ipv6: Option<IpUpdate>,
overrides: Vec<(String, Family, IpUpdate)>,
rate_limit_key: Option<String>,
}
impl UpdateRequest<'_> {
pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
self.hostnames.push(hostname.into());
self
}
pub fn ipv4(mut self, update: IpUpdate) -> Self {
self.ipv4 = Some(update);
self
}
pub fn ipv6(mut self, update: IpUpdate) -> Self {
self.ipv6 = Some(update);
self
}
pub fn address_for(
mut self,
hostname: impl Into<String>,
family: Family,
update: IpUpdate,
) -> Self {
self.overrides.push((hostname.into(), family, update));
self
}
pub fn rate_limit_domain(mut self, domain: impl Into<String>) -> Self {
self.rate_limit_key = Some(domain.into());
self
}
pub async fn send(self) -> Result<()> {
self.send_body().await.map(drop)
}
pub async fn send_body(self) -> Result<String> {
let client = &self.client.client;
let key = self
.rate_limit_key
.as_deref()
.or_else(|| self.hostnames.first().map(String::as_str))
.unwrap_or_default();
let mut req = client.request(
Method::GET,
client.url(&[]),
ScopeSet::per_domain(Scope::DynDns, key),
);
if !self.hostnames.is_empty() {
req = req.query("hostname", &self.hostnames.join(","));
}
if let Some(ipv4) = &self.ipv4 {
req = req.query("myipv4", &ipv4.as_param());
}
if let Some(ipv6) = &self.ipv6 {
req = req.query("myipv6", &ipv6.as_param());
}
for (hostname, family, update) in &self.overrides {
req = req.query(
&format!("{}:{hostname}", family.param()),
&update.as_param(),
);
}
if let Some((username, token)) = &self.client.query_credentials {
req = req
.query("username", username)
.query("password", token.expose());
}
client.send_text(req).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_the_protocol_values() {
assert_eq!(IpUpdate::Preserve.as_param(), "preserve");
assert_eq!(IpUpdate::Remove.as_param(), "");
assert_eq!(IpUpdate::set(["1.2.3.4"]).as_param(), "1.2.3.4");
assert_eq!(
IpUpdate::set(["1.2.3.4", "5.6.7.8"]).as_param(),
"1.2.3.4,5.6.7.8"
);
assert_eq!(
IpUpdate::set(["2a01:a:b:c::/64"]).as_param(),
"2a01:a:b:c::/64"
);
}
#[test]
fn per_family_parameter_names_match_the_protocol() {
assert_eq!(Family::V4.param(), "myipv4");
assert_eq!(Family::V6.param(), "myipv6");
}
}