mirror of
https://github.com/SeriousBug/gandi-live-dns-rust
synced 2024-12-26 15:19:59 -06:00
Add option to use icanhazip as an IP source (#40)
* Add option to use icanhazip as an IP source * skip IP source API tests in CI
This commit is contained in:
parent
98e2931493
commit
eaabec35b4
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -643,6 +643,7 @@ dependencies = [
|
||||||
"governor",
|
"governor",
|
||||||
"httpmock",
|
"httpmock",
|
||||||
"json",
|
"json",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
13
Cargo.toml
13
Cargo.toml
|
@ -9,12 +9,20 @@ strip = "symbols"
|
||||||
lto = true
|
lto = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "0.11", default-features= false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.11", default-features = false, features = [
|
||||||
|
"json",
|
||||||
|
"rustls-tls",
|
||||||
|
] }
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
json = "0.12"
|
json = "0.12"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
directories = "4.0"
|
directories = "4.0"
|
||||||
clap = { version = "3.2", features = ["derive", "cargo", "unicode", "wrap_help"]}
|
clap = { version = "3.2", features = [
|
||||||
|
"derive",
|
||||||
|
"cargo",
|
||||||
|
"unicode",
|
||||||
|
"wrap_help",
|
||||||
|
] }
|
||||||
tokio = { version = "1.20", features = ["full"] }
|
tokio = { version = "1.20", features = ["full"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
@ -25,3 +33,4 @@ die-exit = { git = "https://github.com/Xavientois/die.git", rev = "31d3801f4e216
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
httpmock = "0.6"
|
httpmock = "0.6"
|
||||||
|
regex = "1.6"
|
||||||
|
|
|
@ -15,6 +15,13 @@ api_key = "xxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
# your IP address propagate quickly.
|
# your IP address propagate quickly.
|
||||||
ttl = 300
|
ttl = 300
|
||||||
|
|
||||||
|
# Where to query your IP address from. These options are free and unlimited.
|
||||||
|
# Ipify is used by default. If you want to change it, uncomment the one you want
|
||||||
|
# to use.
|
||||||
|
#
|
||||||
|
# ip_source = "Ipify"
|
||||||
|
# ip_source = "Icanhazip"
|
||||||
|
|
||||||
# For every domain or subdomain you want to update, create an entry below.
|
# For every domain or subdomain you want to update, create an entry below.
|
||||||
|
|
||||||
[[entry]]
|
[[entry]]
|
||||||
|
|
|
@ -17,10 +17,26 @@ fn default_ttl() -> u32 {
|
||||||
return 300;
|
return 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, PartialEq, Debug)]
|
||||||
|
pub enum IPSourceName {
|
||||||
|
Ipify,
|
||||||
|
Icanhazip,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IPSourceName {
|
||||||
|
fn default() -> Self {
|
||||||
|
// Ipify was the first IP source gandi-live-dns had, before it supported
|
||||||
|
// multiple sources. Keeping that as the default.
|
||||||
|
Self::Ipify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
fqdn: String,
|
fqdn: String,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ip_source: IPSourceName,
|
||||||
pub entry: Vec<Entry>,
|
pub entry: Vec<Entry>,
|
||||||
#[serde(default = "default_ttl")]
|
#[serde(default = "default_ttl")]
|
||||||
pub ttl: u32,
|
pub ttl: u32,
|
||||||
|
@ -82,3 +98,78 @@ pub fn validate_config(config: &Config) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::load_config;
|
||||||
|
use crate::{config::IPSourceName, opts::Opts};
|
||||||
|
use std::{env::temp_dir, fs};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_test() {
|
||||||
|
let mut temp = temp_dir().join("gandi-live-dns-test");
|
||||||
|
fs::create_dir_all(&temp).expect("Failed to create test dir");
|
||||||
|
temp.push("test-1.toml");
|
||||||
|
fs::write(
|
||||||
|
&temp,
|
||||||
|
r#"
|
||||||
|
fqdn = "example.com"
|
||||||
|
api_key = "xxx"
|
||||||
|
ttl = 300
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "@"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("Failed to write test config file");
|
||||||
|
|
||||||
|
let opts = Opts {
|
||||||
|
config: Some(temp.to_string_lossy().to_string()),
|
||||||
|
};
|
||||||
|
let conf = load_config(&opts).expect("Failed to load config file");
|
||||||
|
|
||||||
|
assert_eq!(conf.fqdn, "example.com");
|
||||||
|
assert_eq!(conf.api_key, "xxx");
|
||||||
|
assert_eq!(conf.ttl, 300);
|
||||||
|
assert_eq!(conf.entry.len(), 1);
|
||||||
|
assert_eq!(conf.entry[0].name, "@");
|
||||||
|
// default
|
||||||
|
assert_eq!(conf.ip_source, IPSourceName::Ipify);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_change_ip_source() {
|
||||||
|
let mut temp = temp_dir().join("gandi-live-dns-test");
|
||||||
|
fs::create_dir_all(&temp).expect("Failed to create test dir");
|
||||||
|
temp.push("test-2.toml");
|
||||||
|
fs::write(
|
||||||
|
&temp,
|
||||||
|
r#"
|
||||||
|
fqdn = "example.com"
|
||||||
|
api_key = "yyy"
|
||||||
|
ttl = 1200
|
||||||
|
ip_source = "Icanhazip"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "www"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "@"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("Failed to write test config file");
|
||||||
|
|
||||||
|
let opts = Opts {
|
||||||
|
config: Some(temp.to_string_lossy().to_string()),
|
||||||
|
};
|
||||||
|
let conf = load_config(&opts).expect("Failed to load config file");
|
||||||
|
|
||||||
|
assert_eq!(conf.fqdn, "example.com");
|
||||||
|
assert_eq!(conf.api_key, "yyy");
|
||||||
|
assert_eq!(conf.ttl, 1200);
|
||||||
|
assert_eq!(conf.entry.len(), 2);
|
||||||
|
assert_eq!(conf.entry[0].name, "www");
|
||||||
|
assert_eq!(conf.entry[1].name, "@");
|
||||||
|
assert_eq!(conf.ip_source, IPSourceName::Icanhazip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
6
src/ip_source/Readme.md
Normal file
6
src/ip_source/Readme.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
The IP sources. These are APIs that we can query to get the IP address of the
|
||||||
|
current service.
|
||||||
|
|
||||||
|
The tests under this directory are all marked to be skipped, the tests hit the
|
||||||
|
actual APIs and can be flakey in CI. Make sure to run the tests manually if you
|
||||||
|
have to modify the code.
|
60
src/ip_source/icanhazip.rs
Normal file
60
src/ip_source/icanhazip.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
use anyhow;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use super::ip_source::IPSource;
|
||||||
|
|
||||||
|
pub(crate) struct IPSourceIcanhazip {}
|
||||||
|
|
||||||
|
async fn get_ip(api_url: &str) -> anyhow::Result<String> {
|
||||||
|
let response = reqwest::get(api_url).await?;
|
||||||
|
let text = response.text().await?;
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl IPSource for IPSourceIcanhazip {
|
||||||
|
async fn get_ipv4() -> anyhow::Result<String> {
|
||||||
|
Ok(get_ip("https://ipv4.icanhazip.com")
|
||||||
|
.await?
|
||||||
|
// icanazip puts a newline at the end
|
||||||
|
.trim()
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
async fn get_ipv6() -> anyhow::Result<String> {
|
||||||
|
Ok(get_ip("https://ipv6.icanhazip.com")
|
||||||
|
.await?
|
||||||
|
// icanazip puts a newline at the end
|
||||||
|
.trim()
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use super::IPSource;
|
||||||
|
use super::IPSourceIcanhazip;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn ipv4_test() {
|
||||||
|
let ipv4 = IPSourceIcanhazip::get_ipv4()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get the IP address");
|
||||||
|
assert!(Regex::new(r"^\d+[.]\d+[.]\d+[.]\d+$")
|
||||||
|
.unwrap()
|
||||||
|
.is_match(ipv4.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn ipv6_test() {
|
||||||
|
let ipv6 = IPSourceIcanhazip::get_ipv6()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get the IP address");
|
||||||
|
assert!(Regex::new(r"^([0-9a-fA-F]*:){7}[0-9a-fA-F]*$")
|
||||||
|
.unwrap()
|
||||||
|
.is_match(ipv6.as_str()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,3 +20,33 @@ impl IPSource for IPSourceIpify {
|
||||||
get_ip("https://api6.ipify.org").await
|
get_ip("https://api6.ipify.org").await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use super::IPSource;
|
||||||
|
use super::IPSourceIpify;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn ipv4_test() {
|
||||||
|
let ipv4 = IPSourceIpify::get_ipv4()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get the IP address");
|
||||||
|
assert!(Regex::new(r"^\d+[.]\d+[.]\d+[.]\d+$")
|
||||||
|
.unwrap()
|
||||||
|
.is_match(ipv4.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn ipv6_test() {
|
||||||
|
let ipv6 = IPSourceIpify::get_ipv6()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get the IP address");
|
||||||
|
assert!(Regex::new(r"^([0-9a-fA-F]*:){7}[0-9a-fA-F]*$")
|
||||||
|
.unwrap()
|
||||||
|
.is_match(ipv6.as_str()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
|
pub(crate) mod icanhazip;
|
||||||
pub(crate) mod ip_source;
|
pub(crate) mod ip_source;
|
||||||
pub(crate) mod ipify;
|
pub(crate) mod ipify;
|
||||||
|
|
32
src/main.rs
32
src/main.rs
|
@ -3,8 +3,9 @@ use crate::gandi::GandiAPI;
|
||||||
use crate::ip_source::{ip_source::IPSource, ipify::IPSourceIpify};
|
use crate::ip_source::{ip_source::IPSource, ipify::IPSourceIpify};
|
||||||
use anyhow;
|
use anyhow;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use config::IPSourceName;
|
||||||
use futures;
|
use futures;
|
||||||
use opts::Opts;
|
use ip_source::icanhazip::IPSourceIcanhazip;
|
||||||
use reqwest::{header, Client, ClientBuilder, StatusCode};
|
use reqwest::{header, Client, ClientBuilder, StatusCode};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{num::NonZeroU32, sync::Arc, time::Duration};
|
use std::{num::NonZeroU32, sync::Arc, time::Duration};
|
||||||
|
@ -41,9 +42,7 @@ pub struct APIPayload {
|
||||||
pub rrset_ttl: u32,
|
pub rrset_ttl: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run<IP: IPSource>(base_url: &str, opts: Opts) -> anyhow::Result<()> {
|
async fn run<IP: IPSource>(base_url: &str, conf: Config) -> anyhow::Result<()> {
|
||||||
let conf = config::load_config(&opts)
|
|
||||||
.die_with(|error| format!("Failed to read config file: {}", error));
|
|
||||||
config::validate_config(&conf).die_with(|error| format!("Invalid config: {}", error));
|
config::validate_config(&conf).die_with(|error| format!("Invalid config: {}", error));
|
||||||
println!("Finding out the IP address...");
|
println!("Finding out the IP address...");
|
||||||
let ipv4_result = IP::get_ipv4().await;
|
let ipv4_result = IP::get_ipv4().await;
|
||||||
|
@ -122,14 +121,20 @@ async fn run<IP: IPSource>(base_url: &str, opts: Opts) -> anyhow::Result<()> {
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let opts = opts::Opts::parse();
|
let opts = opts::Opts::parse();
|
||||||
run::<IPSourceIpify>("https://api.gandi.net", opts).await
|
let conf = config::load_config(&opts)
|
||||||
|
.die_with(|error| format!("Failed to read config file: {}", error));
|
||||||
|
|
||||||
|
match conf.ip_source {
|
||||||
|
IPSourceName::Ipify => run::<IPSourceIpify>("https://api.gandi.net", conf).await,
|
||||||
|
IPSourceName::Icanhazip => run::<IPSourceIcanhazip>("https://api.gandi.net", conf).await,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::env::temp_dir;
|
use std::env::temp_dir;
|
||||||
|
|
||||||
use crate::{ip_source::ip_source::IPSource, opts::Opts, run};
|
use crate::{config, ip_source::ip_source::IPSource, opts::Opts, run};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use httpmock::MockServer;
|
use httpmock::MockServer;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
@ -172,14 +177,13 @@ mod tests {
|
||||||
then.status(200);
|
then.status(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
run::<IPSourceMock>(
|
let opts = Opts {
|
||||||
server.base_url().as_str(),
|
config: Some(temp.to_string_lossy().to_string()),
|
||||||
Opts {
|
};
|
||||||
config: Some(temp.to_string_lossy().to_string()),
|
let conf = config::load_config(&opts).expect("Failed to load config");
|
||||||
},
|
run::<IPSourceMock>(server.base_url().as_str(), conf)
|
||||||
)
|
.await
|
||||||
.await
|
.expect("Failed when running the update");
|
||||||
.expect("Failed when running the update");
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
mock.assert();
|
mock.assert();
|
||||||
|
|
Loading…
Reference in a new issue