hickory-dns integrated.

This commit is contained in:
Micha Glave
2026-01-08 18:27:44 +01:00
committed by Micha Glave
parent 4c86b21ca7
commit 7f15efc0cd
8 changed files with 652 additions and 162 deletions

383
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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
```

14
config.toml Normal file
View File

@@ -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"]

27
src/config.rs Normal file
View File

@@ -0,0 +1,27 @@
use serde::Deserialize;
use crate::model::User;
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct Config {
pub dns_addr: Option<String>,
pub api_port: Option<u16>,
pub zone_name: String,
#[serde(default)]
pub auths: Vec<User>,
}
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")
}
}

View File

@@ -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<Mutex<HashSet<User>>>,
domains: Arc<Mutex<HashSet<DnsRecord>>>,
}
impl AppState {
fn is_allowed(&self, user: &User, domain: impl Into<String>) -> 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<String>,
}
// DNS server
let dns_addr = config.dns_addr.unwrap_or("[::]:3053".to_owned());
#[axum::debug_handler]
async fn register(
extract::Path(domain): extract::Path<String>,
extract::State(state): extract::State<Arc<AppState>>,
Json(body): Json<CreateUserPayload>,
) -> Json<Value> {
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<Arc<AppState>>,
headers: HeaderMap,
extract::Json(subdomain): extract::Json<JsonDomain>,
) -> Result<Json<Value>, 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<Arc<AppState>>,
headers: HeaderMap,
extract::Json(subdomain): extract::Json<JsonDomain>,
) -> Result<Json<Value>, 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<Arc<AppState>>) -> Markup {
let domains: Vec<Markup> = 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<Value> {
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;
}

View File

@@ -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<Mutex<HashSet<User>>>,
pub domains: Arc<Mutex<HashSet<DnsRecord>>>,
}
impl AppState {
pub fn is_allowed(&self, user: &User, domain: impl Into<String>) -> 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<Self, hickory_proto::rr::RData> {
todo!()
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct User {
pub login: String,
pub key: String,

136
src/webapi.rs Normal file
View File

@@ -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<String>,
}
#[axum::debug_handler]
pub async fn register(
extract::Path(domain): extract::Path<String>,
extract::State(state): extract::State<Arc<AppState>>,
Json(body): Json<CreateUserPayload>,
) -> Json<Value> {
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<Arc<AppState>>,
headers: HeaderMap,
extract::Json(subdomain): extract::Json<JsonDomain>,
) -> Result<Json<Value>, 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<Arc<AppState>>,
headers: HeaderMap,
extract::Json(subdomain): extract::Json<JsonDomain>,
) -> Result<Json<Value>, 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<Arc<AppState>>) -> Markup {
let domains: Vec<Markup> = 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<Value> {
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)
}