diff --git a/Cargo.lock b/Cargo.lock index c2138e4..a958475 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "async-lock" version = "3.4.0" @@ -59,9 +68,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.8.0-alpha.1" +version = "0.8.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4132c8995c63b222c56f38b80748c23323d7012d1f783b0f42e92782c2676690" +checksum = "47b212eb691030da38f1eff381777e431eb6f0760a0d02ffcb1702f1da9894e2" dependencies = [ "axum-core", "bytes", @@ -91,9 +100,9 @@ dependencies = [ [[package]] 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" -checksum = "f3405ae79f3aab93ae35fe2944aff9bf0f5aac918c0229f3528043c5a454d17d" +checksum = "30256e79153b08607dcf9e0a72bd31564bc9228b9f145d1e1a29e3d01ad1fd16" dependencies = [ "bytes", "futures-util", @@ -140,6 +149,25 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "bumpalo" version = "3.16.0" @@ -182,6 +210,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -191,6 +228,16 @@ dependencies = [ "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]] name = "crossbeam-epoch" version = "0.9.18" @@ -206,6 +253,16 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "deranged" version = "0.3.11" @@ -215,6 +272,16 @@ dependencies = [ "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]] name = "domain" version = "0.10.3" @@ -307,6 +374,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -327,6 +400,16 @@ dependencies = [ "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]] name = "getrandom" version = "0.2.15" @@ -344,6 +427,30 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "hashbrown" version = "0.14.5" @@ -396,6 +503,12 @@ dependencies = [ "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]] name = "httparse" version = "1.9.5" @@ -443,6 +556,22 @@ dependencies = [ "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]] name = "itoa" version = "1.0.14" @@ -459,6 +588,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.168" @@ -493,9 +628,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matchit" -version = "0.8.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e8fcd7bd6025a951597d6ba2f8e48a121af7e262f2b52a006a09c8d61f9304" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" @@ -509,6 +644,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "miniz_oxide" version = "0.8.0" @@ -548,7 +693,7 @@ dependencies = [ "rustc_version", "smallvec", "tagptr", - "thiserror", + "thiserror 1.0.69", "triomphe", "uuid", ] @@ -564,7 +709,9 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "tera", "tokio", + "tower-http", ] [[package]] @@ -633,6 +780,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "pin-project-lite" version = "0.2.15" @@ -747,6 +939,35 @@ dependencies = [ "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]] name = "ring" version = "0.17.8" @@ -803,6 +1024,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "scopeguard" version = "1.2.0" @@ -869,6 +1099,17 @@ dependencies = [ "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]] name = "shlex" version = "1.3.0" @@ -929,13 +1170,38 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 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]] @@ -949,6 +1215,17 @@ dependencies = [ "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]] name = "time" version = "0.3.37" @@ -996,6 +1273,19 @@ dependencies = [ "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]] name = "tower" version = "0.5.2" @@ -1011,6 +1301,31 @@ dependencies = [ "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]] name = "tower-layer" version = "0.3.3" @@ -1060,6 +1375,74 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "unicode-ident" version = "1.0.14" @@ -1093,6 +1476,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "wasi" 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" 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]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 478fedb..c3efee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,10 @@ tokio = {version = "1", default-features = false, features = [ "macros", "rt-mul #clap = { version = "4", features = [ "derive", "cargo" ] } #argon2 = { version = "0.5", default-features = false, features = ["alloc", "password-hash"] } #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" ]} -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" } rusqlite = { version = "0.32"} async-trait = { version = "0.1" } +tower-http = { version = "0.6", default-features = false, features = [ "fs" ]} diff --git a/assets/fonts/inclusive-sans/InclusiveSans_wght.ttf b/assets/fonts/inclusive-sans/InclusiveSans_wght.ttf new file mode 100644 index 0000000..c5193d0 Binary files /dev/null and b/assets/fonts/inclusive-sans/InclusiveSans_wght.ttf differ diff --git a/assets/fonts/inclusive-sans/InclusiveSans_wght.woff2 b/assets/fonts/inclusive-sans/InclusiveSans_wght.woff2 new file mode 100644 index 0000000..6ec88e5 Binary files /dev/null and b/assets/fonts/inclusive-sans/InclusiveSans_wght.woff2 differ diff --git a/assets/fonts/lexend/Lexend-VariableFont_wght.ttf b/assets/fonts/lexend/Lexend-VariableFont_wght.ttf new file mode 100644 index 0000000..f4eb50d Binary files /dev/null and b/assets/fonts/lexend/Lexend-VariableFont_wght.ttf differ diff --git a/assets/fonts/lexend/Lexend-VariableFont_wght.woff2 b/assets/fonts/lexend/Lexend-VariableFont_wght.woff2 new file mode 100644 index 0000000..3955f5d Binary files /dev/null and b/assets/fonts/lexend/Lexend-VariableFont_wght.woff2 differ diff --git a/assets/styles/main.css b/assets/styles/main.css new file mode 100644 index 0000000..2ed9d6a --- /dev/null +++ b/assets/styles/main.css @@ -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; +} diff --git a/dev-scripts/config/named.conf b/dev-scripts/config/named.conf new file mode 100644 index 0000000..038c64c --- /dev/null +++ b/dev-scripts/config/named.conf @@ -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"; }; +}; diff --git a/public/images/plus.svg b/public/images/plus.svg deleted file mode 100644 index 5b088c0..0000000 --- a/public/images/plus.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/public/scripts/api.js b/public/scripts/api.js deleted file mode 100644 index a1b560a..0000000 --- a/public/scripts/api.js +++ /dev/null @@ -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, -}; diff --git a/public/scripts/records.js b/public/scripts/records.js deleted file mode 100644 index 5dd03bd..0000000 --- a/public/scripts/records.js +++ /dev/null @@ -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`${keys[0][1]}`; - } else { - return html` -
- ${keys.map(([name, value]) => {return html`
${rdataInputProperties[name].label}
${value}
`})} -
- `; - } -} - -function RecordsByName({ name, recordSets }) { - return html` -
-

${name}

-
- ${Object.entries(recordSets).map( - ([type, records]) => { - return html` -
-

${recordTypeNames[type]}

-
    - ${records.map(record => html`
  • <${FriendlyRecord} type=${type} record=${record}/>
  • `)} -
-
- `; - } - )} -
-
- `; -} - -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` -
-
-

Nouvel enregistrement

-
-
-
-
- -
- - .${ zone } -
-
-
- - -
-
- ${friendlyRecordDataConfig[recordType].fields.map(fieldName => html` -
- - -
- `)} -
-
- - -
-
-
-

Prévisualisation des changements

-

- Ajout - - ${realRecordData.Name === zone ? '@' : realRecordData.Name} ${realRecordData.TTL} ${realRecordData.Class} ${realType} ${realType != '' ? realRecordDataConfig[realType].fields.map(field => realRecordData[field]).join(' ') : ''} - -

-
-
- - -
-
-
- `; -} - -function ZoneRecords({ zone }) { - return html` - <${NewRecordFormFriendly} zone=${zone}/> - -
-
-

Contenu de la zone

- -
- -
- <${RecordListFriendly} zone=${zone} /> -
-
- `; -} - -export default function(element, { zone }) { - render(html`<${ZoneRecords} zone=${zone} />`, element); -}; diff --git a/public/scripts/vendor/licenses.txt b/public/scripts/vendor/licenses.txt deleted file mode 100644 index 602b9c6..0000000 --- a/public/scripts/vendor/licenses.txt +++ /dev/null @@ -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 diff --git a/public/scripts/vendor/preact/LICENSE-htm b/public/scripts/vendor/preact/LICENSE-htm deleted file mode 100644 index f107611..0000000 --- a/public/scripts/vendor/preact/LICENSE-htm +++ /dev/null @@ -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. \ No newline at end of file diff --git a/public/scripts/vendor/preact/LICENSE-preact b/public/scripts/vendor/preact/LICENSE-preact deleted file mode 100644 index da5389a..0000000 --- a/public/scripts/vendor/preact/LICENSE-preact +++ /dev/null @@ -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. diff --git a/public/scripts/vendor/preact/standalone.js b/public/scripts/vendor/preact/standalone.js deleted file mode 100644 index e24f87b..0000000 --- a/public/scripts/vendor/preact/standalone.js +++ /dev/null @@ -1 +0,0 @@ -var e,n,_,t,o,r,u,l={},i=[],c=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function s(e,n){for(var _ in n)e[_]=n[_];return e}function f(e){var n=e.parentNode;n&&n.removeChild(e)}function a(n,_,t){var o,r,u,l={};for(u in _)"key"==u?o=_[u]:"ref"==u?r=_[u]:l[u]=_[u];if(arguments.length>2&&(l.children=arguments.length>3?e.call(arguments,2):t),"function"==typeof n&&null!=n.defaultProps)for(u in n.defaultProps)void 0===l[u]&&(l[u]=n.defaultProps[u]);return p(n,l,o,r,null)}function p(e,t,o,r,u){var l={type:e,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==u?++_:u};return null!=n.vnode&&n.vnode(l),l}function h(e){return e.children}function d(e,n){this.props=e,this.context=n}function v(e,n){if(null==n)return e.__?v(e.__,e.__.__k.indexOf(e)+1):null;for(var _;n0?p(m.type,m.props,m.key,null,m.__v):m)){if(m.__=_,m.__b=_.__b+1,null===(y=H[a])||y&&m.key==y.key&&m.type===y.type)H[a]=void 0;else for(d=0;d=t.__.length&&t.__.push({}),t.__[e]}function G(e){return R=1,z(ie,e)}function z(e,n,_){var t=j(L++,2);return t.t=e,t.__c||(t.__=[_?_(n):ie(void 0,n),function(e){var n=t.t(t.__[0],e);t.__[0]!==n&&(t.__=[n,t.__[1]],t.__c.setState({}))}],t.__c=N),t.__}function J(e,_){var t=j(L++,3);!n.__s&&le(t.__H,_)&&(t.__=e,t.__H=_,N.__H.__h.push(t))}function K(e,_){var t=j(L++,4);!n.__s&&le(t.__H,_)&&(t.__=e,t.__H=_,N.__h.push(t))}function Q(e){return R=5,Y(function(){return{current:e}},[])}function X(e,n,_){R=6,K(function(){"function"==typeof e?e(n()):e&&(e.current=n())},null==_?_:_.concat(e))}function Y(e,n){var _=j(L++,7);return le(_.__H,n)&&(_.__=e(),_.__H=n,_.__h=e),_.__}function Z(e,n){return R=8,Y(function(){return e},n)}function ee(e){var n=N.context[e.__c],_=j(L++,9);return _.c=e,n?(null==_.__&&(_.__=!0,n.sub(N)),n.props.value):e.__}function ne(e,_){n.useDebugValue&&n.useDebugValue(_?_(e):e)}function _e(e){var n=j(L++,10),_=G();return n.__=e,N.componentDidCatch||(N.componentDidCatch=function(e){n.__&&n.__(e),_[1](e)}),[_[0],function(){_[1](void 0)}]}function te(){I.forEach(function(e){if(e.__P)try{e.__H.__h.forEach(re),e.__H.__h.forEach(ue),e.__H.__h=[]}catch(_){e.__H.__h=[],n.__e(_,e.__v)}}),I=[]}n.__b=function(e){N=null,O&&O(e)},n.__r=function(e){V&&V(e),L=0;var n=(N=e.__c).__H;n&&(n.__h.forEach(re),n.__h.forEach(ue),n.__h=[])},n.diffed=function(e){q&&q(e);var _=e.__c;_&&_.__H&&_.__H.__h.length&&(1!==I.push(_)&&W===n.requestAnimationFrame||((W=n.requestAnimationFrame)||function(e){var n,_=function(){clearTimeout(t),oe&&cancelAnimationFrame(n),setTimeout(e)},t=setTimeout(_,100);oe&&(n=requestAnimationFrame(_))})(te)),N=void 0},n.__c=function(e,_){_.some(function(e){try{e.__h.forEach(re),e.__h=e.__h.filter(function(e){return!e.__||ue(e)})}catch(t){_.some(function(e){e.__h&&(e.__h=[])}),_=[],n.__e(t,e.__v)}}),B&&B(e,_)},n.unmount=function(e){$&&$(e);var _=e.__c;if(_&&_.__H)try{_.__H.__.forEach(re)}catch(e){n.__e(e,_.__v)}};var oe="function"==typeof requestAnimationFrame;function re(e){var n=N;"function"==typeof e.__c&&e.__c(),N=n}function ue(e){var n=N;e.__c=e.__(),N=n}function le(e,n){return!e||e.length!==n.length||n.some(function(n,_){return n!==e[_]})}function ie(e,n){return"function"==typeof n?n(e):n}var ce=function(e,n,_,t){var o;n[0]=0;for(var r=1;r=5&&((o||!e&&5===t)&&(u.push(t,0,o,_),t=6),e&&(u.push(t,e,0,_),t=6)),o=""},i=0;i"===n?(t=1,o=""):o=n+o[0]:r?n===r?r="":o+=n:'"'===n||"'"===n?r=n:">"===n?(l(),t=1):t&&("="===n?(t=5,_=o,o=""):"/"===n&&(t<5||">"===e[i][c+1])?(l(),3===t&&(u=u[0]),t=u,(u=u[0]).push(2,0,t),t=0):" "===n||"\t"===n||"\n"===n||"\r"===n?(l(),t=2):o+=n),3===t&&"!--"===o&&(t=4,u=u[0])}return l(),u}(e)),n),arguments,[])).length>1?n:n[0]}.bind(a);export{a as h,fe as html,M as render,d as Component,F as createContext,G as useState,z as useReducer,J as useEffect,K as useLayoutEffect,Q as useRef,X as useImperativeHandle,Y as useMemo,Z as useCallback,ee as useContext,ne as useDebugValue,_e as useErrorBoundary}; diff --git a/public/styles/login.css b/public/styles/login.css deleted file mode 100644 index d1c085f..0000000 --- a/public/styles/login.css +++ /dev/null @@ -1,13 +0,0 @@ -form { - flex-grow: 1; - max-width: 40ch; - margin: 25vh auto 0 auto; -} - -main { - flex-direction: row; -} - -h1 { - text-align: center; -} diff --git a/public/styles/main.css b/public/styles/main.css deleted file mode 100644 index 860c1bd..0000000 --- a/public/styles/main.css +++ /dev/null @@ -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; -} diff --git a/public/styles/zone.css b/public/styles/zone.css deleted file mode 100644 index 2fb3023..0000000 --- a/public/styles/zone.css +++ /dev/null @@ -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) -} diff --git a/src/dns/dns_driver.rs b/src/dns/dns_driver.rs index 5f1181b..1a1da84 100644 --- a/src/dns/dns_driver.rs +++ b/src/dns/dns_driver.rs @@ -11,9 +11,8 @@ use domain::tsig::{Algorithm, Key, KeyName}; use domain::net::client::request::{self, RequestMessage, RequestMessageMulti, SendRequest, SendRequestMulti}; use tokio::net::TcpStream; -use super::{rdata, record}; +use crate::ressouces::{rdata, record}; use super::{RecordDriver, ZoneDriver, DnsDriverError}; -use crate::errors::Error; use async_trait::async_trait; diff --git a/src/dns/mod.rs b/src/dns/mod.rs index f00c618..624cac8 100644 --- a/src/dns/mod.rs +++ b/src/dns/mod.rs @@ -1,12 +1,13 @@ -pub mod rdata; -pub mod record; pub mod dns_driver; use std::sync::Arc; use async_trait::async_trait; +use crate::ressouces::record; + pub type BoxedZoneDriver = Arc; +pub type BoxedRecordDriver = Arc; pub enum DnsDriverError { ConnectionError { reason: Box }, OperationError { reason: Box }, diff --git a/src/dns/rdata.rs b/src/dns/rdata.rs deleted file mode 100644 index 0ad5f67..0000000 --- a/src/dns/rdata.rs +++ /dev/null @@ -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 { - A(rdata::A), - Aaaa(rdata::Aaaa), - Cname(rdata::Cname), - Mx(rdata::Mx), - Ns(rdata::Ns), - Ptr(rdata::Ptr), - Soa(rdata::Soa), - Srv(rdata::Srv), - Txt(rdata::Txt), -} - -impl> From> for RData { - fn from(value: ParsedRData) -> 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 for ParsedRData>, Vec> { - type Error = Vec; - - fn try_from(value: RData) -> Result { - 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::>().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 for A { - fn from(record_data: rdata::A) -> Self { - A { address: record_data.addr().to_string() } - } -} - -impl A { - pub fn parse_record(self) -> Result> { - let mut errors = Vec::new(); - - let address = push_error!(self.address.parse::().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 for Aaaa { - fn from(record_data: rdata::Aaaa) -> Self { - Aaaa { address: record_data.addr().to_string() } - } -} - -impl Aaaa { - pub fn parse_record(self) -> Result> { - let mut errors = Vec::new(); - - let address = push_error!(self.address.parse::().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 From> for Cname { - fn from(record_data: rdata::Cname) -> Self { - Cname { target: record_data.cname().to_string() } - } -} - -impl Cname { - pub fn parse_record(self) -> Result>>, Vec> { - 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 From> for Mx { - fn from(record_data: rdata::Mx) -> Self { - Mx { - preference: record_data.preference(), - mail_exchanger: record_data.exchange().to_string() - } - } -} - -impl Mx { - fn parse_record(self) -> Result>>, Vec> { - 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 From> for Ns { - fn from(record_rdata: rdata::Ns) -> Self { - Ns { - target: record_rdata.nsdname().to_string(), - } - } -} - -impl Ns { - fn parse_record(self) -> Result>>, Vec> { - 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 From> for Ptr { - fn from(record_rdata: rdata::Ptr) -> Self { - Ptr { - target: record_rdata.ptrdname().to_string(), - } - } -} - -impl Ptr { - fn parse_record(self) -> Result>>, Vec> { - 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 From> for Soa { - fn from(record_rdata: rdata::Soa) -> 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>>, Vec> { - 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 From> for Srv { - fn from(record_data: rdata::Srv) -> 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>>, Vec> { - 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> From> for Txt { - fn from(record_data: rdata::Txt) -> 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>, Vec> { - 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 ParsedRData { - 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 RecordData for ParsedRData { - fn rtype(&self) -> Rtype { - ParsedRData::rtype(self) - } -} - -impl<'a, Octs: Octets + ?Sized> ParseRecordData<'a, Octs> for ParsedRData>, Octs::Range<'a>> { - fn parse_rdata( - rtype: Rtype, - parser: &mut Parser<'a, Octs>, - ) -> Result, 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> ComposeRecordData for ParsedRData { - fn rdlen(&self, compress: bool) -> Option { - 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( - &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( - &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), - } - } -} diff --git a/src/dns/record.rs b/src/dns/record.rs deleted file mode 100644 index 4de8cbc..0000000 --- a/src/dns/record.rs +++ /dev/null @@ -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 }, -} - -pub(crate) type DnsRecordImpl = DnsRecord< - Name>, - ParsedRData>,Vec> ->; - -#[derive(Debug, Deserialize, Serialize)] -pub struct Record { - pub name: String, - pub ttl: u32, - #[serde(flatten)] - pub rdata: RData -} - -impl> From>> for Record { - fn from(value: DnsRecord>) -> 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>) -> Result> { - 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::>().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); - -impl RecordList { - fn convert(self, zone_name: &Name>) -> Result, Vec> { - 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 -} - -impl AddRecordsRequest { - pub fn validate(self, zone_name: &str) -> Result { - let zone_name: Name> = 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 })) - } - } -} diff --git a/src/errors.rs b/src/errors.rs index 9d456e4..4d6ab4f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -7,10 +7,11 @@ use axum::Json; use serde::{Serialize, Serializer}; use serde_json::{Value, json}; -use crate::dns::{DnsDriverError, ZoneDriver}; -use crate::dns::record::{RecordError, RecordParseError}; +use crate::dns::DnsDriverError; +use crate::ressouces::record::{RecordError, RecordParseError}; use crate::ressouces::zone::ZoneError; use crate::validation::{DomainValidationError, TxtParseError}; +use crate::template::TemplateError; #[derive(Debug, Serialize)] pub struct Error { @@ -357,6 +358,24 @@ impl From for Error { } } +impl From 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 for Error { fn from(value: RecordError) -> Self { match value { diff --git a/src/main.rs b/src/main.rs index c07dbca..f6c4982 100644 --- a/src/main.rs +++ b/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 dns; mod routes; @@ -28,29 +5,34 @@ mod ressouces; mod database; mod validation; mod macros; +mod template; use std::sync::Arc; use axum::Router; use axum::routing; +use tower_http::services::ServeDir; + use database::sqlite::SqliteDB; -use database::Db; +use database::BoxedDb; use dns::dns_driver::DnsDriverConfig; use dns::dns_driver::TsigConfig; use dns::{ZoneDriver, RecordDriver}; +use template::TemplateEngine; #[derive(Clone)] pub struct AppState { zone: Arc, records: Arc, - db: Arc, + db: BoxedDb, + template_engine: TemplateEngine } - - #[tokio::main] async fn main() { + let template_engine = TemplateEngine::new(std::path::Path::new("./templates")); + let dns_driver = dns::dns_driver::DnsDriver::from_config(DnsDriverConfig { address: "127.0.0.1:5353".parse().unwrap(), tsig: Some(TsigConfig { @@ -66,12 +48,17 @@ async fn main() { zone: dns_driver.clone(), records: dns_driver.clone(), db: Arc::new(SqliteDB::new("db.sqlite".into()).await), + template_engine }; let app = Router::new() - .route("/admin/zones", routing::post(routes::api::zones::create_zone)) - .route("/zones/{zone_name}/records", routing::get(routes::api::zones::get_zone_records)) - .route("/zones/{zone_name}/records", routing::post(routes::api::zones::create_zone_records)) + /* ----- API ----- */ + .route("/api/admin/zones", routing::post(routes::api::zones::create_zone)) + .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); let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap(); diff --git a/src/ressouces/class.rs b/src/ressouces/class.rs deleted file mode 100644 index 3eea893..0000000 --- a/src/ressouces/class.rs +++ /dev/null @@ -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 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 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), - } - } -} diff --git a/src/ressouces/errors.rs b/src/ressouces/errors.rs deleted file mode 100644 index 478ba99..0000000 --- a/src/ressouces/errors.rs +++ /dev/null @@ -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 for UserError { - fn from(e: DieselError) -> Self { - UserError::DbError(e) - } -} - -impl From 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 -} - -#[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 (self, details: T) -> ErrorResponse { - ErrorResponse { - details: serde_json::to_value(details).ok(), - ..self - } - } - - pub fn err(self) -> Result { - 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 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> for ErrorResponse { - fn from(e: Box) -> 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 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 From for Outcome { - fn from(e: ErrorResponse) -> Self { - Outcome::Failure(e.into()) - } -} - - -impl From for (Status, ErrorResponse) { - fn from(e: ErrorResponse) -> Self { - (e.status, e) - } -} - -// TODO: change for Display trait -pub fn make_500(e: E) -> ErrorResponse { - error!("Making 500 for Error: {:?}", e); - - ErrorResponse::new(Status::InternalServerError, "An unexpected error occured".into()) -} diff --git a/src/ressouces/mod.rs b/src/ressouces/mod.rs index a4c95c3..ae3c9f4 100644 --- a/src/ressouces/mod.rs +++ b/src/ressouces/mod.rs @@ -20,3 +20,5 @@ pub use zone::{Zone, AddZoneMemberRequest, CreateZoneRequest}; */ pub mod zone; +pub mod rdata; +pub mod record; diff --git a/src/ressouces/name.rs b/src/ressouces/name.rs deleted file mode 100644 index c768b28..0000000 --- a/src/ressouces/name.rs +++ /dev/null @@ -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(deserializer: D) -> Result - 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(&self, serializer: S) -> Result - 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 { - 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 { - 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 - } -} diff --git a/src/ressouces/rdata.rs b/src/ressouces/rdata.rs index 4ea2d14..afd8b62 100644 --- a/src/ressouces/rdata.rs +++ b/src/ressouces/rdata.rs @@ -1,258 +1,544 @@ -use std::convert::TryFrom; -use std::net::{Ipv6Addr, Ipv4Addr}; +use std::fmt::Write; +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 trust_dns_client::serialize::binary::BinEncoder; -use trust_dns_proto::error::ProtoError; +use crate::errors::Error; +use crate::validation; -use crate::dns; -use super::name::SerdeName; +use crate::macros::{append_errors, push_error}; +use super::record::RecordParseError; - -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(tag = "Type")] +/// 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 { - #[serde(rename_all = "PascalCase")] - A { - address: Ipv4Addr - }, - #[serde(rename_all = "PascalCase")] - AAAA { - address: Ipv6Addr - }, - #[serde(rename_all = "PascalCase")] - CAA { - issuer_critical: bool, - value: String, - 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) + 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), } -impl From for RData { - fn from(rdata: dns::RData) -> RData { - match rdata { - dns::RData::A(address) => RData::A { address }, - dns::RData::AAAA(address) => RData::AAAA { address }, - // 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. - // 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. - // (empty data because I'm lazy) - dns::RData::ANAME(_) => RData::NULL { - data: String::new() - }, - dns::RData::CNAME(target) => RData::CNAME { - 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 RData { + pub fn rtype(&self) -> Rtype { + match self { + RData::A(_) => Rtype::A, + RData::Aaaa(_) => Rtype::AAAA, + RData::Cname(_) => Rtype::CNAME, + RData::Mx(_) => Rtype::MX, + RData::Ns(_) => Rtype::NS, + RData::Ptr(_) => Rtype::PTR, + RData::Soa(_) => Rtype::SOA, + RData::Srv(_) => Rtype::SRV, + RData::Txt(_) => Rtype::TXT, } } } -impl TryFrom for dns::RData { - type Error = ProtoError; +pub enum ParsedRData { + A(rdata::A), + Aaaa(rdata::Aaaa), + Cname(rdata::Cname), + Mx(rdata::Mx), + Ns(rdata::Ns), + Ptr(rdata::Ptr), + Soa(rdata::Soa), + Srv(rdata::Srv), + Txt(rdata::Txt), +} - fn try_from(rdata: RData) -> Result { - Ok(match rdata { - RData::A { address } => dns::RData::A(address), - RData::AAAA { address } => dns::RData::AAAA(address), - // TODO: Round trip test all types below (currently not tested...) - RData::CAA { issuer_critical, value, property_tag } => { - let property = dns::caa::Property::from(property_tag); - let caa_value = { - // TODO: duplicate of trust_dns_client::serialize::txt::rdata_parser::caa::parse - // because caa::read_value is private - match property { - dns::caa::Property::Issue | dns::caa::Property::IssueWild => { - 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())?; - dns::caa::Value::Url(url) - } - dns::caa::Property::Unknown(_) => dns::caa::Value::Unknown(value.as_bytes().to_vec()), - } - }; - dns::RData::CAA(dns::caa::CAA { - issuer_critical, - tag: property, - value: caa_value, - }) - }, - RData::CNAME { target } => dns::RData::CNAME(target.into_inner()), - RData::MX { preference, mail_exchanger } => dns::RData::MX( - dns::mx::MX::new(preference, mail_exchanger.into_inner()) - ), - 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()), - RData::PTR { target } => dns::RData::PTR(target.into_inner()), - RData::SOA { - master_server_name, - maintainer_name, - refresh, - retry, - expire, - minimum, - serial - } => dns::RData::SOA( - dns::soa::SOA::new( - master_server_name.into_inner(), - maintainer_name.into_inner(), - serial, - refresh, - retry, - expire, - minimum, - ) - ), - RData::SRV { server, port, priority, weight } => dns::RData::SRV( - 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 - dns::sshfp::Algorithm::from(algorithm), - dns::sshfp::FingerprintType::from(digest_type), - dns::sshfp::HEX.decode(fingerprint.as_bytes()).map_err(|e| ProtoError::from(format!("{}", e)))? - ) - ), - RData::TXT { text } => dns::RData::TXT(dns::txt::TXT::new(vec![text])), - // TODO: Error out for DNSSEC? Prefer downstream checks? - RData::DNSSEC(_) => todo!(), - // TODO: Disallow unknown? (could be used to bypass unsopported types?) Prefer downstream checks? - RData::Unknown { code: _code, data: _data } => todo!(), - }) +impl> From> for RData { + fn from(value: ParsedRData) -> 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 for ParsedRData>, Vec> { + type Error = Vec; + + fn try_from(value: RData) -> Result { + 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::>().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 for A { + fn from(record_data: rdata::A) -> Self { + A { address: record_data.addr().to_string() } + } +} + +impl A { + pub fn parse_record(self) -> Result> { + let mut errors = Vec::new(); + + let address = push_error!(self.address.parse::().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 for Aaaa { + fn from(record_data: rdata::Aaaa) -> Self { + Aaaa { address: record_data.addr().to_string() } + } +} + +impl Aaaa { + pub fn parse_record(self) -> Result> { + let mut errors = Vec::new(); + + let address = push_error!(self.address.parse::().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 From> for Cname { + fn from(record_data: rdata::Cname) -> Self { + Cname { target: record_data.cname().to_string() } + } +} + +impl Cname { + pub fn parse_record(self) -> Result>>, Vec> { + 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 From> for Mx { + fn from(record_data: rdata::Mx) -> Self { + Mx { + preference: record_data.preference(), + mail_exchanger: record_data.exchange().to_string() + } + } +} + +impl Mx { + fn parse_record(self) -> Result>>, Vec> { + 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 From> for Ns { + fn from(record_rdata: rdata::Ns) -> Self { + Ns { + target: record_rdata.nsdname().to_string(), + } + } +} + +impl Ns { + fn parse_record(self) -> Result>>, Vec> { + 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 From> for Ptr { + fn from(record_rdata: rdata::Ptr) -> Self { + Ptr { + target: record_rdata.ptrdname().to_string(), + } + } +} + +impl Ptr { + fn parse_record(self) -> Result>>, Vec> { + 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 From> for Soa { + fn from(record_rdata: rdata::Soa) -> 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>>, Vec> { + 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 From> for Srv { + fn from(record_data: rdata::Srv) -> 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>>, Vec> { + 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> From> for Txt { + fn from(record_data: rdata::Txt) -> 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>, Vec> { + 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 ParsedRData { + 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 RecordData for ParsedRData { + fn rtype(&self) -> Rtype { + ParsedRData::rtype(self) + } +} + +impl<'a, Octs: Octets + ?Sized> ParseRecordData<'a, Octs> for ParsedRData>, Octs::Range<'a>> { + fn parse_rdata( + rtype: Rtype, + parser: &mut Parser<'a, Octs>, + ) -> Result, 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> ComposeRecordData for ParsedRData { + fn rdlen(&self, compress: bool) -> Option { + 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( + &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( + &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), + } } } diff --git a/src/ressouces/record.rs b/src/ressouces/record.rs index 3fe02a1..aed47b6 100644 --- a/src/ressouces/record.rs +++ b/src/ressouces/record.rs @@ -1,122 +1,128 @@ -use std::convert::{TryFrom, TryInto}; use serde::{Deserialize, Serialize}; -use trust_dns_proto::error::ProtoError; -use crate::dns; -use super::name::SerdeName; -use super::class::DNSClass; -use super::rdata::RData; +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}; -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Record { - #[serde(rename = "Name")] - pub name: SerdeName, - // TODO: Make class optional, default to IN - #[serde(rename = "Class")] - pub dns_class: DNSClass, - #[serde(rename = "TTL")] - pub ttl: u32, - #[serde(flatten)] - pub rdata: 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 }, } -impl From for Record { - fn from(record: dns::Record) -> Record { +pub enum RecordError { + Validation { suberrors: Vec }, +} + +pub(crate) type DnsRecordImpl = DnsRecord< + Name>, + ParsedRData>,Vec> +>; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Record { + pub name: String, + pub ttl: u32, + #[serde(flatten)] + pub rdata: RData +} + +impl> From>> for Record { + fn from(value: DnsRecord>) -> Self { Record { - name: SerdeName(record.name().clone()), - dns_class: record.dns_class().into(), - ttl: record.ttl(), - // Assume data exists, record with empty data should be filtered by caller - rdata: record.into_data().unwrap().into(), + name: value.owner().to_string(), + ttl: value.ttl().as_secs(), + rdata: value.into_data().into(), } } } -impl TryFrom for dns::Record { - type Error = ProtoError; +impl Record { + fn convert(self, zone_name: &Name>) -> Result> { + let mut errors = Vec::new(); - fn try_from(record: Record) -> Result { - 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()); - Ok(trust_dns_record) + let name = push_error!(validation::normalize_domain(&self.name), errors, "/name"); + + let name = name.and_then(|name| push_error!(name.parse::>().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) + } } } -pub type RecordList = Vec; +#[derive(Debug, Deserialize, Serialize)] +pub struct RecordList(pub Vec); -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateRecordsRequest { - pub old_records: RecordList, - pub new_records: RecordList, -} +impl RecordList { + fn convert(self, zone_name: &Name>) -> Result, Vec> { + let mut errors = Vec::new(); + let mut records = Vec::new(); -pub enum RecordListParseError { - ParseError { - bad_records: Vec, - zone: dns::Name, - }, - RecordNotInZone { - zone: dns::Name, - class: dns::DNSClass, - mismatched_class: Vec, - mismatched_zone: Vec, - }, -} + for (index, record) in self.0.into_iter().enumerate() { + let record = append_errors!(record.convert(zone_name), errors, &format!("/{index}")); -pub trait ParseRecordList { - fn try_into_dns_type(self, zone: dns::Name, class: dns::DNSClass) -> Result, RecordListParseError>; -} - -impl ParseRecordList for RecordList { - fn try_into_dns_type(self, zone: dns::Name, class: dns::DNSClass) -> Result, RecordListParseError> { - // TODO: What about relative names (also in cnames and stuff) - let mut bad_records = Vec::new(); - let mut records: Vec = Vec::new(); - let mut mismatched_class: Vec = Vec::new(); - let mut mismatched_zone: Vec = 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 let Some(record) = record { + records.push(record) } } - if !bad_records.is_empty() { - return Err(RecordListParseError::ParseError { - zone, - bad_records, - }); + 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 +} + +impl AddRecordsRequest { + pub fn validate(self, zone_name: &str) -> Result { + let zone_name: Name> = 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) } } diff --git a/src/ressouces/zone.rs b/src/ressouces/zone.rs index f653635..4be11f5 100644 --- a/src/ressouces/zone.rs +++ b/src/ressouces/zone.rs @@ -4,9 +4,10 @@ use rusqlite::Error as RusqliteError; use crate::database::{BoxedDb, sqlite::SqliteDB}; -use crate::dns::{BoxedZoneDriver, DnsDriverError}; +use crate::dns::{BoxedZoneDriver, BoxedRecordDriver, DnsDriverError}; use crate::errors::Error; use crate::macros::push_error; +use crate::ressouces::record::RecordList; use crate::validation; pub enum ZoneError { @@ -39,6 +40,19 @@ impl Zone { db.create_zone(create_zone).await } + + pub async fn get_records(zone_name: &str, db: BoxedDb, record_driver: BoxedRecordDriver) ->Result { + 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)] diff --git a/src/routes/api/zones.rs b/src/routes/api/zones.rs index 0741ad7..4f54e4b 100644 --- a/src/routes/api/zones.rs +++ b/src/routes/api/zones.rs @@ -1,10 +1,10 @@ use axum::extract::{Path, State}; use axum::Json; -use crate::dns::record::{AddRecordsRequest, Record}; use crate::AppState; use crate::errors::Error; use crate::ressouces::zone::{CreateZoneRequest, Zone}; +use crate::ressouces::record::{AddRecordsRequest, Record, RecordList}; pub async fn create_zone( @@ -18,13 +18,9 @@ pub async fn create_zone( pub async fn get_zone_records( Path(zone_name): Path, State(app): State, -) -> Result>, Error> +) -> Result, Error> { - - let zone = app.db.get_zone_by_name(&zone_name).await?; - let records = app.records.get_records(&zone.name).await?; - - Ok(Json(records)) + Zone::get_records(&zone_name, app.db, app.records).await.map(Json) } pub async fn create_zone_records( diff --git a/src/routes/mod.rs b/src/routes/mod.rs index b697dec..9f43e82 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,2 +1,2 @@ -//pub mod ui; +pub mod ui; pub mod api; diff --git a/src/routes/ui/mod.rs b/src/routes/ui/mod.rs index f7ee56d..bcc7e39 100644 --- a/src/routes/ui/mod.rs +++ b/src/routes/ui/mod.rs @@ -1,5 +1,2 @@ -pub mod auth; +//pub mod auth; pub mod zones; - -pub use auth::*; -pub use zones::*; diff --git a/src/routes/ui/zones.rs b/src/routes/ui/zones.rs index dc3ae4d..e9d91cc 100644 --- a/src/routes/ui/zones.rs +++ b/src/routes/ui/zones.rs @@ -1,128 +1,26 @@ +use axum::extract::{Path, State}; 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::models; -use crate::controllers; -use crate::DbConn; -use crate::dns::ZoneConnector; -#[derive(Serialize)] -pub struct RecordsPage { - zone: String -} - - -// TODO: Check if origin changes if application mounted on different path -#[get("/zone//records")] -pub async fn get_zone_records_page(user_info: models::UserInfo, zone: models::AbsoluteName, conn: DbConn, origin: &Origin<'_>) -> Result, 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)?; +pub async fn get_zone_records_page( + Path(zone_name): Path, + State(app): State, +) -> Result, Error> { + let records = Zone::get_records(&zone_name, app.db, app.records).await?; + //records.0.sort_by_key(|record| (&record.name, record.rdata)); Ok(Template::new( - "pages/zone/records.html", + "pages/records.html", + app.template_engine, json!({ - "current_zone": zone.to_utf8(), - "zones": zones, - "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, 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, 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::, - "zones": zones, - "error": None::, - "nav_page": origin.clone().into_normalized().path().as_str(), - "nav_sections": vec!["zones", "_new-zone"], - }) - )) -} - -#[post("/zones/new", data = "")] -pub async fn post_create_zone_page( - conn: DbConn, - dns_api: Box, - user_info: models::UserInfo, - zone_request: Form, - origin: &Origin<'_> -) -> Result, 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::, - "nav_page": origin.clone().into_normalized().path().as_str(), - "nav_sections": vec!["zones", "_new-zone"], + "current_zone": zone_name, + "records": records, }) )) } diff --git a/src/template.rs b/src/template.rs index bd2886f..1d630e9 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,63 +1,73 @@ use std::path::Path; use std::process::exit; +use std::sync::Arc; +use axum::response::{Html, IntoResponse}; use serde::Serialize; -use rocket::request::Request; -use rocket::response::{self, Responder}; -use rocket::http::{Status, ContentType}; - use tera::{Tera, Context}; +use crate::errors::Error; -pub struct TemplateState { - tera: Tera, + +#[derive(Clone)] +pub struct TemplateEngine { + tera: Arc, } -impl TemplateState { +pub enum TemplateError { + SerializationError { reason: Box }, + RenderError { name: String, reason: Box }, +} + +impl TemplateEngine { pub fn new(template_directory: &Path) -> Self { let template_glob = template_directory.join("**").join("*"); 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) => { println!("Loading templates failed: {}", e); exit(1) } } } + + pub fn render(&self, name: &str, context: S) -> Result { + 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 name: &'t str, +pub struct Template<'n, S: Serialize> { + pub name: &'n str, + pub engine: TemplateEngine, pub context: S, } -impl<'r, S: Serialize> Template<'r, S> { - pub fn new(name: &'r str, context: S) -> Self { +impl<'n, S: Serialize> Template<'n, S> { + pub fn new(name: &'n str, engine: TemplateEngine, context: S) -> Self { Template { 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> { - fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> { - let template_state = request.rocket().state::().ok_or(Status::InternalServerError)?; +impl IntoResponse for Template<'_, S> { + fn into_response(self) -> axum::response::Response { + 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(), + } } } diff --git a/templates/bases/app.html b/templates/bases/app.html index 5deb2d4..5650668 100644 --- a/templates/bases/app.html +++ b/templates/bases/app.html @@ -1,41 +1,11 @@ {% extends "bases/base.html" %} -{% import "macros.html" as macros %} {% block content %}
diff --git a/templates/bases/base.html b/templates/bases/base.html index 1f209cb..e543cb6 100644 --- a/templates/bases/base.html +++ b/templates/bases/base.html @@ -4,7 +4,7 @@ {% block title %}{% endblock title %}Nomilo - + {% block styles %}{% endblock styles %} diff --git a/templates/macros.html b/templates/macros.html deleted file mode 100644 index e00c87d..0000000 --- a/templates/macros.html +++ /dev/null @@ -1,11 +0,0 @@ -{% macro nav_link(content, href, current_page, content='', safe_content='', section=False, current_sections=False, props='') %} -{{ content }}{{ safe_content | safe }} -{% endmacro nav_link %} diff --git a/templates/pages/login.html b/templates/pages/login.html deleted file mode 100644 index 24e8fc3..0000000 --- a/templates/pages/login.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "bases/base.html" %} - -{% block title %}Se connecter ⋅ {% endblock title %} -{% block styles %} - -{% endblock styles %} - -{% block content %} -
-
-

Se connecter

- {% if error %} - - {% endif %} - - - - - - -
-
-{% endblock content %} diff --git a/templates/pages/records.html b/templates/pages/records.html new file mode 100644 index 0000000..5bc1407 --- /dev/null +++ b/templates/pages/records.html @@ -0,0 +1,103 @@ +{% extends "bases/app.html" %} +{% block title %}Records - {{ current_zone }} - {% endblock title %} + +{% block main %} +

Zone {{ current_zone }} records

+ +
+

Records

+ + {% set current_domain = '' %} + {% set current_rtype = '' %} + + {% for record in records %} + {% if record.name != current_domain %} + {% if current_domain %} + + + + + + {% endif %} + {% set_global current_domain = record.name %} + {% set_global current_rtype = '' %} +
+
+

{{ record.name }}

+ + + + Add record + +
+
+
    + {% endif %} + {% if record.type != current_rtype %} + {% if current_rtype %} +
+ + {% endif %} + {% set_global current_rtype = record.type %} +
  • + {{ record.type }} +
      +
    • + {% endif %} +
      + {% if record.type == "A" or record.type == "AAAA" %} +
      + + {{ record.rdata.address }} + +
      + {% elif record.type == "MX" %} +
      + + {{ record.rdata.mail_exchanger }} + +
      +
      + + Preference: {{ record.rdata.mail_exchanger }} + +
      + {% elif record.type == "NS" %} +
      + + {{ record.rdata.target }} + +
      + {% endif %} +
      +
      + + + + + + +
      +
    • + {% endfor %} +
    +
  • + +
    +
    +
    +{% endblock main %} diff --git a/templates/pages/zone/records.html b/templates/pages/zone/records.html deleted file mode 100644 index 9ac490e..0000000 --- a/templates/pages/zone/records.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "bases/app.html" %} -{% import "macros.html" as macros %} - -{% block title %}{{ current_zone }} ⋅ Records ⋅ {% endblock title %} -{% block styles %} - -{% endblock styles %} - -{% block main %} -

    Gestion de la zone {{ current_zone }}

    - -
    - - -
    -{% endblock main %} - -{% block scripts %} - -{% endblock scripts %} diff --git a/templates/pages/zones.html b/templates/pages/zones.html deleted file mode 100644 index 016a96f..0000000 --- a/templates/pages/zones.html +++ /dev/null @@ -1,3 +0,0 @@ -{% extends "bases/app.html" %} - -{% block title %}Zones ⋅ {% endblock title %} diff --git a/templates/pages/zones/new.html b/templates/pages/zones/new.html deleted file mode 100644 index 72c0767..0000000 --- a/templates/pages/zones/new.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "bases/app.html" %} - -{% block title %}Ajouter une zone ⋅ {% endblock title %} - -{% block main %} -

    Ajouter une zone

    -
    - {% if error %} - - {% endif %} - - - - -
    -{% endblock main %}