wip: templating
This commit is contained in:
parent
39cef3b600
commit
376a6bd319
44 changed files with 1528 additions and 2604 deletions
418
Cargo.lock
generated
418
Cargo.lock
generated
|
@ -29,6 +29,15 @@ dependencies = [
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-lock"
|
name = "async-lock"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
|
@ -59,9 +68,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.0-alpha.1"
|
version = "0.8.0-rc.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4132c8995c63b222c56f38b80748c23323d7012d1f783b0f42e92782c2676690"
|
checksum = "47b212eb691030da38f1eff381777e431eb6f0760a0d02ffcb1702f1da9894e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -91,9 +100,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.5.0-alpha.1"
|
version = "0.5.0-rc.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3405ae79f3aab93ae35fe2944aff9bf0f5aac918c0229f3528043c5a454d17d"
|
checksum = "30256e79153b08607dcf9e0a72bd31564bc9228b9f145d1e1a29e3d01ad1fd16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -140,6 +149,25 @@ version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.16.0"
|
version = "3.16.0"
|
||||||
|
@ -182,6 +210,15 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.13"
|
version = "0.5.13"
|
||||||
|
@ -191,6 +228,16 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-epoch"
|
name = "crossbeam-epoch"
|
||||||
version = "0.9.18"
|
version = "0.9.18"
|
||||||
|
@ -206,6 +253,16 @@ version = "0.8.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
|
@ -215,6 +272,16 @@ dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "domain"
|
name = "domain"
|
||||||
version = "0.10.3"
|
version = "0.10.3"
|
||||||
|
@ -307,6 +374,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
@ -327,6 +400,16 @@ dependencies = [
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -344,6 +427,30 @@ version = "0.31.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globset"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"bstr",
|
||||||
|
"log",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globwalk"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"ignore",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
@ -396,6 +503,12 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range-header"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.9.5"
|
version = "1.9.5"
|
||||||
|
@ -443,6 +556,22 @@ dependencies = [
|
||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ignore"
|
||||||
|
version = "0.4.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"globset",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"same-file",
|
||||||
|
"walkdir",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
@ -459,6 +588,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.168"
|
version = "0.2.168"
|
||||||
|
@ -493,9 +628,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.8.0"
|
version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56e8fcd7bd6025a951597d6ba2f8e48a121af7e262f2b52a006a09c8d61f9304"
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
|
@ -509,6 +644,16 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
@ -548,7 +693,7 @@ dependencies = [
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tagptr",
|
"tagptr",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"triomphe",
|
"triomphe",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
@ -564,7 +709,9 @@ dependencies = [
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tera",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -633,6 +780,51 @@ version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest"
|
||||||
|
version = "2.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"thiserror 2.0.9",
|
||||||
|
"ucd-trie",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_derive"
|
||||||
|
version = "2.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_generator",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_generator"
|
||||||
|
version = "2.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_meta",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_meta"
|
||||||
|
version = "2.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"pest",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -747,6 +939,35 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.8"
|
version = "0.17.8"
|
||||||
|
@ -803,6 +1024,15 @@ version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -869,6 +1099,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -929,13 +1170,38 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tera"
|
||||||
|
version = "1.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee"
|
||||||
|
dependencies = [
|
||||||
|
"globwalk",
|
||||||
|
"lazy_static",
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"unic-segment",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl 2.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -949,6 +1215,17 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.37"
|
version = "0.3.37"
|
||||||
|
@ -996,6 +1273,19 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -1011,6 +1301,31 @@ dependencies = [
|
||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"http-range-header",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
@ -1060,6 +1375,74 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3"
|
checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ucd-trie"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-char-property"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
|
||||||
|
dependencies = [
|
||||||
|
"unic-char-range",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-char-range"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-common"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-segment"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
|
||||||
|
dependencies = [
|
||||||
|
"unic-ucd-segment",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-ucd-segment"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
|
||||||
|
dependencies = [
|
||||||
|
"unic-char-property",
|
||||||
|
"unic-char-range",
|
||||||
|
"unic-ucd-version",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-ucd-version"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
|
||||||
|
dependencies = [
|
||||||
|
"unic-common",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
@ -1093,6 +1476,16 @@ version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
@ -1179,6 +1572,15 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
|
@ -18,9 +18,10 @@ tokio = {version = "1", default-features = false, features = [ "macros", "rt-mul
|
||||||
#clap = { version = "4", features = [ "derive", "cargo" ] }
|
#clap = { version = "4", features = [ "derive", "cargo" ] }
|
||||||
#argon2 = { version = "0.5", default-features = false, features = ["alloc", "password-hash"] }
|
#argon2 = { version = "0.5", default-features = false, features = ["alloc", "password-hash"] }
|
||||||
#rand = "0.8"
|
#rand = "0.8"
|
||||||
#tera = { version = "1", default-features = false }
|
tera = { version = "1", default-features = false }
|
||||||
domain = { version = "0.10.3", features = [ "tsig", "unstable-client-transport" ]}
|
domain = { version = "0.10.3", features = [ "tsig", "unstable-client-transport" ]}
|
||||||
axum = { version = "0.8.0-alpha.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio" ]}
|
axum = { version = "0.8.0-rc.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio" ]}
|
||||||
bb8 = { version = "0.9" }
|
bb8 = { version = "0.9" }
|
||||||
rusqlite = { version = "0.32"}
|
rusqlite = { version = "0.32"}
|
||||||
async-trait = { version = "0.1" }
|
async-trait = { version = "0.1" }
|
||||||
|
tower-http = { version = "0.6", default-features = false, features = [ "fs" ]}
|
||||||
|
|
BIN
assets/fonts/inclusive-sans/InclusiveSans_wght.ttf
Normal file
BIN
assets/fonts/inclusive-sans/InclusiveSans_wght.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/inclusive-sans/InclusiveSans_wght.woff2
Normal file
BIN
assets/fonts/inclusive-sans/InclusiveSans_wght.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/lexend/Lexend-VariableFont_wght.ttf
Normal file
BIN
assets/fonts/lexend/Lexend-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/lexend/Lexend-VariableFont_wght.woff2
Normal file
BIN
assets/fonts/lexend/Lexend-VariableFont_wght.woff2
Normal file
Binary file not shown.
201
assets/styles/main.css
Normal file
201
assets/styles/main.css
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Lexend';
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("/assets/fonts/lexend/Lexend-VariableFont_wght.woff2") format("woff2"),
|
||||||
|
url("/assets/fonts/lexend/Lexend-VariableFont_wght.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Lexend', 'sans';
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::after, *::before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 55rem;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain header h3.folder-tab {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 1em;
|
||||||
|
border-top-left-radius: .3rem;
|
||||||
|
background-color: #f2e0fd;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: inherit;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain header h3.folder-tab ~ .sep {
|
||||||
|
content: '';
|
||||||
|
width: 3em;
|
||||||
|
background-color: #f2e0fd;
|
||||||
|
height: 100%;
|
||||||
|
clip-path: url("#corner-folder-tab-right");
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records > ul {
|
||||||
|
background: #f2e0fd;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
padding: 1rem;;
|
||||||
|
border-radius: 0 .3rem .3rem .3rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
row-gap: 1rem;
|
||||||
|
column-gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset {
|
||||||
|
display: grid;
|
||||||
|
align-items: baseline;
|
||||||
|
grid-template-columns: subgrid;
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset .rtype {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset .rtype::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: .2rem solid #850085;
|
||||||
|
padding-left: 1em;
|
||||||
|
position: relative;
|
||||||
|
bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset ul {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
article.domain .records .rrset li {
|
||||||
|
align-items: baseline;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset li::before {
|
||||||
|
content: '';
|
||||||
|
height: 1em;
|
||||||
|
width: 1rem;
|
||||||
|
border-bottom: .2rem solid #850085;
|
||||||
|
position: relative;
|
||||||
|
bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset li:not(:first-child)::before {
|
||||||
|
border-left: .2rem solid #850085;
|
||||||
|
border-bottom-left-radius: .3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset li:not(:last-child)::after {
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
width: 1rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 1.1em;
|
||||||
|
border-left: .2rem solid #850085;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset .rdata-main {
|
||||||
|
display: flex;
|
||||||
|
gap: .3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset .rdata-main .pill {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset .rdata-complementary {
|
||||||
|
margin-top: .2em;
|
||||||
|
font-size: .9em;
|
||||||
|
gap: .2rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records .rrset .action {
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
position: relative;
|
||||||
|
top: .15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
border: .1rem solid #bd79bd;
|
||||||
|
border-radius: .3rem;
|
||||||
|
padding: 0 .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
a.button {
|
||||||
|
border: .2rem solid #850085;
|
||||||
|
border-radius: 1.4em;
|
||||||
|
padding: .2em .8em;
|
||||||
|
color: #850085;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .3em;
|
||||||
|
background-color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color .2s, color .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button svg,
|
||||||
|
a.button svg {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records button,
|
||||||
|
.records a.button {
|
||||||
|
background-color: #f2e0fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.icon,
|
||||||
|
a.button.icon {
|
||||||
|
padding: 0;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
button:focus-visible,
|
||||||
|
a.button:hover,
|
||||||
|
a.button:focus-visible {
|
||||||
|
color: white;
|
||||||
|
background-color: #850085;
|
||||||
|
}
|
58
dev-scripts/config/named.conf
Normal file
58
dev-scripts/config/named.conf
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
options {
|
||||||
|
directory "/var/cache/bind";
|
||||||
|
listen-on port 5354 { any; };
|
||||||
|
listen-on-v6 port 5354 { any; };
|
||||||
|
|
||||||
|
empty-zones-enable no;
|
||||||
|
|
||||||
|
allow-recursion {
|
||||||
|
none;
|
||||||
|
};
|
||||||
|
allow-transfer {
|
||||||
|
none;
|
||||||
|
};
|
||||||
|
allow-update {
|
||||||
|
none;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
logging {
|
||||||
|
channel console {
|
||||||
|
stderr;
|
||||||
|
severity debug;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
category default { console; };
|
||||||
|
category general { console; };
|
||||||
|
category database { console; };
|
||||||
|
category security { console; };
|
||||||
|
category config { console; };
|
||||||
|
category resolver { console; };
|
||||||
|
category xfer-in { console; };
|
||||||
|
category xfer-out { console; };
|
||||||
|
category notify { console; };
|
||||||
|
category client { console; };
|
||||||
|
category unmatched { console; };
|
||||||
|
category queries { console; };
|
||||||
|
category network { console; };
|
||||||
|
category update { console; };
|
||||||
|
category dispatch { console; };
|
||||||
|
category dnssec { console; };
|
||||||
|
category lame-servers { console; };
|
||||||
|
};
|
||||||
|
|
||||||
|
key "dev" {
|
||||||
|
algorithm HMAC-SHA256;
|
||||||
|
secret "mbmz4J3Efm1BUjqe12M1RHsOnPjYhKQe+2iKO4tL+a4=";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
zone "example.com." {
|
||||||
|
type primary;
|
||||||
|
file "/var/lib/bind/example.com.zone";
|
||||||
|
notify explicit;
|
||||||
|
allow-transfer { key "dev"; };
|
||||||
|
allow-update { key "dev"; };
|
||||||
|
};
|
|
@ -1,3 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus" viewBox="0 0 16 16">
|
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 245 B |
|
@ -1,44 +0,0 @@
|
||||||
const baseUrl = '/api/v1';
|
|
||||||
|
|
||||||
|
|
||||||
function apiGet(url) {
|
|
||||||
return fetch(`${baseUrl}/${url}`)
|
|
||||||
.then(res => {
|
|
||||||
if (!res.ok) {
|
|
||||||
// do something here
|
|
||||||
throw new Error('Not ok');
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function apiPost(url, data) {
|
|
||||||
return fetch(`${baseUrl}/${url}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
if (!res.ok) {
|
|
||||||
// do something here
|
|
||||||
throw new Error('Not ok');
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getRecords(zone) {
|
|
||||||
return apiGet(`zones/${zone}/records`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createRecords(zone, record) {
|
|
||||||
return apiPost(`zones/${zone}/records`, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
getRecords,
|
|
||||||
createRecords,
|
|
||||||
};
|
|
|
@ -1,328 +0,0 @@
|
||||||
import { html, render, useState, useEffect } from './vendor/preact/standalone.js';
|
|
||||||
|
|
||||||
import { getRecords, createRecords } from './api.js';
|
|
||||||
|
|
||||||
|
|
||||||
const rdataInputProperties = {
|
|
||||||
Address: {label: 'Adresse', type: 'text'},
|
|
||||||
Serial: {label: 'Numéro de série', type: 'number'},
|
|
||||||
Minimum: {label: 'Minimum', type: 'number'},
|
|
||||||
Retry: {label: 'Nouvelle tentative', type: 'number'},
|
|
||||||
Refresh: {label: 'Actualisation', type: 'number'},
|
|
||||||
MaintainerName: {label: 'Contact', type: 'text'},
|
|
||||||
MasterServerName: {label: 'Serveur primaire', type: 'text'},
|
|
||||||
Expire: {label: 'Expiration', type: 'number'},
|
|
||||||
Target: {label: 'Cible', type: 'text'},
|
|
||||||
Service: {label: 'Service', type: 'text'},
|
|
||||||
Protocol: {label: 'Protocole', type: 'text'},
|
|
||||||
Priority: {label: 'Priorité', type: 'number'},
|
|
||||||
Weight: {label: 'Poids', type: 'number'},
|
|
||||||
Port: {label: 'Port', type: 'number'},
|
|
||||||
Server: {label: 'Serveur', type: 'text'},
|
|
||||||
};
|
|
||||||
|
|
||||||
const realRecordDataConfig = {
|
|
||||||
'A': {
|
|
||||||
friendlyType: 'address',
|
|
||||||
fields: ['Address'],
|
|
||||||
},
|
|
||||||
'AAAA': {
|
|
||||||
friendlyType: 'address',
|
|
||||||
fields: ['Address'],
|
|
||||||
},
|
|
||||||
'CNAME': {
|
|
||||||
friendlyType: 'alias',
|
|
||||||
fields: ['Target'],
|
|
||||||
},
|
|
||||||
'SRV': {
|
|
||||||
friendlyType: 'service',
|
|
||||||
fields: [ 'Priority', 'Weight', 'Port', 'Server' ],
|
|
||||||
},
|
|
||||||
'NS': {
|
|
||||||
friendlyType: 'name_server',
|
|
||||||
fields: ['Target'],
|
|
||||||
},
|
|
||||||
'SOA': {
|
|
||||||
friendlyType: 'soa',
|
|
||||||
fields: ['MasterServerName', 'MaintainerName', 'Refresh', 'Retry', 'Expire', 'Minimum', 'Serial'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function defaultBuildData(realRecordType) {
|
|
||||||
const defaultFields = Object.fromEntries(realRecordDataConfig[realRecordType].fields.map(field => [field, null]));
|
|
||||||
return (fields) => {
|
|
||||||
return {...defaultFields, ...fields, Type: realRecordType};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultRecordToFields(realRecord) {
|
|
||||||
const type = realRecord.Type;
|
|
||||||
return realRecordDataConfig[type].fields.map(field => [field, realRecord[field]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultGetName(name) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function srvRecordToFields({ Name, Type, Class, ...fields }) {
|
|
||||||
const [ serviceName, protocol] = Name.split('.');
|
|
||||||
return {
|
|
||||||
Service: serviceName.replace(/^_/, ''),
|
|
||||||
Protocol: protocol.replace(/^_/, ''),
|
|
||||||
...fields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function srvGetName(originalName) {
|
|
||||||
const [_serviceName, _protocol, ...name] = originalName.split('.');
|
|
||||||
return name.join('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAddressRecord(fields) {
|
|
||||||
const address = fields.Address || '';
|
|
||||||
if (address.indexOf('.') >= 0) {
|
|
||||||
fields.Type = 'A';
|
|
||||||
} else if (address.indexOf(':') >= 0) {
|
|
||||||
fields.Type = 'AAAA';
|
|
||||||
} else {
|
|
||||||
fields.Type = '';
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildServiceRecord({ Name, Service, Protocol, ...fields}) {
|
|
||||||
fields.Name = `_${Service}._${Protocol}.${Name}`;
|
|
||||||
fields.Type = 'SRV';
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
const friendlyRecordDataConfig = {
|
|
||||||
'address': {
|
|
||||||
realRecordToFields: defaultRecordToFields,
|
|
||||||
fields: realRecordDataConfig['AAAA'].fields,
|
|
||||||
buildData: buildAddressRecord,
|
|
||||||
getName: defaultGetName,
|
|
||||||
},
|
|
||||||
'alias': {
|
|
||||||
realRecordToFields: defaultRecordToFields,
|
|
||||||
fields: realRecordDataConfig['CNAME'].fields,
|
|
||||||
buildData: defaultBuildData('CNAME'),
|
|
||||||
getName: defaultGetName,
|
|
||||||
},
|
|
||||||
'name_server': {
|
|
||||||
realRecordToFields: defaultRecordToFields,
|
|
||||||
fields: realRecordDataConfig['NS'].fields,
|
|
||||||
buildData: defaultBuildData('NS'),
|
|
||||||
getName: defaultGetName,
|
|
||||||
},
|
|
||||||
'soa': {
|
|
||||||
realRecordToFields: defaultRecordToFields,
|
|
||||||
fields: realRecordDataConfig['SOA'].fields,
|
|
||||||
buildData: defaultBuildData('SOA'),
|
|
||||||
getName: defaultGetName,
|
|
||||||
},
|
|
||||||
'service': {
|
|
||||||
realRecordToFields: srvRecordToFields,
|
|
||||||
fields: ['Service', 'Protocol', 'Priority', 'Weight', 'Port', 'Server'],
|
|
||||||
buildData: buildServiceRecord,
|
|
||||||
getName: srvGetName,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const recordTypeNames = {
|
|
||||||
'address': 'Adresse IP',
|
|
||||||
'service': 'Service',
|
|
||||||
'alias': 'Alias',
|
|
||||||
'name_server': 'Serveur de nom',
|
|
||||||
'soa': 'SOA',
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Name to use with spf for example */
|
|
||||||
function getFriendlyTypeForRecord(name, type) {
|
|
||||||
return realRecordDataConfig[type].friendlyType;
|
|
||||||
}
|
|
||||||
|
|
||||||
function processRecords(records) {
|
|
||||||
return records.reduce((acc, record) => {
|
|
||||||
let type = getFriendlyTypeForRecord(record.Name, record.Type);
|
|
||||||
let name = friendlyRecordDataConfig[type].getName(record.Name);
|
|
||||||
if (!(name in acc)) {
|
|
||||||
acc[name] = {};
|
|
||||||
}
|
|
||||||
if (!(type in acc[name])) {
|
|
||||||
acc[name][type] = [];
|
|
||||||
}
|
|
||||||
acc[name][type].push(record);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function FriendlyRecord({type, record}) {
|
|
||||||
let keys = friendlyRecordDataConfig[type].realRecordToFields(record);
|
|
||||||
if (keys.length == 1) {
|
|
||||||
return html`<span>${keys[0][1]}</span>`;
|
|
||||||
} else {
|
|
||||||
return html`
|
|
||||||
<dl>
|
|
||||||
${keys.map(([name, value]) => {return html`<dt><span>${rdataInputProperties[name].label}</span></dt><dd>${value}</dd>`})}
|
|
||||||
</dl>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function RecordsByName({ name, recordSets }) {
|
|
||||||
return html`
|
|
||||||
<article class="rrsets-per-name">
|
|
||||||
<h3 class="record-name">${name}</h4>
|
|
||||||
<div class="rrsets-per-type">
|
|
||||||
${Object.entries(recordSets).map(
|
|
||||||
([type, records]) => {
|
|
||||||
return html`
|
|
||||||
<article class="rrset-per-type">
|
|
||||||
<h4 class="record-type">${recordTypeNames[type]}</h4>
|
|
||||||
<ul class="rrset-rdata">
|
|
||||||
${records.map(record => html`<li><${FriendlyRecord} type=${type} record=${record}/></li>`)}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function RecordListFriendly({ zone }) {
|
|
||||||
const [records, setRecords] = useState({});
|
|
||||||
const [editable, setEditable] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getRecords(zone)
|
|
||||||
.then((res) => setRecords(processRecords(res)));
|
|
||||||
}, [zone]);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${Object.entries(records).map(
|
|
||||||
([name, recordSets]) => {
|
|
||||||
return html`
|
|
||||||
<${RecordsByName} name=${name} recordSets=${recordSets}/>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NewRecordFormFriendly({ zone }) {
|
|
||||||
const defaultVaules = {Name: '', TTL: 3600, Class: 'IN'};
|
|
||||||
const [recordType, setRecordType] = useState(Object.keys(recordTypeNames)[0]);
|
|
||||||
const [recordData, setRecordData] = useState(defaultVaules);
|
|
||||||
const [realRecordData, setRealRecordData] = useState({});
|
|
||||||
const [realType, setRealType] = useState('');
|
|
||||||
|
|
||||||
const absoluteName = (name) => name ? `${name}.${zone}` : zone;
|
|
||||||
|
|
||||||
const setRecordDataFactory = (field) => {
|
|
||||||
return (e) => {
|
|
||||||
const newData = {...recordData};
|
|
||||||
newData[field] = e.target.type == 'number' ? Number(e.target.value) : e.target.value;
|
|
||||||
const newRealRecordData = friendlyRecordDataConfig[recordType].buildData({...newData, Class: 'IN', Name: absoluteName(newData.Name)})
|
|
||||||
|
|
||||||
setRecordData(newData);
|
|
||||||
setRealRecordData(newRealRecordData);
|
|
||||||
setRealType(newRealRecordData.Type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNewRecord = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const newRecords = [realRecordData];
|
|
||||||
console.log(newRecords)
|
|
||||||
createRecords(zone, newRecords);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetData = (resetName = false) => {
|
|
||||||
setRealType('');
|
|
||||||
const newName = resetName ? defaultVaules.Name : recordData.Name;
|
|
||||||
setRecordData({ Name: newName, TTL: defaultVaules.TTL });
|
|
||||||
setRealRecordData({...defaultVaules, Name: absoluteName(newName)});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => resetData(true), []);
|
|
||||||
|
|
||||||
// TODO: Reset valeurs champs quand changement de type + "annuler" => bound la valeur de l'input au state
|
|
||||||
// TODO: Dans le cas où un domain est dans le RDATA mettre le domaine absolue dans la preview
|
|
||||||
// TODO: Déplacer preview dans son component, faire une vue en "diff" et l'appeler "prévisualisation des changements"
|
|
||||||
// TODO: Validation des données client et serveur
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<section class="new-record">
|
|
||||||
<header>
|
|
||||||
<h2>Nouvel enregistrement</h2>
|
|
||||||
</header>
|
|
||||||
<form>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="domain">Domaine</label>
|
|
||||||
<div class="combined-input">
|
|
||||||
<input type="text" id="domain" name="domain" value=${recordData.Name} onInput=${setRecordDataFactory('Name')}/>
|
|
||||||
<span>.${ zone }</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="record_type">Type d'enregistrement</label>
|
|
||||||
<select id="record_type" name="record_type" onChange=${(e) => { setRecordType(e.target.value); resetData() }}>
|
|
||||||
${Object.entries(recordTypeNames).map(([type, name]) => html`<option value="${type}">${name}</option>`)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
${friendlyRecordDataConfig[recordType].fields.map(fieldName => html`
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="${fieldName}">${rdataInputProperties[fieldName].label}</label>
|
|
||||||
<input id="${fieldName}" type="${rdataInputProperties[fieldName].type}" onInput=${setRecordDataFactory(fieldName)}></input>
|
|
||||||
</div>
|
|
||||||
`)}
|
|
||||||
</div>
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="ttl">Durée dans le cache</label>
|
|
||||||
<input type="number" name="ttl" id="ttl" value=${recordData.TTL} onInput=${setRecordDataFactory('TTL')}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<article class="preview">
|
|
||||||
<h3>Prévisualisation des changements</h3>
|
|
||||||
<p>
|
|
||||||
<img src="/images/plus.svg" alt="Ajout" title="Ajout" class="addition"/>
|
|
||||||
<code class="addition">
|
|
||||||
${realRecordData.Name === zone ? '@' : realRecordData.Name} ${realRecordData.TTL} ${realRecordData.Class} ${realType} ${realType != '' ? realRecordDataConfig[realType].fields.map(field => realRecordData[field]).join(' ') : ''}
|
|
||||||
</code>
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
<div>
|
|
||||||
<input type="submit" onClick=${createNewRecord} value="Ajouter"/>
|
|
||||||
<button type="reset" onClick=${e => { resetData(true); e.preventDefault() }}>Annuler</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ZoneRecords({ zone }) {
|
|
||||||
return html`
|
|
||||||
<${NewRecordFormFriendly} zone=${zone}/>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<header>
|
|
||||||
<h2>Contenu de la zone</h2>
|
|
||||||
<button>Éditer la zone</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="zone-content">
|
|
||||||
<${RecordListFriendly} zone=${zone} />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function(element, { zone }) {
|
|
||||||
render(html`<${ZoneRecords} zone=${zone} />`, element);
|
|
||||||
};
|
|
7
public/scripts/vendor/licenses.txt
vendored
7
public/scripts/vendor/licenses.txt
vendored
|
@ -1,7 +0,0 @@
|
||||||
htm@3.1.1 - Apache-2.0
|
|
||||||
Copyright 2018 Google Inc.
|
|
||||||
Full license: ./preact/LICENSE-htm
|
|
||||||
|
|
||||||
preact@10.7.1 - MIT
|
|
||||||
Copyright (c) 2015-present Jason Miller
|
|
||||||
Full license: ./preact/LICENSE-preact
|
|
202
public/scripts/vendor/preact/LICENSE-htm
vendored
202
public/scripts/vendor/preact/LICENSE-htm
vendored
|
@ -1,202 +0,0 @@
|
||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2018 Google Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
21
public/scripts/vendor/preact/LICENSE-preact
vendored
21
public/scripts/vendor/preact/LICENSE-preact
vendored
|
@ -1,21 +0,0 @@
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-present Jason Miller
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
1
public/scripts/vendor/preact/standalone.js
vendored
1
public/scripts/vendor/preact/standalone.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,13 +0,0 @@
|
||||||
form {
|
|
||||||
flex-grow: 1;
|
|
||||||
max-width: 40ch;
|
|
||||||
margin: 25vh auto 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
min-width: 100vw;
|
|
||||||
margin: 0;
|
|
||||||
font-family: sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--color-primary: 94, 12, 151;
|
|
||||||
--color-hightlight-1: 255, 212, 186;
|
|
||||||
--color-hightlight-2: 208, 44, 167;
|
|
||||||
--color-contrast: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
padding: 1rem;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: rgb(var(--color-primary));
|
|
||||||
text-decoration: none;
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: .3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a::after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 2px solid rgb(var(--color-primary));
|
|
||||||
position: absolute;
|
|
||||||
bottom: .1em;
|
|
||||||
transition: .2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover::after {
|
|
||||||
bottom: .3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.feedback {
|
|
||||||
padding: .35rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.feedback.error {
|
|
||||||
background: #fddede;
|
|
||||||
color: #710000;
|
|
||||||
}
|
|
||||||
|
|
||||||
select,
|
|
||||||
input {
|
|
||||||
border: 1px solid rgb(var(--color-primary));;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
select,
|
|
||||||
button,
|
|
||||||
input {
|
|
||||||
padding: .35rem .35rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
input[type="submit"] {
|
|
||||||
background: rgb(var(--color-primary));
|
|
||||||
color: var(--color-contrast);
|
|
||||||
border-left: 5px solid rgb(var(--color-hightlight-1));
|
|
||||||
border-top: 5px solid rgb(var(--color-hightlight-1));
|
|
||||||
border-right: 5px solid rgb(var(--color-hightlight-2));
|
|
||||||
border-bottom: 5px solid rgb(var(--color-hightlight-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover:not([disabled]),
|
|
||||||
input[type="submit"]:hover:not([disabled]) {
|
|
||||||
background: rgba(var(--color-primary), .8);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active:not([disabled]) ,
|
|
||||||
input[type="submit"]:active:not([disabled]) {
|
|
||||||
border-left: 5px solid rgb(var(--color-hightlight-2));
|
|
||||||
border-top: 5px solid rgb(var(--color-hightlight-2));
|
|
||||||
border-right: 5px solid rgb(var(--color-hightlight-1));
|
|
||||||
border-bottom: 5px solid rgb(var(--color-hightlight-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
button[disabled],
|
|
||||||
input[type="submit"][disabled] {
|
|
||||||
background: rgba(var(--color-primary), .75);
|
|
||||||
border-left-color: rgba(var(--color-hightlight-1), .75);
|
|
||||||
border-top-color: rgba(var(--color-hightlight-1), .75);
|
|
||||||
border-right-color: rgba(var(--color-hightlight-2), .75);
|
|
||||||
border-bottom-color: rgba(var(--color-hightlight-2), .75);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input[type="submit"] {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
form label {
|
|
||||||
margin-top: .75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.main {
|
|
||||||
background: rgb(var(--color-primary));
|
|
||||||
min-width: 25ch;
|
|
||||||
display: flex;
|
|
||||||
flex: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
border-right: 5px solid rgb(var(--color-hightlight-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.main a {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.main a::after {
|
|
||||||
border-bottom: 2px solid var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
nav.main a img {
|
|
||||||
filter: invert(100%);
|
|
||||||
width: 1.4em;
|
|
||||||
margin-bottom: -.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
nav.main ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.main ul li {
|
|
||||||
margin-top: .35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.main ul ul {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
|
@ -1,183 +0,0 @@
|
||||||
nav.secondary ul {
|
|
||||||
display: flex;
|
|
||||||
margin: 0;
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.secondary li {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
main > section {
|
|
||||||
max-width: 120ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > :not(:last-of-type) {
|
|
||||||
margin-right: 2ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content article.rrsets-per-name {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(6, 1fr);
|
|
||||||
grid-gap: 2ch;
|
|
||||||
margin: .5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content article.rrsets-per-name:not(:last-of-type) {
|
|
||||||
border-bottom: 2px solid rgb(var(--color-hightlight-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content h3.record-name,
|
|
||||||
.zone-content h4.record-type {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content h3.record-name {
|
|
||||||
grid-column: 1 / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content div.rrsets-per-type {
|
|
||||||
grid-column: 3 / 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content article.rrset-per-type {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 2ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content h4.record-type {
|
|
||||||
grid-column: 1 / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content ul.rrset-rdata {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
grid-column: 2 / 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content ul.rrset-rdata dl {
|
|
||||||
display: grid;
|
|
||||||
grid-template: auto / max-content 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content ul.rrset-rdata dd {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zone-content ul.rrset-rdata dt span {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: rgb(var(--color-hightlight-1));
|
|
||||||
padding: 0.1em 0.5em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
margin-right: 0.1rem;
|
|
||||||
font-size: .7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form > div.form-row > * {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form > div.form-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 2ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form label {
|
|
||||||
margin-top: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.input-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.combined-input {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.combined-input input {
|
|
||||||
height: min-content;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.combined-input span {
|
|
||||||
font-size: .8rem;
|
|
||||||
padding: .35rem;
|
|
||||||
border: 1px solid rgb(var(--color-primary));;
|
|
||||||
border-left: none;
|
|
||||||
background: rgba(var(--color-hightlight-2),.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
form.disabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form button,
|
|
||||||
.new-record form input[type="submit"] {
|
|
||||||
margin-right: 1ch;
|
|
||||||
margin-top: .75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record header {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form .preview {
|
|
||||||
margin: .5rem 0;
|
|
||||||
border: 1px solid rgb(var(--color-primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form .preview p:first-of-type {
|
|
||||||
margin-top: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form .preview p:last-of-type {
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form .preview p {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form .preview code {
|
|
||||||
padding: 0 .5rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form .preview img {
|
|
||||||
padding: 0 .25rem;
|
|
||||||
border-right: 1px solid #1b841b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form .preview .addition {
|
|
||||||
background: #d9fbd9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-record form .preview h3 {
|
|
||||||
margin: 0;
|
|
||||||
padding: .0rem .5rem 0 .5rem;;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: normal;
|
|
||||||
background: rgb(var(--color-primary));
|
|
||||||
color: var(--color-contrast)
|
|
||||||
}
|
|
|
@ -11,9 +11,8 @@ use domain::tsig::{Algorithm, Key, KeyName};
|
||||||
use domain::net::client::request::{self, RequestMessage, RequestMessageMulti, SendRequest, SendRequestMulti};
|
use domain::net::client::request::{self, RequestMessage, RequestMessageMulti, SendRequest, SendRequestMulti};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
use super::{rdata, record};
|
use crate::ressouces::{rdata, record};
|
||||||
use super::{RecordDriver, ZoneDriver, DnsDriverError};
|
use super::{RecordDriver, ZoneDriver, DnsDriverError};
|
||||||
use crate::errors::Error;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
pub mod rdata;
|
|
||||||
pub mod record;
|
|
||||||
pub mod dns_driver;
|
pub mod dns_driver;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::ressouces::record;
|
||||||
|
|
||||||
pub type BoxedZoneDriver = Arc<dyn ZoneDriver>;
|
pub type BoxedZoneDriver = Arc<dyn ZoneDriver>;
|
||||||
|
pub type BoxedRecordDriver = Arc<dyn RecordDriver>;
|
||||||
pub enum DnsDriverError {
|
pub enum DnsDriverError {
|
||||||
ConnectionError { reason: Box<dyn std::error::Error> },
|
ConnectionError { reason: Box<dyn std::error::Error> },
|
||||||
OperationError { reason: Box<dyn std::error::Error> },
|
OperationError { reason: Box<dyn std::error::Error> },
|
||||||
|
|
528
src/dns/rdata.rs
528
src/dns/rdata.rs
|
@ -1,528 +0,0 @@
|
||||||
use std::fmt::Write;
|
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
|
|
||||||
use domain::base::rdata::ComposeRecordData;
|
|
||||||
use domain::base::scan::Symbol;
|
|
||||||
use domain::base::wire::{Composer, ParseError};
|
|
||||||
use domain::base::{Name, ParseRecordData, ParsedName, RecordData, Rtype, ToName, Ttl};
|
|
||||||
use domain::rdata;
|
|
||||||
use domain::dep::octseq::{Parser, Octets};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::errors::Error;
|
|
||||||
use crate::validation;
|
|
||||||
|
|
||||||
use crate::macros::{append_errors, push_error};
|
|
||||||
use super::record::RecordParseError;
|
|
||||||
|
|
||||||
/// Type used to serialize / deserialize resource records data to response / request
|
|
||||||
///
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
#[serde(tag = "type", content = "rdata")]
|
|
||||||
#[serde(rename_all = "UPPERCASE")]
|
|
||||||
pub enum RData {
|
|
||||||
A(A),
|
|
||||||
Aaaa(Aaaa),
|
|
||||||
// TODO: CAA
|
|
||||||
Cname(Cname),
|
|
||||||
// TODO: DS
|
|
||||||
Mx(Mx),
|
|
||||||
Ns(Ns),
|
|
||||||
Ptr(Ptr),
|
|
||||||
Soa(Soa),
|
|
||||||
Srv(Srv),
|
|
||||||
// TODO: SSHFP
|
|
||||||
// TODO: SVCB / HTTPS
|
|
||||||
// TODO: TLSA
|
|
||||||
Txt(Txt),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ParsedRData<Name, Octs> {
|
|
||||||
A(rdata::A),
|
|
||||||
Aaaa(rdata::Aaaa),
|
|
||||||
Cname(rdata::Cname<Name>),
|
|
||||||
Mx(rdata::Mx<Name>),
|
|
||||||
Ns(rdata::Ns<Name>),
|
|
||||||
Ptr(rdata::Ptr<Name>),
|
|
||||||
Soa(rdata::Soa<Name>),
|
|
||||||
Srv(rdata::Srv<Name>),
|
|
||||||
Txt(rdata::Txt<Octs>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Name: ToString, Octs: AsRef<[u8]>> From<ParsedRData<Name, Octs>> for RData {
|
|
||||||
fn from(value: ParsedRData<Name, Octs>) -> Self {
|
|
||||||
match value {
|
|
||||||
ParsedRData::A(record_rdata) => RData::A(record_rdata.into()),
|
|
||||||
ParsedRData::Aaaa(record_rdata) => RData::Aaaa(record_rdata.into()),
|
|
||||||
ParsedRData::Cname(record_rdata) => RData::Cname(record_rdata.into()),
|
|
||||||
ParsedRData::Mx(record_rdata) => RData::Mx(record_rdata.into()),
|
|
||||||
ParsedRData::Ns(record_rdata) => RData::Ns(record_rdata.into()),
|
|
||||||
ParsedRData::Ptr(record_rdata) => RData::Ptr(record_rdata.into()),
|
|
||||||
ParsedRData::Soa(record_rdata) => RData::Soa(record_rdata.into()),
|
|
||||||
ParsedRData::Srv(record_rdata) => RData::Srv(record_rdata.into()),
|
|
||||||
ParsedRData::Txt(record_rdata) => RData::Txt(record_rdata.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<RData> for ParsedRData<Name<Vec<u8>>, Vec<u8>> {
|
|
||||||
type Error = Vec<Error>;
|
|
||||||
|
|
||||||
fn try_from(value: RData) -> Result<Self, Self::Error> {
|
|
||||||
let rdata = match value {
|
|
||||||
RData::A(record_rdata) => ParsedRData::A(record_rdata.parse_record()?),
|
|
||||||
RData::Aaaa(record_rdata) => ParsedRData::Aaaa(record_rdata.parse_record()?),
|
|
||||||
RData::Cname(record_rdata) => ParsedRData::Cname(record_rdata.parse_record()?),
|
|
||||||
RData::Mx(record_rdata) => ParsedRData::Mx(record_rdata.parse_record()?),
|
|
||||||
RData::Ns(record_rdata) => ParsedRData::Ns(record_rdata.parse_record()?),
|
|
||||||
RData::Ptr(record_rdata) => ParsedRData::Ptr(record_rdata.parse_record()?),
|
|
||||||
RData::Soa(record_rdata) => ParsedRData::Soa(record_rdata.parse_record()?),
|
|
||||||
RData::Srv(record_rdata) => ParsedRData::Srv(record_rdata.parse_record()?),
|
|
||||||
RData::Txt(record_rdata) => ParsedRData::Txt(record_rdata.parse_record()?),
|
|
||||||
};
|
|
||||||
Ok(rdata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
macro_rules! parse_name {
|
|
||||||
($value:expr, $field:ident, $rtype:literal, $errors:expr) => {
|
|
||||||
{
|
|
||||||
let name = push_error!(
|
|
||||||
validation::normalize_domain(&$value.$field),
|
|
||||||
$errors, concat!("/", stringify!($field))
|
|
||||||
);
|
|
||||||
|
|
||||||
let name = name.and_then(|name| {
|
|
||||||
push_error!(
|
|
||||||
name.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(RecordParseError::RDataUnknown {
|
|
||||||
input: $value.$field,
|
|
||||||
field: stringify!(field).to_string(),
|
|
||||||
rtype: $rtype.to_string(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
}),
|
|
||||||
$errors, concat!("/", stringify!($field))
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
name
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- A --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct A {
|
|
||||||
pub address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rdata::A> for A {
|
|
||||||
fn from(record_data: rdata::A) -> Self {
|
|
||||||
A { address: record_data.addr().to_string() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl A {
|
|
||||||
pub fn parse_record(self) -> Result<rdata::A, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let address = push_error!(self.address.parse::<Ipv4Addr>().map_err(|e| {
|
|
||||||
Error::from(RecordParseError::Ip4Address { input: self.address })
|
|
||||||
.with_cause(&e.to_string())
|
|
||||||
.with_path("/address")
|
|
||||||
}), errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(rdata::A::new(address.unwrap()))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- AAAA --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Aaaa {
|
|
||||||
pub address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rdata::Aaaa> for Aaaa {
|
|
||||||
fn from(record_data: rdata::Aaaa) -> Self {
|
|
||||||
Aaaa { address: record_data.addr().to_string() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Aaaa {
|
|
||||||
pub fn parse_record(self) -> Result<rdata::Aaaa, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let address = push_error!(self.address.parse::<Ipv6Addr>().map_err(|e| {
|
|
||||||
Error::from(RecordParseError::Ip6Address { input: self.address })
|
|
||||||
.with_cause(&e.to_string())
|
|
||||||
.with_path("/address")
|
|
||||||
}), errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(rdata::Aaaa::new(address.unwrap()))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- CNAME --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Cname {
|
|
||||||
pub target: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: ToString> From<rdata::Cname<N>> for Cname {
|
|
||||||
fn from(record_data: rdata::Cname<N>) -> Self {
|
|
||||||
Cname { target: record_data.cname().to_string() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cname {
|
|
||||||
pub fn parse_record(self) -> Result<rdata::Cname<Name<Vec<u8>>>, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let cname = parse_name!(self, target, "CNAME", errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(rdata::Cname::new(cname.unwrap()))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- MX --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Mx {
|
|
||||||
pub preference: u16,
|
|
||||||
pub mail_exchanger: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: ToString> From<rdata::Mx<N>> for Mx {
|
|
||||||
fn from(record_data: rdata::Mx<N>) -> Self {
|
|
||||||
Mx {
|
|
||||||
preference: record_data.preference(),
|
|
||||||
mail_exchanger: record_data.exchange().to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mx {
|
|
||||||
fn parse_record(self) -> Result<rdata::Mx<Name<Vec<u8>>>, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let mail_exchanger = parse_name!(self, mail_exchanger, "MX", errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(rdata::Mx::new(self.preference, mail_exchanger.unwrap()))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- NS --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Ns {
|
|
||||||
pub target: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: ToString> From<rdata::Ns<N>> for Ns {
|
|
||||||
fn from(record_rdata: rdata::Ns<N>) -> Self {
|
|
||||||
Ns {
|
|
||||||
target: record_rdata.nsdname().to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ns {
|
|
||||||
fn parse_record(self) -> Result<rdata::Ns<Name<Vec<u8>>>, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let ns_name = parse_name!(self, target, "NS", errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(rdata::Ns::new(ns_name.unwrap()))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- PTR --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Ptr {
|
|
||||||
pub target: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: ToString> From<rdata::Ptr<N>> for Ptr {
|
|
||||||
fn from(record_rdata: rdata::Ptr<N>) -> Self {
|
|
||||||
Ptr {
|
|
||||||
target: record_rdata.ptrdname().to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ptr {
|
|
||||||
fn parse_record(self) -> Result<rdata::Ptr<Name<Vec<u8>>>, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let ptr_name = parse_name!(self, target, "PTR", errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(rdata::Ptr::new(ptr_name.unwrap()))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- SOA --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Soa {
|
|
||||||
pub primary_server: String,
|
|
||||||
pub maintainer: String,
|
|
||||||
pub refresh: u32,
|
|
||||||
pub retry: u32,
|
|
||||||
pub expire: u32,
|
|
||||||
pub minimum: u32,
|
|
||||||
pub serial: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: ToString> From<rdata::Soa<N>> for Soa {
|
|
||||||
fn from(record_rdata: rdata::Soa<N>) -> Self {
|
|
||||||
Soa {
|
|
||||||
primary_server: record_rdata.mname().to_string(),
|
|
||||||
maintainer: record_rdata.rname().to_string(),
|
|
||||||
refresh: record_rdata.refresh().as_secs(),
|
|
||||||
retry: record_rdata.retry().as_secs(),
|
|
||||||
expire: record_rdata.expire().as_secs(),
|
|
||||||
minimum: record_rdata.minimum().as_secs(),
|
|
||||||
serial: record_rdata.serial().into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Soa {
|
|
||||||
fn parse_record(self) -> Result<rdata::Soa<Name<Vec<u8>>>, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let primary_ns = parse_name!(self, primary_server, "SOA", errors);
|
|
||||||
let maintainer = parse_name!(self, maintainer, "SOA", errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(rdata::Soa::new(
|
|
||||||
primary_ns.unwrap(),
|
|
||||||
maintainer.unwrap(),
|
|
||||||
self.refresh.into(),
|
|
||||||
Ttl::from_secs(self.retry),
|
|
||||||
Ttl::from_secs(self.expire),
|
|
||||||
Ttl::from_secs(self.minimum),
|
|
||||||
Ttl::from_secs(self.serial),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- SRV --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Srv {
|
|
||||||
pub server: String,
|
|
||||||
pub port: u16,
|
|
||||||
pub priority: u16,
|
|
||||||
pub weight: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: ToString> From<rdata::Srv<N>> for Srv {
|
|
||||||
fn from(record_data: rdata::Srv<N>) -> Self {
|
|
||||||
Srv {
|
|
||||||
server: record_data.target().to_string(),
|
|
||||||
priority: record_data.priority(),
|
|
||||||
weight: record_data.weight(),
|
|
||||||
port: record_data.port(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Srv {
|
|
||||||
fn parse_record(self) -> Result<rdata::Srv<Name<Vec<u8>>>, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let server = parse_name!(self, server, "SRV", errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(rdata::Srv::new(
|
|
||||||
self.priority,
|
|
||||||
self.weight,
|
|
||||||
self.port,
|
|
||||||
server.unwrap(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* --------- TXT --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Txt {
|
|
||||||
pub text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<O: AsRef<[u8]>> From<rdata::Txt<O>> for Txt {
|
|
||||||
fn from(record_data: rdata::Txt<O>) -> Self {
|
|
||||||
let mut concatenated_text = String::new();
|
|
||||||
for text in record_data.iter() {
|
|
||||||
for c in text {
|
|
||||||
// Escapes '\' and non printable chars
|
|
||||||
let c = Symbol::display_from_octet(*c);
|
|
||||||
write!(concatenated_text, "{}", c).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Txt {
|
|
||||||
text: concatenated_text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Txt {
|
|
||||||
fn parse_record(self) -> Result<rdata::Txt<Vec<u8>>, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
let data = append_errors!(validation::parse_txt_data(&self.text), errors, "/text");
|
|
||||||
let data = data.and_then(|data| {
|
|
||||||
push_error!(rdata::Txt::build_from_slice(&data).map_err(|e| {
|
|
||||||
Error::from(RecordParseError::RDataUnknown {
|
|
||||||
input: self.text,
|
|
||||||
field: "text".into(),
|
|
||||||
rtype: "TXT".into(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
.with_path("/text")
|
|
||||||
}), errors)
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(data.unwrap())
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- ParsedRData: domain traits impl --------- */
|
|
||||||
|
|
||||||
impl<Name, Octs> ParsedRData<Name, Octs> {
|
|
||||||
pub fn rtype(&self) -> Rtype {
|
|
||||||
match self {
|
|
||||||
ParsedRData::A(_) => Rtype::A,
|
|
||||||
ParsedRData::Aaaa(_) => Rtype::AAAA,
|
|
||||||
ParsedRData::Cname(_) => Rtype::CNAME,
|
|
||||||
ParsedRData::Mx(_) => Rtype::MX,
|
|
||||||
ParsedRData::Ns(_) => Rtype::NS,
|
|
||||||
ParsedRData::Ptr(_) => Rtype::PTR,
|
|
||||||
ParsedRData::Soa(_) => Rtype::SOA,
|
|
||||||
ParsedRData::Srv(_) => Rtype::SRV,
|
|
||||||
ParsedRData::Txt(_) => Rtype::TXT,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Name, Octs> RecordData for ParsedRData<Name, Octs> {
|
|
||||||
fn rtype(&self) -> Rtype {
|
|
||||||
ParsedRData::rtype(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Octs: Octets + ?Sized> ParseRecordData<'a, Octs> for ParsedRData<ParsedName<Octs::Range<'a>>, Octs::Range<'a>> {
|
|
||||||
fn parse_rdata(
|
|
||||||
rtype: Rtype,
|
|
||||||
parser: &mut Parser<'a, Octs>,
|
|
||||||
) -> Result<Option<Self>, ParseError> {
|
|
||||||
let record = match rtype {
|
|
||||||
Rtype::A => ParsedRData::A(rdata::A::parse(parser)?),
|
|
||||||
Rtype::AAAA => ParsedRData::Aaaa(rdata::Aaaa::parse(parser)?),
|
|
||||||
Rtype::CNAME => ParsedRData::Cname(rdata::Cname::parse(parser)?),
|
|
||||||
Rtype::MX => ParsedRData::Mx(rdata::Mx::parse(parser)?),
|
|
||||||
Rtype::NS => ParsedRData::Ns(rdata::Ns::parse(parser)?),
|
|
||||||
Rtype::PTR => ParsedRData::Ptr(rdata::Ptr::parse(parser)?),
|
|
||||||
Rtype::SOA => ParsedRData::Soa(rdata::Soa::parse(parser)?),
|
|
||||||
Rtype::SRV => ParsedRData::Srv(rdata::Srv::parse(parser)?),
|
|
||||||
Rtype::TXT => ParsedRData::Txt(rdata::Txt::parse(parser)?),
|
|
||||||
_ => return Ok(None)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(record))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Name: ToName, Octs: AsRef<[u8]>> ComposeRecordData for ParsedRData<Name, Octs> {
|
|
||||||
fn rdlen(&self, compress: bool) -> Option<u16> {
|
|
||||||
match self {
|
|
||||||
ParsedRData::A(record_rdata) => record_rdata.rdlen(compress),
|
|
||||||
ParsedRData::Aaaa(record_rdata) => record_rdata.rdlen(compress),
|
|
||||||
ParsedRData::Cname(record_rdata) => record_rdata.rdlen(compress),
|
|
||||||
ParsedRData::Mx(record_rdata) => record_rdata.rdlen(compress),
|
|
||||||
ParsedRData::Ns(record_rdata) => record_rdata.rdlen(compress),
|
|
||||||
ParsedRData::Ptr(record_rdata) => record_rdata.rdlen(compress),
|
|
||||||
ParsedRData::Soa(record_rdata) => record_rdata.rdlen(compress),
|
|
||||||
ParsedRData::Srv(record_rdata) => record_rdata.rdlen(compress),
|
|
||||||
ParsedRData::Txt(record_rdata) => record_rdata.rdlen(compress),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compose_rdata<Target: Composer + ?Sized>(
|
|
||||||
&self,
|
|
||||||
target: &mut Target,
|
|
||||||
) -> Result<(), Target::AppendError> {
|
|
||||||
match self {
|
|
||||||
ParsedRData::A(record_rdata) => record_rdata.compose_rdata(target),
|
|
||||||
ParsedRData::Aaaa(record_rdata) => record_rdata.compose_rdata(target),
|
|
||||||
ParsedRData::Cname(record_rdata) => record_rdata.compose_rdata(target),
|
|
||||||
ParsedRData::Mx(record_rdata) => record_rdata.compose_rdata(target),
|
|
||||||
ParsedRData::Ns(record_rdata) => record_rdata.compose_rdata(target),
|
|
||||||
ParsedRData::Ptr(record_rdata) => record_rdata.compose_rdata(target),
|
|
||||||
ParsedRData::Soa(record_rdata) => record_rdata.compose_rdata(target),
|
|
||||||
ParsedRData::Srv(record_rdata) => record_rdata.compose_rdata(target),
|
|
||||||
ParsedRData::Txt(record_rdata) => record_rdata.compose_rdata(target),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compose_canonical_rdata<Target: Composer + ?Sized>(
|
|
||||||
&self,
|
|
||||||
target: &mut Target,
|
|
||||||
) -> Result<(), Target::AppendError> {
|
|
||||||
match self {
|
|
||||||
ParsedRData::A(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
|
||||||
ParsedRData::Aaaa(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
|
||||||
ParsedRData::Cname(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
|
||||||
ParsedRData::Mx(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
|
||||||
ParsedRData::Ns(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
|
||||||
ParsedRData::Ptr(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
|
||||||
ParsedRData::Soa(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
|
||||||
ParsedRData::Srv(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
|
||||||
ParsedRData::Txt(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use domain::base::{iana::Class, Name, Record as DnsRecord, Ttl};
|
|
||||||
|
|
||||||
use crate::{errors::Error, validation};
|
|
||||||
use crate::macros::{append_errors, push_error};
|
|
||||||
use super::rdata::{ParsedRData, RData};
|
|
||||||
|
|
||||||
pub enum RecordParseError {
|
|
||||||
Ip4Address { input: String },
|
|
||||||
Ip6Address { input: String },
|
|
||||||
RDataUnknown { input: String, field: String, rtype: String },
|
|
||||||
NameUnknown { input: String },
|
|
||||||
NotInZone { name: String, zone: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum RecordError {
|
|
||||||
Validation { suberrors: Vec<Error> },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) type DnsRecordImpl = DnsRecord<
|
|
||||||
Name<Vec<u8>>,
|
|
||||||
ParsedRData<Name<Vec<u8>>,Vec<u8>>
|
|
||||||
>;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Record {
|
|
||||||
pub name: String,
|
|
||||||
pub ttl: u32,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub rdata: RData
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Name: ToString, Oct: AsRef<[u8]>> From<DnsRecord<Name, ParsedRData<Name, Oct>>> for Record {
|
|
||||||
fn from(value: DnsRecord<Name, ParsedRData<Name, Oct>>) -> Self {
|
|
||||||
Record {
|
|
||||||
name: value.owner().to_string(),
|
|
||||||
ttl: value.ttl().as_secs(),
|
|
||||||
rdata: value.into_data().into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Record {
|
|
||||||
fn convert(self, zone_name: &Name<Vec<u8>>) -> Result<DnsRecordImpl, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let name = push_error!(validation::normalize_domain(&self.name), errors, "/name");
|
|
||||||
|
|
||||||
let name = name.and_then(|name| push_error!(name.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(RecordParseError::NameUnknown {
|
|
||||||
input: self.name.clone()
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
}), errors, "/name"));
|
|
||||||
|
|
||||||
let name = name.and_then(|name| {
|
|
||||||
if !name.ends_with(zone_name) {
|
|
||||||
errors.push(
|
|
||||||
Error::from(RecordParseError::NotInZone { name: self.name, zone: zone_name.to_string() })
|
|
||||||
.with_path("/name")
|
|
||||||
);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(name)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let ttl = Ttl::from_secs(self.ttl);
|
|
||||||
let rdata = append_errors!(ParsedRData::try_from(self.rdata), errors, "/rdata");
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(DnsRecord::new(name.unwrap(), Class::IN, ttl, rdata.unwrap()))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct RecordList(Vec<Record>);
|
|
||||||
|
|
||||||
impl RecordList {
|
|
||||||
fn convert(self, zone_name: &Name<Vec<u8>>) -> Result<Vec<DnsRecordImpl>, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
let mut records = Vec::new();
|
|
||||||
|
|
||||||
for (index, record) in self.0.into_iter().enumerate() {
|
|
||||||
let record = append_errors!(record.convert(zone_name), errors, &format!("/{index}"));
|
|
||||||
|
|
||||||
if let Some(record) = record {
|
|
||||||
records.push(record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(records)
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug,Deserialize)]
|
|
||||||
pub struct AddRecordsRequest {
|
|
||||||
pub new_records: RecordList
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AddRecords {
|
|
||||||
pub new_records: Vec<DnsRecordImpl>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AddRecordsRequest {
|
|
||||||
pub fn validate(self, zone_name: &str) -> Result<AddRecords, Error> {
|
|
||||||
let zone_name: Name<Vec<u8>> = zone_name.parse().expect("zone name is assumed to be valid");
|
|
||||||
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
let records = append_errors!(self.new_records.convert(&zone_name), errors, "/new_records");
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(AddRecords {
|
|
||||||
new_records: records.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(Error::from(RecordError::Validation { suberrors: errors }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,10 +7,11 @@ use axum::Json;
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::dns::{DnsDriverError, ZoneDriver};
|
use crate::dns::DnsDriverError;
|
||||||
use crate::dns::record::{RecordError, RecordParseError};
|
use crate::ressouces::record::{RecordError, RecordParseError};
|
||||||
use crate::ressouces::zone::ZoneError;
|
use crate::ressouces::zone::ZoneError;
|
||||||
use crate::validation::{DomainValidationError, TxtParseError};
|
use crate::validation::{DomainValidationError, TxtParseError};
|
||||||
|
use crate::template::TemplateError;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
|
@ -357,6 +358,24 @@ impl From<RecordParseError> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<TemplateError> for Error {
|
||||||
|
fn from(value: TemplateError) -> Self {
|
||||||
|
match value {
|
||||||
|
TemplateError::RenderError { name, reason } => {
|
||||||
|
Error::new("template:render", "Failed to render the template")
|
||||||
|
.with_details(json!({
|
||||||
|
"name": name
|
||||||
|
}))
|
||||||
|
.with_cause(&reason.to_string())
|
||||||
|
},
|
||||||
|
TemplateError::SerializationError { reason } => {
|
||||||
|
Error::new("template:serialization", "Failed to serialize context")
|
||||||
|
.with_cause(&reason.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<RecordError > for Error {
|
impl From<RecordError > for Error {
|
||||||
fn from(value: RecordError) -> Self {
|
fn from(value: RecordError) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
|
47
src/main.rs
47
src/main.rs
|
@ -1,26 +1,3 @@
|
||||||
//#![feature(proc_macro_hygiene, decl_macro)]
|
|
||||||
|
|
||||||
/*
|
|
||||||
#[macro_use] extern crate rocket;
|
|
||||||
#[macro_use] extern crate diesel;
|
|
||||||
#[macro_use] extern crate diesel_migrations;
|
|
||||||
*/
|
|
||||||
|
|
||||||
//mod routes;
|
|
||||||
//mod cli;
|
|
||||||
//mod config;
|
|
||||||
//mod models;
|
|
||||||
//mod schema;
|
|
||||||
//mod template;
|
|
||||||
//mod controllers;
|
|
||||||
|
|
||||||
//use std::process::exit;
|
|
||||||
|
|
||||||
//use clap::Parser;
|
|
||||||
//use figment::{Figment, Profile, providers::{Format, Toml, Env}};
|
|
||||||
//use rocket_sync_db_pools::database;
|
|
||||||
//use diesel::prelude::*;
|
|
||||||
|
|
||||||
mod errors;
|
mod errors;
|
||||||
mod dns;
|
mod dns;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
@ -28,29 +5,34 @@ mod ressouces;
|
||||||
mod database;
|
mod database;
|
||||||
mod validation;
|
mod validation;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
mod template;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum::routing;
|
use axum::routing;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
use database::sqlite::SqliteDB;
|
use database::sqlite::SqliteDB;
|
||||||
use database::Db;
|
use database::BoxedDb;
|
||||||
use dns::dns_driver::DnsDriverConfig;
|
use dns::dns_driver::DnsDriverConfig;
|
||||||
use dns::dns_driver::TsigConfig;
|
use dns::dns_driver::TsigConfig;
|
||||||
use dns::{ZoneDriver, RecordDriver};
|
use dns::{ZoneDriver, RecordDriver};
|
||||||
|
use template::TemplateEngine;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
zone: Arc<dyn ZoneDriver>,
|
zone: Arc<dyn ZoneDriver>,
|
||||||
records: Arc<dyn RecordDriver>,
|
records: Arc<dyn RecordDriver>,
|
||||||
db: Arc<dyn Db>,
|
db: BoxedDb,
|
||||||
|
template_engine: TemplateEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
let template_engine = TemplateEngine::new(std::path::Path::new("./templates"));
|
||||||
|
|
||||||
let dns_driver = dns::dns_driver::DnsDriver::from_config(DnsDriverConfig {
|
let dns_driver = dns::dns_driver::DnsDriver::from_config(DnsDriverConfig {
|
||||||
address: "127.0.0.1:5353".parse().unwrap(),
|
address: "127.0.0.1:5353".parse().unwrap(),
|
||||||
tsig: Some(TsigConfig {
|
tsig: Some(TsigConfig {
|
||||||
|
@ -66,12 +48,17 @@ async fn main() {
|
||||||
zone: dns_driver.clone(),
|
zone: dns_driver.clone(),
|
||||||
records: dns_driver.clone(),
|
records: dns_driver.clone(),
|
||||||
db: Arc::new(SqliteDB::new("db.sqlite".into()).await),
|
db: Arc::new(SqliteDB::new("db.sqlite".into()).await),
|
||||||
|
template_engine
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/admin/zones", routing::post(routes::api::zones::create_zone))
|
/* ----- API ----- */
|
||||||
.route("/zones/{zone_name}/records", routing::get(routes::api::zones::get_zone_records))
|
.route("/api/admin/zones", routing::post(routes::api::zones::create_zone))
|
||||||
.route("/zones/{zone_name}/records", routing::post(routes::api::zones::create_zone_records))
|
.route("/api/zones/{zone_name}/records", routing::get(routes::api::zones::get_zone_records))
|
||||||
|
.route("/api/zones/{zone_name}/records", routing::post(routes::api::zones::create_zone_records))
|
||||||
|
/* ----- UI ----- */
|
||||||
|
.route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_zone_records_page))
|
||||||
|
.nest_service("/assets", ServeDir::new("assets"))
|
||||||
.with_state(app_state);
|
.with_state(app_state);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::dns;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub enum DNSClass {
|
|
||||||
IN,
|
|
||||||
CH,
|
|
||||||
HS,
|
|
||||||
NONE,
|
|
||||||
ANY,
|
|
||||||
OPT(u16),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<dns::DNSClass> for DNSClass {
|
|
||||||
fn from(dns_class: dns::DNSClass) -> DNSClass {
|
|
||||||
match dns_class {
|
|
||||||
dns::DNSClass::IN => DNSClass::IN,
|
|
||||||
dns::DNSClass::CH => DNSClass::CH,
|
|
||||||
dns::DNSClass::HS => DNSClass::HS,
|
|
||||||
dns::DNSClass::NONE => DNSClass::NONE,
|
|
||||||
dns::DNSClass::ANY => DNSClass::ANY,
|
|
||||||
dns::DNSClass::OPT(v) => DNSClass::OPT(v),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DNSClass> for dns::DNSClass {
|
|
||||||
fn from(dns_class: DNSClass) -> dns::DNSClass {
|
|
||||||
match dns_class {
|
|
||||||
DNSClass::IN => dns::DNSClass::IN,
|
|
||||||
DNSClass::CH => dns::DNSClass::CH,
|
|
||||||
DNSClass::HS => dns::DNSClass::HS,
|
|
||||||
DNSClass::NONE => dns::DNSClass::NONE,
|
|
||||||
DNSClass::ANY => dns::DNSClass::ANY,
|
|
||||||
DNSClass::OPT(v) => dns::DNSClass::OPT(v),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::json;
|
|
||||||
use rocket::http::Status;
|
|
||||||
use rocket::request::{Request, Outcome};
|
|
||||||
use rocket::response::{self, Response, Responder};
|
|
||||||
use rocket::serde::json::Json;
|
|
||||||
use serde_json::Value;
|
|
||||||
use diesel::result::Error as DieselError;
|
|
||||||
use argon2::password_hash::errors::Error as PasswordHashError;
|
|
||||||
|
|
||||||
use crate::dns::ConnectorError;
|
|
||||||
use crate::models;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum UserError {
|
|
||||||
ZoneNotFound,
|
|
||||||
NotFound,
|
|
||||||
UserConflict,
|
|
||||||
BadCreds,
|
|
||||||
MissingToken,
|
|
||||||
ExpiredSession,
|
|
||||||
MalformedHeader,
|
|
||||||
PermissionDenied,
|
|
||||||
DbError(DieselError),
|
|
||||||
PasswordError(PasswordHashError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DieselError> for UserError {
|
|
||||||
fn from(e: DieselError) -> Self {
|
|
||||||
UserError::DbError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PasswordHashError> for UserError {
|
|
||||||
fn from(e: PasswordHashError) -> Self {
|
|
||||||
UserError::PasswordError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
pub struct ErrorResponse {
|
|
||||||
#[serde(with = "StatusDef")]
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub status: Status,
|
|
||||||
pub message: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub details: Option<Value>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(remote = "Status")]
|
|
||||||
struct StatusDef {
|
|
||||||
code: u16,
|
|
||||||
#[serde(rename = "status", getter = "Status::reason")]
|
|
||||||
reason: Option<&'static str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ErrorResponse {
|
|
||||||
pub fn new(status: Status, message: String) -> ErrorResponse {
|
|
||||||
ErrorResponse {
|
|
||||||
status,
|
|
||||||
message,
|
|
||||||
details: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_details<T: Serialize> (self, details: T) -> ErrorResponse {
|
|
||||||
ErrorResponse {
|
|
||||||
details: serde_json::to_value(details).ok(),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn err<R>(self) -> Result<R, ErrorResponse> {
|
|
||||||
Err(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'r> Responder<'r, 'static> for ErrorResponse {
|
|
||||||
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
|
|
||||||
let status = self.status;
|
|
||||||
Response::build_from(Json(self).respond_to(req)?).status(status).ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<UserError> for ErrorResponse {
|
|
||||||
fn from(e: UserError) -> Self {
|
|
||||||
match e {
|
|
||||||
UserError::BadCreds => ErrorResponse::new(Status::Unauthorized, "Provided credentials or token do not match any existing user".into()),
|
|
||||||
UserError::UserConflict => ErrorResponse::new(Status::Conflict, "This user already exists".into()),
|
|
||||||
UserError::NotFound => ErrorResponse::new(Status::NotFound, "User does not exist".into()),
|
|
||||||
UserError::MissingToken => ErrorResponse::new(Status::Unauthorized, "Missing authorization token".into()),
|
|
||||||
UserError::ExpiredSession => ErrorResponse::new(Status::Unauthorized, "The provided session token has expired".into()),
|
|
||||||
UserError::MalformedHeader => ErrorResponse::new(Status::BadRequest, "Malformed authorization header".into()),
|
|
||||||
UserError::PermissionDenied => ErrorResponse::new(Status::Forbidden, "Bearer is not authorized to access the resource".into()),
|
|
||||||
UserError::ZoneNotFound => ErrorResponse::new(Status::NotFound, "DNS zone does not exist".into()),
|
|
||||||
UserError::DbError(e) => make_500(e),
|
|
||||||
UserError::PasswordError(e) => make_500(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Box<dyn ConnectorError>> for ErrorResponse {
|
|
||||||
fn from(e: Box<dyn ConnectorError>) -> Self {
|
|
||||||
if e.is_proto_error() {
|
|
||||||
error!("{}", e);
|
|
||||||
return make_500(e);
|
|
||||||
} else {
|
|
||||||
warn!("{}", e);
|
|
||||||
let error = ErrorResponse::new(
|
|
||||||
Status::NotFound,
|
|
||||||
"Zone could not be found".into()
|
|
||||||
);
|
|
||||||
if let Some(zone) = e.zone_name() {
|
|
||||||
return error.with_details(json!({
|
|
||||||
"zone_name": zone.to_utf8()
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<models::RecordListParseError> for ErrorResponse {
|
|
||||||
fn from(e: models::RecordListParseError) -> Self {
|
|
||||||
match e {
|
|
||||||
models::RecordListParseError::RecordNotInZone { zone, class, mismatched_class, mismatched_zone} => {
|
|
||||||
ErrorResponse::new(
|
|
||||||
Status::BadRequest,
|
|
||||||
"Record list contains records that do not belong to the zone".into()
|
|
||||||
).with_details(
|
|
||||||
json!({
|
|
||||||
"zone_name": zone.to_utf8(),
|
|
||||||
"class": models::DNSClass::from(class),
|
|
||||||
"mismatched_class": mismatched_class,
|
|
||||||
"mismatched_zone": mismatched_zone,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
models::RecordListParseError::ParseError { zone, bad_records } => {
|
|
||||||
ErrorResponse::new(
|
|
||||||
Status::BadRequest,
|
|
||||||
"Record list contains records that could not be parsed into DNS records".into()
|
|
||||||
).with_details(
|
|
||||||
json!({
|
|
||||||
"zone_name": zone.to_utf8(),
|
|
||||||
"records": bad_records
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl<S> From<ErrorResponse> for Outcome<S, ErrorResponse> {
|
|
||||||
fn from(e: ErrorResponse) -> Self {
|
|
||||||
Outcome::Failure(e.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl From<ErrorResponse> for (Status, ErrorResponse) {
|
|
||||||
fn from(e: ErrorResponse) -> Self {
|
|
||||||
(e.status, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: change for Display trait
|
|
||||||
pub fn make_500<E: std::fmt::Debug>(e: E) -> ErrorResponse {
|
|
||||||
error!("Making 500 for Error: {:?}", e);
|
|
||||||
|
|
||||||
ErrorResponse::new(Status::InternalServerError, "An unexpected error occured".into())
|
|
||||||
}
|
|
|
@ -20,3 +20,5 @@ pub use zone::{Zone, AddZoneMemberRequest, CreateZoneRequest};
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pub mod zone;
|
pub mod zone;
|
||||||
|
pub mod rdata;
|
||||||
|
pub mod record;
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
|
|
||||||
use rocket::request::FromParam;
|
|
||||||
use rocket::form::{self, FromFormField, ValueField};
|
|
||||||
use serde::{Deserialize, Serialize, Deserializer, Serializer};
|
|
||||||
use trust_dns_proto::error::ProtoError;
|
|
||||||
|
|
||||||
use crate::dns::Name;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SerdeName(pub(crate)Name);
|
|
||||||
|
|
||||||
impl Deref for SerdeName {
|
|
||||||
type Target = Name;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for SerdeName {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>
|
|
||||||
{
|
|
||||||
use serde::de::Error;
|
|
||||||
|
|
||||||
String::deserialize(deserializer)
|
|
||||||
.and_then(|string|
|
|
||||||
Name::from_utf8(&string)
|
|
||||||
.map_err(|e| Error::custom(e.to_string()))
|
|
||||||
).map( SerdeName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for SerdeName {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer
|
|
||||||
{
|
|
||||||
self.0.to_utf8().serialize(serializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SerdeName {
|
|
||||||
pub fn into_inner(self) -> Name {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_absolute_name(name: &str) -> Result<AbsoluteName, ProtoError> {
|
|
||||||
let mut name = Name::from_utf8(name)?;
|
|
||||||
if !name.is_fqdn() {
|
|
||||||
name.set_fqdn(true);
|
|
||||||
}
|
|
||||||
Ok(AbsoluteName(SerdeName(name)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct AbsoluteName(SerdeName);
|
|
||||||
|
|
||||||
impl<'r> FromParam<'r> for AbsoluteName {
|
|
||||||
type Error = ProtoError;
|
|
||||||
|
|
||||||
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
|
||||||
let name = parse_absolute_name(param)?;
|
|
||||||
Ok(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'v> FromFormField<'v> for AbsoluteName {
|
|
||||||
fn from_value(field: ValueField<'v>) -> form::Result<'v, Self> {
|
|
||||||
let name = parse_absolute_name(field.value)
|
|
||||||
.map_err(|_| form::Error::validation("Invalid name"))?;
|
|
||||||
|
|
||||||
Ok(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl Deref for AbsoluteName {
|
|
||||||
type Target = Name;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AbsoluteName {
|
|
||||||
pub fn into_inner(self) -> Name {
|
|
||||||
self.0.0
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,258 +1,544 @@
|
||||||
use std::convert::TryFrom;
|
use std::fmt::Write;
|
||||||
use std::net::{Ipv6Addr, Ipv4Addr};
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
|
|
||||||
use base64::{Engine, engine::general_purpose};
|
use domain::base::rdata::ComposeRecordData;
|
||||||
|
use domain::base::scan::Symbol;
|
||||||
|
use domain::base::wire::{Composer, ParseError};
|
||||||
|
use domain::base::{Name, ParseRecordData, ParsedName, RecordData, Rtype, ToName, Ttl};
|
||||||
|
use domain::rdata;
|
||||||
|
use domain::dep::octseq::{Parser, Octets};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use trust_dns_client::serialize::binary::BinEncoder;
|
use crate::errors::Error;
|
||||||
use trust_dns_proto::error::ProtoError;
|
use crate::validation;
|
||||||
|
|
||||||
use crate::dns;
|
use crate::macros::{append_errors, push_error};
|
||||||
use super::name::SerdeName;
|
use super::record::RecordParseError;
|
||||||
|
|
||||||
|
/// Type used to serialize / deserialize resource records data to response / request
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
///
|
||||||
#[serde(tag = "Type")]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(tag = "type", content = "rdata")]
|
||||||
#[serde(rename_all = "UPPERCASE")]
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
pub enum RData {
|
pub enum RData {
|
||||||
#[serde(rename_all = "PascalCase")]
|
A(A),
|
||||||
A {
|
Aaaa(Aaaa),
|
||||||
address: Ipv4Addr
|
// TODO: CAA
|
||||||
},
|
Cname(Cname),
|
||||||
#[serde(rename_all = "PascalCase")]
|
// TODO: DS
|
||||||
AAAA {
|
Mx(Mx),
|
||||||
address: Ipv6Addr
|
Ns(Ns),
|
||||||
},
|
Ptr(Ptr),
|
||||||
#[serde(rename_all = "PascalCase")]
|
Soa(Soa),
|
||||||
CAA {
|
Srv(Srv),
|
||||||
issuer_critical: bool,
|
// TODO: SSHFP
|
||||||
value: String,
|
// TODO: SVCB / HTTPS
|
||||||
property_tag: String,
|
|
||||||
},
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
CNAME {
|
|
||||||
target: SerdeName
|
|
||||||
},
|
|
||||||
// HINFO(HINFO),
|
|
||||||
// HTTPS(SVCB),
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
MX {
|
|
||||||
preference: u16,
|
|
||||||
mail_exchanger: SerdeName
|
|
||||||
},
|
|
||||||
// NAPTR(NAPTR),
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
NULL {
|
|
||||||
data: String
|
|
||||||
},
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
NS {
|
|
||||||
target: SerdeName
|
|
||||||
},
|
|
||||||
// OPENPGPKEY(OPENPGPKEY),
|
|
||||||
// OPT(OPT),
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
PTR {
|
|
||||||
target: SerdeName
|
|
||||||
},
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
SOA {
|
|
||||||
master_server_name: SerdeName,
|
|
||||||
maintainer_name: SerdeName,
|
|
||||||
refresh: i32,
|
|
||||||
retry: i32,
|
|
||||||
expire: i32,
|
|
||||||
minimum: u32,
|
|
||||||
serial: u32
|
|
||||||
},
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
SRV {
|
|
||||||
server: SerdeName,
|
|
||||||
port: u16,
|
|
||||||
priority: u16,
|
|
||||||
weight: u16,
|
|
||||||
},
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
SSHFP {
|
|
||||||
algorithm: u8,
|
|
||||||
digest_type: u8,
|
|
||||||
fingerprint: String,
|
|
||||||
},
|
|
||||||
// SVCB(SVCB),
|
|
||||||
// TLSA(TLSA),
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
TXT {
|
|
||||||
text: String
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: Eventually allow deserialization of DNSSEC records
|
|
||||||
#[serde(skip)]
|
|
||||||
DNSSEC(dns::DNSSECRData),
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
Unknown {
|
|
||||||
code: u16,
|
|
||||||
data: String,
|
|
||||||
},
|
|
||||||
// ZERO,
|
|
||||||
|
|
||||||
// TODO: DS (added in https://github.com/bluejekyll/trust-dns/pull/1635)
|
|
||||||
// TODO: TLSA
|
// TODO: TLSA
|
||||||
|
Txt(Txt),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<dns::RData> for RData {
|
impl RData {
|
||||||
fn from(rdata: dns::RData) -> RData {
|
pub fn rtype(&self) -> Rtype {
|
||||||
match rdata {
|
match self {
|
||||||
dns::RData::A(address) => RData::A { address },
|
RData::A(_) => Rtype::A,
|
||||||
dns::RData::AAAA(address) => RData::AAAA { address },
|
RData::Aaaa(_) => Rtype::AAAA,
|
||||||
// Still a draft, no iana number yet, I don't to put something that is not currently supported so that's why NULL and not unknown.
|
RData::Cname(_) => Rtype::CNAME,
|
||||||
// TODO: probably need better error here, I don't know what to do about that as this would require to change the From for something else.
|
RData::Mx(_) => Rtype::MX,
|
||||||
// (empty data because I'm lazy)
|
RData::Ns(_) => Rtype::NS,
|
||||||
dns::RData::ANAME(_) => RData::NULL {
|
RData::Ptr(_) => Rtype::PTR,
|
||||||
data: String::new()
|
RData::Soa(_) => Rtype::SOA,
|
||||||
},
|
RData::Srv(_) => Rtype::SRV,
|
||||||
dns::RData::CNAME(target) => RData::CNAME {
|
RData::Txt(_) => Rtype::TXT,
|
||||||
target: SerdeName(target)
|
|
||||||
},
|
|
||||||
dns::RData::CAA(caa) => {
|
|
||||||
let value_str = caa.value().to_string();
|
|
||||||
|
|
||||||
RData::CAA {
|
|
||||||
issuer_critical: caa.issuer_critical(),
|
|
||||||
// Remove first and last char (byte) because string is quoted (") (should be a safe operation)
|
|
||||||
value: value_str[1..(value_str.len())].into(),
|
|
||||||
property_tag: caa.tag().as_str().to_string(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dns::RData::MX(mx) => RData::MX {
|
|
||||||
preference: mx.preference(),
|
|
||||||
mail_exchanger: SerdeName(mx.exchange().clone())
|
|
||||||
},
|
|
||||||
dns::RData::NULL(null) => RData::NULL {
|
|
||||||
data: general_purpose::STANDARD.encode(null.anything())
|
|
||||||
},
|
|
||||||
dns::RData::NS(target) => RData::NS {
|
|
||||||
target: SerdeName(target)
|
|
||||||
},
|
|
||||||
dns::RData::PTR(target) => RData::PTR {
|
|
||||||
target: SerdeName(target)
|
|
||||||
},
|
|
||||||
dns::RData::SOA(soa) => RData::SOA {
|
|
||||||
master_server_name: SerdeName(soa.mname().clone()),
|
|
||||||
maintainer_name: SerdeName(soa.rname().clone()),
|
|
||||||
refresh: soa.refresh(),
|
|
||||||
retry: soa.retry(),
|
|
||||||
expire: soa.expire(),
|
|
||||||
minimum: soa.minimum(),
|
|
||||||
serial: soa.serial()
|
|
||||||
},
|
|
||||||
dns::RData::SRV(srv) => RData::SRV {
|
|
||||||
server: SerdeName(srv.target().clone()),
|
|
||||||
port: srv.port(),
|
|
||||||
priority: srv.priority(),
|
|
||||||
weight: srv.weight(),
|
|
||||||
},
|
|
||||||
dns::RData::SSHFP(sshfp) => RData::SSHFP {
|
|
||||||
algorithm: sshfp.algorithm().into(),
|
|
||||||
digest_type: sshfp.fingerprint_type().into(),
|
|
||||||
fingerprint: dns::sshfp::HEX.encode(sshfp.fingerprint()),
|
|
||||||
},
|
|
||||||
//TODO: This might alter data if not utf8 compatible, probably need to be replaced
|
|
||||||
//TODO: check whether concatenating txt data is harmful or not
|
|
||||||
dns::RData::TXT(txt) => RData::TXT { text: format!("{}", txt) },
|
|
||||||
dns::RData::DNSSEC(data) => RData::DNSSEC(data),
|
|
||||||
rdata => {
|
|
||||||
let code = rdata.to_record_type().into();
|
|
||||||
let mut data = Vec::new();
|
|
||||||
let mut encoder = BinEncoder::new(&mut data);
|
|
||||||
// TODO: need better error handling (use TryFrom ?)
|
|
||||||
rdata.emit(&mut encoder).expect("could not encode data");
|
|
||||||
|
|
||||||
RData::Unknown {
|
|
||||||
code,
|
|
||||||
data: general_purpose::STANDARD.encode(data),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<RData> for dns::RData {
|
pub enum ParsedRData<Name, Octs> {
|
||||||
type Error = ProtoError;
|
A(rdata::A),
|
||||||
|
Aaaa(rdata::Aaaa),
|
||||||
|
Cname(rdata::Cname<Name>),
|
||||||
|
Mx(rdata::Mx<Name>),
|
||||||
|
Ns(rdata::Ns<Name>),
|
||||||
|
Ptr(rdata::Ptr<Name>),
|
||||||
|
Soa(rdata::Soa<Name>),
|
||||||
|
Srv(rdata::Srv<Name>),
|
||||||
|
Txt(rdata::Txt<Octs>),
|
||||||
|
}
|
||||||
|
|
||||||
fn try_from(rdata: RData) -> Result<Self, Self::Error> {
|
impl<Name: ToString, Octs: AsRef<[u8]>> From<ParsedRData<Name, Octs>> for RData {
|
||||||
Ok(match rdata {
|
fn from(value: ParsedRData<Name, Octs>) -> Self {
|
||||||
RData::A { address } => dns::RData::A(address),
|
match value {
|
||||||
RData::AAAA { address } => dns::RData::AAAA(address),
|
ParsedRData::A(record_rdata) => RData::A(record_rdata.into()),
|
||||||
// TODO: Round trip test all types below (currently not tested...)
|
ParsedRData::Aaaa(record_rdata) => RData::Aaaa(record_rdata.into()),
|
||||||
RData::CAA { issuer_critical, value, property_tag } => {
|
ParsedRData::Cname(record_rdata) => RData::Cname(record_rdata.into()),
|
||||||
let property = dns::caa::Property::from(property_tag);
|
ParsedRData::Mx(record_rdata) => RData::Mx(record_rdata.into()),
|
||||||
let caa_value = {
|
ParsedRData::Ns(record_rdata) => RData::Ns(record_rdata.into()),
|
||||||
// TODO: duplicate of trust_dns_client::serialize::txt::rdata_parser::caa::parse
|
ParsedRData::Ptr(record_rdata) => RData::Ptr(record_rdata.into()),
|
||||||
// because caa::read_value is private
|
ParsedRData::Soa(record_rdata) => RData::Soa(record_rdata.into()),
|
||||||
match property {
|
ParsedRData::Srv(record_rdata) => RData::Srv(record_rdata.into()),
|
||||||
dns::caa::Property::Issue | dns::caa::Property::IssueWild => {
|
ParsedRData::Txt(record_rdata) => RData::Txt(record_rdata.into()),
|
||||||
let value = dns::caa::read_issuer(value.as_bytes())?;
|
}
|
||||||
dns::caa::Value::Issuer(value.0, value.1)
|
}
|
||||||
}
|
}
|
||||||
dns::caa::Property::Iodef => {
|
|
||||||
let url = dns::caa::read_iodef(value.as_bytes())?;
|
impl TryFrom<RData> for ParsedRData<Name<Vec<u8>>, Vec<u8>> {
|
||||||
dns::caa::Value::Url(url)
|
type Error = Vec<Error>;
|
||||||
}
|
|
||||||
dns::caa::Property::Unknown(_) => dns::caa::Value::Unknown(value.as_bytes().to_vec()),
|
fn try_from(value: RData) -> Result<Self, Self::Error> {
|
||||||
}
|
let rdata = match value {
|
||||||
};
|
RData::A(record_rdata) => ParsedRData::A(record_rdata.parse_record()?),
|
||||||
dns::RData::CAA(dns::caa::CAA {
|
RData::Aaaa(record_rdata) => ParsedRData::Aaaa(record_rdata.parse_record()?),
|
||||||
issuer_critical,
|
RData::Cname(record_rdata) => ParsedRData::Cname(record_rdata.parse_record()?),
|
||||||
tag: property,
|
RData::Mx(record_rdata) => ParsedRData::Mx(record_rdata.parse_record()?),
|
||||||
value: caa_value,
|
RData::Ns(record_rdata) => ParsedRData::Ns(record_rdata.parse_record()?),
|
||||||
})
|
RData::Ptr(record_rdata) => ParsedRData::Ptr(record_rdata.parse_record()?),
|
||||||
},
|
RData::Soa(record_rdata) => ParsedRData::Soa(record_rdata.parse_record()?),
|
||||||
RData::CNAME { target } => dns::RData::CNAME(target.into_inner()),
|
RData::Srv(record_rdata) => ParsedRData::Srv(record_rdata.parse_record()?),
|
||||||
RData::MX { preference, mail_exchanger } => dns::RData::MX(
|
RData::Txt(record_rdata) => ParsedRData::Txt(record_rdata.parse_record()?),
|
||||||
dns::mx::MX::new(preference, mail_exchanger.into_inner())
|
};
|
||||||
),
|
Ok(rdata)
|
||||||
RData::NULL { data } => dns::RData::NULL(
|
}
|
||||||
dns::null::NULL::with(
|
}
|
||||||
general_purpose::STANDARD.decode(data).map_err(|e| ProtoError::from(format!("{}", e)))?
|
|
||||||
)
|
|
||||||
),
|
|
||||||
RData::NS { target } => dns::RData::NS(target.into_inner()),
|
macro_rules! parse_name {
|
||||||
RData::PTR { target } => dns::RData::PTR(target.into_inner()),
|
($value:expr, $field:ident, $rtype:literal, $errors:expr) => {
|
||||||
RData::SOA {
|
{
|
||||||
master_server_name,
|
let name = push_error!(
|
||||||
maintainer_name,
|
validation::normalize_domain(&$value.$field),
|
||||||
refresh,
|
$errors, concat!("/", stringify!($field))
|
||||||
retry,
|
);
|
||||||
expire,
|
|
||||||
minimum,
|
let name = name.and_then(|name| {
|
||||||
serial
|
push_error!(
|
||||||
} => dns::RData::SOA(
|
name.parse::<Name<_>>().map_err(|e| {
|
||||||
dns::soa::SOA::new(
|
Error::from(RecordParseError::RDataUnknown {
|
||||||
master_server_name.into_inner(),
|
input: $value.$field,
|
||||||
maintainer_name.into_inner(),
|
field: stringify!(field).to_string(),
|
||||||
serial,
|
rtype: $rtype.to_string(),
|
||||||
refresh,
|
}).with_cause(&e.to_string())
|
||||||
retry,
|
}),
|
||||||
expire,
|
$errors, concat!("/", stringify!($field))
|
||||||
minimum,
|
)
|
||||||
)
|
});
|
||||||
),
|
|
||||||
RData::SRV { server, port, priority, weight } => dns::RData::SRV(
|
name
|
||||||
dns::srv::SRV::new(priority, weight, port, server.into_inner())
|
}
|
||||||
),
|
};
|
||||||
RData::SSHFP { algorithm, digest_type, fingerprint } => dns::RData::SSHFP(
|
}
|
||||||
dns::sshfp::SSHFP::new(
|
|
||||||
// NOTE: This allows unassigned algorithms
|
/* --------- A --------- */
|
||||||
dns::sshfp::Algorithm::from(algorithm),
|
|
||||||
dns::sshfp::FingerprintType::from(digest_type),
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
dns::sshfp::HEX.decode(fingerprint.as_bytes()).map_err(|e| ProtoError::from(format!("{}", e)))?
|
pub struct A {
|
||||||
)
|
pub address: String,
|
||||||
),
|
}
|
||||||
RData::TXT { text } => dns::RData::TXT(dns::txt::TXT::new(vec![text])),
|
|
||||||
// TODO: Error out for DNSSEC? Prefer downstream checks?
|
impl From<rdata::A> for A {
|
||||||
RData::DNSSEC(_) => todo!(),
|
fn from(record_data: rdata::A) -> Self {
|
||||||
// TODO: Disallow unknown? (could be used to bypass unsopported types?) Prefer downstream checks?
|
A { address: record_data.addr().to_string() }
|
||||||
RData::Unknown { code: _code, data: _data } => todo!(),
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
impl A {
|
||||||
|
pub fn parse_record(self) -> Result<rdata::A, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let address = push_error!(self.address.parse::<Ipv4Addr>().map_err(|e| {
|
||||||
|
Error::from(RecordParseError::Ip4Address { input: self.address })
|
||||||
|
.with_cause(&e.to_string())
|
||||||
|
.with_path("/address")
|
||||||
|
}), errors);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(rdata::A::new(address.unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- AAAA --------- */
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Aaaa {
|
||||||
|
pub address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rdata::Aaaa> for Aaaa {
|
||||||
|
fn from(record_data: rdata::Aaaa) -> Self {
|
||||||
|
Aaaa { address: record_data.addr().to_string() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Aaaa {
|
||||||
|
pub fn parse_record(self) -> Result<rdata::Aaaa, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let address = push_error!(self.address.parse::<Ipv6Addr>().map_err(|e| {
|
||||||
|
Error::from(RecordParseError::Ip6Address { input: self.address })
|
||||||
|
.with_cause(&e.to_string())
|
||||||
|
.with_path("/address")
|
||||||
|
}), errors);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(rdata::Aaaa::new(address.unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- CNAME --------- */
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Cname {
|
||||||
|
pub target: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: ToString> From<rdata::Cname<N>> for Cname {
|
||||||
|
fn from(record_data: rdata::Cname<N>) -> Self {
|
||||||
|
Cname { target: record_data.cname().to_string() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cname {
|
||||||
|
pub fn parse_record(self) -> Result<rdata::Cname<Name<Vec<u8>>>, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let cname = parse_name!(self, target, "CNAME", errors);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(rdata::Cname::new(cname.unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- MX --------- */
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Mx {
|
||||||
|
pub preference: u16,
|
||||||
|
pub mail_exchanger: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: ToString> From<rdata::Mx<N>> for Mx {
|
||||||
|
fn from(record_data: rdata::Mx<N>) -> Self {
|
||||||
|
Mx {
|
||||||
|
preference: record_data.preference(),
|
||||||
|
mail_exchanger: record_data.exchange().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mx {
|
||||||
|
fn parse_record(self) -> Result<rdata::Mx<Name<Vec<u8>>>, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let mail_exchanger = parse_name!(self, mail_exchanger, "MX", errors);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(rdata::Mx::new(self.preference, mail_exchanger.unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- NS --------- */
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Ns {
|
||||||
|
pub target: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: ToString> From<rdata::Ns<N>> for Ns {
|
||||||
|
fn from(record_rdata: rdata::Ns<N>) -> Self {
|
||||||
|
Ns {
|
||||||
|
target: record_rdata.nsdname().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ns {
|
||||||
|
fn parse_record(self) -> Result<rdata::Ns<Name<Vec<u8>>>, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let ns_name = parse_name!(self, target, "NS", errors);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(rdata::Ns::new(ns_name.unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- PTR --------- */
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Ptr {
|
||||||
|
pub target: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: ToString> From<rdata::Ptr<N>> for Ptr {
|
||||||
|
fn from(record_rdata: rdata::Ptr<N>) -> Self {
|
||||||
|
Ptr {
|
||||||
|
target: record_rdata.ptrdname().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ptr {
|
||||||
|
fn parse_record(self) -> Result<rdata::Ptr<Name<Vec<u8>>>, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let ptr_name = parse_name!(self, target, "PTR", errors);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(rdata::Ptr::new(ptr_name.unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- SOA --------- */
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Soa {
|
||||||
|
pub primary_server: String,
|
||||||
|
pub maintainer: String,
|
||||||
|
pub refresh: u32,
|
||||||
|
pub retry: u32,
|
||||||
|
pub expire: u32,
|
||||||
|
pub minimum: u32,
|
||||||
|
pub serial: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: ToString> From<rdata::Soa<N>> for Soa {
|
||||||
|
fn from(record_rdata: rdata::Soa<N>) -> Self {
|
||||||
|
Soa {
|
||||||
|
primary_server: record_rdata.mname().to_string(),
|
||||||
|
maintainer: record_rdata.rname().to_string(),
|
||||||
|
refresh: record_rdata.refresh().as_secs(),
|
||||||
|
retry: record_rdata.retry().as_secs(),
|
||||||
|
expire: record_rdata.expire().as_secs(),
|
||||||
|
minimum: record_rdata.minimum().as_secs(),
|
||||||
|
serial: record_rdata.serial().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Soa {
|
||||||
|
fn parse_record(self) -> Result<rdata::Soa<Name<Vec<u8>>>, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let primary_ns = parse_name!(self, primary_server, "SOA", errors);
|
||||||
|
let maintainer = parse_name!(self, maintainer, "SOA", errors);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(rdata::Soa::new(
|
||||||
|
primary_ns.unwrap(),
|
||||||
|
maintainer.unwrap(),
|
||||||
|
self.refresh.into(),
|
||||||
|
Ttl::from_secs(self.retry),
|
||||||
|
Ttl::from_secs(self.expire),
|
||||||
|
Ttl::from_secs(self.minimum),
|
||||||
|
Ttl::from_secs(self.serial),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- SRV --------- */
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Srv {
|
||||||
|
pub server: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub priority: u16,
|
||||||
|
pub weight: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: ToString> From<rdata::Srv<N>> for Srv {
|
||||||
|
fn from(record_data: rdata::Srv<N>) -> Self {
|
||||||
|
Srv {
|
||||||
|
server: record_data.target().to_string(),
|
||||||
|
priority: record_data.priority(),
|
||||||
|
weight: record_data.weight(),
|
||||||
|
port: record_data.port(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Srv {
|
||||||
|
fn parse_record(self) -> Result<rdata::Srv<Name<Vec<u8>>>, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let server = parse_name!(self, server, "SRV", errors);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(rdata::Srv::new(
|
||||||
|
self.priority,
|
||||||
|
self.weight,
|
||||||
|
self.port,
|
||||||
|
server.unwrap(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --------- TXT --------- */
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Txt {
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O: AsRef<[u8]>> From<rdata::Txt<O>> for Txt {
|
||||||
|
fn from(record_data: rdata::Txt<O>) -> Self {
|
||||||
|
let mut concatenated_text = String::new();
|
||||||
|
for text in record_data.iter() {
|
||||||
|
for c in text {
|
||||||
|
// Escapes '\' and non printable chars
|
||||||
|
let c = Symbol::display_from_octet(*c);
|
||||||
|
write!(concatenated_text, "{}", c).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Txt {
|
||||||
|
text: concatenated_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Txt {
|
||||||
|
fn parse_record(self) -> Result<rdata::Txt<Vec<u8>>, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
let data = append_errors!(validation::parse_txt_data(&self.text), errors, "/text");
|
||||||
|
let data = data.and_then(|data| {
|
||||||
|
push_error!(rdata::Txt::build_from_slice(&data).map_err(|e| {
|
||||||
|
Error::from(RecordParseError::RDataUnknown {
|
||||||
|
input: self.text,
|
||||||
|
field: "text".into(),
|
||||||
|
rtype: "TXT".into(),
|
||||||
|
}).with_cause(&e.to_string())
|
||||||
|
.with_path("/text")
|
||||||
|
}), errors)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(data.unwrap())
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- ParsedRData: domain traits impl --------- */
|
||||||
|
|
||||||
|
impl<Name, Octs> ParsedRData<Name, Octs> {
|
||||||
|
pub fn rtype(&self) -> Rtype {
|
||||||
|
match self {
|
||||||
|
ParsedRData::A(_) => Rtype::A,
|
||||||
|
ParsedRData::Aaaa(_) => Rtype::AAAA,
|
||||||
|
ParsedRData::Cname(_) => Rtype::CNAME,
|
||||||
|
ParsedRData::Mx(_) => Rtype::MX,
|
||||||
|
ParsedRData::Ns(_) => Rtype::NS,
|
||||||
|
ParsedRData::Ptr(_) => Rtype::PTR,
|
||||||
|
ParsedRData::Soa(_) => Rtype::SOA,
|
||||||
|
ParsedRData::Srv(_) => Rtype::SRV,
|
||||||
|
ParsedRData::Txt(_) => Rtype::TXT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Name, Octs> RecordData for ParsedRData<Name, Octs> {
|
||||||
|
fn rtype(&self) -> Rtype {
|
||||||
|
ParsedRData::rtype(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Octs: Octets + ?Sized> ParseRecordData<'a, Octs> for ParsedRData<ParsedName<Octs::Range<'a>>, Octs::Range<'a>> {
|
||||||
|
fn parse_rdata(
|
||||||
|
rtype: Rtype,
|
||||||
|
parser: &mut Parser<'a, Octs>,
|
||||||
|
) -> Result<Option<Self>, ParseError> {
|
||||||
|
let record = match rtype {
|
||||||
|
Rtype::A => ParsedRData::A(rdata::A::parse(parser)?),
|
||||||
|
Rtype::AAAA => ParsedRData::Aaaa(rdata::Aaaa::parse(parser)?),
|
||||||
|
Rtype::CNAME => ParsedRData::Cname(rdata::Cname::parse(parser)?),
|
||||||
|
Rtype::MX => ParsedRData::Mx(rdata::Mx::parse(parser)?),
|
||||||
|
Rtype::NS => ParsedRData::Ns(rdata::Ns::parse(parser)?),
|
||||||
|
Rtype::PTR => ParsedRData::Ptr(rdata::Ptr::parse(parser)?),
|
||||||
|
Rtype::SOA => ParsedRData::Soa(rdata::Soa::parse(parser)?),
|
||||||
|
Rtype::SRV => ParsedRData::Srv(rdata::Srv::parse(parser)?),
|
||||||
|
Rtype::TXT => ParsedRData::Txt(rdata::Txt::parse(parser)?),
|
||||||
|
_ => return Ok(None)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(record))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Name: ToName, Octs: AsRef<[u8]>> ComposeRecordData for ParsedRData<Name, Octs> {
|
||||||
|
fn rdlen(&self, compress: bool) -> Option<u16> {
|
||||||
|
match self {
|
||||||
|
ParsedRData::A(record_rdata) => record_rdata.rdlen(compress),
|
||||||
|
ParsedRData::Aaaa(record_rdata) => record_rdata.rdlen(compress),
|
||||||
|
ParsedRData::Cname(record_rdata) => record_rdata.rdlen(compress),
|
||||||
|
ParsedRData::Mx(record_rdata) => record_rdata.rdlen(compress),
|
||||||
|
ParsedRData::Ns(record_rdata) => record_rdata.rdlen(compress),
|
||||||
|
ParsedRData::Ptr(record_rdata) => record_rdata.rdlen(compress),
|
||||||
|
ParsedRData::Soa(record_rdata) => record_rdata.rdlen(compress),
|
||||||
|
ParsedRData::Srv(record_rdata) => record_rdata.rdlen(compress),
|
||||||
|
ParsedRData::Txt(record_rdata) => record_rdata.rdlen(compress),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compose_rdata<Target: Composer + ?Sized>(
|
||||||
|
&self,
|
||||||
|
target: &mut Target,
|
||||||
|
) -> Result<(), Target::AppendError> {
|
||||||
|
match self {
|
||||||
|
ParsedRData::A(record_rdata) => record_rdata.compose_rdata(target),
|
||||||
|
ParsedRData::Aaaa(record_rdata) => record_rdata.compose_rdata(target),
|
||||||
|
ParsedRData::Cname(record_rdata) => record_rdata.compose_rdata(target),
|
||||||
|
ParsedRData::Mx(record_rdata) => record_rdata.compose_rdata(target),
|
||||||
|
ParsedRData::Ns(record_rdata) => record_rdata.compose_rdata(target),
|
||||||
|
ParsedRData::Ptr(record_rdata) => record_rdata.compose_rdata(target),
|
||||||
|
ParsedRData::Soa(record_rdata) => record_rdata.compose_rdata(target),
|
||||||
|
ParsedRData::Srv(record_rdata) => record_rdata.compose_rdata(target),
|
||||||
|
ParsedRData::Txt(record_rdata) => record_rdata.compose_rdata(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compose_canonical_rdata<Target: Composer + ?Sized>(
|
||||||
|
&self,
|
||||||
|
target: &mut Target,
|
||||||
|
) -> Result<(), Target::AppendError> {
|
||||||
|
match self {
|
||||||
|
ParsedRData::A(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
||||||
|
ParsedRData::Aaaa(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
||||||
|
ParsedRData::Cname(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
||||||
|
ParsedRData::Mx(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
||||||
|
ParsedRData::Ns(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
||||||
|
ParsedRData::Ptr(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
||||||
|
ParsedRData::Soa(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
||||||
|
ParsedRData::Srv(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
||||||
|
ParsedRData::Txt(record_rdata) => record_rdata.compose_canonical_rdata(target),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,122 +1,128 @@
|
||||||
use std::convert::{TryFrom, TryInto};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use trust_dns_proto::error::ProtoError;
|
|
||||||
|
|
||||||
use crate::dns;
|
use domain::base::{iana::Class, Name, Record as DnsRecord, Ttl};
|
||||||
use super::name::SerdeName;
|
|
||||||
use super::class::DNSClass;
|
|
||||||
use super::rdata::RData;
|
|
||||||
|
|
||||||
|
use crate::{errors::Error, validation};
|
||||||
|
use crate::macros::{append_errors, push_error};
|
||||||
|
use super::rdata::{ParsedRData, RData};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
pub enum RecordParseError {
|
||||||
pub struct Record {
|
Ip4Address { input: String },
|
||||||
#[serde(rename = "Name")]
|
Ip6Address { input: String },
|
||||||
pub name: SerdeName,
|
RDataUnknown { input: String, field: String, rtype: String },
|
||||||
// TODO: Make class optional, default to IN
|
NameUnknown { input: String },
|
||||||
#[serde(rename = "Class")]
|
NotInZone { name: String, zone: String },
|
||||||
pub dns_class: DNSClass,
|
|
||||||
#[serde(rename = "TTL")]
|
|
||||||
pub ttl: u32,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub rdata: RData,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<dns::Record> for Record {
|
pub enum RecordError {
|
||||||
fn from(record: dns::Record) -> Record {
|
Validation { suberrors: Vec<Error> },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type DnsRecordImpl = DnsRecord<
|
||||||
|
Name<Vec<u8>>,
|
||||||
|
ParsedRData<Name<Vec<u8>>,Vec<u8>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Record {
|
||||||
|
pub name: String,
|
||||||
|
pub ttl: u32,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub rdata: RData
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Name: ToString, Oct: AsRef<[u8]>> From<DnsRecord<Name, ParsedRData<Name, Oct>>> for Record {
|
||||||
|
fn from(value: DnsRecord<Name, ParsedRData<Name, Oct>>) -> Self {
|
||||||
Record {
|
Record {
|
||||||
name: SerdeName(record.name().clone()),
|
name: value.owner().to_string(),
|
||||||
dns_class: record.dns_class().into(),
|
ttl: value.ttl().as_secs(),
|
||||||
ttl: record.ttl(),
|
rdata: value.into_data().into(),
|
||||||
// Assume data exists, record with empty data should be filtered by caller
|
|
||||||
rdata: record.into_data().unwrap().into(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<Record> for dns::Record {
|
impl Record {
|
||||||
type Error = ProtoError;
|
fn convert(self, zone_name: &Name<Vec<u8>>) -> Result<DnsRecordImpl, Vec<Error>> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
fn try_from(record: Record) -> Result<Self, Self::Error> {
|
let name = push_error!(validation::normalize_domain(&self.name), errors, "/name");
|
||||||
let mut trust_dns_record = dns::Record::from_rdata(record.name.into_inner(), record.ttl, record.rdata.try_into()?);
|
|
||||||
trust_dns_record.set_dns_class(record.dns_class.into());
|
let name = name.and_then(|name| push_error!(name.parse::<Name<_>>().map_err(|e| {
|
||||||
Ok(trust_dns_record)
|
Error::from(RecordParseError::NameUnknown {
|
||||||
|
input: self.name.clone()
|
||||||
|
}).with_cause(&e.to_string())
|
||||||
|
}), errors, "/name"));
|
||||||
|
|
||||||
|
let name = name.and_then(|name| {
|
||||||
|
if !name.ends_with(zone_name) {
|
||||||
|
errors.push(
|
||||||
|
Error::from(RecordParseError::NotInZone { name: self.name, zone: zone_name.to_string() })
|
||||||
|
.with_path("/name")
|
||||||
|
);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(name)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ttl = Ttl::from_secs(self.ttl);
|
||||||
|
let rdata = append_errors!(ParsedRData::try_from(self.rdata), errors, "/rdata");
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(DnsRecord::new(name.unwrap(), Class::IN, ttl, rdata.unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub type RecordList = Vec<Record>;
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct RecordList(pub Vec<Record>);
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
impl RecordList {
|
||||||
#[serde(rename_all = "camelCase")]
|
fn convert(self, zone_name: &Name<Vec<u8>>) -> Result<Vec<DnsRecordImpl>, Vec<Error>> {
|
||||||
pub struct UpdateRecordsRequest {
|
let mut errors = Vec::new();
|
||||||
pub old_records: RecordList,
|
let mut records = Vec::new();
|
||||||
pub new_records: RecordList,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum RecordListParseError {
|
for (index, record) in self.0.into_iter().enumerate() {
|
||||||
ParseError {
|
let record = append_errors!(record.convert(zone_name), errors, &format!("/{index}"));
|
||||||
bad_records: Vec<Record>,
|
|
||||||
zone: dns::Name,
|
|
||||||
},
|
|
||||||
RecordNotInZone {
|
|
||||||
zone: dns::Name,
|
|
||||||
class: dns::DNSClass,
|
|
||||||
mismatched_class: Vec<Record>,
|
|
||||||
mismatched_zone: Vec<Record>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ParseRecordList {
|
if let Some(record) = record {
|
||||||
fn try_into_dns_type(self, zone: dns::Name, class: dns::DNSClass) -> Result<Vec<dns::Record>, RecordListParseError>;
|
records.push(record)
|
||||||
}
|
|
||||||
|
|
||||||
impl ParseRecordList for RecordList {
|
|
||||||
fn try_into_dns_type(self, zone: dns::Name, class: dns::DNSClass) -> Result<Vec<dns::Record>, RecordListParseError> {
|
|
||||||
// TODO: What about relative names (also in cnames and stuff)
|
|
||||||
let mut bad_records = Vec::new();
|
|
||||||
let mut records: Vec<dns::Record> = Vec::new();
|
|
||||||
let mut mismatched_class: Vec<Record> = Vec::new();
|
|
||||||
let mut mismatched_zone: Vec<Record> = Vec::new();
|
|
||||||
|
|
||||||
for record in self.into_iter() {
|
|
||||||
let this_record = record.clone();
|
|
||||||
if let Ok(record) = dns::Record::try_from(record) {
|
|
||||||
let mut good_record = true;
|
|
||||||
|
|
||||||
if !zone.zone_of(record.name()) {
|
|
||||||
mismatched_zone.push(this_record.clone());
|
|
||||||
good_record = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if record.dns_class() != class {
|
|
||||||
mismatched_class.push(this_record.clone());
|
|
||||||
good_record = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if good_record {
|
|
||||||
records.push(record);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bad_records.push(this_record.clone());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bad_records.is_empty() {
|
if errors.is_empty() {
|
||||||
return Err(RecordListParseError::ParseError {
|
Ok(records)
|
||||||
zone,
|
} else {
|
||||||
bad_records,
|
Err(errors)
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Deserialize)]
|
||||||
|
pub struct AddRecordsRequest {
|
||||||
|
pub new_records: RecordList
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AddRecords {
|
||||||
|
pub new_records: Vec<DnsRecordImpl>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddRecordsRequest {
|
||||||
|
pub fn validate(self, zone_name: &str) -> Result<AddRecords, Error> {
|
||||||
|
let zone_name: Name<Vec<u8>> = zone_name.parse().expect("zone name is assumed to be valid");
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
let records = append_errors!(self.new_records.convert(&zone_name), errors, "/new_records");
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(AddRecords {
|
||||||
|
new_records: records.unwrap(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(Error::from(RecordError::Validation { suberrors: errors }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !mismatched_class.is_empty() || !mismatched_zone.is_empty() {
|
|
||||||
return Err(RecordListParseError::RecordNotInZone {
|
|
||||||
zone,
|
|
||||||
class,
|
|
||||||
mismatched_zone,
|
|
||||||
mismatched_class
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(records)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,10 @@ use rusqlite::Error as RusqliteError;
|
||||||
|
|
||||||
|
|
||||||
use crate::database::{BoxedDb, sqlite::SqliteDB};
|
use crate::database::{BoxedDb, sqlite::SqliteDB};
|
||||||
use crate::dns::{BoxedZoneDriver, DnsDriverError};
|
use crate::dns::{BoxedZoneDriver, BoxedRecordDriver, DnsDriverError};
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::macros::push_error;
|
use crate::macros::push_error;
|
||||||
|
use crate::ressouces::record::RecordList;
|
||||||
use crate::validation;
|
use crate::validation;
|
||||||
|
|
||||||
pub enum ZoneError {
|
pub enum ZoneError {
|
||||||
|
@ -39,6 +40,19 @@ impl Zone {
|
||||||
|
|
||||||
db.create_zone(create_zone).await
|
db.create_zone(create_zone).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_records(zone_name: &str, db: BoxedDb, record_driver: BoxedRecordDriver) ->Result<RecordList, Error> {
|
||||||
|
let zone = db.get_zone_by_name(zone_name).await?;
|
||||||
|
let mut records = record_driver.get_records(&zone.name).await?;
|
||||||
|
|
||||||
|
records.sort_by(|r1, r2| {
|
||||||
|
let key1 = (&r1.name, r1.rdata.rtype());
|
||||||
|
let key2 = (&r2.name, r2.rdata.rtype());
|
||||||
|
key1.cmp(&key2)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(RecordList(records))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
|
|
||||||
use crate::dns::record::{AddRecordsRequest, Record};
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::ressouces::zone::{CreateZoneRequest, Zone};
|
use crate::ressouces::zone::{CreateZoneRequest, Zone};
|
||||||
|
use crate::ressouces::record::{AddRecordsRequest, Record, RecordList};
|
||||||
|
|
||||||
|
|
||||||
pub async fn create_zone(
|
pub async fn create_zone(
|
||||||
|
@ -18,13 +18,9 @@ pub async fn create_zone(
|
||||||
pub async fn get_zone_records(
|
pub async fn get_zone_records(
|
||||||
Path(zone_name): Path<String>,
|
Path(zone_name): Path<String>,
|
||||||
State(app): State<AppState>,
|
State(app): State<AppState>,
|
||||||
) -> Result<Json<Vec<Record>>, Error>
|
) -> Result<Json<RecordList>, Error>
|
||||||
{
|
{
|
||||||
|
Zone::get_records(&zone_name, app.db, app.records).await.map(Json)
|
||||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
|
||||||
let records = app.records.get_records(&zone.name).await?;
|
|
||||||
|
|
||||||
Ok(Json(records))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_zone_records(
|
pub async fn create_zone_records(
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
//pub mod ui;
|
pub mod ui;
|
||||||
pub mod api;
|
pub mod api;
|
||||||
|
|
|
@ -1,5 +1,2 @@
|
||||||
pub mod auth;
|
//pub mod auth;
|
||||||
pub mod zones;
|
pub mod zones;
|
||||||
|
|
||||||
pub use auth::*;
|
|
||||||
pub use zones::*;
|
|
||||||
|
|
|
@ -1,128 +1,26 @@
|
||||||
|
use axum::extract::{Path, State};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use serde::Serialize;
|
|
||||||
use rocket::http::{Status};
|
|
||||||
use rocket::http::uri::Origin;
|
|
||||||
use rocket::form::Form;
|
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::errors::Error;
|
||||||
|
use crate::ressouces::zone::Zone;
|
||||||
use crate::template::Template;
|
use crate::template::Template;
|
||||||
use crate::models;
|
|
||||||
use crate::controllers;
|
|
||||||
use crate::DbConn;
|
|
||||||
use crate::dns::ZoneConnector;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
pub async fn get_zone_records_page(
|
||||||
pub struct RecordsPage {
|
Path(zone_name): Path<String>,
|
||||||
zone: String
|
State(app): State<AppState>,
|
||||||
}
|
) -> Result<Template<'static, Value>, Error> {
|
||||||
|
let records = Zone::get_records(&zone_name, app.db, app.records).await?;
|
||||||
|
|
||||||
// TODO: Check if origin changes if application mounted on different path
|
|
||||||
#[get("/zone/<zone>/records")]
|
|
||||||
pub async fn get_zone_records_page(user_info: models::UserInfo, zone: models::AbsoluteName, conn: DbConn, origin: &Origin<'_>) -> Result<Template<'static, Value>, Status> {
|
|
||||||
let zone_name = zone.to_utf8();
|
|
||||||
|
|
||||||
let zones = conn.run(move |c| {
|
|
||||||
if user_info.is_admin() {
|
|
||||||
models::Zone::get_by_name(c, &zone_name)?;
|
|
||||||
models::Zone::get_all(c)
|
|
||||||
} else {
|
|
||||||
user_info.get_zone(c, &zone_name)?;
|
|
||||||
user_info.get_zones(c)
|
|
||||||
|
|
||||||
}
|
|
||||||
}).await.map_err(|e| models::ErrorResponse::from(e).status)?;
|
|
||||||
|
|
||||||
|
//records.0.sort_by_key(|record| (&record.name, record.rdata));
|
||||||
|
|
||||||
Ok(Template::new(
|
Ok(Template::new(
|
||||||
"pages/zone/records.html",
|
"pages/records.html",
|
||||||
|
app.template_engine,
|
||||||
json!({
|
json!({
|
||||||
"current_zone": zone.to_utf8(),
|
"current_zone": zone_name,
|
||||||
"zones": zones,
|
"records": records,
|
||||||
"nav_page": origin.clone().into_normalized().path().as_str(),
|
|
||||||
"nav_sections": vec!["zones", zone.to_utf8().as_str()],
|
|
||||||
})
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/zones")]
|
|
||||||
pub async fn get_zones_page(user_info: models::UserInfo, conn: DbConn, origin: &Origin<'_>) -> Result<Template<'static, Value>, Status> {
|
|
||||||
let zones = controllers::get_zones(
|
|
||||||
&conn,
|
|
||||||
user_info
|
|
||||||
).await.map_err(|e| e.status)?;
|
|
||||||
|
|
||||||
Ok(Template::new(
|
|
||||||
"pages/zones.html",
|
|
||||||
json!({
|
|
||||||
"zones": zones,
|
|
||||||
"nav_page": origin.clone().into_normalized().path().as_str(),
|
|
||||||
"nav_sections": vec!["zones"],
|
|
||||||
})
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[get("/zones/new")]
|
|
||||||
pub async fn get_create_zone_page(
|
|
||||||
conn: DbConn,
|
|
||||||
user_info: models::UserInfo,
|
|
||||||
origin: &Origin<'_>
|
|
||||||
) -> Result<Template<'static, Value>, Status> {
|
|
||||||
|
|
||||||
user_info
|
|
||||||
.check_admin()
|
|
||||||
.map_err(|e| models::ErrorResponse::from(e).status)?;
|
|
||||||
|
|
||||||
let zones = controllers::get_zones(
|
|
||||||
&conn,
|
|
||||||
user_info
|
|
||||||
).await.map_err(|e| e.status)?;
|
|
||||||
|
|
||||||
Ok(Template::new(
|
|
||||||
"pages/zones/new.html",
|
|
||||||
json!({
|
|
||||||
"zone": None::<models::Zone>,
|
|
||||||
"zones": zones,
|
|
||||||
"error": None::<String>,
|
|
||||||
"nav_page": origin.clone().into_normalized().path().as_str(),
|
|
||||||
"nav_sections": vec!["zones", "_new-zone"],
|
|
||||||
})
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/zones/new", data = "<zone_request>")]
|
|
||||||
pub async fn post_create_zone_page(
|
|
||||||
conn: DbConn,
|
|
||||||
dns_api: Box<dyn ZoneConnector>,
|
|
||||||
user_info: models::UserInfo,
|
|
||||||
zone_request: Form<models::CreateZoneRequest>,
|
|
||||||
origin: &Origin<'_>
|
|
||||||
) -> Result<Template<'static, Value>, Status> {
|
|
||||||
user_info
|
|
||||||
.check_admin()
|
|
||||||
.map_err(|e| models::ErrorResponse::from(e).status)?;
|
|
||||||
|
|
||||||
let zone = controllers::create_zone(
|
|
||||||
&conn,
|
|
||||||
dns_api,
|
|
||||||
user_info.clone(),
|
|
||||||
zone_request.into_inner()
|
|
||||||
).await.map_err(|e| e.status)?;
|
|
||||||
|
|
||||||
let zones = controllers::get_zones(
|
|
||||||
&conn,
|
|
||||||
user_info
|
|
||||||
).await.map_err(|e| e.status)?;
|
|
||||||
|
|
||||||
Ok(Template::new(
|
|
||||||
"pages/zones/new.html",
|
|
||||||
json!({
|
|
||||||
"zone": Some(zone),
|
|
||||||
"zones": zones,
|
|
||||||
"error": None::<String>,
|
|
||||||
"nav_page": origin.clone().into_normalized().path().as_str(),
|
|
||||||
"nav_sections": vec!["zones", "_new-zone"],
|
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +1,73 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::response::{Html, IntoResponse};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use rocket::request::Request;
|
|
||||||
use rocket::response::{self, Responder};
|
|
||||||
use rocket::http::{Status, ContentType};
|
|
||||||
|
|
||||||
use tera::{Tera, Context};
|
use tera::{Tera, Context};
|
||||||
|
|
||||||
|
use crate::errors::Error;
|
||||||
|
|
||||||
pub struct TemplateState {
|
|
||||||
tera: Tera,
|
#[derive(Clone)]
|
||||||
|
pub struct TemplateEngine {
|
||||||
|
tera: Arc<Tera>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemplateState {
|
pub enum TemplateError {
|
||||||
|
SerializationError { reason: Box<dyn std::error::Error> },
|
||||||
|
RenderError { name: String, reason: Box<dyn std::error::Error> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateEngine {
|
||||||
pub fn new(template_directory: &Path) -> Self {
|
pub fn new(template_directory: &Path) -> Self {
|
||||||
let template_glob = template_directory.join("**").join("*");
|
let template_glob = template_directory.join("**").join("*");
|
||||||
match Tera::new(template_glob.to_str().expect("valid glob path string")) {
|
match Tera::new(template_glob.to_str().expect("valid glob path string")) {
|
||||||
Ok(tera) => TemplateState { tera },
|
Ok(tera) => TemplateEngine { tera: Arc::new(tera) },
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Loading templates failed: {}", e);
|
println!("Loading templates failed: {}", e);
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render<S: Serialize>(&self, name: &str, context: S) -> Result<String, TemplateError> {
|
||||||
|
let context = Context::from_serialize(context).map_err(|e| {
|
||||||
|
TemplateError::SerializationError { reason: Box::new(e) }
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let content = self.tera.render(name, &context).map_err(|e| {
|
||||||
|
TemplateError::RenderError { name: name.into(), reason: Box::new(e) }
|
||||||
|
})?;
|
||||||
|
|
||||||
|
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Template<'t, S: Serialize> {
|
pub struct Template<'n, S: Serialize> {
|
||||||
pub name: &'t str,
|
pub name: &'n str,
|
||||||
|
pub engine: TemplateEngine,
|
||||||
pub context: S,
|
pub context: S,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'r, S: Serialize> Template<'r, S> {
|
impl<'n, S: Serialize> Template<'n, S> {
|
||||||
pub fn new(name: &'r str, context: S) -> Self {
|
pub fn new(name: &'n str, engine: TemplateEngine, context: S) -> Self {
|
||||||
Template {
|
Template {
|
||||||
name,
|
name,
|
||||||
context
|
engine,
|
||||||
|
context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(self, tera: &Tera) -> Result<(ContentType, String), Status> {
|
|
||||||
let context = Context::from_serialize(self.context).map_err(|e| {
|
|
||||||
error!("Failed to serialize context: {}", e);
|
|
||||||
Status::InternalServerError
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let content = tera.render(self.name, &context).map_err(|e| {
|
|
||||||
error!("Failed to render template `{}`: {}", self.name, e);
|
|
||||||
Status::InternalServerError
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok((ContentType::HTML, content))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'r, 't, S: Serialize> Responder<'r, 'static> for Template<'t, S> {
|
impl<S: Serialize> IntoResponse for Template<'_, S> {
|
||||||
fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> {
|
fn into_response(self) -> axum::response::Response {
|
||||||
let template_state = request.rocket().state::<TemplateState>().ok_or(Status::InternalServerError)?;
|
let res = self.engine.render(self.name, self.context);
|
||||||
|
|
||||||
self.render(&template_state.tera).respond_to(request)
|
match res {
|
||||||
|
Ok(content) => Html(content).into_response(),
|
||||||
|
Err(err) => Error::from(err).into_response(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,11 @@
|
||||||
{% extends "bases/base.html" %}
|
{% extends "bases/base.html" %}
|
||||||
{% import "macros.html" as macros %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<nav aria-label="Principal" class="main">
|
<nav aria-label="Principal" class="main">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/profile">Mon profil</a></li>
|
<li><a href="#">Account</a></li>
|
||||||
<li>
|
<li><a href="#">Zones</a></li>
|
||||||
{{ macros::nav_link(
|
<li><a href="#">Admin</a></li>
|
||||||
content="Mes zones",
|
|
||||||
href="/zones",
|
|
||||||
current_page=nav_page,
|
|
||||||
section="zones",
|
|
||||||
current_sections=nav_sections,
|
|
||||||
) }}
|
|
||||||
<ul>
|
|
||||||
{% for zone in zones %}
|
|
||||||
<li>
|
|
||||||
{{ macros::nav_link(
|
|
||||||
content=zone.name,
|
|
||||||
href="/zone/" ~ zone.name,
|
|
||||||
current_page=nav_page,
|
|
||||||
section=zone.name,
|
|
||||||
current_sections=nav_sections,
|
|
||||||
) }}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
<li>
|
|
||||||
{{ macros::nav_link(
|
|
||||||
safe_content='<img alt="" src="/images/plus.svg"> Ajouter une zone',
|
|
||||||
href="/zones/new",
|
|
||||||
current_page=nav_page,
|
|
||||||
section="_new-zone",
|
|
||||||
current_sections=nav_sections,
|
|
||||||
) }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<main>
|
<main>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}{% endblock title %}Nomilo</title>
|
<title>{% block title %}{% endblock title %}Nomilo</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/styles/main.css">
|
<link rel="stylesheet" type="text/css" href="/assets/styles/main.css">
|
||||||
{% block styles %}{% endblock styles %}
|
{% block styles %}{% endblock styles %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
{% macro nav_link(content, href, current_page, content='', safe_content='', section=False, current_sections=False, props='') %}
|
|
||||||
<a
|
|
||||||
href="{{ href }}"
|
|
||||||
{{ props }}
|
|
||||||
{% if current_page == href %}
|
|
||||||
aria-current="page"
|
|
||||||
{% elif section and section in current_sections %}
|
|
||||||
aria-current="location"
|
|
||||||
{% endif %}
|
|
||||||
>{{ content }}{{ safe_content | safe }}</a>
|
|
||||||
{% endmacro nav_link %}
|
|
|
@ -1,25 +0,0 @@
|
||||||
{% extends "bases/base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Se connecter ⋅ {% endblock title %}
|
|
||||||
{% block styles %}
|
|
||||||
<link rel="stylesheet" type="text/css" href="/styles/login.css">
|
|
||||||
{% endblock styles %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<main>
|
|
||||||
<form method="POST" action="/login">
|
|
||||||
<h1>Se connecter</h1>
|
|
||||||
{% if error %}
|
|
||||||
<p class="feedback error" role="alert">
|
|
||||||
{{ error }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<label for="email">Adresse e-mail</label>
|
|
||||||
<input type="email" id="email" name="email">
|
|
||||||
<label for="password">Mot de passe</label>
|
|
||||||
<input type="password" id="password" name="password">
|
|
||||||
<input type="submit" value="Se connecter">
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
{% endblock content %}
|
|
103
templates/pages/records.html
Normal file
103
templates/pages/records.html
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
{% extends "bases/app.html" %}
|
||||||
|
{% block title %}Records - {{ current_zone }} - {% endblock title %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<h1>Zone {{ current_zone }} records</h1>
|
||||||
|
<svg width="0" height="0" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="corner-folder-tab-right" clipPathUnits="objectBoundingBox">
|
||||||
|
<path d="m 0,0 c .25,0 0.75,1 1,1 l -1,0 z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<section>
|
||||||
|
<h2>Records</h2>
|
||||||
|
|
||||||
|
{% set current_domain = '' %}
|
||||||
|
{% set current_rtype = '' %}
|
||||||
|
|
||||||
|
{% for record in records %}
|
||||||
|
{% if record.name != current_domain %}
|
||||||
|
{% if current_domain %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
{% set_global current_domain = record.name %}
|
||||||
|
{% set_global current_rtype = '' %}
|
||||||
|
<article class="domain">
|
||||||
|
<header>
|
||||||
|
<h3 class="folder-tab">{{ record.name }}</h3>
|
||||||
|
<span class="sep"></span>
|
||||||
|
<a href="#" class="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||||
|
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
|
||||||
|
</svg>
|
||||||
|
Add record
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<div class="records">
|
||||||
|
<ul>
|
||||||
|
{% endif %}
|
||||||
|
{% if record.type != current_rtype %}
|
||||||
|
{% if current_rtype %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% set_global current_rtype = record.type %}
|
||||||
|
<li class="rrset">
|
||||||
|
<span class="rtype">{{ record.type }}</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
{% endif %}
|
||||||
|
<div class="rdata">
|
||||||
|
{% if record.type == "A" or record.type == "AAAA" %}
|
||||||
|
<div class="rdata-main">
|
||||||
|
<span class="pill">
|
||||||
|
{{ record.rdata.address }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% elif record.type == "MX" %}
|
||||||
|
<div class="rdata-main">
|
||||||
|
<span class="pill">
|
||||||
|
{{ record.rdata.mail_exchanger }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="rdata-complementary">
|
||||||
|
<span class="pill">
|
||||||
|
Preference: {{ record.rdata.mail_exchanger }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% elif record.type == "NS" %}
|
||||||
|
<div class="rdata-main">
|
||||||
|
<span class="pill">
|
||||||
|
{{ record.rdata.target }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<button class="icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
|
||||||
|
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<a class="button icon" href="#">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
{% endblock main %}
|
|
@ -1,43 +0,0 @@
|
||||||
{% extends "bases/app.html" %}
|
|
||||||
{% import "macros.html" as macros %}
|
|
||||||
|
|
||||||
{% block title %}{{ current_zone }} ⋅ Records ⋅ {% endblock title %}
|
|
||||||
{% block styles %}
|
|
||||||
<link rel="stylesheet" type="text/css" href="/styles/zone.css">
|
|
||||||
{% endblock styles %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<h1>Gestion de la zone {{ current_zone }}</h1>
|
|
||||||
<nav class="secondary" aria-label="Secondaire">
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
{{ macros::nav_link(
|
|
||||||
content="Enregistrements",
|
|
||||||
href="/zone/" ~ current_zone ~ "/records",
|
|
||||||
current_page=nav_page,
|
|
||||||
) }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{{ macros::nav_link(
|
|
||||||
content="Membres",
|
|
||||||
href="/zone/" ~ current_zone ~ "/members",
|
|
||||||
current_page=nav_page,
|
|
||||||
) }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<section>
|
|
||||||
<zone-content>
|
|
||||||
</zone-content>
|
|
||||||
</section>
|
|
||||||
{% endblock main %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script type="module">
|
|
||||||
const zoneName = '{{ current_zone }}';
|
|
||||||
|
|
||||||
import initRecordsComponent from '/scripts/records.js';
|
|
||||||
|
|
||||||
initRecordsComponent(document.querySelector('zone-content'), { zone: zoneName });
|
|
||||||
</script>
|
|
||||||
{% endblock scripts %}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{% extends "bases/app.html" %}
|
|
||||||
|
|
||||||
{% block title %}Zones ⋅ {% endblock title %}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends "bases/app.html" %}
|
|
||||||
|
|
||||||
{% block title %}Ajouter une zone ⋅ {% endblock title %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<h1>Ajouter une zone</h1>
|
|
||||||
<form method="POST" action="/zones/new">
|
|
||||||
{% if error %}
|
|
||||||
<p class="feedback error" role="alert">
|
|
||||||
{{ error }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<label for="zone_name">Nom de la zone</label>
|
|
||||||
<input type="text" id="zone_name" name="name">
|
|
||||||
<input type="submit" value="Créer la zone">
|
|
||||||
</form>
|
|
||||||
{% endblock main %}
|
|
Loading…
Reference in a new issue