Compare commits
24 Commits
09afecf396
...
v1.0
Author | SHA1 | Date | |
---|---|---|---|
787f74e3ec | |||
9b68b32354 | |||
e5c9cb6024 | |||
0cd2d364aa | |||
f34c4609a5 | |||
5466090256 | |||
b6f447e39f | |||
8e5d472018 | |||
b22616e209 | |||
4fd9a9832a | |||
2e2d723b2a | |||
4f3e689218 | |||
1e2a739e5b | |||
b4aa724e26 | |||
74a29878be | |||
c74361bb56 | |||
bb8a83cb62 | |||
e5a40d5ae7 | |||
cd191df5c5 | |||
2d31f61af9 | |||
7b419ab8b7 | |||
0eaddf064e | |||
8de3a762ab | |||
0973bca227 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
||||
/target
|
||||
logs
|
||||
dynip-cloudflare.toml
|
||||
|
420
Cargo.lock
generated
420
Cargo.lock
generated
@ -18,61 +18,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -81,6 +38,12 @@ version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@ -146,9 +109,9 @@ checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.2"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47de7e88bbbd467951ae7f5a6f34f70d1b4d9cfce53d5fd70f74ebe118b3db56"
|
||||
checksum = "9711f33475c22aab363b05564a17d7b789bf3dfec5ebabb586adee56f0e271b5"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@ -157,10 +120,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
name = "chrono"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
@ -178,6 +159,23 @@ version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "destructure_traitobject"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7"
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
@ -204,15 +202,18 @@ name = "dynip-cloudflare"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"colored",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"log",
|
||||
"log4rs",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-route",
|
||||
"netlink-sys",
|
||||
"reqwest",
|
||||
"rtnetlink",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@ -228,29 +229,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"humantime",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@ -359,7 +337,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -559,6 +537,29 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
@ -585,12 +586,6 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
@ -606,6 +601,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
@ -643,6 +644,43 @@ name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log-mdc"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7"
|
||||
|
||||
[[package]]
|
||||
name = "log4rs"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"chrono",
|
||||
"derivative",
|
||||
"fnv",
|
||||
"humantime",
|
||||
"libc",
|
||||
"log",
|
||||
"log-mdc",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
"thread-id",
|
||||
"typemap-ors",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@ -769,6 +807,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
@ -817,7 +864,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -844,6 +891,15 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
@ -896,7 +952,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -917,6 +973,12 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
@ -935,6 +997,36 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.2"
|
||||
@ -955,35 +1047,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.5"
|
||||
@ -1142,9 +1205,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.0"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
@ -1155,9 +1218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.11.0"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
|
||||
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@ -1172,6 +1235,16 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||
dependencies = [
|
||||
"ordered-float",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.204"
|
||||
@ -1180,7 +1253,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1215,6 +1288,19 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
@ -1261,6 +1347,17 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.71"
|
||||
@ -1328,7 +1425,17 @@ checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread-id"
|
||||
version = "4.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1373,7 +1480,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1496,6 +1603,15 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typemap-ors"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867"
|
||||
dependencies = [
|
||||
"unsafe-any-ors",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.15"
|
||||
@ -1517,6 +1633,21 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-any-ors"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad"
|
||||
dependencies = [
|
||||
"destructure_traitobject",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@ -1534,12 +1665,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
@ -1582,7 +1707,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -1616,7 +1741,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -1637,6 +1762,37 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
11
Cargo.toml
11
Cargo.toml
@ -5,16 +5,23 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
chrono = "0.4.38"
|
||||
dirs = "5.0.1"
|
||||
env_logger = "0.11.3"
|
||||
futures = "0.3.30"
|
||||
log = "0.4.22"
|
||||
netlink-packet-core = "0.7.0"
|
||||
netlink-packet-route = "0.19.0"
|
||||
netlink-sys = { version = "0.8.6", features = ["tokio"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
rtnetlink = "0.14.1" # Updated to a version compatible with netlink-packet-route v0.20.1
|
||||
rtnetlink = "0.14.1"
|
||||
serde = { version = "1.0.204", features = ["rc", "derive"] }
|
||||
serde_json = "1.0.120"
|
||||
tokio = { version = "1.38.0", features = ["full"] }
|
||||
toml = "0.8.14"
|
||||
log4rs = { version = "1.3.0", features = [
|
||||
"console_appender",
|
||||
"file_appender",
|
||||
"rolling_file_appender",
|
||||
] }
|
||||
scopeguard = "1.2.0"
|
||||
colored = "2.1.0"
|
||||
|
24
LICENSE
Normal file
24
LICENSE
Normal file
@ -0,0 +1,24 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2024, Love Billenius
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
80
README.md
Normal file
80
README.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Dynamic IP Updater with Cloudflare DNS
|
||||
|
||||
This project is a dynamic IP updater for Cloudflare DNS records. It listens for changes in the IPv4 address of the network interface and updates the DNS records in Cloudflare accordingly.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically detects changes in the public IP address.
|
||||
- Updates Cloudflare DNS records when the public IP changes.
|
||||
- Listens for network interface events to detect IP changes.
|
||||
- Handles errors and retries updates to Cloudflare.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust and Cargo installed.
|
||||
- Cloudflare account with API token.
|
||||
- Network interface with dynamic IP address.
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository:**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/yourusername/dynip-cloudflare.git
|
||||
cd dynip-cloudflare
|
||||
```
|
||||
|
||||
2. **Build the project:**
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
3. **Create configuration file:**
|
||||
|
||||
Create a `config.toml` file in the current directory or in the configuration directory (typically `~/.config/dynip-cloudflare/`) with the following content:
|
||||
|
||||
```toml
|
||||
zone_id = "your_zone_id"
|
||||
api_key = "your_api_key"
|
||||
domains = ["example.com", "sub.example.com"]
|
||||
max_errors_in_row = 5
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Run the program:**
|
||||
|
||||
```sh
|
||||
./target/release/dynip-cloudflare
|
||||
```
|
||||
|
||||
2. **Logging:**
|
||||
|
||||
The program logs to both stderr and a rotating file located in the `logs` directory. The log level is set to `Info`.
|
||||
|
||||
## Configuration
|
||||
|
||||
### `config.toml`
|
||||
|
||||
- `zone_id`: The Zone ID of your Cloudflare domain.
|
||||
- `api_key`: The API key for your Cloudflare account.
|
||||
- `domains`: A list of domain names to update.
|
||||
- `max_errors_in_row`: Maximum number of consecutive errors before the program stops. Will be set to 10 as default
|
||||
|
||||
### Example
|
||||
|
||||
```toml
|
||||
zone_id = "your_zone_id"
|
||||
api_key = "your_api_key"
|
||||
domains = ["example.com", "sub.example.com"]
|
||||
max_errors_in_row = 5
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The program handles errors gracefully, with retries and logging of error messages. If the number of consecutive errors exceeds `max_errors_in_row`, the program will stop.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
116
src/config.rs
116
src/config.rs
@ -1,51 +1,129 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use crate::utils::duration_to_string;
|
||||
use anyhow;
|
||||
use dirs;
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{env, path::PathBuf};
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use tokio::{fs, io::AsyncReadExt};
|
||||
|
||||
use crate::PROGRAM_NAME;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub cloudflare_zone_id: Box<str>,
|
||||
pub cloudflare_api_key: Box<str>,
|
||||
pub zone_id: String,
|
||||
pub api_key: String,
|
||||
pub domains: Vec<Box<str>>,
|
||||
#[serde(default)]
|
||||
pub max_errors_in_row: Option<usize>,
|
||||
#[serde(with = "duration_format", default)]
|
||||
pub max_duration: Option<Duration>,
|
||||
}
|
||||
|
||||
pub async fn get_config_path() -> Option<PathBuf> {
|
||||
pub async fn get_config_path() -> Result<PathBuf, Vec<PathBuf>> {
|
||||
let mut tried_paths = Vec::with_capacity(2);
|
||||
|
||||
match env::current_dir() {
|
||||
Ok(current_dir) => {
|
||||
let cwd_config = current_dir.join(format!("{}.toml", PROGRAM_NAME));
|
||||
if let Ok(meta) = fs::metadata(&cwd_config).await {
|
||||
if meta.is_file() {
|
||||
return Some(cwd_config);
|
||||
}
|
||||
|
||||
let is_valid_path = fs::metadata(&cwd_config)
|
||||
.await
|
||||
.map_or_else(|_| false, |meta| meta.is_file());
|
||||
|
||||
if is_valid_path {
|
||||
return Ok(cwd_config);
|
||||
} else {
|
||||
tried_paths.push(cwd_config);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to get current directory {:?}", &e);
|
||||
warn!("Failed to get current working directory: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match dirs::config_dir() {
|
||||
Some(config_dir) => {
|
||||
let config_file = config_dir.join(PROGRAM_NAME).join("config.toml");
|
||||
if let Ok(meta) = fs::metadata(&config_file).await {
|
||||
if meta.is_file() {
|
||||
return Some(config_file);
|
||||
}
|
||||
|
||||
let is_valid_path = fs::metadata(&config_file)
|
||||
.await
|
||||
.map_or_else(|_| false, |meta| meta.is_file());
|
||||
if is_valid_path {
|
||||
return Ok(config_file);
|
||||
} else {
|
||||
tried_paths.push(config_file);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
None => {
|
||||
warn!("No configuration directory found.");
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
Err(tried_paths)
|
||||
}
|
||||
|
||||
pub async fn read_config(path: &PathBuf) -> anyhow::Result<Config> {
|
||||
let mut file = fs::File::open(&path).await?;
|
||||
pub async fn read_config<P: AsRef<Path>>(path: &P) -> anyhow::Result<Config> {
|
||||
let mut file = fs::File::open(path).await?;
|
||||
let mut buf = String::new();
|
||||
file.read_to_string(&mut buf).await?;
|
||||
Ok(toml::from_str(&buf)?)
|
||||
}
|
||||
|
||||
mod duration_format {
|
||||
use super::*;
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn serialize<S>(duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match duration.as_ref() {
|
||||
Some(duration) => serializer.serialize_str(duration_to_string(duration).trim()),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::<Box<str>>::deserialize(deserializer)?.map_or(Ok(None), |s| {
|
||||
parse_duration(&s)
|
||||
.map(Some)
|
||||
.map_err(serde::de::Error::custom)
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_duration(s: &str) -> Result<Duration, String> {
|
||||
let mut total_duration = Duration::new(0, 0);
|
||||
let units = [("d", 86400), ("h", 3600), ("m", 60), ("s", 1)];
|
||||
let mut remainder = s;
|
||||
|
||||
for &(unit, factor) in &units {
|
||||
if let Some(idx) = remainder.find(unit) {
|
||||
let (value, rest) = remainder.split_at(idx);
|
||||
let value: u64 = value.trim().parse().map_err(|_| "Invalid number")?;
|
||||
total_duration += Duration::from_secs(value * factor);
|
||||
remainder = &rest[unit.len()..];
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = remainder.find("ns") {
|
||||
let (value, rest) = remainder.split_at(idx);
|
||||
let value: u32 = value.trim().parse().map_err(|_| "Invalid number")?;
|
||||
total_duration += Duration::new(0, value);
|
||||
remainder = &rest["ns".len()..];
|
||||
}
|
||||
|
||||
if !remainder.trim().is_empty() {
|
||||
return Err("Invalid duration format".to_string());
|
||||
}
|
||||
|
||||
Ok(total_duration)
|
||||
}
|
||||
}
|
||||
|
43
src/exit_listener.rs
Normal file
43
src/exit_listener.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
self, signal,
|
||||
sync::{futures::Notified, Notify},
|
||||
};
|
||||
|
||||
pub struct ExitListener {
|
||||
should_exit: Arc<AtomicBool>,
|
||||
notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl ExitListener {
|
||||
pub fn new() -> Self {
|
||||
let this = Self {
|
||||
should_exit: Arc::new(AtomicBool::new(false)),
|
||||
notify: Arc::new(Notify::new()),
|
||||
};
|
||||
|
||||
let should_exit = this.should_exit.clone();
|
||||
let notify = this.notify.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to install CTRL+C signal handler");
|
||||
should_exit.store(true, Ordering::SeqCst);
|
||||
notify.notify_one();
|
||||
});
|
||||
this
|
||||
}
|
||||
|
||||
pub fn notified(&self) -> Notified<'_> {
|
||||
self.notify.notified()
|
||||
}
|
||||
|
||||
pub fn should_exit(&self) -> bool {
|
||||
self.should_exit.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
@ -1,55 +1,34 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use log::{error, info};
|
||||
use reqwest::Client;
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub struct CloudflareClient {
|
||||
use super::cloudflare_responses::{CloudflareResponse, DnsRecord};
|
||||
use crate::get_current_public_ipv4;
|
||||
|
||||
pub struct CloudflareClient<A, Z>
|
||||
where
|
||||
A: fmt::Display,
|
||||
Z: fmt::Display,
|
||||
{
|
||||
client: Client,
|
||||
domains: Vec<Arc<str>>,
|
||||
domains: Vec<Box<str>>,
|
||||
current_ip: Ipv4Addr,
|
||||
api_key: Box<str>,
|
||||
zone_id: Box<str>,
|
||||
}
|
||||
// Some external site to check this
|
||||
async fn get_current_public_ipv4(client: &Client) -> Result<Ipv4Addr> {
|
||||
let response = client
|
||||
.get("https://api.ipify.org?format=json")
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<HashMap<String, String>>()
|
||||
.await?;
|
||||
|
||||
Ok(response
|
||||
.get("ip")
|
||||
.context("Field 'ip' wasn't found")?
|
||||
.parse()?)
|
||||
api_key: A,
|
||||
zone_id: Z,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
struct DnsRecord {
|
||||
id: String,
|
||||
#[serde(rename = "type")]
|
||||
record_type: Box<str>,
|
||||
name: Box<str>,
|
||||
content: Box<str>,
|
||||
ttl: u32,
|
||||
proxied: bool,
|
||||
locked: bool,
|
||||
zone_id: Box<str>,
|
||||
zone_name: Box<str>,
|
||||
modified_on: Box<str>,
|
||||
created_on: Box<str>,
|
||||
meta: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl CloudflareClient {
|
||||
pub async fn new(api_key: Box<str>, zone_id: Box<str>, domains: Vec<Arc<str>>) -> Result<Self> {
|
||||
impl<Api, Zone> CloudflareClient<Api, Zone>
|
||||
where
|
||||
Api: fmt::Display,
|
||||
Zone: fmt::Display,
|
||||
{
|
||||
pub async fn new(api_key: Api, zone_id: Zone, domains: Vec<Box<str>>) -> Result<Self> {
|
||||
let force_ipv4 = IpAddr::from([0, 0, 0, 0]);
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.local_address(force_ipv4)
|
||||
@ -57,33 +36,40 @@ impl CloudflareClient {
|
||||
|
||||
let current_ip = get_current_public_ipv4(&client).await?;
|
||||
|
||||
Ok(Self {
|
||||
let this = Self {
|
||||
client,
|
||||
domains,
|
||||
current_ip,
|
||||
api_key,
|
||||
zone_id,
|
||||
})
|
||||
};
|
||||
this.update_dns_records(current_ip).await?;
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
pub async fn check(&mut self) -> Result<()> {
|
||||
let new_ip = get_current_public_ipv4(&self.client).await?;
|
||||
if new_ip == self.current_ip {
|
||||
info!("IP '{}' is already set", new_ip);
|
||||
return Ok(());
|
||||
}
|
||||
self.update_dns_records(new_ip).await;
|
||||
info!(
|
||||
"IP has changed from '{}' -> '{}'",
|
||||
&self.current_ip, &new_ip
|
||||
);
|
||||
self.update_dns_records(new_ip).await?;
|
||||
self.current_ip = new_ip;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_dns_records(&self, new_ip: Ipv4Addr) {
|
||||
async fn update_dns_records(&self, new_ip: Ipv4Addr) -> Result<()> {
|
||||
for domain in &self.domains {
|
||||
let records = match self.get_dns_records(domain).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Could not getch dns records for domain '{}': {:?}",
|
||||
"Could not fetch DNS records for domain '{}': {:?}",
|
||||
&domain, &e
|
||||
);
|
||||
continue;
|
||||
@ -93,6 +79,10 @@ impl CloudflareClient {
|
||||
let new_ip_s = new_ip.to_string().into_boxed_str();
|
||||
for mut record in records.into_iter() {
|
||||
if record.content == new_ip_s {
|
||||
info!(
|
||||
"On {}, ip already updated to '{}'",
|
||||
&record.name, &record.content
|
||||
);
|
||||
continue;
|
||||
}
|
||||
info!(
|
||||
@ -104,17 +94,21 @@ impl CloudflareClient {
|
||||
"On {}, failed to update {}: '{}' -> '{}': {:?}",
|
||||
&domain, record.name, record.content, &new_ip_s, &e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_dns_records(&self, domain: &str) -> Result<Vec<DnsRecord>> {
|
||||
async fn get_dns_records<D: fmt::Display>(&self, domain: &D) -> Result<Vec<DnsRecord>> {
|
||||
let url = format!(
|
||||
"https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A&name={}",
|
||||
self.zone_id, domain
|
||||
);
|
||||
let mut response = self
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
@ -122,12 +116,23 @@ impl CloudflareClient {
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<HashMap<String, Vec<DnsRecord>>>()
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
response
|
||||
.remove("result")
|
||||
.context("Key result not in return")
|
||||
let cloudflare_response: CloudflareResponse =
|
||||
serde_json::from_str(&response).context("Failed to deserialize Cloudflare response")?;
|
||||
|
||||
if !cloudflare_response.success {
|
||||
error!(
|
||||
"Cloudflare API returned errors: {:?}",
|
||||
cloudflare_response.errors
|
||||
);
|
||||
return Err(anyhow::anyhow!("Cloudflare API returned errors"));
|
||||
}
|
||||
|
||||
cloudflare_response
|
||||
.result
|
||||
.context("Key 'result' not found in response")
|
||||
}
|
||||
|
||||
async fn update_dns_record(&self, record: &mut DnsRecord, ip_content: Box<str>) -> Result<()> {
|
27
src/internet/cloudflare_responses.rs
Normal file
27
src/internet/cloudflare_responses.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde;
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct DnsRecord {
|
||||
pub id: Box<str>,
|
||||
#[serde(rename = "type")]
|
||||
pub record_type: Box<str>,
|
||||
pub name: Box<str>,
|
||||
pub content: Box<str>,
|
||||
pub ttl: u32,
|
||||
pub proxied: bool,
|
||||
pub locked: bool,
|
||||
pub zone_id: Box<str>,
|
||||
pub zone_name: Box<str>,
|
||||
pub modified_on: Box<str>,
|
||||
pub created_on: Box<str>,
|
||||
pub meta: HashMap<Box<str>, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct CloudflareResponse {
|
||||
pub success: bool,
|
||||
pub errors: Box<[HashMap<Box<str>, serde_json::Value>]>,
|
||||
pub messages: Box<[HashMap<Box<str>, serde_json::Value>]>,
|
||||
pub result: Option<Vec<DnsRecord>>,
|
||||
}
|
3
src/internet/mod.rs
Normal file
3
src/internet/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod cloudflare;
|
||||
pub mod cloudflare_responses;
|
||||
pub use cloudflare::CloudflareClient;
|
24
src/lib.rs
24
src/lib.rs
@ -1,6 +1,24 @@
|
||||
mod cloudflare;
|
||||
mod config;
|
||||
pub use config::{Config, get_config_path, read_config};
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
mod config;
|
||||
mod exit_listener;
|
||||
mod logging;
|
||||
mod message_handler;
|
||||
mod network_change_listener;
|
||||
mod public_ip;
|
||||
pub mod utils;
|
||||
|
||||
mod tests;
|
||||
mod internet;
|
||||
|
||||
pub use internet::CloudflareClient;
|
||||
pub use config::{get_config_path, read_config, Config};
|
||||
pub use exit_listener::ExitListener;
|
||||
pub use logging::init_logger;
|
||||
pub use message_handler::MessageHandler;
|
||||
pub use network_change_listener::NetworkChangeListener;
|
||||
pub use public_ip::get_current_public_ipv4;
|
||||
|
||||
pub const PROGRAM_NAME: &'static str = "dynip-cloudflare";
|
||||
pub const MAX_ERORS_IN_ROW_DEFAULT: usize = 10;
|
||||
const LOG_DIR: &str = "logs";
|
||||
|
89
src/logging.rs
Normal file
89
src/logging.rs
Normal file
@ -0,0 +1,89 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use chrono::Local;
|
||||
use colored::*;
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::{ConsoleAppender, Target};
|
||||
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
|
||||
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
|
||||
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
|
||||
use log4rs::config::{Appender, Config, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use log4rs::encode::{self, Encode};
|
||||
use log4rs::filter::threshold::ThresholdFilter;
|
||||
|
||||
use crate::{LOG_DIR, PROGRAM_NAME};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ColoredPatternEncoder;
|
||||
|
||||
impl ColoredPatternEncoder {
|
||||
pub fn new() -> Self {
|
||||
ColoredPatternEncoder {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for ColoredPatternEncoder {
|
||||
fn encode(&self, w: &mut dyn encode::Write, record: &log::Record) -> anyhow::Result<()> {
|
||||
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
let level = match record.level() {
|
||||
log::Level::Error => record.level().to_string().red().bold(),
|
||||
log::Level::Warn => record.level().to_string().yellow().bold(),
|
||||
log::Level::Info => record.level().to_string().green().bold(),
|
||||
log::Level::Debug => record.level().to_string().blue().bold(),
|
||||
log::Level::Trace => record.level().to_string().purple().bold(),
|
||||
};
|
||||
let message = record.args().to_string().cyan();
|
||||
Ok(writeln!(w, "{} [{}] {}", now.bold(), level, message)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logger() {
|
||||
let today = Local::now().format("%Y-%m-%d").to_string();
|
||||
|
||||
if std::fs::metadata(LOG_DIR).is_err() {
|
||||
std::fs::create_dir(LOG_DIR).expect("Failed to create log dir");
|
||||
}
|
||||
|
||||
let log_file_path = format!("{}/{}-{}.log", LOG_DIR, PROGRAM_NAME, today);
|
||||
let log_file_pattern = format!("{}/{}-{}-{{}}.log", LOG_DIR, PROGRAM_NAME, today);
|
||||
|
||||
let level = log::LevelFilter::Info;
|
||||
|
||||
let stderr = ConsoleAppender::builder()
|
||||
.encoder(Box::new(ColoredPatternEncoder::new()))
|
||||
.target(Target::Stderr)
|
||||
.build();
|
||||
|
||||
const TRIGGER_FILE_SIZE: u64 = 5 * 1024; // 5KB as max log file size to roll
|
||||
let trigger = SizeTrigger::new(TRIGGER_FILE_SIZE);
|
||||
let roller = FixedWindowRoller::builder()
|
||||
.base(0)
|
||||
.build(&log_file_pattern, 3)
|
||||
.unwrap();
|
||||
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
|
||||
|
||||
let logfile = log4rs::append::rolling_file::RollingFileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(
|
||||
"{d(%Y-%m-%d %H:%M:%S)} [{l}] {m}\n",
|
||||
)))
|
||||
.build(log_file_path, Box::new(policy))
|
||||
.unwrap();
|
||||
|
||||
let config = Config::builder()
|
||||
.appender(Appender::builder().build("logfile", Box::new(logfile)))
|
||||
.appender(
|
||||
Appender::builder()
|
||||
.filter(Box::new(ThresholdFilter::new(level)))
|
||||
.build("stderr", Box::new(stderr)),
|
||||
)
|
||||
.build(
|
||||
Root::builder()
|
||||
.appender("logfile")
|
||||
.appender("stderr")
|
||||
.build(LevelFilter::Trace),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
log4rs::init_config(config).expect("Failed to initialize logger");
|
||||
}
|
145
src/main.rs
145
src/main.rs
@ -1,91 +1,80 @@
|
||||
use env_logger;
|
||||
use futures::stream::StreamExt;
|
||||
use log::{debug, error, info, log_enabled, Level};
|
||||
use netlink_packet_core::NetlinkPayload;
|
||||
use netlink_packet_route::RouteNetlinkMessage as RtnlMessage;
|
||||
use netlink_sys::{AsyncSocket, SocketAddr};
|
||||
use rtnetlink::new_connection;
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use dynip_cloudflare::{get_config_path, read_config, Config};
|
||||
use futures::future::{self, Either};
|
||||
use log::{error, info};
|
||||
use scopeguard::defer;
|
||||
use tokio::time;
|
||||
|
||||
const RTNLGRP_LINK: u32 = 1;
|
||||
const RTNLGRP_IPV4_IFADDR: u32 = 5;
|
||||
|
||||
const fn nl_mgrp(group: u32) -> u32 {
|
||||
if group > 31 {
|
||||
panic!("use netlink_sys::Socket::add_membership() for this group");
|
||||
}
|
||||
if group == 0 {
|
||||
0
|
||||
} else {
|
||||
1 << (group - 1)
|
||||
}
|
||||
}
|
||||
use dynip_cloudflare::{
|
||||
utils, CloudflareClient, ExitListener, MessageHandler, NetworkChangeListener,
|
||||
MAX_ERORS_IN_ROW_DEFAULT,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
let config_path = match get_config_path().await {
|
||||
Some(cp) => cp,
|
||||
None => {
|
||||
error!("Failed to find any config file");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let config = match read_config(&config_path).await {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
error!("Failed to read and parse config file {:?}", &e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let (mut conn, mut _handle, mut messages) = new_connection().unwrap();
|
||||
let groups = nl_mgrp(RTNLGRP_LINK) | nl_mgrp(RTNLGRP_IPV4_IFADDR);
|
||||
|
||||
let addr = SocketAddr::new(0, groups);
|
||||
|
||||
if let Err(e) = conn.socket_mut().socket_mut().bind(&addr) {
|
||||
error!("Failed to bind to socket: {:?}", &e);
|
||||
return;
|
||||
dynip_cloudflare::init_logger();
|
||||
defer! {
|
||||
log::logger().flush();
|
||||
}
|
||||
let exit_listener = ExitListener::new();
|
||||
|
||||
tokio::spawn(conn);
|
||||
info!("Listening for IPv4 address changes and interface connect/disconnect events...");
|
||||
let config = match utils::get_config().await {
|
||||
Some(aux) => aux,
|
||||
None => return,
|
||||
};
|
||||
|
||||
while let Some((message, _)) = messages.next().await {
|
||||
match message.payload {
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::NewAddress(msg)) => {
|
||||
if log_enabled!(Level::Debug) {
|
||||
debug!("New IPv4 address message: {:?}", msg);
|
||||
} else {
|
||||
info!("New IPv4 address");
|
||||
let mut cloudflare =
|
||||
match CloudflareClient::new(config.api_key, config.zone_id, config.domains).await {
|
||||
Ok(cloudflare) => cloudflare,
|
||||
Err(e) => {
|
||||
error!("Failed to initialize cloudflare client: {:?}", &e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut network_change_listener = match NetworkChangeListener::new() {
|
||||
Some(aux) => {
|
||||
info!("Listening for IPv4 address changes and interface connect/disconnect events...");
|
||||
aux
|
||||
}
|
||||
None => {
|
||||
error!("Failed to initialize networkchangelistener");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut message_handler = MessageHandler::new(
|
||||
&mut cloudflare,
|
||||
config.max_errors_in_row.unwrap_or(MAX_ERORS_IN_ROW_DEFAULT),
|
||||
);
|
||||
|
||||
let mut interval = config.max_duration.map(|duration| {
|
||||
let mut interval = time::interval(duration);
|
||||
interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
|
||||
interval
|
||||
});
|
||||
|
||||
while !exit_listener.should_exit() {
|
||||
let tick_future = match interval.as_mut() {
|
||||
Some(interval) => Either::Left(interval.tick()),
|
||||
None => Either::Right(future::pending::<tokio::time::Instant>()),
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = exit_listener.notified() => break,
|
||||
_ = tick_future => {
|
||||
if let Some(duration) = config.max_duration.as_ref() {
|
||||
let duration_string = utils::duration_to_string(duration);
|
||||
let log_string = format!("{} has passed since last check, checking...", duration_string.trim());
|
||||
message_handler.log_and_check(Some(&log_string), Option::<&&str>::None).await;
|
||||
}
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::DelAddress(msg)) => {
|
||||
if log_enabled!(Level::Debug) {
|
||||
debug!("Deleted IPv4 address message: {:?}", msg);
|
||||
} else {
|
||||
info!("Deleted IPv4 address");
|
||||
}
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::NewLink(link)) => {
|
||||
if log_enabled!(Level::Debug) {
|
||||
debug!("New link message (interface connected): {:?}", link);
|
||||
} else {
|
||||
info!("New link (interface connected)");
|
||||
}
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => {
|
||||
if log_enabled!(Level::Debug) {
|
||||
debug!("Deleted link message (interface disconnected): {:?}", link);
|
||||
} else {
|
||||
info!("Deleted link (interface disconnected)");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if log_enabled!(Level::Debug) {
|
||||
debug!("Unhandled message payload: {:?}", message.payload);
|
||||
message = network_change_listener.next_message() => {
|
||||
if let Some((message, _)) = message {
|
||||
if let Some(interval) = interval.as_mut() {
|
||||
interval.reset();
|
||||
}
|
||||
message_handler.handle_message(message).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
90
src/message_handler.rs
Normal file
90
src/message_handler.rs
Normal file
@ -0,0 +1,90 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use std::fmt;
|
||||
use log::{debug, error, info};
|
||||
use netlink_packet_core::{NetlinkMessage, NetlinkPayload};
|
||||
use netlink_packet_route::RouteNetlinkMessage as RtnlMessage;
|
||||
|
||||
use crate::CloudflareClient;
|
||||
|
||||
pub struct MessageHandler<'a, Api, Zone>
|
||||
where
|
||||
Api: fmt::Display,
|
||||
Zone: fmt::Display,
|
||||
{
|
||||
cloudflare: &'a mut CloudflareClient<Api, Zone>,
|
||||
errs_counter: usize,
|
||||
errs_max: usize,
|
||||
}
|
||||
|
||||
impl<'a, Api, Zone> MessageHandler<'a, Api, Zone>
|
||||
where
|
||||
Api: fmt::Display,
|
||||
Zone: fmt::Display,
|
||||
{
|
||||
pub fn new(cloudflare: &'a mut CloudflareClient<Api, Zone>, errs_max: usize) -> Self {
|
||||
Self {
|
||||
cloudflare,
|
||||
errs_counter: 0,
|
||||
errs_max,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_message(&mut self, message: NetlinkMessage<RtnlMessage>) -> Option<()> {
|
||||
match message.payload {
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::NewAddress(msg)) => {
|
||||
self.log_and_check(Some("New IPv4 address"), Some(&msg))
|
||||
.await
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::DelAddress(msg)) => {
|
||||
self.log_info("Deleted IPv4 address", &msg).await
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::NewLink(link)) => {
|
||||
self.log_and_check(Some("New link (interface connected)"), Some(&link))
|
||||
.await
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => {
|
||||
self.log_info("Deleted link (interface disconnected)", &link)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
debug!("Unhandled message payload: {:?}", message.payload);
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn log_and_check<D, M>(&mut self, log_msg: Option<&D>, msg: Option<&M>) -> Option<()>
|
||||
where
|
||||
D: fmt::Display + ?Sized,
|
||||
M: fmt::Debug,
|
||||
{
|
||||
if let Some(s) = log_msg {
|
||||
info!("{}", s);
|
||||
}
|
||||
if let Some(m) = msg {
|
||||
if let Some(lm) = log_msg {
|
||||
debug!("{}: {:?}", lm, m);
|
||||
} else {
|
||||
debug!("{:?}", m);
|
||||
}
|
||||
}
|
||||
if let Err(e) = self.cloudflare.check().await {
|
||||
self.errs_counter += 1;
|
||||
error!(
|
||||
"Failed to check cloudflare ({}/{}): {:?}",
|
||||
self.errs_counter, self.errs_max, &e
|
||||
);
|
||||
if self.errs_counter >= self.errs_max {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
async fn log_info<M: fmt::Debug>(&self, log_msg: &str, msg: &M) -> Option<()> {
|
||||
info!("{}", log_msg);
|
||||
debug!("{:?} message: {:?}", log_msg, msg);
|
||||
Some(())
|
||||
}
|
||||
}
|
60
src/network_change_listener.rs
Normal file
60
src/network_change_listener.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use futures::{
|
||||
channel::mpsc::UnboundedReceiver,
|
||||
stream::{Next, StreamExt},
|
||||
};
|
||||
use log::error;
|
||||
use netlink_packet_core::NetlinkMessage;
|
||||
use netlink_packet_route::RouteNetlinkMessage;
|
||||
use netlink_sys::{AsyncSocket, SocketAddr};
|
||||
use rtnetlink::new_connection;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const RTNLGRP_LINK: u32 = 1;
|
||||
const RTNLGRP_IPV4_IFADDR: u32 = 5;
|
||||
|
||||
const fn nl_mgrp(group: u32) -> u32 {
|
||||
if group > 31 {
|
||||
panic!("use netlink_sys::Socket::add_membership() for this group");
|
||||
}
|
||||
if group == 0 {
|
||||
0
|
||||
} else {
|
||||
1 << (group - 1)
|
||||
}
|
||||
}
|
||||
|
||||
type Messages = UnboundedReceiver<(NetlinkMessage<RouteNetlinkMessage>, SocketAddr)>;
|
||||
pub struct NetworkChangeListener {
|
||||
messages: Messages,
|
||||
thread_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl NetworkChangeListener {
|
||||
pub fn new() -> Option<Self> {
|
||||
let (mut conn, mut _handle, messages) = new_connection().ok()?;
|
||||
let groups = nl_mgrp(RTNLGRP_LINK) | nl_mgrp(RTNLGRP_IPV4_IFADDR);
|
||||
|
||||
let addr = SocketAddr::new(0, groups);
|
||||
|
||||
if let Err(e) = conn.socket_mut().socket_mut().bind(&addr) {
|
||||
error!("Failed to bind to socket: {:?}", &e);
|
||||
return None;
|
||||
}
|
||||
|
||||
let thread_handle = tokio::spawn(conn);
|
||||
Some(Self {
|
||||
messages,
|
||||
thread_handle,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_message(&mut self) -> Next<'_, Messages> {
|
||||
self.messages.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NetworkChangeListener {
|
||||
fn drop(&mut self) {
|
||||
self.thread_handle.abort();
|
||||
}
|
||||
}
|
52
src/public_ip.rs
Normal file
52
src/public_ip.rs
Normal file
@ -0,0 +1,52 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::error;
|
||||
use reqwest::Client;
|
||||
use std::{collections::HashMap, net::Ipv4Addr};
|
||||
|
||||
async fn ipify_org(client: &Client) -> Result<Ipv4Addr> {
|
||||
let response = client
|
||||
.get("https://api.ipify.org?format=json")
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<HashMap<String, String>>()
|
||||
.await?;
|
||||
|
||||
Ok(response
|
||||
.get("ip")
|
||||
.context("Field 'ip' wasn't found")?
|
||||
.parse()?)
|
||||
}
|
||||
|
||||
async fn ifconfig_me(client: &Client) -> Result<Ipv4Addr> {
|
||||
Ok(client
|
||||
.get("https://ifconfig.me")
|
||||
.header("user-agent", "curl/8.8.0")
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text()
|
||||
.await?
|
||||
.parse()?)
|
||||
}
|
||||
|
||||
pub async fn get_current_public_ipv4(client: &Client) -> Result<Ipv4Addr> {
|
||||
let e_ipify = match ipify_org(client).await {
|
||||
Ok(ipv4) => return Ok(ipv4),
|
||||
Err(e) => {
|
||||
error!("Failed to get ip from ipify.org: {:?}", &e);
|
||||
e
|
||||
}
|
||||
};
|
||||
|
||||
ifconfig_me(client).await.map_err(|e_ifconfig| {
|
||||
error!("Failed to get ip from ifconfig.me: {:?}", &e_ifconfig);
|
||||
anyhow!(
|
||||
"Failed to get ip from ipify.org with error '{:?}', and ifconfig.me with error {:?}",
|
||||
&e_ipify,
|
||||
&e_ifconfig
|
||||
)
|
||||
})
|
||||
}
|
59
src/tests/config_serialization.rs
Normal file
59
src/tests/config_serialization.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use crate::Config;
|
||||
use std::time::Duration;
|
||||
use toml;
|
||||
|
||||
const TOML_STR_ONE: &str = r#"
|
||||
zone_id = ""
|
||||
api_key = ""
|
||||
domains = [""]
|
||||
max_duration = "1d 2h 30m 45s 500000000ns"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize() {
|
||||
let config: Config = toml::from_str(TOML_STR_ONE).unwrap();
|
||||
assert_eq!(config.max_duration, Some(Duration::new(95445, 500000000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize() {
|
||||
let config = Config {
|
||||
zone_id: "".into(),
|
||||
api_key: "".into(),
|
||||
domains: vec!["".into()],
|
||||
max_errors_in_row: None,
|
||||
max_duration: Some(Duration::new(95445, 500000000)),
|
||||
};
|
||||
let toml_str = toml::to_string(&config).unwrap();
|
||||
assert_eq!(TOML_STR_ONE.trim(), toml_str.trim());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_none() {
|
||||
let toml_str = r#"
|
||||
zone_id = ""
|
||||
api_key = ""
|
||||
domains = [""]
|
||||
max_errors_in_row = 5
|
||||
"#;
|
||||
let config: Config = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(config.max_duration, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_none() {
|
||||
let toml_to_be = r#"
|
||||
zone_id = ""
|
||||
api_key = ""
|
||||
domains = [""]
|
||||
"#;
|
||||
let config = Config {
|
||||
zone_id: "".into(),
|
||||
api_key: "".into(),
|
||||
domains: vec!["".into()],
|
||||
max_errors_in_row: None,
|
||||
max_duration: None,
|
||||
};
|
||||
let toml_str = toml::to_string(&config).unwrap();
|
||||
assert_eq!(toml_to_be.trim(), toml_str.trim());
|
||||
}
|
2
src/tests/mod.rs
Normal file
2
src/tests/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
#[cfg(test)]
|
||||
mod config_serialization;
|
61
src/utils.rs
Normal file
61
src/utils.rs
Normal file
@ -0,0 +1,61 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{get_config_path, read_config, Config};
|
||||
use log::error;
|
||||
|
||||
pub async fn get_config() -> Option<Config> {
|
||||
let config_path = match get_config_path().await {
|
||||
Ok(path) => path,
|
||||
Err(tried_paths) => {
|
||||
let extra: String = if !tried_paths.is_empty() {
|
||||
let joined = tried_paths
|
||||
.iter()
|
||||
.filter_map(|path| path.to_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!(", tried the paths: {}", &joined)
|
||||
} else {
|
||||
String::with_capacity(0)
|
||||
};
|
||||
error!("Failed to find any config file{}", &extra);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let read_result = read_config(&config_path).await;
|
||||
if let Err(e) = &read_result {
|
||||
error!("Error occurred while getting config path: {:?}", e);
|
||||
}
|
||||
read_result.ok()
|
||||
}
|
||||
pub fn duration_to_string(duration: &Duration) -> String {
|
||||
let mut secs = duration.as_secs();
|
||||
let nanos = duration.subsec_nanos();
|
||||
|
||||
let days = secs / 86400;
|
||||
secs %= 86400;
|
||||
let hours = secs / 3600;
|
||||
secs %= 3600;
|
||||
let minutes = secs / 60;
|
||||
secs %= 60;
|
||||
|
||||
let mut ret = String::new();
|
||||
if days > 0 {
|
||||
ret.push_str(&format!("{}d ", days));
|
||||
}
|
||||
if hours > 0 {
|
||||
ret.push_str(&format!("{}h ", hours));
|
||||
}
|
||||
if minutes > 0 {
|
||||
ret.push_str(&format!("{}m ", minutes));
|
||||
}
|
||||
if secs > 0 {
|
||||
ret.push_str(&format!("{}s ", secs));
|
||||
}
|
||||
if nanos > 0 {
|
||||
ret.push_str(&format!("{}ns", nanos));
|
||||
}
|
||||
ret
|
||||
}
|
Reference in New Issue
Block a user