diff --git a/Cargo.lock b/Cargo.lock index 976bc36..38a0bd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "async-trait" version = "0.1.89" @@ -94,6 +100,18 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[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 = "bytes" @@ -117,12 +135,86 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "config" +version = "0.15.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -138,6 +230,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 = "displaydoc" version = "0.2.5" @@ -149,6 +251,24 @@ dependencies = [ "syn", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -161,12 +281,29 @@ dependencies = [ "syn", ] +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "find-msvc-tools" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -222,6 +359,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.16" @@ -245,6 +392,30 @@ dependencies = [ "wasip2", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -505,6 +676,17 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "libc" version = "0.2.179" @@ -611,12 +793,71 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -748,6 +989,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "ryu" version = "1.0.22" @@ -764,6 +1029,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -808,6 +1085,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -820,6 +1106,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -926,6 +1223,8 @@ name = "tiny-dns" version = "0.1.0" dependencies = [ "axum", + "config", + "hickory-proto", "hickory-server", "http", "maud", @@ -936,6 +1235,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -1000,6 +1308,37 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.2" @@ -1086,6 +1425,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicase" version = "2.8.1" @@ -1098,6 +1455,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "untrusted" version = "0.9.0" @@ -1306,6 +1669,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -1318,6 +1690,17 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index f7d1cc9..2c59ac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] axum = { version = "0.8", features = ["macros"] } hickory-server = "0.25" +hickory-proto = "0.25" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } @@ -13,3 +14,4 @@ maud = { version = "0.27", features = ["axum"] } http = "1.4" tracing = "0.1" tower-http = { version = "0.6.8", features = ["fs", "tracing"] } +config = "0.15" diff --git a/README.md b/README.md index 8dea36c..fd8eb79 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,17 @@ Ein einfacher DNS-Server, der für ACME DNS Abfragen genutzt werden kann. - `GET /status` : Gibt den Status des Servers zurück. # Basiert auf DNS-01 + + +### Update URL + +```bash +curl http://\[::1\]:3000/update -H "X-Api-User: mig" -H "X-Api-Key: geheimnis" \ +--json '{"subdomain": "acme.norbb.de", "rdata": "___validation_token_received_from_the_ca___"}' +``` + +### Test URL + +```bash +dig @localhost -p 8053 -t TXT acme.norbb.de +``` diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..cc9481a --- /dev/null +++ b/config.toml @@ -0,0 +1,14 @@ +#api_port = 3002 +dns_addr = "localhost:8053" + +zone_name = "acme.example.com" + +[[auths]] +login = "mig" +key = "geheimnis" +scope = ["norbb.de", "domain2"] + +[[auths]] +login = "user" +key = "secret" +scope = ["example.com"] diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8253be3 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,27 @@ +use serde::Deserialize; + +use crate::model::User; + +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct Config { + pub dns_addr: Option, + pub api_port: Option, + pub zone_name: String, + #[serde(default)] + pub auths: Vec, +} + +impl Config { + pub fn new() -> Self { + use config::File; + let config = config::Config::builder() + .add_source(File::with_name("/etc/tiny-dns.toml").required(false)) + .add_source(File::with_name("config.toml").required(false)) + .build() + .expect("Failed to load config"); + config + .try_deserialize() + .expect("Failed to deserialize config") + } +} diff --git a/src/main.rs b/src/main.rs index 4b7da76..be009ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,171 +1,39 @@ -use axum::{ - Router, extract, - response::Json, - routing::{get, post}, -}; -use http::{StatusCode, header::HeaderMap}; -use maud::{DOCTYPE, Markup, html}; -use serde::Deserialize; -use serde_json::{Value, json}; -use std::{ - collections::HashSet, - sync::{Arc, Mutex}, -}; -use tower_http::services::ServeDir; -use tracing::info; +use hickory_server::{ServerFuture, authority::Catalog, proto::rr::LowerName}; +use std::str::FromStr; +use tokio::net::UdpSocket; +mod config; mod model; -use model::{DnsRecord, User}; - -use crate::model::JsonDomain; - -#[derive(Debug, Default)] -struct AppState { - auths: Arc>>, - domains: Arc>>, -} - -impl AppState { - fn is_allowed(&self, user: &User, domain: impl Into) -> bool { - let domain = domain.into(); - self.auths.lock().map_or(false, |a| { - a.iter().any(|u| { - u.login == user.login - && u.key == user.key - && u.scope.iter().any(|s| domain.contains(s)) - }) - }) - } -} +mod webapi; +use config::Config; +use model::AppState; #[tokio::main] async fn main() { - let shared_state = Arc::new(AppState { - auths: Arc::new(Mutex::new( - [User { - key: "geheimnis".to_string(), - login: "mig".to_string(), - scope: vec!["norbb.de".to_string(), "domain2".to_string()], - }] - .into_iter() - .collect(), - )), - domains: Arc::new(Mutex::new(HashSet::new())), - }); - // build our application with a single route - let app = Router::new() - .route("/", get(|| async { "Hello, World!" })) - .route("/register/{domain}", post(register)) - .route("/update", post(update)) - .route("/delete", post(delete)) - .route("/status", get(status)) - .nest_service("/dist", ServeDir::new("dist")) - .with_state(shared_state); - let listen = "[::]:3000"; + let config = Config::new(); + let api = webapi::router(&config); + let listen = config + .api_port + .map(|port| format!("[::]:{port}")) + .unwrap_or("[::]:3000".to_owned()); println!("Starting tiny-dns on: http://{listen}"); // run our app with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind(listen).await.unwrap(); - axum::serve(listener, app).await.unwrap(); -} + let api_listener = tokio::net::TcpListener::bind(listen).await.unwrap(); -#[derive(Deserialize, Debug)] -struct CreateUserPayload { - email: String, - password: Option, -} + // DNS server + let dns_addr = config.dns_addr.unwrap_or("[::]:3053".to_owned()); -#[axum::debug_handler] -async fn register( - extract::Path(domain): extract::Path, - extract::State(state): extract::State>, - Json(body): Json, -) -> Json { - dbg!(&body); - Json(json!({ "domain": domain, - "email": body.email, - "password": body.password.unwrap_or_default() })) -} + let mut catalog = Catalog::new(); + catalog.upsert(LowerName::from_str("example.com").unwrap(), vec![]); + let mut server_future = ServerFuture::new(catalog); + let socket = UdpSocket::bind(&dns_addr) + .await + .expect("Failed to bind DNS socket"); + server_future.register_socket(socket); + println!("DNS server running on {}", dns_addr); -#[axum::debug_handler] -async fn update( - extract::State(state): extract::State>, - headers: HeaderMap, - extract::Json(subdomain): extract::Json, -) -> Result, StatusCode> { - let user = User::from(&headers); - // let (_, domain) = DnsRecord::split(&subdomain.subdomain); - if !state.is_allowed(&user, &subdomain.subdomain) { - info!( - "Unauthorized update attempt by user: {} @ {}", - user.login, subdomain.subdomain - ); - return Err(StatusCode::FORBIDDEN); - } - let sub = DnsRecord::from(subdomain); - dbg!(&sub); - if let Ok(mut domains) = state.domains.lock() { - domains.replace(sub); - Ok(Json(json!("OK"))) - } else { - Err(StatusCode::INTERNAL_SERVER_ERROR) - } -} - -#[axum::debug_handler] -async fn delete( - extract::State(state): extract::State>, - headers: HeaderMap, - extract::Json(subdomain): extract::Json, -) -> Result, StatusCode> { - let user = User::from(&headers); - if !state.is_allowed(&user, &subdomain.subdomain) { - info!( - "Unauthorized update attempt by user: {} @ {}", - user.login, subdomain.subdomain - ); - return Err(StatusCode::FORBIDDEN); - } - let sub = DnsRecord::from(subdomain); - if let Ok(mut domains) = state.domains.lock() { - domains.remove(&sub); - Ok(Json(json!("OK"))) - } else { - Err(StatusCode::INTERNAL_SERVER_ERROR) - } -} - -#[axum::debug_handler] -async fn status(extract::State(state): extract::State>) -> Markup { - let domains: Vec = state - .domains - .lock() - .expect("Failed to lock AppState.domains") - .iter() - .map(|d| { - html! { - div .row { - (d) div .cell { (d.age()) } - } - } - }) - .collect(); - html! { - (DOCTYPE) - meta charset="utf-8"; - title { "Status" } - link rel="stylesheet" href="dist/styles.css"; - h1 { "Status" } - h4 { "Domains"} - div .table { - @for le in &domains { - { (le) } - } - } - - } -} - -#[axum::debug_handler] -async fn health(headers: HeaderMap) -> Json { - Json(json!({ "status": "OK" })) + let api_handle = axum::serve(api_listener, api); + let dns = server_future.block_until_done(); + let _ = api_handle.await; + let _ = dns.await; } diff --git a/src/model.rs b/src/model.rs index fa1afe7..767c66c 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,12 +1,37 @@ +use hickory_proto::rr::RecordData; use http::HeaderMap; use maud::{Markup, Render, html}; use serde::{Deserialize, Serialize}; +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, +}; use std::{ fmt::Display, hash::Hash, time::{SystemTime, UNIX_EPOCH}, }; +#[derive(Debug, Default)] +pub struct AppState { + pub auths: Arc>>, + pub domains: Arc>>, +} + +impl AppState { + pub fn is_allowed(&self, user: &User, domain: impl Into) -> bool { + let domain = domain.into(); + self.auths.lock().is_ok_and(|a| { + a.iter().any(|u| { + u.login == user.login + && u.key == user.key + && u.scope.iter().any(|s| domain.contains(s)) + }) + }) + } +} + +#[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Deserialize, Serialize)] pub enum DnsType { A, @@ -32,7 +57,7 @@ pub struct JsonDomain { pub rdata: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct DnsRecord { domain: String, qname: String, @@ -136,7 +161,28 @@ impl PartialEq for DnsRecord { } } -#[derive(Debug)] +impl RecordData for DnsRecord { + fn into_rdata(self) -> hickory_proto::rr::RData { + todo!() + } + + fn is_update(&self) -> bool { + true + } + + fn record_type(&self) -> hickory_proto::rr::RecordType { + todo!() + } + + fn try_borrow(data: &hickory_proto::rr::RData) -> Option<&Self> { + todo!() + } + fn try_from_rdata(data: hickory_proto::rr::RData) -> Result { + todo!() + } +} + +#[derive(Debug, Deserialize, Clone)] pub struct User { pub login: String, pub key: String, diff --git a/src/webapi.rs b/src/webapi.rs new file mode 100644 index 0000000..dec5fad --- /dev/null +++ b/src/webapi.rs @@ -0,0 +1,136 @@ +use crate::AppState; +use crate::config::Config; +use crate::model::{DnsRecord, JsonDomain, User}; +use axum::{ + Router, extract, + response::Json, + routing::{get, post}, +}; +use http::{StatusCode, header::HeaderMap}; +use maud::{DOCTYPE, Markup, html}; +use serde::Deserialize; +use serde_json::{Value, json}; +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, +}; +use tower_http::services::ServeDir; +use tracing::info; + +#[derive(Deserialize, Debug)] +pub struct CreateUserPayload { + email: String, + password: Option, +} + +#[axum::debug_handler] +pub async fn register( + extract::Path(domain): extract::Path, + extract::State(state): extract::State>, + Json(body): Json, +) -> Json { + dbg!(&body); + Json(json!({ "domain": domain, + "email": body.email, + "password": body.password.unwrap_or_default() })) +} + +#[axum::debug_handler] +pub async fn update( + extract::State(state): extract::State>, + headers: HeaderMap, + extract::Json(subdomain): extract::Json, +) -> Result, StatusCode> { + let user = User::from(&headers); + // let (_, domain) = DnsRecord::split(&subdomain.subdomain); + if !state.is_allowed(&user, &subdomain.subdomain) { + info!( + "Unauthorized update attempt by user: {} @ {}", + user.login, subdomain.subdomain + ); + return Err(StatusCode::FORBIDDEN); + } + let sub = DnsRecord::from(subdomain); + dbg!(&sub); + if let Ok(mut domains) = state.domains.lock() { + domains.replace(sub); + Ok(Json(json!("OK"))) + } else { + Err(StatusCode::INTERNAL_SERVER_ERROR) + } +} + +#[axum::debug_handler] +pub async fn delete( + extract::State(state): extract::State>, + headers: HeaderMap, + extract::Json(subdomain): extract::Json, +) -> Result, StatusCode> { + let user = User::from(&headers); + if !state.is_allowed(&user, &subdomain.subdomain) { + info!( + "Unauthorized update attempt by user: {} @ {}", + user.login, subdomain.subdomain + ); + return Err(StatusCode::FORBIDDEN); + } + let sub = DnsRecord::from(subdomain); + if let Ok(mut domains) = state.domains.lock() { + domains.remove(&sub); + Ok(Json(json!("OK"))) + } else { + Err(StatusCode::INTERNAL_SERVER_ERROR) + } +} + +#[axum::debug_handler] +pub async fn status(extract::State(state): extract::State>) -> Markup { + let domains: Vec = state + .domains + .lock() + .expect("Failed to lock AppState.domains") + .iter() + .map(|d| { + html! { + div .row { + (d) div .cell { (d.age()) } + } + } + }) + .collect(); + html! { + (DOCTYPE) + meta charset="utf-8"; + title { "Status" } + link rel="stylesheet" href="dist/styles.css"; + h1 { "Status" } + h4 { "Domains"} + div .table { + @for le in &domains { + { (le) } + } + } + + } +} + +#[axum::debug_handler] +pub async fn health(headers: HeaderMap) -> Json { + Json(json!({ "status": "OK" })) +} + +pub fn router(config: &Config) -> Router { + let shared_state = Arc::new(AppState { + auths: Arc::new(Mutex::new(config.auths.iter().cloned().collect())), + domains: Arc::new(Mutex::new(HashSet::new())), + }); + // build our application with a single route + Router::new() + .route("/", get(|| async { "Hello, World!" })) + .route("/register/{domain}", post(register)) + .route("/update", post(update)) + .route("/delete", post(delete)) + .route("/status", get(status)) + .nest_service("/dist", ServeDir::new("dist")) + .with_state(shared_state) +}