Compare commits

...

16 commits

Author SHA1 Message Date
Kaan Barmore-Genç 847b6eb1c5
Upload to ghcr.io, document the fact that it's there 2023-03-16 00:10:45 -04:00
Kaan Barmore-Genç 706251c4d8
Update to 1.8.0 2023-02-13 01:04:05 -05:00
Kaan Barmore-Genç d4dffb19e9
Update deps (#95) 2023-02-13 01:03:34 -05:00
Kaan Barmore-Genç 5c6b38f7b0
Do retry update after a failure & fix tests (#94)
* Do retry update after a failure & fix tests

* Fix formatting

* Fix clippy errors

* Add codecov ignore for ip_source files
2023-02-13 00:58:23 -05:00
jannikac 8413555d2f
Implement thiserror and improve output formatting (#91)
* implemented an error struct with all possible errors using thiserror

* Replaced die_with with ClientError enum derived with thiserror.
This enables prettier and more structured error handling than anyhow

* Added proper parsing of Gandi API Responses.
This also makes it possible to output prettier logs.

* improved error message

* anyhow is better for main fn because it outputs anyhow errors correctly
2023-02-10 22:08:54 -05:00
Kaan Barmore-Genç 5cdd7b9e83
Update readme file name 2023-02-01 23:58:47 -05:00
Kaan Barmore-Genç 1bb4c7af1c
update build script 2023-02-01 23:57:01 -05:00
Kaan Barmore-Genç b71a78118b
Release 1.7.0 2023-02-01 23:24:09 -05:00
Kaan Barmore-Genç 27a60d3ac2
Add new IP source "seeip" (#90) 2023-02-01 23:19:57 -05:00
Kaan Barmore-Genç 327b14a00a
Skip updating the IP address if it did not change (#88)
* Skip updating the IP address if it did not change

* Update readme
2023-02-01 02:00:09 -05:00
Kaan Barmore-Genç f8060fad42
Avoid multiple main versions & concurrently get ipv4 and ipv6 addresses (#87)
* Avoid having multiple versions of main run function

* Concurrently get ipv4 and ipv6 addresses
2023-02-01 00:21:02 -05:00
Kaan Barmore-Genç 7e7a9da65e
Switch back to die-exit (#86)
We had temporarily moved to die-exit-2, a forked release, but the
original project has made a release now!
2023-01-31 00:16:02 -05:00
Kaan Barmore-Genç e95cf42b69
Update dependencies (#85) 2023-01-31 00:07:02 -05:00
allcontributors[bot] 4ada0b7fb4
docs: add jannikac as a contributor for code (#73)
* docs: update README.md [skip ci]

* docs: create .all-contributorsrc [skip ci]

* Update README.md

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
Co-authored-by: Kaan Barmore-Genç <kaan@bgenc.net>
2022-12-18 10:35:09 -05:00
Kaan Barmore-Genç d52ca4b840
Rename readme 2022-12-18 10:29:08 -05:00
Kaan Barmore-Genç ed83f7dedc
Add contributors block 2022-12-18 10:25:00 -05:00
17 changed files with 933 additions and 358 deletions

25
.all-contributorsrc Normal file
View file

@ -0,0 +1,25 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitConvention": "angular",
"contributors": [
{
"login": "jannikac",
"name": "jannikac",
"avatar_url": "https://avatars.githubusercontent.com/u/21014142?v=4",
"profile": "https://github.com/jannikac",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "gandi-live-dns-rust",
"projectOwner": "SeriousBug"
}

3
.clippy.toml Normal file
View file

@ -0,0 +1,3 @@
# assert_eq!(..., true) or false is a lot clearer when testing functions that
# return booleans.
bool_assert_comparison = false

4
.codecov.yml Normal file
View file

@ -0,0 +1,4 @@
ignore:
# These are tested, but the tests hit external services which is not
# necessarily smart to do in CI, so they get skipped.
- "src/ip_source"

443
Cargo.lock generated
View file

@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.67"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7724808837b77f4b4de9d283820f9d98bcf496d5692934b857a2399d31ff22e6"
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]]
name = "ascii-canvas"
@ -168,9 +168,9 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524"
[[package]]
name = "async-trait"
version = "0.1.60"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3"
checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
dependencies = [
"proc-macro2",
"quote",
@ -179,9 +179,9 @@ dependencies = [
[[package]]
name = "atomic-waker"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599"
[[package]]
name = "atty"
@ -206,6 +206,12 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "basic-cookies"
version = "0.1.4"
@ -254,15 +260,15 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.11.1"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "bytes"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "castaway"
@ -272,9 +278,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
[[package]]
name = "cc"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "cfg-if"
@ -284,9 +290,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.0.29"
version = "4.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d"
checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76"
dependencies = [
"bitflags",
"clap_derive",
@ -302,9 +308,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.0.21"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8"
dependencies = [
"heck",
"proc-macro-error",
@ -315,18 +321,18 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "concurrent-queue"
version = "2.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e"
dependencies = [
"crossbeam-utils",
]
@ -401,10 +407,10 @@ dependencies = [
]
[[package]]
name = "die-exit-2"
name = "die-exit"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9ac3d64c794ef4965e2efc9f9eb3c815f40201a25fe47ee19c0801ec428b7c"
checksum = "38c7de4151fc657b3ef1c90aebd13b25d089166f1f8c1d0a7950ebdfe6499f49"
[[package]]
name = "diff"
@ -455,9 +461,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.8.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "ena"
@ -470,9 +476,9 @@ dependencies = [
[[package]]
name = "encoding_rs"
version = "0.8.31"
version = "0.8.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
dependencies = [
"cfg-if",
]
@ -536,9 +542,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
dependencies = [
"futures-channel",
"futures-core",
@ -551,9 +557,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
dependencies = [
"futures-core",
"futures-sink",
@ -561,15 +567,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
[[package]]
name = "futures-executor"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
dependencies = [
"futures-core",
"futures-task",
@ -578,9 +584,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
[[package]]
name = "futures-lite"
@ -599,9 +605,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
dependencies = [
"proc-macro2",
"quote",
@ -610,15 +616,15 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
[[package]]
name = "futures-task"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
[[package]]
name = "futures-timer"
@ -628,9 +634,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
[[package]]
name = "futures-util"
version = "0.3.25"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
dependencies = [
"futures-channel",
"futures-core",
@ -646,20 +652,22 @@ dependencies = [
[[package]]
name = "gandi-live-dns"
version = "1.5.0"
version = "1.8.0"
dependencies = [
"anyhow",
"async-trait",
"clap",
"die-exit-2",
"die-exit",
"directories",
"futures",
"governor",
"httpmock",
"json",
"lazy_static",
"regex",
"reqwest",
"serde",
"thiserror",
"tokio",
"toml",
]
@ -677,9 +685,9 @@ dependencies = [
[[package]]
name = "gloo-timers"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98c4a8d6391675c6b2ee1a6c8d06e8e2d03605c44cec1270675985a4c2a5500b"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"futures-channel",
"futures-core",
@ -732,9 +740,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
@ -754,6 +762,12 @@ dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "http"
version = "0.2.8"
@ -797,7 +811,7 @@ dependencies = [
"assert-json-diff",
"async-object-pool",
"async-trait",
"base64",
"base64 0.13.1",
"basic-cookies",
"crossbeam-utils",
"form_urlencoded",
@ -818,9 +832,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "0.14.23"
version = "0.14.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
dependencies = [
"bytes",
"futures-channel",
@ -884,30 +898,30 @@ dependencies = [
[[package]]
name = "io-lifetimes"
version = "1.0.3"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
dependencies = [
"libc",
"windows-sys 0.42.0",
"windows-sys 0.45.0",
]
[[package]]
name = "ipnet"
version = "2.7.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e"
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
[[package]]
name = "is-terminal"
version = "0.4.1"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330"
checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef"
dependencies = [
"hermit-abi 0.2.6",
"hermit-abi 0.3.1",
"io-lifetimes",
"rustix",
"windows-sys 0.42.0",
"windows-sys 0.45.0",
]
[[package]]
@ -954,9 +968,9 @@ checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "js-sys"
version = "0.3.60"
version = "0.3.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
dependencies = [
"wasm-bindgen",
]
@ -1022,9 +1036,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
[[package]]
name = "libc"
version = "0.2.138"
version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "libnghttp2-sys"
@ -1119,6 +1133,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
[[package]]
name = "nom8"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
dependencies = [
"memchr",
]
[[package]]
name = "nonzero_ext"
version = "0.3.0"
@ -1127,19 +1150,19 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
[[package]]
name = "num_cpus"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi 0.1.19",
"hermit-abi 0.2.6",
"libc",
]
[[package]]
name = "once_cell"
version = "1.16.0"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "openssl-probe"
@ -1149,9 +1172,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.79"
version = "0.9.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5454462c0eced1e97f2ec09036abc8da362e66802f66fd20f86854d9d8cbcbc4"
checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
dependencies = [
"autocfg",
"cc",
@ -1184,15 +1207,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.5"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-sys 0.42.0",
"windows-sys 0.45.0",
]
[[package]]
@ -1203,9 +1226,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "petgraph"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143"
checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
dependencies = [
"fixedbitset",
"indexmap",
@ -1316,9 +1339,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.48"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9d89e5dba24725ae5678020bf8f1357a9aa7ff10736b551adbcd3f8d17d766f"
checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
dependencies = [
"unicode-ident",
]
@ -1341,9 +1364,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556d0f47a940e895261e77dc200d5eadfc6ef644c179c6f5edfc105e3a2292c8"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
@ -1380,9 +1403,9 @@ dependencies = [
[[package]]
name = "raw-cpuid"
version = "10.6.0"
version = "10.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb"
checksum = "c307f7aacdbab3f0adee67d52739a1d71112cc068d6fab169ddeb18e48877fad"
dependencies = [
"bitflags",
]
@ -1409,9 +1432,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.7.0"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
dependencies = [
"aho-corasick",
"memchr",
@ -1426,11 +1449,11 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "reqwest"
version = "0.11.13"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9"
dependencies = [
"base64",
"base64 0.21.0",
"bytes",
"encoding_rs",
"futures-core",
@ -1480,23 +1503,23 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.36.5"
version = "0.36.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588"
checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.42.0",
"windows-sys 0.45.0",
]
[[package]]
name = "rustls"
version = "0.20.7"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
dependencies = [
"log",
"ring",
@ -1506,11 +1529,11 @@ dependencies = [
[[package]]
name = "rustls-pemfile"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
dependencies = [
"base64",
"base64 0.21.0",
]
[[package]]
@ -1527,12 +1550,11 @@ checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
[[package]]
name = "schannel"
version = "0.1.20"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
dependencies = [
"lazy_static",
"windows-sys 0.36.1",
"windows-sys 0.42.0",
]
[[package]]
@ -1553,18 +1575,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.151"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.151"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
@ -1573,9 +1595,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.90"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8778cc0b528968fe72abec38b5db5a20a70d148116cd9325d2bc5f5180ca3faf"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
dependencies = [
"itoa",
"ryu",
@ -1592,6 +1614,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1606,9 +1637,9 @@ dependencies = [
[[package]]
name = "signal-hook"
version = "0.3.14"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
dependencies = [
"libc",
"signal-hook-registry",
@ -1616,9 +1647,9 @@ dependencies = [
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
@ -1698,9 +1729,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.106"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ee3a69cd2c7e06684677e5629b3878b253af05e4714964204279c6bc02cf0b"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
dependencies = [
"proc-macro2",
"quote",
@ -1720,9 +1751,9 @@ dependencies = [
[[package]]
name = "termcolor"
version = "1.1.3"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
@ -1777,15 +1808,15 @@ dependencies = [
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.23.0"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
dependencies = [
"autocfg",
"bytes",
@ -1825,9 +1856,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.4"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
dependencies = [
"bytes",
"futures-core",
@ -1839,11 +1870,36 @@ dependencies = [
[[package]]
name = "toml"
version = "0.5.10"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f"
checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5"
dependencies = [
"indexmap",
"nom8",
"serde",
"serde_spanned",
"toml_datetime",
]
[[package]]
@ -1897,9 +1953,9 @@ dependencies = [
[[package]]
name = "try-lock"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "unicase"
@ -1912,9 +1968,9 @@ dependencies = [
[[package]]
name = "unicode-bidi"
version = "0.3.8"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
[[package]]
name = "unicode-ident"
@ -2012,9 +2068,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.83"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@ -2022,9 +2078,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.83"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
dependencies = [
"bumpalo",
"log",
@ -2037,9 +2093,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.33"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
dependencies = [
"cfg-if",
"js-sys",
@ -2049,9 +2105,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.83"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -2059,9 +2115,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.83"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
dependencies = [
"proc-macro2",
"quote",
@ -2072,15 +2128,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.83"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
[[package]]
name = "web-sys"
version = "0.3.60"
version = "0.3.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -2145,19 +2201,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
@ -2165,85 +2208,79 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.0",
"windows_x86_64_msvc",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winreg"

View file

@ -1,11 +1,11 @@
[package]
name = "gandi-live-dns"
description = "Automatically updates your IP address in Gandi's Live DNS. Makes it possible to use Gandi as a dynamic DNS system."
version = "1.6.0"
version = "1.8.0"
edition = "2021"
authors = ["Kaan Barmore-Genç <kaan@bgenc.net>"]
license = "MIT"
readme = "Readme.md"
readme = "README.md"
repository = "https://github.com/SeriousBug/gandi-live-dns-rust"
[profile.release]
@ -17,7 +17,7 @@ reqwest = { version = "0.11", default-features = false, features = [
"json",
"rustls-tls",
] }
toml = "0.5"
toml = "0.7"
json = "0.12"
serde = { version = "1.0", features = ["derive"] }
directories = "4.0"
@ -32,12 +32,14 @@ futures = "0.3"
anyhow = "1.0"
governor = "0.5"
async-trait = "0.1"
die-exit-2 = "0.4"
die-exit = "0.4"
thiserror = "1.0.38"
[dev-dependencies]
httpmock = "0.6"
regex = "1.6"
lazy_static = "1.4.0"
[dev-dependencies.die-exit-2]
[dev-dependencies.die-exit]
version = "0.4"
features = ["test"]

View file

@ -1,6 +1,14 @@
## gandi-live-dns-rust
## Gandi Live Dns Rust <!-- omit in toc -->
[![tests](https://img.shields.io/github/actions/workflow/status/SeriousBug/gandi-live-dns-rust/test.yml?label=tests&branch=master)](https://github.com/SeriousBug/gandi-live-dns-rust/actions/workflows/test.yml) [![Test coverage report](https://img.shields.io/codecov/c/github/SeriousBug/gandi-live-dns-rust)](https://codecov.io/gh/SeriousBug/gandi-live-dns-rust) [![lint checks](https://img.shields.io/github/actions/workflow/status/SeriousBug/gandi-live-dns-rust/lint.yml?label=lints&branch=master)](https://github.com/SeriousBug/gandi-live-dns-rust/actions/workflows/lint.yml) [![Releases](https://img.shields.io/github/v/release/SeriousBug/gandi-live-dns-rust?include_prereleases)](https://github.com/SeriousBug/gandi-live-dns-rust/releases) [![Docker Image Size](https://img.shields.io/docker/image-size/seriousbug/gandi-live-dns-rust)](https://hub.docker.com/r/seriousbug/gandi-live-dns-rust) [![MIT license](https://img.shields.io/github/license/SeriousBug/gandi-live-dns-rust)](https://github.com/SeriousBug/gandi-live-dns-rust/blob/master/LICENSE.txt)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?label=contributors)](#contributors) <!-- ALL-CONTRIBUTORS-BADGE:END -->
[![tests](https://img.shields.io/github/actions/workflow/status/SeriousBug/gandi-live-dns-rust/test.yml?label=tests&branch=master)](https://github.com/SeriousBug/gandi-live-dns-rust/actions/workflows/test.yml)
[![Test coverage report](https://img.shields.io/codecov/c/github/SeriousBug/gandi-live-dns-rust)](https://codecov.io/gh/SeriousBug/gandi-live-dns-rust)
[![lint checks](https://img.shields.io/github/actions/workflow/status/SeriousBug/gandi-live-dns-rust/lint.yml?label=lints&branch=master)](https://github.com/SeriousBug/gandi-live-dns-rust/actions/workflows/lint.yml)
[![Releases](https://img.shields.io/github/v/release/SeriousBug/gandi-live-dns-rust?include_prereleases)](https://github.com/SeriousBug/gandi-live-dns-rust/releases)
[![Docker Image Size](https://img.shields.io/docker/image-size/seriousbug/gandi-live-dns-rust)](https://hub.docker.com/r/seriousbug/gandi-live-dns-rust)
[![MIT license](https://img.shields.io/github/license/SeriousBug/gandi-live-dns-rust)](https://github.com/SeriousBug/gandi-live-dns-rust/blob/master/LICENSE.txt)
A program that can set the IP addresses for configured DNS entries in
[Gandi](https://gandi.net)'s domain configuration. Thanks to Gandi's
@ -13,6 +21,23 @@ program can update both IPv4 and IPv6 addresses for one or more domains and
subdomains. It can be used as a one-shot tool managed with a systemd timer
or cron, or a long-running process that reschedules itself.
## Table of Contents <!-- omit in toc -->
- [Usage](#usage)
- [System packages](#system-packages)
- [Prebuilt binaries](#prebuilt-binaries)
- [With docker](#with-docker)
- [From source](#from-source)
- [Automation](#automation)
- [By running as a background process](#by-running-as-a-background-process)
- [Skipped updates](#skipped-updates)
- [With a Systemd timer](#with-a-systemd-timer)
- [Development](#development)
- [Local builds](#local-builds)
- [Making a release](#making-a-release)
- [Alternatives](#alternatives)
- [Contributors](#contributors)
## Usage
The Gandi Live DNS API is rate limited at 30 requests per minute. This program
@ -44,8 +69,14 @@ Download the latest version from the releases page, extract it from the archive,
### With docker
Use the [seriousbug/gandi-live-dns-rust](https://hub.docker.com/r/seriousbug/gandi-live-dns-rust) Docker images, which are available for x86_64,
arm64, armv6, and armv7 platforms. Follow the steps below to use these images.
Container images are available on both Github Packages and Docker Hub.
- [ghcr.io/seriousbug/gandi-live-dns-rust](https://github.com/users/seriousbug/packages/container/package/gandi-live-dns-rust)
- [docker.io/seriousbug/gandi-live-dns-rust](https://hub.docker.com/r/seriousbug/gandi-live-dns-rust)
The container images are built multi-arch, with support for x86_64, arm64,
armv7, and armv6 platforms. Follow the steps below to use them. You can use
`seriousbug/gandi-live-dns-rust` directly which will default to Docker Hub,
otherwise add `ghcr.io` in the examples below to use Github Packages.
- Create a file `gandi.toml`, then copy and paste the contents of [`example.toml`](https://raw.githubusercontent.com/SeriousBug/gandi-live-dns-rust/master/example.toml)
- Follow the instructions in the example config to get your API key and put it in the config
@ -84,15 +115,23 @@ docker run --rm -it -v $(pwd)/gandi.toml:/gandi.toml:ro seriousbug/gandi-live-dn
Or with a `docker-compose.yml` file, add it in the arguments:
```yml
gandi-live-dns:
image: seriousbug/gandi-live-dns-rust:latest
restart: always
volumes:
- ./gandi.toml:/gandi.toml:ro
# Repeat the update every day
command: --repeat=86400
gandi-live-dns:
image: seriousbug/gandi-live-dns-rust:latest
restart: always
volumes:
- ./gandi.toml:/gandi.toml:ro
# Repeat the update every day
command: --repeat=86400
```
#### Skipped updates
In background process mode, the tool will avoid sending an update to Gandi if
your IP address has not changed since the last update. This only works so long
as the tool continues to run, it will send an update when restarted even if your
IP address has not changed. You can also override this behavior by adding
`always_update = true` to the top of your config file.
### With a Systemd timer
The `Packaging` folder contains a Systemd service and timer, which you can use
@ -128,7 +167,8 @@ Docker with `docker login`. Then follow these steps:
- Create a release on Github
- Make sure to create a tag for the release version on `master`
- Upload the binary archives to the Github release
- Update the AUR version manually
- Update the AUR version
- Run `cargo publish` to update the crates.io version
## Alternatives
@ -136,3 +176,28 @@ Docker with `docker login`. Then follow these steps:
- [ Adam Vigneaux's Bash based updater, with a docker image](https://github.com/AdamVig/gandi-dynamic-dns)
- [Yago Riveiro's Python based updater](https://github.com/yriveiro/giu)
- [ Maxime Le Conte des Floris' Go based updater](https://github.com/mlcdf/dyndns)
## Contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center"><a href="https://github.com/jannikac"><img src="https://avatars.githubusercontent.com/u/21014142?v=4?s=100" width="100px;" alt="jannikac"/><br /><sub><b>jannikac</b></sub></a><br /><a href="https://github.com/SeriousBug/gandi-live-dns-rust/commits?author=jannikac" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->

View file

@ -19,8 +19,9 @@ ttl = 300
# Ipify is used by default. If you want to change it, uncomment the one you want
# to use.
#
# ip_source = "Ipify"
# ip_source = "Icanhazip"
#ip_source = "Ipify" # An open source and public service. https://github.com/rdegges/ipify-api
#ip_source = "Icanhazip" # A free service, currently run by Cloudflare. https://major.io/2021/06/06/a-new-future-for-icanhazip/
#ip_source = "SeeIP" # A free service, run by UNVIO, LLC. https://seeip.org/
# For every domain or subdomain you want to update, create an entry below.

View file

@ -3,9 +3,7 @@
# Make sure `cross` is installed.
# You'll also need `sed`, a relatively recent version of `tar`, and `7z`.
#
# This script runs does `sudo docker` to build and push the release to docker.
# If you have rootless docker set up, remove sudo from this variable.
DOCKER="sudo docker"
DOCKER="docker"
#
shopt -s extglob
# Trap errors and interrupts
@ -49,6 +47,9 @@ VERSION=$(sed -nr 's/^version *= *"([0-9.]+)"/\1/p' Cargo.toml | head --lines=1)
# Make the builds
for target in "${!TARGETS[@]}"; do
echo Building "${target}"
# Keeping the cached builds seem to be breaking things when going between targets
# This wouldn't be a problem if these were running in a matrix on the CI...
rm -rf target/release/
cross build -j $(($(nproc) / 2)) --release --target "${target}"
if [[ "${target}" =~ .*"windows".* ]]; then
zip -j "gandi-live-dns.${VERSION}.${TARGETS[${target}]}.zip" target/"${target}"/release/gandi-live-dns.exe 1>/dev/null
@ -76,4 +77,6 @@ ${DOCKER} buildx build . \
--file "Dockerfile" \
--tag "seriousbug/gandi-live-dns-rust:latest" \
--tag "seriousbug/gandi-live-dns-rust:${VERSION}" \
--tag "ghcr.io/seriousbug/gandi-live-dns-rust:latest" \
--tag "ghcr.io/seriousbug/gandi-live-dns-rust:${VERSION}" \
--push

View file

@ -1,13 +1,26 @@
use crate::opts;
use directories::ProjectDirs;
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use std::{fs, io};
use thiserror::Error;
fn default_types() -> Vec<String> {
DEFAULT_TYPES.iter().map(|v| v.to_string()).collect()
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Failed to read config file: {0} ")]
Io(#[from] io::Error),
#[error("Failed to parse config file: {0}")]
Parse(#[from] toml::de::Error),
#[error("Entry '{0}' has invalid type '{1}'")]
Validation(String, String),
#[error("Can't find config directory")]
ConfigNotFound(),
}
#[derive(Deserialize, Debug)]
pub struct Entry {
pub name: String,
@ -25,6 +38,7 @@ fn default_ttl() -> u32 {
pub enum IPSourceName {
Ipify,
Icanhazip,
SeeIP,
}
impl Default for IPSourceName {
@ -44,6 +58,8 @@ pub struct Config {
pub entry: Vec<Entry>,
#[serde(default = "default_ttl")]
pub ttl: u32,
#[serde(default)]
pub always_update: bool,
}
const DEFAULT_TYPES: &[&str] = &["A"];
@ -62,18 +78,20 @@ impl Config {
}
}
fn load_config_from<P: std::convert::AsRef<std::path::Path>>(path: P) -> anyhow::Result<Config> {
fn load_config_from<P: std::convert::AsRef<std::path::Path>>(
path: P,
) -> Result<Config, ConfigError> {
let contents = fs::read_to_string(path)?;
Ok(toml::from_str(&contents)?)
}
pub fn load_config(opts: &opts::Opts) -> anyhow::Result<Config> {
pub fn load_config(opts: &opts::Opts) -> Result<Config, ConfigError> {
let mut config = match &opts.config {
Some(config_path) => load_config_from(config_path),
None => {
let confpath = ProjectDirs::from("me", "kaangenc", "gandi-dynamic-dns")
.map(|dir| PathBuf::from(dir.config_dir()).join("config.toml"))
.ok_or_else(|| anyhow::anyhow!("Can't find config directory"));
.ok_or(ConfigError::ConfigNotFound());
confpath
.and_then(|path| {
println!("Checking for config: {}", path.to_string_lossy());
@ -102,11 +120,14 @@ pub fn load_config(opts: &opts::Opts) -> anyhow::Result<Config> {
Ok(config)
}
pub fn validate_config(config: &Config) -> anyhow::Result<()> {
pub fn validate_config(config: &Config) -> Result<(), ConfigError> {
for entry in &config.entry {
for entry_type in Config::types(entry) {
if entry_type != "A" && entry_type != "AAAA" {
anyhow::bail!("Entry {} has invalid type {}", entry.name, entry_type);
return Err(ConfigError::Validation(
entry.name.clone(),
entry_type.to_string(),
));
}
}
}
@ -156,6 +177,7 @@ name = "@"
assert_eq!(conf.entry[1].types, vec!["A".to_string()]);
// default
assert_eq!(conf.ip_source, IPSourceName::Ipify);
assert_eq!(conf.always_update, false);
}
#[test]
@ -170,6 +192,7 @@ fqdn = "example.com"
api_key = "yyy"
ttl = 1200
ip_source = "Icanhazip"
always_update = true
[[entry]]
name = "www"
@ -193,6 +216,7 @@ name = "@"
assert_eq!(conf.entry[0].name, "www");
assert_eq!(conf.entry[1].name, "@");
assert_eq!(conf.ip_source, IPSourceName::Icanhazip);
assert_eq!(conf.always_update, true);
}
#[test]

View file

@ -2,5 +2,5 @@ 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
actual APIs and can be flaky in CI. Make sure to run the tests manually if you
have to modify the code.

9
src/ip_source/common.rs Normal file
View file

@ -0,0 +1,9 @@
use async_trait::async_trait;
use crate::ClientError;
#[async_trait]
pub trait IPSource {
async fn get_ipv4(&self) -> Result<String, ClientError>;
async fn get_ipv6(&self) -> Result<String, ClientError>;
}

View file

@ -1,10 +1,12 @@
use async_trait::async_trait;
use super::ip_source::IPSource;
use crate::ClientError;
pub(crate) struct IPSourceIcanhazip {}
use super::common::IPSource;
async fn get_ip(api_url: &str) -> anyhow::Result<String> {
pub(crate) struct IPSourceIcanhazip;
async fn get_ip(api_url: &str) -> Result<String, ClientError> {
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<String> {
#[async_trait]
impl IPSource for IPSourceIcanhazip {
async fn get_ipv4() -> anyhow::Result<String> {
async fn get_ipv4(&self) -> Result<String, ClientError> {
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> {
async fn get_ipv6(&self) -> Result<String, ClientError> {
Ok(get_ip("https://ipv6.icanhazip.com")
.await?
// icanazip puts a newline at the end
@ -32,13 +34,15 @@ impl IPSource for IPSourceIcanhazip {
mod tests {
use regex::Regex;
use super::IPSource;
use crate::ip_source::common::IPSource;
use super::IPSourceIcanhazip;
#[tokio::test]
#[ignore]
async fn ipv4_test() {
let ipv4 = IPSourceIcanhazip::get_ipv4()
let ipv4 = IPSourceIcanhazip
.get_ipv4()
.await
.expect("Failed to get the IP address");
assert!(Regex::new(r"^\d+[.]\d+[.]\d+[.]\d+$")
@ -49,7 +53,8 @@ mod tests {
#[tokio::test]
#[ignore]
async fn ipv6_test() {
let ipv6 = IPSourceIcanhazip::get_ipv6()
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]*$")

View file

@ -1,7 +0,0 @@
use async_trait::async_trait;
#[async_trait]
pub trait IPSource {
async fn get_ipv4() -> anyhow::Result<String>;
async fn get_ipv6() -> anyhow::Result<String>;
}

View file

@ -1,10 +1,12 @@
use async_trait::async_trait;
use super::ip_source::IPSource;
use crate::ClientError;
pub(crate) struct IPSourceIpify {}
use super::common::IPSource;
async fn get_ip(api_url: &str) -> anyhow::Result<String> {
pub(crate) struct IPSourceIpify;
async fn get_ip(api_url: &str) -> Result<String, ClientError> {
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<String> {
#[async_trait]
impl IPSource for IPSourceIpify {
async fn get_ipv4() -> anyhow::Result<String> {
async fn get_ipv4(&self) -> Result<String, ClientError> {
get_ip("https://api.ipify.org").await
}
async fn get_ipv6() -> anyhow::Result<String> {
async fn get_ipv6(&self) -> Result<String, ClientError> {
get_ip("https://api6.ipify.org").await
}
}
@ -30,7 +32,8 @@ mod tests {
#[tokio::test]
#[ignore]
async fn ipv4_test() {
let ipv4 = IPSourceIpify::get_ipv4()
let ipv4 = IPSourceIpify
.get_ipv4()
.await
.expect("Failed to get the IP address");
assert!(Regex::new(r"^\d+[.]\d+[.]\d+[.]\d+$")
@ -41,7 +44,8 @@ mod tests {
#[tokio::test]
#[ignore]
async fn ipv6_test() {
let ipv6 = IPSourceIpify::get_ipv6()
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]*$")

View file

@ -1,3 +1,4 @@
pub(crate) mod common;
pub(crate) mod icanhazip;
pub(crate) mod ip_source;
pub(crate) mod ipify;
pub(crate) mod seeip;

55
src/ip_source/seeip.rs Normal file
View file

@ -0,0 +1,55 @@
use async_trait::async_trait;
use crate::ClientError;
use super::common::IPSource;
pub(crate) struct IPSourceSeeIP;
async fn get_ip(api_url: &str) -> Result<String, ClientError> {
let response = reqwest::get(api_url).await?;
let text = response.text().await?;
Ok(text)
}
#[async_trait]
impl IPSource for IPSourceSeeIP {
async fn get_ipv4(&self) -> Result<String, ClientError> {
get_ip("https://ip4.seeip.org").await
}
async fn get_ipv6(&self) -> Result<String, ClientError> {
get_ip("https://ip6.seeip.org").await
}
}
#[cfg(test)]
mod tests {
use regex::Regex;
use super::IPSource;
use super::IPSourceSeeIP;
#[tokio::test]
#[ignore]
async fn ipv4_test() {
let ipv4 = IPSourceSeeIP
.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 = IPSourceSeeIP
.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()))
}
}

View file

@ -1,28 +1,63 @@
use crate::config::Config;
use crate::gandi::GandiAPI;
use crate::ip_source::{ip_source::IPSource, ipify::IPSourceIpify};
use crate::ip_source::{common::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 serde::{Deserialize, Serialize};
use std::{num::NonZeroU32, sync::Arc, time::Duration};
use tokio::join;
use tokio::{self, task::JoinHandle, time::sleep};
mod config;
mod gandi;
mod ip_source;
mod opts;
use die_exit_2::*;
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<Client> {
#[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<Client, ClientError> {
let client_builder = ClientBuilder::new();
let key = format!("Apikey {}", api_key);
let key = format!("Apikey {api_key}");
let mut auth_value = header::HeaderValue::from_str(&key)?;
let mut headers = header::HeaderMap::new();
auth_value.set_sensitive(true);
@ -39,77 +74,202 @@ pub struct APIPayload {
pub rrset_ttl: u32,
}
async fn run<IP: IPSource>(base_url: &str, conf: &Config) -> anyhow::Result<()> {
config::validate_config(conf).die_with(|error| format!("Invalid config: {}", error));
println!("Finding out the IP address...");
let ipv4_result = IP::get_ipv4().await;
let ipv6_result = IP::get_ipv6().await;
let ipv4 = ipv4_result.as_ref();
let ipv6 = ipv6_result.as_ref();
println!("Found these:");
match ipv4 {
Ok(ip) => println!("\tIPv4: {}", ip),
Err(err) => eprintln!("\tIPv4 failed: {}", err),
}
match ipv6 {
Ok(ip) => println!("\tIPv6: {}", ip),
Err(err) => eprintln!("\tIPv6 failed: {}", err),
}
#[derive(Debug)]
struct ResponseFeedback {
entry_name: String,
entry_type: String,
response: Result<String, ApiError>,
}
let client = api_client(&conf.api_key)?;
let mut tasks: Vec<JoinHandle<(StatusCode, String)>> = Vec::new();
println!("Attempting to update DNS entries now");
#[derive(Deserialize)]
// Allowing dead code because this is the API response we get from Gandi.
// We don't necessarily need all the fields, but we get them anyway.
#[allow(dead_code)]
struct ApiResponse {
message: String,
cause: Option<String>,
code: Option<i32>,
object: Option<String>,
}
let governor = Arc::new(governor::RateLimiter::direct(governor::Quota::per_minute(
NonZeroU32::new(GANDI_RATE_LIMIT).die("Governor rate is 0"),
)));
let retry_jitter =
governor::Jitter::new(Duration::ZERO, Duration::from_secs(GANDI_DELAY_JITTER));
async fn run(
base_url: &str,
ip_source: &Box<dyn IPSource>,
conf: &Config,
opts: &Opts,
) -> Result<(), ClientError> {
let mut last_ipv4: Option<String> = None;
let mut last_ipv6: Option<String> = None;
for entry in &conf.entry {
for entry_type in Config::types(entry) {
let fqdn = Config::fqdn(entry, conf).to_string();
let url = GandiAPI {
fqdn: &fqdn,
rrset_name: &entry.name,
rrset_type: entry_type,
base_url,
}
.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),
};
let payload = APIPayload {
rrset_values: vec![ip.to_string()],
rrset_ttl: Config::ttl(entry, conf),
};
let req = client.put(url).json(&payload);
let task_governor = governor.clone();
let entry_type = entry_type.to_string();
let task = tokio::task::spawn(async move {
task_governor.until_ready_with_jitter(retry_jitter).await;
println!("Updating {} record for {}", entry_type, &fqdn);
match req.send().await {
Ok(response) => (
response.status(),
response
.text()
.await
.unwrap_or_else(|error| error.to_string()),
),
Err(error) => (StatusCode::IM_A_TEAPOT, error.to_string()),
}
});
tasks.push(task);
loop {
println!("Finding out the IP address...");
let (ipv4_result, ipv6_result) = join!(ip_source.get_ipv4(), ip_source.get_ipv6());
let ipv4 = ipv4_result.as_ref();
let ipv6 = ipv6_result.as_ref();
println!("Found these:");
match ipv4 {
Ok(ip) => println!("\tIPv4: {ip}"),
Err(err) => eprintln!("\tIPv4 failed: {err}"),
}
match ipv6 {
Ok(ip) => println!("\tIPv6: {ip}"),
Err(err) => eprintln!("\tIPv6 failed: {err}"),
}
}
let results = futures::future::try_join_all(tasks).await?;
println!("Updates done for {} entries", results.len());
for (status, body) in results {
println!("{} - {}", status, body);
let ipv4_same = last_ipv4
.as_ref()
.map(|p| ipv4.map(|q| p == q).unwrap_or(false))
.unwrap_or(false);
let ipv6_same = last_ipv6
.as_ref()
.map(|p| ipv6.map(|q| p == q).unwrap_or(false))
.unwrap_or(false);
if !ipv4_same || !ipv6_same || conf.always_update {
let client = api_client(&conf.api_key)?;
let mut tasks: Vec<JoinHandle<Result<ResponseFeedback, ClientError>>> = Vec::new();
println!("Attempting to update DNS entries now");
let governor = Arc::new(governor::RateLimiter::direct(governor::Quota::per_minute(
NonZeroU32::new(GANDI_RATE_LIMIT).die("Governor rate is 0"),
)));
let retry_jitter =
governor::Jitter::new(Duration::ZERO, Duration::from_secs(GANDI_DELAY_JITTER));
for entry in &conf.entry {
for entry_type in Config::types(entry) {
let fqdn = Config::fqdn(entry, conf).to_string();
let url = GandiAPI {
fqdn: &fqdn,
rrset_name: &entry.name,
rrset_type: entry_type,
base_url,
}
.url();
let ip = match 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),
};
let req = client.put(url).json(&payload);
let task_governor = governor.clone();
let entry_type = entry_type.to_string();
let entry_name = entry.name.to_string();
let task: JoinHandle<Result<ResponseFeedback, ClientError>> =
tokio::task::spawn(async move {
task_governor.until_ready_with_jitter(retry_jitter).await;
println!("Updating {} record for {}", entry_type, &fqdn);
let resp = req.send().await?;
let response_feedback = match resp.status() {
StatusCode::CREATED => {
let body: ApiResponse = resp.json().await?;
ResponseFeedback {
entry_name,
entry_type,
response: Ok(body.message),
}
}
StatusCode::UNAUTHORIZED => ResponseFeedback {
entry_name,
entry_type,
response: Err(ApiError::Unauthorized()),
},
StatusCode::FORBIDDEN => {
let body: ApiResponse = resp.json().await?;
ResponseFeedback {
entry_name: entry_name.clone(),
entry_type,
response: Err(ApiError::Forbidden {
message: body.message,
}),
}
}
_ => {
let status = resp.status();
let body: ApiResponse = resp.json().await?;
ResponseFeedback {
entry_name,
entry_type,
response: Err(ApiError::Unknown(status, body.message)),
}
}
};
Ok(response_feedback)
});
tasks.push(task);
}
}
let results = futures::future::try_join_all(tasks).await?;
// Only count successfull requests
println!(
"Updates done for {} entries",
results
.iter()
.filter_map(|item| item.as_ref().ok())
.filter(|item| item.response.is_ok())
.count()
);
for item in &results {
match item {
Ok(value) => println!(
"{}",
match &value.response {
Ok(val) => format!(
"Record '{}' ({}): {}",
value.entry_name, value.entry_type, val
),
Err(err) => format!(
"Record '{}' ({}): {}",
value.entry_name, value.entry_type, err
),
}
),
Err(err) => println!("{err}"),
}
}
if results
.iter()
// all tasks finished OK, and all responses were OK as well
.all(|result| result.as_ref().map(|v| v.response.is_ok()).unwrap_or(false))
{
// Only then we update the last seen IP, because we want to
// retry updates in case the last update just happened to fail
last_ipv4 = ipv4.ok().map(|v| v.to_string());
last_ipv6 = ipv6.ok().map(|v| v.to_string());
} else if opts.repeat.is_some() {
println!("Some operations failed. They will be retried during the next repeat.")
}
} else {
println!("IP address has not changed since last update");
}
if let Some(repeat) = opts.repeat {
// If configured to repeat, do so
sleep(Duration::from_secs(repeat)).await;
continue;
}
// Otherwise this is one-shot, we should exit now
break;
}
Ok(())
@ -118,53 +278,45 @@ async fn run<IP: IPSource>(base_url: &str, conf: &Config) -> anyhow::Result<()>
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
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)?;
// run indefinitely if repeat is given
if let Some(delay) = opts.repeat {
loop {
run_dispatch(&conf).await.ok();
sleep(Duration::from_secs(delay)).await
}
}
// otherwise run just once
else {
run_dispatch(&conf).await?;
Ok(())
}
}
async fn run_dispatch(conf: &Config) -> anyhow::Result<()> {
match conf.ip_source {
IPSourceName::Ipify => run::<IPSourceIpify>("https://api.gandi.net", conf).await,
IPSourceName::Icanhazip => run::<IPSourceIcanhazip>("https://api.gandi.net", conf).await,
}
let ip_source: Box<dyn IPSource> = match conf.ip_source {
IPSourceName::Ipify => Box::new(IPSourceIpify),
IPSourceName::Icanhazip => Box::new(IPSourceIcanhazip),
IPSourceName::SeeIP => Box::new(IPSourceSeeIP),
};
config::validate_config(&conf)?;
run("https://api.gandi.net", &ip_source, &conf, &opts).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::env::temp_dir;
use crate::{config, ip_source::ip_source::IPSource, opts::Opts, run};
use crate::{config, ip_source::common::IPSource, opts::Opts, run, ClientError};
use async_trait::async_trait;
use httpmock::MockServer;
use tokio::fs;
use lazy_static::lazy_static;
use std::{
env::temp_dir,
sync::atomic::{AtomicBool, Ordering::SeqCst},
time::Duration,
};
use tokio::{fs, task::LocalSet, time::sleep};
struct IPSourceMock {}
struct IPSourceMock;
#[async_trait]
impl IPSource for IPSourceMock {
async fn get_ipv4() -> anyhow::Result<String> {
async fn get_ipv4(&self) -> Result<String, ClientError> {
Ok("192.168.0.0".to_string())
}
async fn get_ipv6() -> anyhow::Result<String> {
async fn get_ipv6(&self) -> Result<String, ClientError> {
Ok("fe80:0000:0208:74ff:feda:625c".to_string())
}
}
#[tokio::test]
async fn create_repo_success_test() {
async fn single_shot() {
let mut temp = temp_dir().join("gandi-live-dns-test");
fs::create_dir_all(&temp)
.await
@ -186,7 +338,8 @@ mod tests {
"/v5/livedns/domains/{fqdn}/records/{rname}/{rtype}"
))
.body_contains("192.168.0.0");
then.status(200);
then.status(201)
.body("{\"cause\":\"\", \"code\":201, \"message\":\"\", \"object\":\"\"}");
});
let opts = Opts {
@ -194,11 +347,202 @@ mod tests {
..Opts::default()
};
let conf = config::load_config(&opts).expect("Failed to load config");
run::<IPSourceMock>(server.base_url().as_str(), &conf)
let ip_source: Box<dyn IPSource> = Box::new(IPSourceMock);
run(server.base_url().as_str(), &ip_source, &conf, &opts)
.await
.expect("Failed when running the update");
// Assert
mock.assert();
}
#[test]
fn repeat() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
LocalSet::new().block_on(&runtime, async {
let mut temp = temp_dir().join("gandi-live-dns-test");
fs::create_dir_all(&temp)
.await
.expect("Failed to create test dir");
temp.push("test.toml");
fs::write(
&temp,
"fqdn = \"example.com\"\napi_key = \"xxx\"\nttl = 300\n[[entry]]\nname =\"@\"\n",
)
.await
.expect("Failed to write test config file");
let fqdn = "example.com";
let rname = "@";
let rtype = "A";
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method("PUT")
.path(format!(
"/v5/livedns/domains/{fqdn}/records/{rname}/{rtype}"
))
.body_contains("192.168.0.0");
then.status(201)
.body("{\"cause\":\"\", \"code\":201, \"message\":\"\", \"object\":\"\"}");
});
let server_url = server.base_url();
let handle = tokio::task::spawn_local(async move {
let opts = Opts {
config: Some(temp.to_string_lossy().to_string()),
repeat: Some(1),
..Opts::default()
};
let conf = config::load_config(&opts).expect("Failed to load config");
let ip_source: Box<dyn IPSource> = Box::new(IPSourceMock);
run(&server_url, &ip_source, &conf, &opts)
.await
.expect("Failed when running the update");
});
sleep(Duration::from_secs(3)).await;
handle.abort();
// Only should update once because the IP doesn't change
mock.assert();
});
}
#[test]
fn repeat_with_failure() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
LocalSet::new().block_on(&runtime, async {
let mut temp = temp_dir().join("gandi-live-dns-test");
fs::create_dir_all(&temp)
.await
.expect("Failed to create test dir");
temp.push("test.toml");
fs::write(
&temp,
"fqdn = \"example.com\"\napi_key = \"xxx\"\nttl = 300\n[[entry]]\nname =\"@\"\n",
)
.await
.expect("Failed to write test config file");
let fqdn = "example.com";
let rname = "@";
let rtype = "A";
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method("PUT")
.path(format!(
"/v5/livedns/domains/{fqdn}/records/{rname}/{rtype}"
))
.body_contains("192.168.0.0")
.matches(|_| {
// Don't match during the first call, but do during the second call
lazy_static! {
static ref FIRST_CALL: AtomicBool = AtomicBool::new(true);
}
if FIRST_CALL.load(SeqCst) {
FIRST_CALL.store(false, SeqCst);
return true;
}
false
});
then.status(500)
.body("{\"cause\":\"\", \"code\":500, \"message\":\"Something went wrong\", \"object\":\"\"}");
});
let mock_fail = server.mock(|when, then| {
when.method("PUT")
.path(format!(
"/v5/livedns/domains/{fqdn}/records/{rname}/{rtype}"
))
.body_contains("192.168.0.0");
then.status(201)
.body("{\"cause\":\"\", \"code\":201, \"message\":\"\", \"object\":\"\"}");
});
let server_url = server.base_url();
let handle = tokio::task::spawn_local(async move {
let opts = Opts {
config: Some(temp.to_string_lossy().to_string()),
repeat: Some(1),
..Opts::default()
};
let conf = config::load_config(&opts).expect("Failed to load config");
let ip_source: Box<dyn IPSource> = Box::new(IPSourceMock);
run(&server_url, &ip_source, &conf, &opts)
.await
.expect("Failed when running the update");
});
sleep(Duration::from_secs(4)).await;
handle.abort();
// The first call failed
mock_fail.assert();
// We then retried since the first call failed. The retry succeeds
// so we don't retry again.
mock.assert();
});
}
#[test]
fn repeat_always_update() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
LocalSet::new().block_on(&runtime, async {
let mut temp = temp_dir().join("gandi-live-dns-test");
fs::create_dir_all(&temp)
.await
.expect("Failed to create test dir");
temp.push("test.toml");
fs::write(
&temp,
"fqdn = \"example.com\"\nalways_update = true\napi_key = \"xxx\"\nttl = 300\n[[entry]]\nname =\"@\"\n",
)
.await
.expect("Failed to write test config file");
let fqdn = "example.com";
let rname = "@";
let rtype = "A";
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method("PUT")
.path(format!(
"/v5/livedns/domains/{fqdn}/records/{rname}/{rtype}"
))
.body_contains("192.168.0.0");
then.status(201).body("{\"cause\":\"\", \"code\":201, \"message\":\"\", \"object\":\"\"}");
});
let server_url = server.base_url();
let handle = tokio::task::spawn_local(async move {
let opts = Opts {
config: Some(temp.to_string_lossy().to_string()),
repeat: Some(1),
..Opts::default()
};
let conf = config::load_config(&opts).expect("Failed to load config");
let ip_source: Box<dyn IPSource> = Box::new(IPSourceMock);
run(&server_url, &ip_source, &conf, &opts)
.await
.expect("Failed when running the update");
});
sleep(Duration::from_secs(3)).await;
handle.abort();
// Should update multiple times since always_update
assert!(mock.hits() > 1);
});
}
}