241 lines
5.7 KiB
Rust
241 lines
5.7 KiB
Rust
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,
|
|
AAAA,
|
|
#[default]
|
|
TXT,
|
|
}
|
|
|
|
impl Display for DnsType {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
DnsType::A => write!(f, "A"),
|
|
DnsType::AAAA => write!(f, "AAAA"),
|
|
DnsType::TXT => write!(f, "TXT"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct JsonDomain {
|
|
pub subdomain: String,
|
|
pub rtype: Option<DnsType>,
|
|
pub rdata: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
pub struct DnsRecord {
|
|
domain: String,
|
|
qname: String,
|
|
time: u64,
|
|
rtype: DnsType,
|
|
rdata: String,
|
|
}
|
|
|
|
impl DnsRecord {
|
|
pub fn now() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs()
|
|
}
|
|
|
|
/// Split a full domain into subdomain and domain.
|
|
pub fn split(fulldomain: &str) -> (String, String) {
|
|
let parts: Vec<&str> = fulldomain.split('.').collect();
|
|
|
|
if parts.len() < 2 {
|
|
("_".to_owned(), fulldomain.to_owned())
|
|
} else {
|
|
let l = parts.len() - 2;
|
|
(parts[0..l].join("."), parts[l..].join("."))
|
|
}
|
|
}
|
|
|
|
/// Create a new domain with a lifetime of one hour.
|
|
pub fn new_txt(subdomain: String, txt: String) -> Self {
|
|
let (qname, domain) = Self::split(&subdomain);
|
|
DnsRecord {
|
|
domain,
|
|
qname,
|
|
time: Self::now() + 3600,
|
|
rtype: DnsType::TXT,
|
|
rdata: txt,
|
|
}
|
|
}
|
|
|
|
pub fn age(&self) -> u64 {
|
|
Self::now() + 3600 - self.time
|
|
}
|
|
}
|
|
|
|
impl Display for DnsRecord {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"{}: {} {} {}",
|
|
self.domain, self.qname, self.rtype, self.rdata
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Render for DnsRecord {
|
|
fn render(&self) -> Markup {
|
|
html! {
|
|
div .cell { (self.domain) }
|
|
div .cell { (self.qname) }
|
|
div .cell { (self.rtype) }
|
|
div .cell { (self.rdata) }
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<JsonDomain> for DnsRecord {
|
|
fn from(json: JsonDomain) -> Self {
|
|
let (qname, domain) = Self::split(&json.subdomain);
|
|
DnsRecord {
|
|
domain,
|
|
qname,
|
|
time: SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs()
|
|
+ 3600,
|
|
rtype: json.rtype.unwrap_or_default(),
|
|
rdata: json.rdata,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Hash for DnsRecord {
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
self.domain.hash(state);
|
|
self.qname.hash(state);
|
|
self.rtype.hash(state);
|
|
self.rdata.hash(state);
|
|
}
|
|
}
|
|
|
|
impl Eq for DnsRecord {}
|
|
|
|
impl PartialEq for DnsRecord {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.domain == other.domain
|
|
&& self.qname == other.qname
|
|
&& self.rtype == other.rtype
|
|
&& self.rdata == other.rdata
|
|
}
|
|
}
|
|
|
|
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,
|
|
pub scope: Vec<String>,
|
|
}
|
|
|
|
impl From<&HeaderMap> for User {
|
|
fn from(headers: &HeaderMap) -> Self {
|
|
let login = headers
|
|
.get("x-api-user")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or_default()
|
|
.to_string();
|
|
let key = headers
|
|
.get("x-api-key")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or_default()
|
|
.to_string();
|
|
User {
|
|
login,
|
|
key,
|
|
scope: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Eq for User {}
|
|
|
|
impl PartialEq for User {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.login == other.login && self.key == other.key
|
|
}
|
|
}
|
|
|
|
impl Hash for User {
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
self.login.hash(state);
|
|
self.key.hash(state);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn domain_split() {
|
|
let (subdomain, domain) = DnsRecord::split("example.com");
|
|
assert_eq!(subdomain, "");
|
|
assert_eq!(domain, "example.com");
|
|
|
|
let (subdomain, domain) = DnsRecord::split("sub.example.com");
|
|
assert_eq!(subdomain, "sub");
|
|
assert_eq!(domain, "example.com");
|
|
|
|
let (subdomain, domain) = DnsRecord::split("sub.sub.example.com");
|
|
assert_eq!(subdomain, "sub.sub");
|
|
assert_eq!(domain, "example.com");
|
|
}
|