hickory-dns integrated.
This commit is contained in:
27
src/config.rs
Normal file
27
src/config.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
188
src/main.rs
188
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<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;
|
||||
}
|
||||
|
||||
50
src/model.rs
50
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<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
136
src/webapi.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user