Compare commits
No commits in common. "rewrite-v0.2" and "main" have entirely different histories.
rewrite-v0
...
main
77 changed files with 3869 additions and 6789 deletions
2283
Cargo.lock
generated
2283
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
46
Cargo.toml
46
Cargo.toml
|
@ -1,31 +1,35 @@
|
||||||
[package]
|
[package]
|
||||||
name = "nomilo"
|
name = "nomilo"
|
||||||
version = "0.2.0-dev"
|
version = "0.1.0-dev"
|
||||||
authors = ["DNS Witch Collective <dns-witch@dns-witch.eu.org>"]
|
authors = ["DNS Witch Collective <dns-witch@familier.net.eu.org>"]
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://git.familier.net.eu.org/dns-witch/nomilo"
|
repository = "https://git.familier.net.eu.org/dns-witch/nomilo"
|
||||||
|
|
||||||
[dependencies]
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
trust-dns-client = { version = "0.22", features = ["dnssec-openssl"] }
|
||||||
|
trust-dns-proto = "0.22"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
#uuid = { version = "1.11", features = ["v4", "serde"] }
|
rocket = { version = "0.5.0-rc.2", features = ["json"], default-features = false }
|
||||||
#chrono = { version = "0.4", features = ["serde"] }
|
rocket_sync_db_pools = { default-features = false, features = ["diesel_sqlite_pool"], version = "0.1.0-rc.2"}
|
||||||
#humantime = "2.1"
|
base64 = "0.21"
|
||||||
tokio = {version = "1", default-features = false, features = [ "macros", "rt-multi-thread" ] }
|
uuid = { version = "0.8", features = ["v4", "serde"] }
|
||||||
#clap = { version = "4", features = [ "derive", "cargo" ] }
|
diesel = { version = "1.4", features = ["sqlite", "chrono"] }
|
||||||
#argon2 = { version = "0.5", default-features = false, features = ["alloc", "password-hash"] }
|
diesel_migrations = "1.4"
|
||||||
#rand = "0.8"
|
diesel-derive-enum = { version = "1", features = ["sqlite"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
humantime = "2.1"
|
||||||
|
tokio = "1"
|
||||||
|
figment = { version = "0.10", features = ["toml", "env"] }
|
||||||
|
clap = {version = "3", features = ["derive", "cargo"]}
|
||||||
|
argon2 = {version = "0.4", 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" ]}
|
# From trust-dns-client
|
||||||
axum = { version = "0.8.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio", "original-uri" ]}
|
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
bb8 = { version = "0.9" }
|
# From rocket / cookie-rs
|
||||||
rusqlite = { version = "0.32"}
|
time = "0.3"
|
||||||
async-trait = { version = "0.1" }
|
|
||||||
tower-http = { version = "0.6", default-features = false, features = [ "fs" ]}
|
|
||||||
fluent-bundle = "0.15.3"
|
|
||||||
unic-langid = "*"
|
|
||||||
tower = "*"
|
|
||||||
serde_urlencoded = "*"
|
|
||||||
|
|
13
README.md
13
README.md
|
@ -2,3 +2,16 @@
|
||||||
|
|
||||||
> This projet is in a very experimental state
|
> This projet is in a very experimental state
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```
|
||||||
|
nomilo server run
|
||||||
|
nomilo user add --name Admin --password supersecret --email admin@localhost --is-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you prefer to not run the webserver before setting up the first user
|
||||||
|
```
|
||||||
|
nomilo server migrate
|
||||||
|
nomilo user add --name Admin --password supersecret --email admin@localhost --is-admin
|
||||||
|
nomilo server run
|
||||||
|
```
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,55 +0,0 @@
|
||||||
function templateString(text, args) {
|
|
||||||
for (const argName in args) {
|
|
||||||
text = text.replace(`{${argName}}`, args[argName]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFormRow(templateName) {
|
|
||||||
const allRows = document.querySelectorAll(`[data-new-item-template="${templateName}"]`);
|
|
||||||
const templateElement = allRows[0];
|
|
||||||
const nextId = allRows.length + 1;
|
|
||||||
|
|
||||||
const newItem = templateElement.cloneNode(true);
|
|
||||||
|
|
||||||
newItem.querySelectorAll('[data-new-item-skip]')
|
|
||||||
.forEach(node => node.remove());
|
|
||||||
|
|
||||||
// Reset form fields
|
|
||||||
newItem.querySelectorAll('input')
|
|
||||||
.forEach(input => { input.value = '' });
|
|
||||||
|
|
||||||
newItem.querySelectorAll('textarea')
|
|
||||||
.forEach(input => { input.value = '' });
|
|
||||||
|
|
||||||
// Template attributes
|
|
||||||
const templatedAttrNodes = newItem.querySelectorAll('[data-new-item-template-attr]');
|
|
||||||
for (const node of templatedAttrNodes) {
|
|
||||||
const attributes = node.dataset.newItemTemplateAttr.split(/\s/);
|
|
||||||
for (const attribute of attributes) {
|
|
||||||
const templatedString = node.getAttribute(`data-template-${attribute}`);
|
|
||||||
node.setAttribute(attribute, templateString(templatedString, {i: nextId}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template content
|
|
||||||
const templatedNodes = newItem.querySelectorAll('[data-new-item-template-content]');
|
|
||||||
for (const node of templatedNodes) {
|
|
||||||
const templatedString = node.dataset.newItemTemplateContent;
|
|
||||||
node.innerHTML = templateString(templatedString, {i: nextId})
|
|
||||||
}
|
|
||||||
|
|
||||||
allRows[allRows.length - 1].insertAdjacentElement('afterend', newItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUpAddFormRow() {
|
|
||||||
const buttons = document.querySelectorAll('button[data-new-item]');
|
|
||||||
|
|
||||||
for (const button of buttons) {
|
|
||||||
button.addEventListener('click', () => addFormRow(button.dataset.newItem))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', setUpAddFormRow);
|
|
|
@ -1,301 +0,0 @@
|
||||||
@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;
|
|
||||||
padding: .5rem
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-weight: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
article.domain {
|
|
||||||
margin-bottom: 2em;
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 3em;
|
|
||||||
|
|
||||||
h3.folder-tab {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 1rem;
|
|
||||||
border-top-left-radius: .3rem;
|
|
||||||
background-color: #f2e0fd;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: inherit;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
|
|
||||||
~ .sep {
|
|
||||||
content: '';
|
|
||||||
width: 3em;
|
|
||||||
background-color: #f2e0fd;
|
|
||||||
height: 100%;
|
|
||||||
clip-path: url("#corner-folder-tab-right");
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.records {
|
|
||||||
background: #f2e0fd;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0 .3rem .3rem .3rem;
|
|
||||||
|
|
||||||
button,
|
|
||||||
a.button {
|
|
||||||
background-color: #f2e0fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rrset {
|
|
||||||
.rtype {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding: 1rem 0 1rem 2rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
align-items: baseline;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
gap: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rdata {
|
|
||||||
display: flex;
|
|
||||||
gap: .2rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rdata-main {
|
|
||||||
display: flex;
|
|
||||||
gap: .3rem;
|
|
||||||
margin-right: .1rem;
|
|
||||||
|
|
||||||
.pill {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rdata-complementary {
|
|
||||||
font-size: .9em;
|
|
||||||
gap: .2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
height: 1em;
|
|
||||||
width: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon {
|
|
||||||
padding: 0;
|
|
||||||
width: 2em;
|
|
||||||
height: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
color: white;
|
|
||||||
background-color: #850085;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form h3 {
|
|
||||||
margin: 0;
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
/*content: '';*/
|
|
||||||
height: 1px;
|
|
||||||
flex: 1;
|
|
||||||
display: block;
|
|
||||||
background-color: black;
|
|
||||||
margin-left: .5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
margin-top: .5rem;
|
|
||||||
|
|
||||||
border-left: .25rem solid #a8a8a8;
|
|
||||||
padding-left: .5rem;
|
|
||||||
|
|
||||||
legend {
|
|
||||||
padding: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
bottom: -.55em;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + button.form-new-item {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"] {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input {
|
|
||||||
display: grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-template-rows: auto auto;
|
|
||||||
grid-auto-columns: 1fr;
|
|
||||||
column-gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: #a20000;
|
|
||||||
color: #fff;
|
|
||||||
margin: 0;
|
|
||||||
margin-top: .15rem;
|
|
||||||
padding: .25rem;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error::before {
|
|
||||||
content: '';
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
background-color: #fff;
|
|
||||||
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-exclamation-triangle' viewBox='0 0 16 16'><path d='M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z'/><path d='M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z'/></svg>");
|
|
||||||
display: inline-block;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: cover;
|
|
||||||
margin-right: .25rem;
|
|
||||||
margin-bottom: -.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help {
|
|
||||||
margin: 0;
|
|
||||||
margin-top: .15rem;
|
|
||||||
font-style: oblique;
|
|
||||||
font-size: .9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
textarea {
|
|
||||||
padding: .25rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-action {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.form-new-item {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
text-decoration: underline;
|
|
||||||
border-radius: 0;
|
|
||||||
color: #850085;
|
|
||||||
margin-top: .5rem;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
background: none;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #850085;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
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"; };
|
|
||||||
};
|
|
|
@ -1,16 +1,8 @@
|
||||||
services:
|
services:
|
||||||
|
|
||||||
knot:
|
knot:
|
||||||
image: cznic/knot:3.4
|
image: cznic/knot
|
||||||
volumes:
|
volumes:
|
||||||
- ./zones:/storage/zones:ro
|
- ./zones:/storage/zones:ro
|
||||||
- ./config:/config:ro
|
- ./config:/config:ro
|
||||||
command: knotd --verbose
|
command: knotd
|
||||||
network_mode: host
|
|
||||||
named:
|
|
||||||
image: internetsystemsconsortium/bind9:9.20
|
|
||||||
volumes:
|
|
||||||
- ./zones:/var/lib/bind:ro
|
|
||||||
- ./config:/etc/bind:ro
|
|
||||||
#command: named -g
|
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|
|
@ -6,16 +6,7 @@ example.com. IN SOA ns.example.com. admin.example.com. (
|
||||||
300 ; minimum (5 minutes)
|
300 ; minimum (5 minutes)
|
||||||
)
|
)
|
||||||
|
|
||||||
example.com. 600 IN A 198.51.100.3
|
|
||||||
example.com. 600 IN AAAA 2001:db8:cafe:bc68::2
|
|
||||||
|
|
||||||
example.com. 3600 IN MX 1 srv1.example.com.
|
|
||||||
example.com. 3600 IN MX 100 mail.example.net.
|
|
||||||
|
|
||||||
_imap._tcp.example.com. 3600 IN SRV 1 1 143 mail.example.net.
|
|
||||||
|
|
||||||
example.com. 84600 IN NS ns.example.com.
|
example.com. 84600 IN NS ns.example.com.
|
||||||
ns.example.com. 84600 IN A 198.51.100.3
|
|
||||||
|
|
||||||
srv1.example.com. 600 IN A 198.51.100.3
|
srv1.example.com. 600 IN A 198.51.100.3
|
||||||
srv1.example.com. 600 IN AAAA 2001:db8:cafe:bc68::2
|
srv1.example.com. 600 IN AAAA 2001:db8:cafe:bc68::2
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
## Zone content
|
|
||||||
|
|
||||||
zone-header = Zone { $zone_name }
|
|
||||||
|
|
||||||
zone-content-title = Zone content
|
|
||||||
zone-content-records-header = Records
|
|
||||||
zone-content-aliases-header = Aliases
|
|
||||||
|
|
||||||
zone-content-section-web-header = Web
|
|
||||||
zone-content-section-mail-header = E-mail
|
|
||||||
zone-content-section-services-header = Services
|
|
||||||
zone-content-section-general-header = General
|
|
||||||
|
|
||||||
zone-content-record-type-addresses =
|
|
||||||
.type-name = IP addresses
|
|
||||||
|
|
||||||
zone-content-record-type-mailservers =
|
|
||||||
.type-name = E-mail servers
|
|
||||||
.data-preference = Preference: { $preference }
|
|
||||||
|
|
||||||
zone-content-record-type-nameservers =
|
|
||||||
.type-name = Name servers
|
|
||||||
|
|
||||||
zone-content-record-type-service =
|
|
||||||
.type-name = Service
|
|
||||||
.data-priority = Priority: { $priority }
|
|
||||||
.data-weight = Weight: { $weight }
|
|
||||||
|
|
||||||
zone-content-new-record-button = New record
|
|
||||||
|
|
||||||
## Create record
|
|
||||||
|
|
||||||
new-record-title = New record
|
|
||||||
|
|
||||||
record-creation-process-heading = Create a new record in zone { $zone }
|
|
||||||
|
|
||||||
record-choose-name-heading = Choose the subdomain name of the new record
|
|
||||||
|
|
||||||
record-input-name =
|
|
||||||
.input-label = Subdomain of the new record
|
|
||||||
.help-description = Only the subdomain, without the parent domain. For instance, "www" to create the subdomain "www.{ $zone }".
|
|
||||||
.error-domain-characters_not_permitted = Domain name label "{ $label }" contains characters not permitted. The allowed characters are lowercase alphanumeric characters (a-z and 0-9), the dash ('-'), the underscore ('_') and the forward slash ('/').
|
|
||||||
|
|
||||||
button-create-record-next-step = Next step
|
|
||||||
|
|
||||||
record-input-ttl =
|
|
||||||
.input-label = Duration in cache (TTL)
|
|
||||||
.help = In seconds, optional, default to 1 hour
|
|
||||||
.error-input-type_error = The duration in cache must be a positif integer number.
|
|
||||||
.error-record-parse-number = The duration in cache must be a positif integer number.
|
|
||||||
|
|
||||||
record-input-addresses =
|
|
||||||
.input-label = IP address #{ $index }
|
|
||||||
.error-record-parse-ip = Unexpected IP address format. The IP address
|
|
||||||
should be either an IPv4 address, like <code>198.51.100.3</code>, or an IPv6
|
|
||||||
address, like <code>2001:db8:cafe:bc68::2</code>.
|
|
||||||
.error-input-missing_value = At least one IP addresses is required.
|
|
||||||
|
|
||||||
button-add-address = Add an other address
|
|
||||||
|
|
||||||
record-input-mailservers =
|
|
||||||
.legend = Mail server #{ $index }
|
|
||||||
.input-label-server-name = Server name
|
|
||||||
.input-label-preference = Preference
|
|
||||||
|
|
||||||
button-add-mailserver = Add an other mailserver
|
|
||||||
|
|
||||||
record-input-spf =
|
|
||||||
.legend = Sender policy (SPF)
|
|
||||||
.input-label = Sender policy
|
|
||||||
|
|
||||||
record-input-dmarc =
|
|
||||||
.legend = Error reporting policy (DMARC)
|
|
||||||
.input-label = Error reporting policy
|
|
||||||
|
|
||||||
record-input-dkim =
|
|
||||||
.legend = Cryptographic signature key (DKIM) #{ $index }
|
|
||||||
.input-label-selector = Selector
|
|
||||||
.input-label-signing-key = Signing key
|
|
||||||
|
|
||||||
button-add-dkim-key = Add an other signature key
|
|
||||||
|
|
||||||
record-config-web-heading = Configure a web site for the domain <strong>{ $name }</strong>
|
|
||||||
|
|
||||||
record-config-section-web =
|
|
||||||
.servers = Web servers
|
|
||||||
|
|
||||||
record-config-mail-heading = Configure e-mails for the domain <strong>{ $name }</strong>
|
|
||||||
|
|
||||||
record-config-section-mail =
|
|
||||||
.servers = Mail servers
|
|
||||||
.security = Security
|
|
||||||
.dkim = Cryptographic signature
|
|
||||||
|
|
||||||
button-save-configuration = Save configuration
|
|
|
@ -1,95 +0,0 @@
|
||||||
## Zone content
|
|
||||||
|
|
||||||
zone-header = Zone { $zone_name }
|
|
||||||
|
|
||||||
zone-content-title = Contenu de la zone
|
|
||||||
zone-content-records-header = Enregistrements
|
|
||||||
zone-content-aliases-header = Alias
|
|
||||||
|
|
||||||
zone-content-section-web-header = Web
|
|
||||||
zone-content-section-mail-header = Courriel
|
|
||||||
zone-content-section-services-header = Services
|
|
||||||
zone-content-section-general-header = Général
|
|
||||||
|
|
||||||
zone-content-record-type-addresses =
|
|
||||||
.type-name = Adresses IP
|
|
||||||
|
|
||||||
zone-content-record-type-mailservers =
|
|
||||||
.type-name = Serveurs de courriel
|
|
||||||
.data-preference = Préférence : { $preference }
|
|
||||||
|
|
||||||
zone-content-record-type-nameservers =
|
|
||||||
.type-name = Serveurs de noms
|
|
||||||
|
|
||||||
zone-content-record-type-service =
|
|
||||||
.type-name = Services
|
|
||||||
.data-priority = Priorité : { $priority }
|
|
||||||
.data-weight = Poids : { $weight }
|
|
||||||
|
|
||||||
zone-content-new-record-button = Nouvel enregistrement
|
|
||||||
|
|
||||||
## Create record
|
|
||||||
|
|
||||||
new-record-title = Nouvel enregistrement
|
|
||||||
|
|
||||||
record-creation-process-heading = Créer un nouvel enregistrement dans la zone { $zone }
|
|
||||||
|
|
||||||
record-choose-name-heading = Choisir le nom du sous-domaine du nouvel enregistrement
|
|
||||||
|
|
||||||
record-input-name =
|
|
||||||
.input-label = Sous-domaine du nouvel enregistrement
|
|
||||||
.help-description = Seulement le sous-domaine, sans le domaine parent. Par exemple, “www” pour créer le sous-domaine “www.{ $zone }”.
|
|
||||||
.error-domain-characters_not_permitted = Le segment “{ $label }” du nom de domain contient des caractères interdits. Les caractères autorisés sont les caractères alphanumériques (a-z et 0-9), le tiret (“-”), le tiret bas (“_”) et le slash (“/”).
|
|
||||||
|
|
||||||
button-create-record-next-step = Étape suivante
|
|
||||||
|
|
||||||
record-input-ttl =
|
|
||||||
.input-label = Durée dans le cache (TTL)
|
|
||||||
.help = En secondes, optionnel, 1 heure par défaut
|
|
||||||
.error-input-type_error = La durée dans le cache doit être un nombre entier positif.
|
|
||||||
.error-record-parse-number = La durée dans le cache doit être un nombre entier positif.
|
|
||||||
|
|
||||||
record-input-addresses =
|
|
||||||
.input-label = Adresse IP #{ $index }
|
|
||||||
.error-record-parse-ip = Format d’adresse IP inconnu. L’adresse IP doit être
|
|
||||||
soit une adresse IPv4, comme <code>198.51.100.3</code>, soit une adresse IPv6,
|
|
||||||
comme <code>2001:db8:cafe:bc68::2</code>.
|
|
||||||
.error-input-missing_value = Au moins une adresse IP est requise.
|
|
||||||
|
|
||||||
button-add-address = Ajouter une autre adresse
|
|
||||||
|
|
||||||
record-input-mailservers =
|
|
||||||
.legend = Serveur de courriel #{ $index }
|
|
||||||
.input-label-server-name = Nom du serveur
|
|
||||||
.input-label-preference = Préférence
|
|
||||||
|
|
||||||
button-add-mailserver = Ajouter un autre serveur de courriel
|
|
||||||
|
|
||||||
record-input-spf =
|
|
||||||
.input-label = Politique d’envoi
|
|
||||||
.legend = Politique d’envoi (SPF)
|
|
||||||
|
|
||||||
record-input-dmarc =
|
|
||||||
.legend = Politique de signalement d’erreurs (DMARC)
|
|
||||||
.input-label = Politique de signalement d’erreurs
|
|
||||||
|
|
||||||
record-input-dkim =
|
|
||||||
.legend = Clé de signature cryptographique #{ $index }
|
|
||||||
.input-label-selector = Sélecteur
|
|
||||||
.input-label-signing-key = Clé de signature
|
|
||||||
|
|
||||||
button-add-dkim-key = Ajouter une autre clé de signature
|
|
||||||
|
|
||||||
record-config-web-heading = Configurer un site web pour le domaine <strong>{ $name }</strong>
|
|
||||||
|
|
||||||
record-config-section-web =
|
|
||||||
.servers = Serveurs Web
|
|
||||||
|
|
||||||
record-config-mail-heading = Configurer le courriel pour le domaine <strong>{ $name }</strong>
|
|
||||||
|
|
||||||
record-config-section-mail =
|
|
||||||
.servers = Serveurs de courriel
|
|
||||||
.security = Sécurité
|
|
||||||
.dkim = Signature cryptographique (DKIM)
|
|
||||||
|
|
||||||
button-save-configuration = Sauvegarder la configuration
|
|
3
public/images/plus.svg
Normal file
3
public/images/plus.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 245 B |
44
public/scripts/api.js
Normal file
44
public/scripts/api.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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,
|
||||||
|
};
|
328
public/scripts/records.js
Normal file
328
public/scripts/records.js
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
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`<span>${keys[0][1]}</span>`;
|
||||||
|
} else {
|
||||||
|
return html`
|
||||||
|
<dl>
|
||||||
|
${keys.map(([name, value]) => {return html`<dt><span>${rdataInputProperties[name].label}</span></dt><dd>${value}</dd>`})}
|
||||||
|
</dl>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function RecordsByName({ name, recordSets }) {
|
||||||
|
return html`
|
||||||
|
<article class="rrsets-per-name">
|
||||||
|
<h3 class="record-name">${name}</h4>
|
||||||
|
<div class="rrsets-per-type">
|
||||||
|
${Object.entries(recordSets).map(
|
||||||
|
([type, records]) => {
|
||||||
|
return html`
|
||||||
|
<article class="rrset-per-type">
|
||||||
|
<h4 class="record-type">${recordTypeNames[type]}</h4>
|
||||||
|
<ul class="rrset-rdata">
|
||||||
|
${records.map(record => html`<li><${FriendlyRecord} type=${type} record=${record}/></li>`)}
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`
|
||||||
|
<section class="new-record">
|
||||||
|
<header>
|
||||||
|
<h2>Nouvel enregistrement</h2>
|
||||||
|
</header>
|
||||||
|
<form>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="domain">Domaine</label>
|
||||||
|
<div class="combined-input">
|
||||||
|
<input type="text" id="domain" name="domain" value=${recordData.Name} onInput=${setRecordDataFactory('Name')}/>
|
||||||
|
<span>.${ zone }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="record_type">Type d'enregistrement</label>
|
||||||
|
<select id="record_type" name="record_type" onChange=${(e) => { setRecordType(e.target.value); resetData() }}>
|
||||||
|
${Object.entries(recordTypeNames).map(([type, name]) => html`<option value="${type}">${name}</option>`)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
${friendlyRecordDataConfig[recordType].fields.map(fieldName => html`
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="${fieldName}">${rdataInputProperties[fieldName].label}</label>
|
||||||
|
<input id="${fieldName}" type="${rdataInputProperties[fieldName].type}" onInput=${setRecordDataFactory(fieldName)}></input>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="ttl">Durée dans le cache</label>
|
||||||
|
<input type="number" name="ttl" id="ttl" value=${recordData.TTL} onInput=${setRecordDataFactory('TTL')}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<article class="preview">
|
||||||
|
<h3>Prévisualisation des changements</h3>
|
||||||
|
<p>
|
||||||
|
<img src="/images/plus.svg" alt="Ajout" title="Ajout" class="addition"/>
|
||||||
|
<code class="addition">
|
||||||
|
${realRecordData.Name === zone ? '@' : realRecordData.Name} ${realRecordData.TTL} ${realRecordData.Class} ${realType} ${realType != '' ? realRecordDataConfig[realType].fields.map(field => realRecordData[field]).join(' ') : ''}
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<div>
|
||||||
|
<input type="submit" onClick=${createNewRecord} value="Ajouter"/>
|
||||||
|
<button type="reset" onClick=${e => { resetData(true); e.preventDefault() }}>Annuler</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ZoneRecords({ zone }) {
|
||||||
|
return html`
|
||||||
|
<${NewRecordFormFriendly} zone=${zone}/>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<header>
|
||||||
|
<h2>Contenu de la zone</h2>
|
||||||
|
<button>Éditer la zone</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="zone-content">
|
||||||
|
<${RecordListFriendly} zone=${zone} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(element, { zone }) {
|
||||||
|
render(html`<${ZoneRecords} zone=${zone} />`, element);
|
||||||
|
};
|
7
public/scripts/vendor/licenses.txt
vendored
Normal file
7
public/scripts/vendor/licenses.txt
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
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
|
202
public/scripts/vendor/preact/LICENSE-htm
vendored
Normal file
202
public/scripts/vendor/preact/LICENSE-htm
vendored
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
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.
|
21
public/scripts/vendor/preact/LICENSE-preact
vendored
Normal file
21
public/scripts/vendor/preact/LICENSE-preact
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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.
|
1
public/scripts/vendor/preact/standalone.js
vendored
Normal file
1
public/scripts/vendor/preact/standalone.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
public/styles/login.css
Normal file
13
public/styles/login.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
form {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 40ch;
|
||||||
|
margin: 25vh auto 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
158
public/styles/main.css
Normal file
158
public/styles/main.css
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
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;
|
||||||
|
}
|
183
public/styles/zone.css
Normal file
183
public/styles/zone.css
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -1,65 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::resources::zone::ZoneModel;
|
|
||||||
|
|
||||||
pub trait Db: ZoneModel + Send + Sync {}
|
|
||||||
pub type BoxedDb = Arc<dyn Db>;
|
|
||||||
|
|
||||||
impl Db for sqlite::SqliteDB {}
|
|
||||||
|
|
||||||
pub mod sqlite {
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct SqliteDB {
|
|
||||||
pub pool: bb8::Pool<SqliteConnManager>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SqliteDB {
|
|
||||||
pub async fn new(path: PathBuf) -> Self {
|
|
||||||
let pool = bb8::Pool::builder()
|
|
||||||
.build(SqliteConnManager::new(path))
|
|
||||||
.await
|
|
||||||
.expect("Unable to connect to database");
|
|
||||||
|
|
||||||
SqliteDB {
|
|
||||||
pool,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct SqliteConnManager {
|
|
||||||
path: Arc<PathBuf>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SqliteConnManager {
|
|
||||||
pub fn new(path: PathBuf) -> Self {
|
|
||||||
SqliteConnManager {
|
|
||||||
path: Arc::new(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl bb8::ManageConnection for SqliteConnManager {
|
|
||||||
type Connection = rusqlite::Connection;
|
|
||||||
type Error = rusqlite::Error;
|
|
||||||
|
|
||||||
async fn connect(&self) -> Result<Self::Connection, Self::Error> {
|
|
||||||
let opt = self.clone();
|
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
rusqlite::Connection::open(opt.path.as_ref())
|
|
||||||
}).await.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
|
|
||||||
tokio::task::block_in_place(|| conn.execute_batch(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_broken(&self, _conn: &mut Self::Connection) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
271
src/dns/dns_connector.rs
Normal file
271
src/dns/dns_connector.rs
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
use trust_dns_proto::DnsHandle;
|
||||||
|
use trust_dns_client::client::ClientHandle;
|
||||||
|
use trust_dns_client::rr::{DNSClass, RecordType};
|
||||||
|
use trust_dns_client::op::{UpdateMessage, OpCode, MessageType, Message, Query, ResponseCode, Edns};
|
||||||
|
use trust_dns_client::error::ClientError;
|
||||||
|
|
||||||
|
use super::{Name, Record, RData};
|
||||||
|
use super::client::{ClientResponse, DnsClient};
|
||||||
|
use super::connector::{RecordConnector, ZoneConnector, ConnectorError, ConnectorResult};
|
||||||
|
|
||||||
|
|
||||||
|
const MAX_PAYLOAD_LEN: u16 = 1232;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DnsConnectorError {
|
||||||
|
ClientError(ClientError),
|
||||||
|
ResponceNotOk {
|
||||||
|
code: ResponseCode,
|
||||||
|
zone: Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DnsConnectorClient {
|
||||||
|
client: DnsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DnsConnectorClient {
|
||||||
|
pub fn new(client: DnsClient) -> Self {
|
||||||
|
DnsConnectorClient {
|
||||||
|
client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectorError for DnsConnectorError {
|
||||||
|
fn zone_name(&self) -> Option<Name> {
|
||||||
|
if let DnsConnectorError::ResponceNotOk { code: _code, zone } = self {
|
||||||
|
Some(zone.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_proto_error(&self) -> bool {
|
||||||
|
return matches!(self, DnsConnectorError::ClientError(_));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DnsConnectorError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
DnsConnectorError::ClientError(e) => {
|
||||||
|
write!(f, "DNS client error: {}", e)
|
||||||
|
},
|
||||||
|
DnsConnectorError::ResponceNotOk { code, zone } => {
|
||||||
|
write!(f, "Query for zone \"{}\" failed with code \"{}\"", zone, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_edns(message: &mut Message) {
|
||||||
|
let edns = message.extensions_mut().get_or_insert_with(Edns::new);
|
||||||
|
edns.set_max_payload(MAX_PAYLOAD_LEN);
|
||||||
|
edns.set_version(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RecordConnector for DnsConnectorClient {
|
||||||
|
//type Error = DnsConnectorError;
|
||||||
|
|
||||||
|
async fn get_records(&mut self, zone: Name, class: DNSClass) -> ConnectorResult<Vec<Record>>
|
||||||
|
{
|
||||||
|
let response = {
|
||||||
|
let query = self.client.query(zone.clone(), class, RecordType::AXFR);
|
||||||
|
match query.await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
Ok(v) => v,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if response.response_code() != ResponseCode::NoError {
|
||||||
|
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
||||||
|
code: response.response_code(),
|
||||||
|
zone: zone,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let answers = response.answers();
|
||||||
|
let mut records: Vec<_> = answers.to_vec().into_iter()
|
||||||
|
.filter(|record| record.data().is_some() && !matches!(record.data().unwrap(), RData::NULL { .. } | RData::DNSSEC(_)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// AXFR response ends with SOA, we remove it so it is not doubled in the response.
|
||||||
|
records.pop();
|
||||||
|
Ok(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_records(&mut self, zone: Name, class: DNSClass, new_records: Vec<Record>) -> ConnectorResult<()>
|
||||||
|
{
|
||||||
|
// Taken from trust_dns_client::op::update_message::append
|
||||||
|
// The original function can not be used as is because it takes a RecordSet and not a Record list
|
||||||
|
|
||||||
|
let mut zone_query = Query::new();
|
||||||
|
zone_query.set_name(zone.clone())
|
||||||
|
.set_query_class(class)
|
||||||
|
.set_query_type(RecordType::SOA);
|
||||||
|
|
||||||
|
let mut message = Message::new();
|
||||||
|
|
||||||
|
// TODO: set random / time based id
|
||||||
|
message
|
||||||
|
.set_id(0)
|
||||||
|
.set_message_type(MessageType::Query)
|
||||||
|
.set_op_code(OpCode::Update)
|
||||||
|
.set_recursion_desired(false);
|
||||||
|
message.add_zone(zone_query);
|
||||||
|
message.add_updates(new_records);
|
||||||
|
|
||||||
|
set_edns(&mut message);
|
||||||
|
|
||||||
|
let response = match ClientResponse(self.client.send(message)).await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
|
||||||
|
if response.response_code() != ResponseCode::NoError {
|
||||||
|
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
||||||
|
code: response.response_code(),
|
||||||
|
zone: zone,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_records(&mut self, zone: Name, class: DNSClass, old_records: Vec<Record>, new_records: Vec<Record>) -> ConnectorResult<()>
|
||||||
|
{
|
||||||
|
// Taken from trust_dns_client::op::update_message::compare_and_swap
|
||||||
|
// The original function can not be used as is because it takes a RecordSet and not a Record list
|
||||||
|
|
||||||
|
// for updates, the query section is used for the zone
|
||||||
|
let mut zone_query = Query::new();
|
||||||
|
zone_query.set_name(zone.clone())
|
||||||
|
.set_query_class(class)
|
||||||
|
.set_query_type(RecordType::SOA);
|
||||||
|
|
||||||
|
let mut message: Message = Message::new();
|
||||||
|
|
||||||
|
// build the message
|
||||||
|
// TODO: set random / time based id
|
||||||
|
message
|
||||||
|
.set_id(0)
|
||||||
|
.set_message_type(MessageType::Query)
|
||||||
|
.set_op_code(OpCode::Update)
|
||||||
|
.set_recursion_desired(false);
|
||||||
|
message.add_zone(zone_query);
|
||||||
|
|
||||||
|
// make sure the record is what is expected
|
||||||
|
let mut prerequisite = old_records.clone();
|
||||||
|
for record in prerequisite.iter_mut() {
|
||||||
|
record.set_ttl(0);
|
||||||
|
}
|
||||||
|
message.add_pre_requisites(prerequisite);
|
||||||
|
|
||||||
|
// add the delete for the old record
|
||||||
|
let mut delete = old_records;
|
||||||
|
for record in delete.iter_mut() {
|
||||||
|
// the class must be none for delete
|
||||||
|
record.set_dns_class(DNSClass::NONE);
|
||||||
|
// the TTL should be 0
|
||||||
|
record.set_ttl(0);
|
||||||
|
}
|
||||||
|
message.add_updates(delete);
|
||||||
|
|
||||||
|
// insert the new record...
|
||||||
|
message.add_updates(new_records);
|
||||||
|
|
||||||
|
// Extended dns
|
||||||
|
set_edns(&mut message);
|
||||||
|
|
||||||
|
let response = match ClientResponse(self.client.send(message)).await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
|
||||||
|
if response.response_code() != ResponseCode::NoError {
|
||||||
|
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
||||||
|
code: response.response_code(),
|
||||||
|
zone: zone,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_records(&mut self, zone: Name, class: DNSClass, records: Vec<Record>) -> ConnectorResult<()>
|
||||||
|
{
|
||||||
|
// for updates, the query section is used for the zone
|
||||||
|
let mut zone_query = Query::new();
|
||||||
|
zone_query.set_name(zone.clone())
|
||||||
|
.set_query_class(class)
|
||||||
|
.set_query_type(RecordType::SOA);
|
||||||
|
|
||||||
|
let mut message: Message = Message::new();
|
||||||
|
|
||||||
|
// build the message
|
||||||
|
// TODO: set random / time based id
|
||||||
|
message
|
||||||
|
.set_id(0)
|
||||||
|
.set_message_type(MessageType::Query)
|
||||||
|
.set_op_code(OpCode::Update)
|
||||||
|
.set_recursion_desired(false);
|
||||||
|
message.add_zone(zone_query);
|
||||||
|
|
||||||
|
let mut delete = records;
|
||||||
|
for record in delete.iter_mut() {
|
||||||
|
// the class must be none for delete
|
||||||
|
record.set_dns_class(DNSClass::NONE);
|
||||||
|
// the TTL should be 0
|
||||||
|
record.set_ttl(0);
|
||||||
|
}
|
||||||
|
message.add_updates(delete);
|
||||||
|
|
||||||
|
// Extended dns
|
||||||
|
set_edns(&mut message);
|
||||||
|
|
||||||
|
let response = match ClientResponse(self.client.send(message)).await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
Ok(v) => v,
|
||||||
|
};
|
||||||
|
|
||||||
|
if response.response_code() != ResponseCode::NoError {
|
||||||
|
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
||||||
|
code: response.response_code(),
|
||||||
|
zone: zone,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ZoneConnector for DnsConnectorClient {
|
||||||
|
async fn zone_exists(&mut self, zone: Name, class: DNSClass) -> ConnectorResult<()>
|
||||||
|
{
|
||||||
|
let response = {
|
||||||
|
info!("Querying SOA for name {}", zone);
|
||||||
|
let query = self.client.query(zone.clone(), class, RecordType::SOA);
|
||||||
|
match query.await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
Ok(v) => v,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if response.response_code() != ResponseCode::NoError {
|
||||||
|
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
||||||
|
code: response.response_code(),
|
||||||
|
zone: zone,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,577 +0,0 @@
|
||||||
use std::io;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use domain::base::iana::{Opcode, Rcode};
|
|
||||||
use domain::base::{message_builder, name, wire};
|
|
||||||
use domain::base::{MessageBuilder, Name, Rtype};
|
|
||||||
use domain::net::client::{tsig, stream};
|
|
||||||
use domain::tsig::{Algorithm, Key, KeyName};
|
|
||||||
use domain::net::client::request::{self, RequestMessage, RequestMessageMulti, SendRequest, SendRequestMulti};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
|
|
||||||
use crate::resources::dns::internal;
|
|
||||||
use crate::proto;
|
|
||||||
use crate::errors::Error;
|
|
||||||
use super::{RecordDriver, ZoneDriver, DnsDriverError};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
pub struct DnsDriverConfig {
|
|
||||||
pub address: SocketAddr,
|
|
||||||
pub tsig: Option<TsigConfig>
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TsigConfig {
|
|
||||||
pub key_name: KeyName,
|
|
||||||
pub secret: Vec<u8>,
|
|
||||||
pub algorithm: Algorithm
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct DnsDriver {
|
|
||||||
pub addr: SocketAddr,
|
|
||||||
pub tsig_key: Option<Arc<Key>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type TsigDnsClient = tsig::Connection<stream::Connection<
|
|
||||||
tsig::RequestMessage<request::RequestMessage<Vec<u8>>, Arc<Key>>,
|
|
||||||
tsig::RequestMessage<request::RequestMessageMulti<Vec<u8>>, Arc<Key>>
|
|
||||||
>, Arc<Key>>;
|
|
||||||
|
|
||||||
impl DnsDriver {
|
|
||||||
pub fn from_config(config: DnsDriverConfig) -> Self {
|
|
||||||
let key = config.tsig.map(|tsig_config| {
|
|
||||||
Arc::new(
|
|
||||||
Key::new(
|
|
||||||
tsig_config.algorithm,
|
|
||||||
&tsig_config.secret,
|
|
||||||
tsig_config.key_name,
|
|
||||||
None,
|
|
||||||
None
|
|
||||||
).expect("Failed to build key"),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
addr: config.address,
|
|
||||||
tsig_key: key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn client<Req, ReqMulti>(&self) -> Result<stream::Connection<Req, ReqMulti>, DnsDriverError>
|
|
||||||
where
|
|
||||||
Req: request::ComposeRequest + Send + Sync + 'static,
|
|
||||||
ReqMulti: request::ComposeRequestMulti + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
let mut stream_config = stream::Config::default();
|
|
||||||
stream_config.set_response_timeout(
|
|
||||||
Duration::from_millis(100),
|
|
||||||
);
|
|
||||||
let tcp_connect = TcpStream::connect(self.addr).await?;
|
|
||||||
|
|
||||||
let (tcp_conn, transport) = stream::Connection::with_config(
|
|
||||||
tcp_connect, stream_config
|
|
||||||
);
|
|
||||||
|
|
||||||
tokio::spawn(transport.run());
|
|
||||||
|
|
||||||
Ok(tcp_conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn tsig_client(&self) -> Result<Option<TsigDnsClient>, DnsDriverError>
|
|
||||||
{
|
|
||||||
if let Some(ref key) = self.tsig_key {
|
|
||||||
let conn = self.client().await?;
|
|
||||||
Ok(Some(tsig::Connection::new(key.clone(), conn)))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ZoneDriver for DnsDriver {
|
|
||||||
async fn zone_exists(&self, zone: &str) -> Result<(), DnsDriverError> {
|
|
||||||
let client = self.client::<_, RequestMessageMulti<Vec<u8>>>().await?;
|
|
||||||
|
|
||||||
let mut msg = MessageBuilder::new_vec().question();
|
|
||||||
msg.push((
|
|
||||||
Name::vec_from_str(zone)?,
|
|
||||||
Rtype::SOA,
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let req = RequestMessage::new(msg)?;
|
|
||||||
|
|
||||||
let res = SendRequest::send_request(&client, req)
|
|
||||||
.get_response()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let rcode = res.header().rcode();
|
|
||||||
|
|
||||||
match rcode {
|
|
||||||
Rcode::NOERROR => Ok(()),
|
|
||||||
Rcode::NXDOMAIN | Rcode::REFUSED => Err(DnsDriverError::ZoneNotFound {
|
|
||||||
name: zone.to_string(),
|
|
||||||
}),
|
|
||||||
rcode => Err(DnsDriverError::ServerError {
|
|
||||||
rcode: rcode.to_string(),
|
|
||||||
name: zone.to_string(),
|
|
||||||
qtype: Rtype::SOA.to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl RecordDriver for DnsDriver {
|
|
||||||
/// ------------- AXFR -------------
|
|
||||||
|
|
||||||
async fn get_records(&self, zone: &str) -> Result<internal::RecordList, DnsDriverError> {
|
|
||||||
let mut msg = MessageBuilder::new_vec();
|
|
||||||
msg.header_mut().set_ad(true);
|
|
||||||
|
|
||||||
let mut msg = msg.question();
|
|
||||||
msg.push((
|
|
||||||
Name::vec_from_str(zone)?,
|
|
||||||
Rtype::AXFR,
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let req = RequestMessageMulti::new(msg)?;
|
|
||||||
|
|
||||||
let tsig_client = self.tsig_client().await?;
|
|
||||||
|
|
||||||
let mut request = if let Some(client) = tsig_client {
|
|
||||||
SendRequestMulti::send_request(&client, req)
|
|
||||||
} else {
|
|
||||||
let client = self.client::<RequestMessage<Vec<u8>>,_>().await?;
|
|
||||||
SendRequestMulti::send_request(&client, req)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut records = Vec::new();
|
|
||||||
|
|
||||||
while let Some(reply) = request.get_response().await? {
|
|
||||||
let rcode = reply.header().rcode();
|
|
||||||
|
|
||||||
if rcode != Rcode::NOERROR {
|
|
||||||
return Err(DnsDriverError::ServerError {
|
|
||||||
rcode: rcode.to_string(),
|
|
||||||
name: zone.to_string(),
|
|
||||||
qtype: Rtype::AXFR.to_string()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let answer = reply.answer()?;
|
|
||||||
|
|
||||||
for record in answer.limit_to::<proto::dns::ParsedRData<_, _>>() {
|
|
||||||
let record = record?;
|
|
||||||
records.push(record.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AXFR response ends with SOA, we remove it so it is not doubled in the response.
|
|
||||||
records.pop();
|
|
||||||
|
|
||||||
Ok(internal::RecordList { records })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ------------- Dynamic Update - RFC 2136 -------------
|
|
||||||
///
|
|
||||||
/// 2 - Update Message Format
|
|
||||||
/// +---------------------+
|
|
||||||
/// | Header |
|
|
||||||
/// +---------------------+
|
|
||||||
/// | Zone | specifies the zone to be updated (RFC1035 Question)
|
|
||||||
/// +---------------------+
|
|
||||||
/// | Prerequisite | RRs or RRsets which must (not) preexist (RFC1035 Answer)
|
|
||||||
/// +---------------------+
|
|
||||||
/// | Update | RRs or RRsets to be added or deleted (RFC1035 Authority)
|
|
||||||
/// +---------------------+
|
|
||||||
/// | Additional Data | additional data
|
|
||||||
/// +---------------------+
|
|
||||||
/// 2.2 - Message Header
|
|
||||||
///
|
|
||||||
/// OPCODE is set to UPDATE.
|
|
||||||
/// UPDATE uses only one flag bit (QR).
|
|
||||||
///
|
|
||||||
/// 2.3 - Zone Section
|
|
||||||
///
|
|
||||||
/// The ZNAME is the zone name, the ZTYPE must be SOA, and the ZCLASS is
|
|
||||||
/// the zone's class.
|
|
||||||
///
|
|
||||||
/// 3.2.4 - Table Of Metavalues Used In Prerequisite Section
|
|
||||||
///
|
|
||||||
/// TTL must be specified as zero (0) for all prerequisite
|
|
||||||
///
|
|
||||||
/// CLASS TYPE RDATA Meaning
|
|
||||||
/// ------------------------------------------------------------
|
|
||||||
/// ANY ANY empty Name is in use
|
|
||||||
/// ANY rrset empty RRset exists (value independent)
|
|
||||||
/// NONE ANY empty Name is not in use
|
|
||||||
/// NONE rrset empty RRset does not exist
|
|
||||||
/// zone rrset rr RRset exists (value dependent) - Match against ALL RR in a RRset!!
|
|
||||||
///
|
|
||||||
/// 3.4.2.6 - Table Of Metavalues Used In Update Section
|
|
||||||
///
|
|
||||||
/// CLASS TYPE RDATA Meaning
|
|
||||||
/// ---------------------------------------------------------
|
|
||||||
/// ANY ANY empty Delete all RRsets from a name - TTL must be specified as zero (0)
|
|
||||||
/// ANY rrset empty Delete an RRset - TTL must be specified as zero (0)
|
|
||||||
/// NONE rrset rr Delete an RR from an RRset - TTL must be specified as zero (0)
|
|
||||||
/// zone rrset rr Add to an RRset
|
|
||||||
|
|
||||||
|
|
||||||
async fn add_records(&self, zone: &str, new_records: internal::RecordList) -> Result<(), DnsDriverError> {
|
|
||||||
let mut msg = MessageBuilder::new_vec();
|
|
||||||
msg.header_mut().set_opcode(Opcode::UPDATE);
|
|
||||||
|
|
||||||
let mut msg = msg.question();
|
|
||||||
msg.push((
|
|
||||||
Name::vec_from_str(zone)?,
|
|
||||||
Rtype::SOA,
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let mut msg = msg.authority();
|
|
||||||
|
|
||||||
for record in new_records.records {
|
|
||||||
let record = proto::dns::RecordImpl::try_from(record)?;
|
|
||||||
msg.push(record)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let req = RequestMessage::new(msg)?;
|
|
||||||
|
|
||||||
let tsig_client = self.tsig_client().await?;
|
|
||||||
|
|
||||||
let mut request = if let Some(client) = tsig_client {
|
|
||||||
SendRequest::send_request(&client, req)
|
|
||||||
} else {
|
|
||||||
let client = self.client::<_, RequestMessageMulti<Vec<u8>>>().await?;
|
|
||||||
SendRequest::send_request(&client, req)
|
|
||||||
};
|
|
||||||
|
|
||||||
let reply = request.get_response().await?;
|
|
||||||
let rcode = reply.header().rcode();
|
|
||||||
|
|
||||||
if rcode != Rcode::NOERROR {
|
|
||||||
Err(DnsDriverError::ServerError {
|
|
||||||
rcode: rcode.to_string(),
|
|
||||||
name: zone.to_string(),
|
|
||||||
qtype: "UPDATE".to_string(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for DnsDriverError {
|
|
||||||
fn from(value: io::Error) -> Self {
|
|
||||||
DnsDriverError::ConnectionError { reason: Box::new(value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<request::Error> for DnsDriverError {
|
|
||||||
fn from(value: request::Error) -> Self {
|
|
||||||
DnsDriverError::ConnectionError { reason: Box::new(value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<message_builder::PushError> for DnsDriverError {
|
|
||||||
fn from(value: message_builder::PushError) -> Self {
|
|
||||||
DnsDriverError::OperationError { reason: Box::new(value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<name::FromStrError> for DnsDriverError {
|
|
||||||
fn from(value: name::FromStrError) -> Self {
|
|
||||||
DnsDriverError::OperationError { reason: Box::new(value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<wire::ParseError> for DnsDriverError {
|
|
||||||
fn from(value: wire::ParseError) -> Self {
|
|
||||||
DnsDriverError::OperationError { reason: Box::new(value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Error> for DnsDriverError {
|
|
||||||
fn from(value: Error) -> Self {
|
|
||||||
DnsDriverError::OperationError { reason: Box::new(value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
use trust_dns_proto::DnsHandle;
|
|
||||||
use trust_dns_client::client::ClientHandle;
|
|
||||||
use trust_dns_client::rr::{DNSClass, RecordType};
|
|
||||||
use trust_dns_client::op::{UpdateMessage, OpCode, MessageType, Message, Query, ResponseCode, Edns};
|
|
||||||
use trust_dns_client::error::ClientError;
|
|
||||||
|
|
||||||
use super::{Name, Record, RData};
|
|
||||||
use super::client::{ClientResponse, DnsClient};
|
|
||||||
use super::connector::{RecordConnector, ZoneConnector, ConnectorError, ConnectorResult};
|
|
||||||
|
|
||||||
|
|
||||||
const MAX_PAYLOAD_LEN: u16 = 1232;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum DnsConnectorError {
|
|
||||||
ClientError(ClientError),
|
|
||||||
ResponceNotOk {
|
|
||||||
code: ResponseCode,
|
|
||||||
zone: Name,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DnsConnectorClient {
|
|
||||||
client: DnsClient
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DnsConnectorClient {
|
|
||||||
pub fn new(client: DnsClient) -> Self {
|
|
||||||
DnsConnectorClient {
|
|
||||||
client
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectorError for DnsConnectorError {
|
|
||||||
fn zone_name(&self) -> Option<Name> {
|
|
||||||
if let DnsConnectorError::ResponceNotOk { code: _code, zone } = self {
|
|
||||||
Some(zone.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_proto_error(&self) -> bool {
|
|
||||||
return matches!(self, DnsConnectorError::ClientError(_));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for DnsConnectorError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
DnsConnectorError::ClientError(e) => {
|
|
||||||
write!(f, "DNS client error: {}", e)
|
|
||||||
},
|
|
||||||
DnsConnectorError::ResponceNotOk { code, zone } => {
|
|
||||||
write!(f, "Query for zone \"{}\" failed with code \"{}\"", zone, code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_edns(message: &mut Message) {
|
|
||||||
let edns = message.extensions_mut().get_or_insert_with(Edns::new);
|
|
||||||
edns.set_max_payload(MAX_PAYLOAD_LEN);
|
|
||||||
edns.set_version(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl RecordConnector for DnsConnectorClient {
|
|
||||||
//type Error = DnsConnectorError;
|
|
||||||
|
|
||||||
async fn get_records(&mut self, zone: Name, class: DNSClass) -> ConnectorResult<Vec<Record>>
|
|
||||||
{
|
|
||||||
let response = {
|
|
||||||
let query = self.client.query(zone.clone(), class, RecordType::AXFR);
|
|
||||||
match query.await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
Ok(v) => v,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.response_code() != ResponseCode::NoError {
|
|
||||||
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
|
||||||
code: response.response_code(),
|
|
||||||
zone: zone,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let answers = response.answers();
|
|
||||||
let mut records: Vec<_> = answers.to_vec().into_iter()
|
|
||||||
.filter(|record| record.data().is_some() && !matches!(record.data().unwrap(), RData::NULL { .. } | RData::DNSSEC(_)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// AXFR response ends with SOA, we remove it so it is not doubled in the response.
|
|
||||||
records.pop();
|
|
||||||
Ok(records)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_records(&mut self, zone: Name, class: DNSClass, new_records: Vec<Record>) -> ConnectorResult<()>
|
|
||||||
{
|
|
||||||
// Taken from trust_dns_client::op::update_message::append
|
|
||||||
// The original function can not be used as is because it takes a RecordSet and not a Record list
|
|
||||||
|
|
||||||
let mut zone_query = Query::new();
|
|
||||||
zone_query.set_name(zone.clone())
|
|
||||||
.set_query_class(class)
|
|
||||||
.set_query_type(RecordType::SOA);
|
|
||||||
|
|
||||||
let mut message = Message::new();
|
|
||||||
|
|
||||||
// TODO: set random / time based id
|
|
||||||
message
|
|
||||||
.set_id(0)
|
|
||||||
.set_message_type(MessageType::Query)
|
|
||||||
.set_op_code(OpCode::Update)
|
|
||||||
.set_recursion_desired(false);
|
|
||||||
message.add_zone(zone_query);
|
|
||||||
message.add_updates(new_records);
|
|
||||||
|
|
||||||
set_edns(&mut message);
|
|
||||||
|
|
||||||
let response = match ClientResponse(self.client.send(message)).await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
Ok(v) => v,
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.response_code() != ResponseCode::NoError {
|
|
||||||
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
|
||||||
code: response.response_code(),
|
|
||||||
zone: zone,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_records(&mut self, zone: Name, class: DNSClass, old_records: Vec<Record>, new_records: Vec<Record>) -> ConnectorResult<()>
|
|
||||||
{
|
|
||||||
// Taken from trust_dns_client::op::update_message::compare_and_swap
|
|
||||||
// The original function can not be used as is because it takes a RecordSet and not a Record list
|
|
||||||
|
|
||||||
// for updates, the query section is used for the zone
|
|
||||||
let mut zone_query = Query::new();
|
|
||||||
zone_query.set_name(zone.clone())
|
|
||||||
.set_query_class(class)
|
|
||||||
.set_query_type(RecordType::SOA);
|
|
||||||
|
|
||||||
let mut message: Message = Message::new();
|
|
||||||
|
|
||||||
// build the message
|
|
||||||
// TODO: set random / time based id
|
|
||||||
message
|
|
||||||
.set_id(0)
|
|
||||||
.set_message_type(MessageType::Query)
|
|
||||||
.set_op_code(OpCode::Update)
|
|
||||||
.set_recursion_desired(false);
|
|
||||||
message.add_zone(zone_query);
|
|
||||||
|
|
||||||
// make sure the record is what is expected
|
|
||||||
let mut prerequisite = old_records.clone();
|
|
||||||
for record in prerequisite.iter_mut() {
|
|
||||||
record.set_ttl(0);
|
|
||||||
}
|
|
||||||
message.add_pre_requisites(prerequisite);
|
|
||||||
|
|
||||||
// add the delete for the old record
|
|
||||||
let mut delete = old_records;
|
|
||||||
for record in delete.iter_mut() {
|
|
||||||
// the class must be none for delete
|
|
||||||
record.set_dns_class(DNSClass::NONE);
|
|
||||||
// the TTL should be 0
|
|
||||||
record.set_ttl(0);
|
|
||||||
}
|
|
||||||
message.add_updates(delete);
|
|
||||||
|
|
||||||
// insert the new record...
|
|
||||||
message.add_updates(new_records);
|
|
||||||
|
|
||||||
// Extended dns
|
|
||||||
set_edns(&mut message);
|
|
||||||
|
|
||||||
let response = match ClientResponse(self.client.send(message)).await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
Ok(v) => v,
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.response_code() != ResponseCode::NoError {
|
|
||||||
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
|
||||||
code: response.response_code(),
|
|
||||||
zone: zone,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_records(&mut self, zone: Name, class: DNSClass, records: Vec<Record>) -> ConnectorResult<()>
|
|
||||||
{
|
|
||||||
// for updates, the query section is used for the zone
|
|
||||||
let mut zone_query = Query::new();
|
|
||||||
zone_query.set_name(zone.clone())
|
|
||||||
.set_query_class(class)
|
|
||||||
.set_query_type(RecordType::SOA);
|
|
||||||
|
|
||||||
let mut message: Message = Message::new();
|
|
||||||
|
|
||||||
// build the message
|
|
||||||
// TODO: set random / time based id
|
|
||||||
message
|
|
||||||
.set_id(0)
|
|
||||||
.set_message_type(MessageType::Query)
|
|
||||||
.set_op_code(OpCode::Update)
|
|
||||||
.set_recursion_desired(false);
|
|
||||||
message.add_zone(zone_query);
|
|
||||||
|
|
||||||
let mut delete = records;
|
|
||||||
for record in delete.iter_mut() {
|
|
||||||
// the class must be none for delete
|
|
||||||
record.set_dns_class(DNSClass::NONE);
|
|
||||||
// the TTL should be 0
|
|
||||||
record.set_ttl(0);
|
|
||||||
}
|
|
||||||
message.add_updates(delete);
|
|
||||||
|
|
||||||
// Extended dns
|
|
||||||
set_edns(&mut message);
|
|
||||||
|
|
||||||
let response = match ClientResponse(self.client.send(message)).await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
Ok(v) => v,
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.response_code() != ResponseCode::NoError {
|
|
||||||
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
|
||||||
code: response.response_code(),
|
|
||||||
zone: zone,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ZoneConnector for DnsConnectorClient {
|
|
||||||
async fn zone_exists(&mut self, zone: Name, class: DNSClass) -> ConnectorResult<()>
|
|
||||||
{
|
|
||||||
let response = {
|
|
||||||
info!("Querying SOA for name {}", zone);
|
|
||||||
let query = self.client.query(zone.clone(), class, RecordType::SOA);
|
|
||||||
match query.await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
Ok(v) => v,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.response_code() != ResponseCode::NoError {
|
|
||||||
return Err(Box::new(DnsConnectorError::ResponceNotOk {
|
|
||||||
code: response.response_code(),
|
|
||||||
zone: zone,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -1,38 +1,3 @@
|
||||||
pub mod dns_driver;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use crate::resources::dns::internal;
|
|
||||||
|
|
||||||
pub type BoxedZoneDriver = Arc<dyn ZoneDriver>;
|
|
||||||
pub type BoxedRecordDriver = Arc<dyn RecordDriver>;
|
|
||||||
pub enum DnsDriverError {
|
|
||||||
ConnectionError { reason: Box<dyn std::error::Error> },
|
|
||||||
OperationError { reason: Box<dyn std::error::Error> },
|
|
||||||
ServerError { rcode: String, name: String, qtype: String },
|
|
||||||
ZoneNotFound { name: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait ZoneDriver: Send + Sync {
|
|
||||||
// get_zones
|
|
||||||
// add_zone
|
|
||||||
// delete_zone
|
|
||||||
async fn zone_exists(&self, zone: &str) -> Result<(), DnsDriverError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait RecordDriver: Send + Sync {
|
|
||||||
async fn get_records(&self, zone: &str) -> Result<internal::RecordList, DnsDriverError>;
|
|
||||||
async fn add_records(&self, zone: &str, new_records: internal::RecordList) -> Result<(), DnsDriverError>;
|
|
||||||
//async fn update_records(&mut self, zone: dns::Name, class: dns::DNSClass, old_records: Vec<dns::Record>, new_records: Vec<dns::Record>) -> ConnectorResult<()>;
|
|
||||||
//async fn delete_records(&mut self, zone: dns::Name, class: dns::DNSClass, records: Vec<dns::Record>) -> ConnectorResult<()>;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod dns_connector;
|
pub mod dns_connector;
|
||||||
pub mod connector;
|
pub mod connector;
|
||||||
|
@ -88,4 +53,3 @@ impl<'r> FromRequest<'r> for Box<dyn ZoneConnector> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
455
src/errors.rs
455
src/errors.rs
|
@ -1,455 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use axum::http::{self, StatusCode};
|
|
||||||
use axum::response::{AppendHeaders, IntoResponse, Response};
|
|
||||||
|
|
||||||
use axum::Json;
|
|
||||||
use serde::{Serialize, Serializer};
|
|
||||||
use serde_json::{Value, json};
|
|
||||||
|
|
||||||
use crate::dns::DnsDriverError;
|
|
||||||
use crate::resources::dns::external::rdata::RDataValidationError;
|
|
||||||
use crate::resources::dns::external::record::{RecordError, RecordValidationError};
|
|
||||||
use crate::resources::dns::friendly::InputDataError;
|
|
||||||
use crate::resources::zone::ZoneError;
|
|
||||||
use crate::validation::{DomainValidationError, TxtParseError};
|
|
||||||
use crate::template::TemplateError;
|
|
||||||
use crate::proto::dns::ProtoDnsError;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct Error {
|
|
||||||
#[serde(skip)]
|
|
||||||
cause: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", serialize_with = "serialize_status")]
|
|
||||||
status: Option<StatusCode>,
|
|
||||||
code: String,
|
|
||||||
description: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
details: Option<Value>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
path: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
errors: Option<Vec<Error>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize_status<S>(status: &Option<StatusCode>, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where S: Serializer
|
|
||||||
{
|
|
||||||
if let Some(status) = status {
|
|
||||||
serializer.serialize_u16(status.as_u16())
|
|
||||||
} else {
|
|
||||||
serializer.serialize_unit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error_map(errors: Vec<Error>) -> HashMap<String, Error> {
|
|
||||||
errors.into_iter()
|
|
||||||
.filter_map(|mut error|
|
|
||||||
error.path.take().map(|path| (path, error))
|
|
||||||
).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
pub fn new(code: &str, description: &str) -> Self {
|
|
||||||
Error {
|
|
||||||
cause: None,
|
|
||||||
status: None,
|
|
||||||
code: code.into(),
|
|
||||||
description: description.into(),
|
|
||||||
details: None,
|
|
||||||
path: None,
|
|
||||||
errors: None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_cause(self, cause: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
cause: Some(cause.into()),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_status(self, status: StatusCode) -> Self {
|
|
||||||
Self {
|
|
||||||
status: Some(status),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_path(self, path: &str) -> Self {
|
|
||||||
if let Some(current_path) = self.path {
|
|
||||||
Self {
|
|
||||||
path: Some(format!("{path}{current_path}")),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Self {
|
|
||||||
path: Some(path.into()),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_details<T: Serialize> (self, details: T) -> Self {
|
|
||||||
let mut new_details = serde_json::to_value(details).expect("failed to convert details to serde_json::Value");
|
|
||||||
let details = self.details;
|
|
||||||
|
|
||||||
// append new details to existing details
|
|
||||||
if let Some(mut details) = details {
|
|
||||||
if let Some(object) = details.as_object_mut() {
|
|
||||||
if let Some(new_object) = new_details.as_object_mut() {
|
|
||||||
object.append(new_object);
|
|
||||||
|
|
||||||
return Self {
|
|
||||||
details: Some(details),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
details: Some(new_details),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_suberrors(self, mut errors: Vec<Error>) -> Self {
|
|
||||||
for error in &mut errors {
|
|
||||||
error.status = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
errors: Some(errors),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn override_status(self, status: StatusCode) -> Self {
|
|
||||||
if self.status.is_some() {
|
|
||||||
self.with_status(status)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.description)?;
|
|
||||||
|
|
||||||
if let Some(cause) = &self.cause {
|
|
||||||
write!(f, ": {}", cause)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.status.is_some() || self.details.is_some() {
|
|
||||||
write!(f, " (")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(status) = &self.status {
|
|
||||||
write!(f, "status = {}", status)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(details) = &self.details {
|
|
||||||
if self.status.is_some() {
|
|
||||||
write!(f, ", ")?;
|
|
||||||
}
|
|
||||||
write!(f, "details = {}", serde_json::to_string(details).expect("Failed to serialize error details"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.status.is_some() || self.details.is_some() {
|
|
||||||
write!(f, ")")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
if let Some(status) = self.status {
|
|
||||||
(status, Json(self)).into_response()
|
|
||||||
} else {
|
|
||||||
eprintln!("{}", self);
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
AppendHeaders([
|
|
||||||
(http::header::CONTENT_TYPE, "application/json")
|
|
||||||
]),
|
|
||||||
r#"{"status": 500,"description":"Internal server error","code":"internal"}"#
|
|
||||||
).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<bb8::RunError<rusqlite::Error>> for Error {
|
|
||||||
fn from(value: bb8::RunError<rusqlite::Error>) -> Self {
|
|
||||||
Error::new("db:pool", "Failed to get database connection from pool")
|
|
||||||
.with_cause(&value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rusqlite::Error> for Error {
|
|
||||||
fn from(value: rusqlite::Error) -> Self {
|
|
||||||
Error::new("db:sqlite", "Sqlite failure")
|
|
||||||
.with_cause(&format!("{:?}", value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ZoneError> for Error {
|
|
||||||
fn from(value: ZoneError) -> Self {
|
|
||||||
match value {
|
|
||||||
ZoneError::ZoneConflict { name } => {
|
|
||||||
Error::new("zone:conflict", "Zone {zone_name} already exists")
|
|
||||||
.with_details(json!({
|
|
||||||
"zone_name": name
|
|
||||||
}))
|
|
||||||
.with_status(StatusCode::CONFLICT)
|
|
||||||
},
|
|
||||||
ZoneError::NotFound { name } => {
|
|
||||||
Error::new("zone:not_found", "The zone {zone_name} could not be found")
|
|
||||||
.with_details(json!({
|
|
||||||
"zone_name": name
|
|
||||||
}))
|
|
||||||
.with_status(StatusCode::NOT_FOUND)
|
|
||||||
},
|
|
||||||
ZoneError::Validation { suberrors } => {
|
|
||||||
Error::new("zone:validation", "Error while validating zone input data")
|
|
||||||
.with_suberrors(suberrors)
|
|
||||||
.with_status(StatusCode::BAD_REQUEST)
|
|
||||||
},
|
|
||||||
ZoneError::NotExistsNs { name } => {
|
|
||||||
Error::new("zone:not_exists_ns", "The zone {zone_name} does not exist on the name server")
|
|
||||||
.with_details(json!({
|
|
||||||
"zone_name": name
|
|
||||||
}))
|
|
||||||
.with_status(StatusCode::BAD_REQUEST)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomainValidationError> for Error {
|
|
||||||
fn from(value: DomainValidationError) -> Self {
|
|
||||||
match value {
|
|
||||||
DomainValidationError::CharactersNotPermitted { label } => {
|
|
||||||
Error::new("domain:characters_not_permitted", "Domain name label {label} contains characters not permitted. The allowed characters are lowercase alphanumeric characters (a-z and 0-9), the dash ('-'), the underscore ('_') and the forward slash ('/').")
|
|
||||||
.with_details(json!({
|
|
||||||
"label": label
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
DomainValidationError::EmptyDomain => {
|
|
||||||
Error::new("domain:empty_domain", "Domain name can not be empty or the root domain ('.')")
|
|
||||||
},
|
|
||||||
DomainValidationError::EmptyLabel => {
|
|
||||||
Error::new("domain:empty_label", "Domain name contains empty labels (repeated dots)")
|
|
||||||
},
|
|
||||||
DomainValidationError::DomainTooLong { length } => {
|
|
||||||
Error::new("domain:domain_too_long", "Domain name too long ({length} characters), the maximum length is 255 characters")
|
|
||||||
.with_details(json!({
|
|
||||||
"length": length
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
DomainValidationError::LabelToolLong { length, label } => {
|
|
||||||
Error::new("domain:label_too_long", "Domain name label {label} is too long ({label_length} characters), the maximum length is 63 characters")
|
|
||||||
.with_details(json!({
|
|
||||||
"label": label,
|
|
||||||
"length": length,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TxtParseError> for Error {
|
|
||||||
fn from(value: TxtParseError) -> Self {
|
|
||||||
match value {
|
|
||||||
TxtParseError::BadEscapeDigitIndexTooHigh { sequence } => {
|
|
||||||
Error::new("record:txt:parse:escape_decimal_index_too_high", "Octect escape sequence should be between 000 and 255. Offending escape sequence: \\{sequence}")
|
|
||||||
.with_details(json!({
|
|
||||||
"sequence": sequence
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
TxtParseError::BadEscapeDigitsNotDigits { sequence } => {
|
|
||||||
Error::new("record:txt:parse:escape_decimal_not_digits", "Expected an octect escape sequence due to the presence of a back slash (\\) followed by a digit but found non digit characters. Offending escape sequence: \\{sequence}")
|
|
||||||
.with_details(json!({
|
|
||||||
"sequence": sequence
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
TxtParseError::BadEscapeDigitsTooShort { sequence } => {
|
|
||||||
Error::new("record:txt:parse:escape_decimal_too_short", "Expected an octect escape sequence due to the presence of a back slash (\\) followed by a digit but found found {sequence_lenght} characters instead of three. Offending escape sequence: \\{sequence}")
|
|
||||||
.with_details(json!({
|
|
||||||
"sequence": sequence,
|
|
||||||
"sequence_lenght": sequence.len()
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
TxtParseError::MissingEscape => {
|
|
||||||
Error::new("record:txt:parse:escape_missing", "Expected an escape sequence due to the presence of a back slash (\\) at the end of the input but found nothing")
|
|
||||||
},
|
|
||||||
TxtParseError::NonAscii { character } => {
|
|
||||||
Error::new("record:txt:parse:non_ascii", "Found a non ASCII character ({character}). Only printable ASCII characters are allowed.")
|
|
||||||
.with_details(json!({
|
|
||||||
"character": character
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl From<DnsDriverError> for Error {
|
|
||||||
fn from(value: DnsDriverError) -> Self {
|
|
||||||
|
|
||||||
match value {
|
|
||||||
DnsDriverError::ConnectionError { reason } => {
|
|
||||||
Error::new("dns:connection", "Error while connecting to the name server")
|
|
||||||
.with_cause(&reason.to_string())
|
|
||||||
},
|
|
||||||
DnsDriverError::OperationError { reason } => {
|
|
||||||
Error::new("dns:operation", "DNS operation error")
|
|
||||||
.with_cause(&reason.to_string())
|
|
||||||
},
|
|
||||||
DnsDriverError::ServerError { rcode, name, qtype } => {
|
|
||||||
Error::new("dns:server", "Unexpected response to query")
|
|
||||||
.with_details(json!({
|
|
||||||
"rcode": rcode,
|
|
||||||
"name": name,
|
|
||||||
"qtype": qtype,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
DnsDriverError::ZoneNotFound { name } => {
|
|
||||||
Error::new("dns:zone_not_found", "The zone {zone_name} does not exist on the name server")
|
|
||||||
.with_details(json!({
|
|
||||||
"zone_name": name
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RDataValidationError> for Error {
|
|
||||||
fn from(value: RDataValidationError) -> Self {
|
|
||||||
match value {
|
|
||||||
RDataValidationError::Ip4Address { input } => {
|
|
||||||
Error::new("record:parse:ip4", "The following IPv4 address {input} is invalid. IPv4 addresses should have four numbers, each between 0 and 255, separated by dots.")
|
|
||||||
.with_details(json!({
|
|
||||||
"input": input
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
RDataValidationError::Ip6Address { input } => {
|
|
||||||
Error::new("record:parse:ip6", "The following IPv4 address {input} is invalid. IPv6 addresses should have eight groups of four hexadecimal digit separated by colons. Leftmost zeros in a group can be omitted, sequence of zeros can be shorted by a double colons.")
|
|
||||||
.with_details(json!({
|
|
||||||
"input": input
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
RDataValidationError::IpAddress { input } => {
|
|
||||||
Error::new("record:parse:ip", "The IP address {input} is invalid. It should be either an IPv4 address, four numbers between 0 and 255 separated by dots, or and IPv6 address, eight groups of four hexadecimal digit separated by colons.")
|
|
||||||
.with_details(json!({
|
|
||||||
"input": input
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
RDataValidationError::Number { min, max } => {
|
|
||||||
Error::new("record:parse:number", "Expected a number between {min} and {max}")
|
|
||||||
.with_details(json!({
|
|
||||||
"min": min,
|
|
||||||
"max": max,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RecordValidationError> for Error {
|
|
||||||
fn from(value: RecordValidationError) -> Self {
|
|
||||||
match value {
|
|
||||||
RecordValidationError::NotInZone { name, zone } => {
|
|
||||||
Error::new("record:parse:not_in_zone", "The domain name {name} is not in the current zone ({zone})")
|
|
||||||
.with_details(json!({
|
|
||||||
"name": name,
|
|
||||||
"zone": zone
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RecordError > for Error {
|
|
||||||
fn from(value: RecordError) -> Self {
|
|
||||||
match value {
|
|
||||||
RecordError::Validation { suberrors } => {
|
|
||||||
Error::new("record:validation", "Error while validating input records")
|
|
||||||
.with_suberrors(suberrors)
|
|
||||||
.with_status(StatusCode::BAD_REQUEST)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TemplateError> for Error {
|
|
||||||
fn from(value: TemplateError) -> Self {
|
|
||||||
match value {
|
|
||||||
TemplateError::RenderError { name, reason } => {
|
|
||||||
let mut this_reason = reason.as_ref();
|
|
||||||
let mut cause = format!("{}", this_reason);
|
|
||||||
|
|
||||||
while let Some(source) = this_reason.source() {
|
|
||||||
cause.push_str(&format!(": {source}"));
|
|
||||||
this_reason = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
Error::new("template:render", "Failed to render the template")
|
|
||||||
.with_details(json!({
|
|
||||||
"name": name
|
|
||||||
}))
|
|
||||||
.with_cause(&cause)
|
|
||||||
},
|
|
||||||
TemplateError::SerializationError { reason } => {
|
|
||||||
Error::new("template:serialization", "Failed to serialize context")
|
|
||||||
.with_cause(&reason.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ProtoDnsError> for Error {
|
|
||||||
fn from(value: ProtoDnsError) -> Self {
|
|
||||||
match value {
|
|
||||||
ProtoDnsError::RDataUnknown { input, field, rtype } => {
|
|
||||||
Error::new("proto:dns:rdata_unknown", "Unknown error while converting internal rdata to proto rdata")
|
|
||||||
.with_details(json!({
|
|
||||||
"input": input,
|
|
||||||
"field": field,
|
|
||||||
"rtype": rtype,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
ProtoDnsError::NameParseError { input } => {
|
|
||||||
Error::new("proto:dns:name_unknown", "Unknown error while parsing name")
|
|
||||||
.with_details(json!({
|
|
||||||
"input": input
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<InputDataError> for Error {
|
|
||||||
fn from(value: InputDataError) -> Self {
|
|
||||||
match value {
|
|
||||||
InputDataError::TypeError { expected, found } => {
|
|
||||||
Error::new("input:type_error", "Expected to find type {expected} found type {found}.")
|
|
||||||
.with_details(json!({
|
|
||||||
"expected": expected,
|
|
||||||
"found": found,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
InputDataError::MissingValue => {
|
|
||||||
Error::new("input:missing_value", "The value at the given path is required.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
250
src/form.rs
250
src/form.rs
|
@ -1,250 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use axum::extract::{Request, FromRequest};
|
|
||||||
use axum::response::{Response, IntoResponse};
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::Form;
|
|
||||||
|
|
||||||
impl<S> FromRequest<S> for Node
|
|
||||||
where
|
|
||||||
S: Send + Sync,
|
|
||||||
{
|
|
||||||
type Rejection = Response;
|
|
||||||
|
|
||||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
|
||||||
let Form(data): Form<Vec<(String, String)>> = Form::from_request(req, state)
|
|
||||||
.await
|
|
||||||
.map_err(IntoResponse::into_response)?;
|
|
||||||
|
|
||||||
let node = Node::from_key_value(data)
|
|
||||||
.map_err(|_| StatusCode::UNPROCESSABLE_ENTITY.into_response())?;
|
|
||||||
|
|
||||||
Ok(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum FormError {
|
|
||||||
MismatchedType
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Node {
|
|
||||||
Value(String),
|
|
||||||
Map(HashMap<String, Node>),
|
|
||||||
Sequence(Sequence)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node {
|
|
||||||
pub fn from_key_value(data: Vec<(String, String)>) -> Result<Node, FormError> {
|
|
||||||
let mut form = Node::Map(HashMap::new());
|
|
||||||
for (key, value) in data {
|
|
||||||
|
|
||||||
// Consider empty value not filled and remove them
|
|
||||||
if value.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = Self::parse_key(&key);
|
|
||||||
let mut parent_node = &mut form;
|
|
||||||
|
|
||||||
for window in path.windows(2) {
|
|
||||||
let parent_key = &window[0];
|
|
||||||
let child_key = &window[1];
|
|
||||||
|
|
||||||
parent_node = match (parent_node, parent_key) {
|
|
||||||
(&mut Node::Map(ref mut map), Key::Attribute(key)) => {
|
|
||||||
map.entry(key.clone()).or_insert_with(|| child_key.new_node())
|
|
||||||
},
|
|
||||||
(&mut Node::Sequence(Sequence::ImplicitIndex(ref mut list)), Key::ImplicitIndex) => {
|
|
||||||
list.push(child_key.new_node());
|
|
||||||
list.last_mut().unwrap()
|
|
||||||
},
|
|
||||||
(&mut Node::Sequence(Sequence::ExplicitIndex(ref mut list)), Key::ExplicitIndex(index)) => {
|
|
||||||
list.entry(*index).or_insert_with(|| child_key.new_node())
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
return Err(FormError::MismatchedType);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let last_key = path.last().unwrap();
|
|
||||||
match (parent_node, last_key) {
|
|
||||||
(&mut Node::Map(ref mut map), Key::Attribute(key)) => {
|
|
||||||
map.insert(key.clone(), Node::Value(value));
|
|
||||||
},
|
|
||||||
(&mut Node::Sequence(Sequence::ImplicitIndex(ref mut list)), Key::ImplicitIndex) => {
|
|
||||||
list.push(Node::Value(value))
|
|
||||||
},
|
|
||||||
(&mut Node::Sequence(Sequence::ExplicitIndex(ref mut list)), Key::ExplicitIndex(index)) => {
|
|
||||||
list.insert(*index, Node::Value(value));
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
return Err(FormError::MismatchedType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(form)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_key(key: &str) -> Vec<Key> {
|
|
||||||
let keys = if let Some((head, tail)) = key.split_once('[') {
|
|
||||||
let mut keys = vec![head];
|
|
||||||
keys.extend(tail.trim_end_matches(']').split("]["));
|
|
||||||
keys
|
|
||||||
} else {
|
|
||||||
vec![key]
|
|
||||||
};
|
|
||||||
|
|
||||||
keys.iter().map(|key| {
|
|
||||||
if key.is_empty() {
|
|
||||||
Key::ImplicitIndex
|
|
||||||
} else if let Ok(index) = key.parse::<usize>() {
|
|
||||||
Key::ExplicitIndex(index)
|
|
||||||
} else {
|
|
||||||
Key::Attribute(key.to_string())
|
|
||||||
}
|
|
||||||
}).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_json_value(self) -> serde_json::Value {
|
|
||||||
match self {
|
|
||||||
Node::Value(value) => serde_json::Value::String(value),
|
|
||||||
Node::Map(map) => {
|
|
||||||
let map = map.into_iter()
|
|
||||||
.map(|(key, node)| (key, node.into_json_value()))
|
|
||||||
.collect();
|
|
||||||
serde_json::Value::Object(map)
|
|
||||||
},
|
|
||||||
Node::Sequence(list) => {
|
|
||||||
let array = list.to_vec()
|
|
||||||
.into_iter()
|
|
||||||
.map(|node| node.into_json_value())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
serde_json::Value::Array(array)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Sequence {
|
|
||||||
ImplicitIndex(Vec<Node>),
|
|
||||||
ExplicitIndex(HashMap<usize, Node>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sequence {
|
|
||||||
pub fn to_vec(self) -> Vec<Node> {
|
|
||||||
match self {
|
|
||||||
Sequence::ImplicitIndex(list) => list,
|
|
||||||
Sequence::ExplicitIndex(map) => {
|
|
||||||
let mut key_values: Vec<(usize, Node)> = map.into_iter().collect();
|
|
||||||
key_values.sort_by_key(|(k, _)| *k);
|
|
||||||
key_values.into_iter().map(|(_, v)| v).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Key {
|
|
||||||
ExplicitIndex(usize),
|
|
||||||
ImplicitIndex,
|
|
||||||
Attribute(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Key {
|
|
||||||
pub fn new_node(&self) -> Node {
|
|
||||||
match self {
|
|
||||||
Key::ExplicitIndex(_) => Node::Sequence(Sequence::ExplicitIndex(HashMap::new())),
|
|
||||||
Key::ImplicitIndex => Node::Sequence(Sequence::ImplicitIndex(Vec::new())),
|
|
||||||
Key::Attribute(_) => Node::Map(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_parse_key() {
|
|
||||||
let key = "records[0][addresses][][address]".to_string();
|
|
||||||
let parsed_key = vec![
|
|
||||||
Key::Attribute("records".to_string()),
|
|
||||||
Key::ExplicitIndex(0),
|
|
||||||
Key::Attribute("addresses".to_string()),
|
|
||||||
Key::ImplicitIndex,
|
|
||||||
Key::Attribute("address".to_string()),
|
|
||||||
];
|
|
||||||
assert_eq!(Node::parse_key(&key), parsed_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_parse_key_value() {
|
|
||||||
let form_data = vec![
|
|
||||||
("records[0][addresses][][address]".to_string(), "123".to_string()),
|
|
||||||
("records[0][addresses][][address]".to_string(), "abc".to_string()),
|
|
||||||
];
|
|
||||||
let mut address1 = HashMap::new();
|
|
||||||
address1.insert("address".to_string(), Node::Value("123".to_string()));
|
|
||||||
let mut address2 = HashMap::new();
|
|
||||||
address2.insert("address".to_string(), Node::Value("abc".to_string()));
|
|
||||||
|
|
||||||
let addresses = vec![Node::Map(address1), Node::Map(address2)];
|
|
||||||
|
|
||||||
let mut record = HashMap::new();
|
|
||||||
record.insert("addresses".to_string(), Node::Sequence(Sequence::ImplicitIndex(addresses)));
|
|
||||||
|
|
||||||
let mut record_list = HashMap::new();
|
|
||||||
record_list.insert(0, Node::Map(record));
|
|
||||||
|
|
||||||
let mut form = HashMap::new();
|
|
||||||
form.insert("records".to_string(), Node::Sequence(Sequence::ExplicitIndex(record_list)));
|
|
||||||
|
|
||||||
let parsed_form = Node::Map(form);
|
|
||||||
|
|
||||||
assert_eq!(Node::from_key_value(form_data).unwrap(), parsed_form);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_json_value() {
|
|
||||||
let mut address1 = HashMap::new();
|
|
||||||
address1.insert("address".to_string(), Node::Value("123".to_string()));
|
|
||||||
let mut address2 = HashMap::new();
|
|
||||||
address2.insert("address".to_string(), Node::Value("abc".to_string()));
|
|
||||||
|
|
||||||
let addresses = vec![Node::Map(address1), Node::Map(address2)];
|
|
||||||
|
|
||||||
let mut record = HashMap::new();
|
|
||||||
record.insert("addresses".to_string(), Node::Sequence(Sequence::ImplicitIndex(addresses)));
|
|
||||||
|
|
||||||
let mut record_list = HashMap::new();
|
|
||||||
record_list.insert(0, Node::Map(record));
|
|
||||||
|
|
||||||
let mut form = HashMap::new();
|
|
||||||
form.insert("records".to_string(), Node::Sequence(Sequence::ExplicitIndex(record_list)));
|
|
||||||
|
|
||||||
let parsed_form = Node::Map(form);
|
|
||||||
|
|
||||||
let json_value = json!({
|
|
||||||
"records": [
|
|
||||||
{
|
|
||||||
"addresses": [
|
|
||||||
{ "address": "123" },
|
|
||||||
{ "address": "abc" },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(parsed_form.into_json_value(), json_value);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,243 +0,0 @@
|
||||||
use std::{collections::HashMap, fs, path::Path, str::FromStr};
|
|
||||||
|
|
||||||
use fluent_bundle::{concurrent::FluentBundle, FluentArgs, FluentResource, FluentValue};
|
|
||||||
use unic_langid::LanguageIdentifier;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Localization {
|
|
||||||
bundles: std::sync::Arc<HashMap<LanguageIdentifier, FluentBundle<FluentResource>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Localization {
|
|
||||||
pub fn init(locale_directory: &Path) -> Self {
|
|
||||||
let mut bundles = HashMap::new();
|
|
||||||
let directory_content =fs::read_dir(locale_directory)
|
|
||||||
.expect("Unable to read locales directory");
|
|
||||||
|
|
||||||
for lang_dir in directory_content {
|
|
||||||
let lang_dir = lang_dir.expect("I/O error");
|
|
||||||
if lang_dir.path().is_dir() {
|
|
||||||
let langid: LanguageIdentifier = lang_dir.file_name()
|
|
||||||
.into_string()
|
|
||||||
.expect("String convertion failed")
|
|
||||||
.parse()
|
|
||||||
.expect("Failed to parse language tag");
|
|
||||||
|
|
||||||
let mut bundle = FluentBundle::new_concurrent(vec![langid.clone()]);
|
|
||||||
|
|
||||||
let directory_content = fs::read_dir(lang_dir.path())
|
|
||||||
.expect("Unable to read locales directory");
|
|
||||||
for resource in directory_content {
|
|
||||||
let resource = resource.expect("I/O error");
|
|
||||||
|
|
||||||
if resource.path().is_file() {
|
|
||||||
let resource = fs::read_to_string(resource.path())
|
|
||||||
.expect("Failed to open file");
|
|
||||||
|
|
||||||
let resource = FluentResource::try_new(resource)
|
|
||||||
.expect("Failed to parse an FTL string.");
|
|
||||||
|
|
||||||
bundle
|
|
||||||
.add_resource(resource)
|
|
||||||
.expect("Failed to add FTL resources to the bundle.");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bundles.insert(langid, bundle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Localization {
|
|
||||||
bundles: std::sync::Arc::new(bundles),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn language_middleware(&self, default_locale: LanguageIdentifier) -> ExtractLanguageLayer {
|
|
||||||
ExtractLanguageLayer {
|
|
||||||
default_locale,
|
|
||||||
supported_locales: std::sync::Arc::new(self.bundles.keys().cloned().collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tera::Function for Localization {
|
|
||||||
fn call(&self, args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
|
|
||||||
let locale = args.get("lang")
|
|
||||||
.and_then(|lang| lang.as_str())
|
|
||||||
.and_then(|lang| LanguageIdentifier::from_str(lang).ok())
|
|
||||||
.ok_or(tera::Error::msg("localize: Missing lang parameter"))?;
|
|
||||||
|
|
||||||
let msg = args.get("msg")
|
|
||||||
.and_then(|lang| lang.as_str())
|
|
||||||
.ok_or(tera::Error::msg("localize: Missing msg parameter"))?;
|
|
||||||
|
|
||||||
let attribute = args.get("attr")
|
|
||||||
.and_then(|lang| lang.as_str());
|
|
||||||
|
|
||||||
let bundle = self.bundles.get(&locale)
|
|
||||||
.ok_or_else(|| tera::Error::msg(format!("localize: Could not find locale {locale}")))?;
|
|
||||||
|
|
||||||
let message = if let Some(message) = bundle.get_message(msg) {
|
|
||||||
message
|
|
||||||
} else {
|
|
||||||
eprintln!("[warn] localize: could not find message '{msg}'");
|
|
||||||
return Ok(tera::Value::from(format!("{{{msg}}}")));
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
let pattern = if let Some(attribute) = attribute {
|
|
||||||
let pattern = message.get_attribute(attribute)
|
|
||||||
.map(|attribute| attribute.value());
|
|
||||||
|
|
||||||
if let Some(pattern) = pattern {
|
|
||||||
pattern
|
|
||||||
} else {
|
|
||||||
eprintln!("[warn] localize: could not find message '{msg}.{attribute}'");
|
|
||||||
return Ok(tera::Value::from(format!("{{{msg}.{attribute}}}")));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.value()
|
|
||||||
.ok_or_else(|| tera::Error::msg(format!("localize: Message {msg} has no value")))?
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut msg_args = FluentArgs::new();
|
|
||||||
|
|
||||||
let extra_args = args.get("extra_args")
|
|
||||||
.and_then(|extra_args| extra_args.as_object());
|
|
||||||
|
|
||||||
if let Some(extra_args) = extra_args {
|
|
||||||
for (key, value) in extra_args {
|
|
||||||
msg_args.set(key, fluent_value_from_tera(value, key)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (key, value) in args {
|
|
||||||
if key != "msg" && key != "lang" && key != "attr" && key != "extra_args" {
|
|
||||||
msg_args.set(key, fluent_value_from_tera(value, key)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let localized_message = bundle.format_pattern(pattern, Some(&msg_args), &mut errors);
|
|
||||||
|
|
||||||
for err in errors {
|
|
||||||
eprint!("[warn] localization error: {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tera::Value::from(localized_message))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_safe(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fluent_value_from_tera<'s>(value: &'s tera::Value, name: &str) -> tera::Result<FluentValue<'s>> {
|
|
||||||
match value {
|
|
||||||
tera::Value::String(string) => Ok(FluentValue::from(string)),
|
|
||||||
tera::Value::Number(number) => Ok(FluentValue::from(number.as_f64())),
|
|
||||||
_ => Err(tera::Error::msg(format!("localize: Argument {name} can only be a string or a number"))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ExtractLanguageLayer {
|
|
||||||
default_locale: LanguageIdentifier,
|
|
||||||
supported_locales: std::sync::Arc<Vec<LanguageIdentifier>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> tower::Layer<S> for ExtractLanguageLayer {
|
|
||||||
|
|
||||||
type Service = ExtractLanguageService<S>;
|
|
||||||
|
|
||||||
fn layer(&self, inner: S) -> Self::Service {
|
|
||||||
ExtractLanguageService {
|
|
||||||
inner,
|
|
||||||
default_locale: self.default_locale.clone(),
|
|
||||||
supported_locales: self.supported_locales.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ExtractLanguageService<S> {
|
|
||||||
inner: S,
|
|
||||||
default_locale: LanguageIdentifier,
|
|
||||||
supported_locales: std::sync::Arc<Vec<LanguageIdentifier>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> ExtractLanguageService<S> {
|
|
||||||
// https://httpwg.org/specs/rfc9110.html#field.accept-language
|
|
||||||
// TODO: Test language selection for compound locales (eg. fr-FR)
|
|
||||||
pub fn language_from_header(&self, headers: &axum::http::HeaderMap) -> LanguageIdentifier {
|
|
||||||
let lang_preferences = headers
|
|
||||||
.get("Accept-Language")
|
|
||||||
.and_then(|val| val.to_str().ok());
|
|
||||||
|
|
||||||
if let Some(lang_preferences) = lang_preferences {
|
|
||||||
let mut languages = Vec::new();
|
|
||||||
|
|
||||||
for lang_item in lang_preferences.split(",") {
|
|
||||||
let lang_item = lang_item.trim();
|
|
||||||
let lang_config = lang_item.split_once(';');
|
|
||||||
if let Some((lang_id, config)) = lang_config {
|
|
||||||
let lang_id = lang_id.trim();
|
|
||||||
let preference = config.trim().strip_prefix("q=")
|
|
||||||
.and_then(|value| value.parse::<f32>().ok());
|
|
||||||
if let Some(preference) = preference {
|
|
||||||
languages.push((lang_id.trim(), preference));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
languages.push((lang_item, 1f32));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
languages.sort_by(|l1, l2| l1.1.total_cmp(&l2.1));
|
|
||||||
|
|
||||||
let iter = languages.into_iter()
|
|
||||||
.rev()
|
|
||||||
.filter(|l| l.1 >= 0.001 && l.1 <= 1f32)
|
|
||||||
.filter_map(|(lang, _)| LanguageIdentifier::from_str(lang).ok());
|
|
||||||
|
|
||||||
for lang in iter {
|
|
||||||
for supported_lang in self.supported_locales.iter() {
|
|
||||||
if lang == *supported_lang {
|
|
||||||
return lang;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.default_locale.clone()
|
|
||||||
} else {
|
|
||||||
self.default_locale.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> tower::Service<axum::extract::Request> for ExtractLanguageService<S>
|
|
||||||
where
|
|
||||||
S: tower::Service<axum::extract::Request, Response = axum::response::Response> + Send + 'static,
|
|
||||||
S::Future: Send + 'static,
|
|
||||||
{
|
|
||||||
type Response = S::Response;
|
|
||||||
type Error = S::Error;
|
|
||||||
// `BoxFuture` is a type alias for `Pin<Box<dyn Future + Send + 'a>>`
|
|
||||||
type Future = std::pin::Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
|
||||||
|
|
||||||
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
|
|
||||||
self.inner.poll_ready(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(&mut self, mut req: axum::extract::Request) -> Self::Future {
|
|
||||||
let headers = req.headers();
|
|
||||||
|
|
||||||
let lang = self.language_from_header(headers);
|
|
||||||
|
|
||||||
req.extensions_mut().insert(lang);
|
|
||||||
|
|
||||||
Box::pin(self.inner.call(req))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
macro_rules! push_error {
|
|
||||||
($value:expr, $errors:expr) => {
|
|
||||||
match $value {
|
|
||||||
Err(error) => { $errors.push(error); None },
|
|
||||||
Ok(value) => Some(value)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($value:expr, $errors:expr, $path:expr) => {
|
|
||||||
match $value {
|
|
||||||
Err(error) => { $errors.push(error.with_path($path)); None },
|
|
||||||
Ok(value) => Some(value)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! append_errors {
|
|
||||||
($value:expr, $errors:expr) => {
|
|
||||||
match $value {
|
|
||||||
Err(mut err) => { $errors.append(&mut err); None },
|
|
||||||
Ok(value) => Some(value)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($value:expr, $errors:expr, $path:expr) => {
|
|
||||||
match $value {
|
|
||||||
Err(err) => { $errors.extend(err.into_iter().map(|e| {
|
|
||||||
e.with_path($path)
|
|
||||||
})); None },
|
|
||||||
Ok(value) => Some(value)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! check_type {
|
|
||||||
($value:expr, Number, $errors:expr) => {
|
|
||||||
if let serde_json::Value::Number(value) = $value {
|
|
||||||
Some(value)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
let value = if let serde_json::Value::String(ref value) = $value {
|
|
||||||
value.parse::<i64>().ok()
|
|
||||||
.map(serde_json::Number::from)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if value.is_some() {
|
|
||||||
value
|
|
||||||
} else {
|
|
||||||
use crate::resources::dns::friendly::InputDataError;
|
|
||||||
use crate::resources::dns::friendly::ValueType;
|
|
||||||
use crate::errors::Error;
|
|
||||||
|
|
||||||
$errors.push(Error::from(InputDataError::TypeError { expected: ValueType::Number, found: ValueType::from_value(&$value) }));
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($value:expr, $type:ident, $errors:expr) => {
|
|
||||||
if let serde_json::Value::$type(value) = $value {
|
|
||||||
Some(value)
|
|
||||||
} else {
|
|
||||||
use crate::resources::dns::friendly::InputDataError;
|
|
||||||
use crate::resources::dns::friendly::ValueType;
|
|
||||||
use crate::errors::Error;
|
|
||||||
|
|
||||||
$errors.push(Error::from(InputDataError::TypeError { expected: ValueType::$type, found: ValueType::from_value(&$value) }));
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! get_object_value {
|
|
||||||
($value:expr, $key:expr, $errors:expr) => {
|
|
||||||
if let Some(value) = $value.remove($key) {
|
|
||||||
Some(value)
|
|
||||||
} else {
|
|
||||||
use crate::resources::dns::friendly::InputDataError;
|
|
||||||
use crate::errors::Error;
|
|
||||||
|
|
||||||
$errors.push(
|
|
||||||
Error::from(InputDataError::MissingValue)
|
|
||||||
.with_path(concat!("/", $key))
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub(crate) use append_errors;
|
|
||||||
pub(crate) use push_error;
|
|
||||||
pub(crate) use check_type;
|
|
||||||
pub(crate) use get_object_value;
|
|
95
src/main.rs
95
src/main.rs
|
@ -1,84 +1,26 @@
|
||||||
mod errors;
|
#![feature(proc_macro_hygiene, decl_macro)]
|
||||||
mod dns;
|
|
||||||
|
|
||||||
|
#[macro_use] extern crate rocket;
|
||||||
|
#[macro_use] extern crate diesel;
|
||||||
|
#[macro_use] extern crate diesel_migrations;
|
||||||
|
|
||||||
mod routes;
|
mod routes;
|
||||||
mod resources;
|
mod cli;
|
||||||
mod database;
|
mod config;
|
||||||
mod validation;
|
mod dns;
|
||||||
mod macros;
|
mod models;
|
||||||
|
mod schema;
|
||||||
mod template;
|
mod template;
|
||||||
mod proto;
|
mod controllers;
|
||||||
mod localization;
|
|
||||||
mod form;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::process::exit;
|
||||||
|
|
||||||
use axum::Router;
|
use clap::Parser;
|
||||||
use axum::routing;
|
use figment::{Figment, Profile, providers::{Format, Toml, Env}};
|
||||||
use tower_http::services::ServeDir;
|
use rocket_sync_db_pools::database;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
use database::sqlite::SqliteDB;
|
|
||||||
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<dyn ZoneDriver>,
|
|
||||||
records: Arc<dyn RecordDriver>,
|
|
||||||
db: BoxedDb,
|
|
||||||
template_engine: TemplateEngine
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let localization = localization::Localization::init(std::path::Path::new("./locales"));
|
|
||||||
|
|
||||||
let template_engine = TemplateEngine::new(
|
|
||||||
std::path::Path::new("./templates"),
|
|
||||||
localization.clone()
|
|
||||||
);
|
|
||||||
|
|
||||||
let dns_driver = dns::dns_driver::DnsDriver::from_config(DnsDriverConfig {
|
|
||||||
address: "127.0.0.1:5353".parse().unwrap(),
|
|
||||||
tsig: Some(TsigConfig {
|
|
||||||
key_name: "dev".parse().unwrap(),
|
|
||||||
secret: domain::utils::base64::decode::<Vec<u8>>("mbmz4J3Efm1BUjqe12M1RHsOnPjYhKQe+2iKO4tL+a4=").unwrap(),
|
|
||||||
algorithm: domain::tsig::Algorithm::Sha256,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let dns_driver = Arc::new(dns_driver);
|
|
||||||
|
|
||||||
let app_state = AppState {
|
|
||||||
zone: dns_driver.clone(),
|
|
||||||
records: dns_driver.clone(),
|
|
||||||
db: Arc::new(SqliteDB::new("db.sqlite".into()).await),
|
|
||||||
template_engine
|
|
||||||
};
|
|
||||||
|
|
||||||
let app = Router::new()
|
|
||||||
/* ----- 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_records_page))
|
|
||||||
.route("/zones/{zone_name}/records/new", routing::get(routes::ui::zones::get_new_record_page))
|
|
||||||
.route("/zones/{zone_name}/records/new", routing::post(routes::ui::zones::post_new_record))
|
|
||||||
.nest_service("/assets", ServeDir::new("assets"))
|
|
||||||
.with_state(app_state)
|
|
||||||
.layer(localization.language_middleware("en".parse().unwrap()));
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
use crate::cli::{NomiloCli, NomiloCommand};
|
use crate::cli::{NomiloCli, NomiloCommand};
|
||||||
|
|
||||||
#[database("sqlite")]
|
#[database("sqlite")]
|
||||||
|
@ -121,4 +63,3 @@ fn main() {
|
||||||
let nomilo = NomiloCli::parse();
|
let nomilo = NomiloCli::parse();
|
||||||
nomilo.run(figment, app_config);
|
nomilo.run(figment, app_config);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
40
src/models/class.rs
Normal file
40
src/models/class.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::dns;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub enum DNSClass {
|
||||||
|
IN,
|
||||||
|
CH,
|
||||||
|
HS,
|
||||||
|
NONE,
|
||||||
|
ANY,
|
||||||
|
OPT(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<dns::DNSClass> 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<DNSClass> 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
176
src/models/errors.rs
Normal file
176
src/models/errors.rs
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
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<DieselError> for UserError {
|
||||||
|
fn from(e: DieselError) -> Self {
|
||||||
|
UserError::DbError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PasswordHashError> 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<Value>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<T: Serialize> (self, details: T) -> ErrorResponse {
|
||||||
|
ErrorResponse {
|
||||||
|
details: serde_json::to_value(details).ok(),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn err<R>(self) -> Result<R, ErrorResponse> {
|
||||||
|
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<UserError> 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<Box<dyn ConnectorError>> for ErrorResponse {
|
||||||
|
fn from(e: Box<dyn ConnectorError>) -> 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<models::RecordListParseError> 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<S> From<ErrorResponse> for Outcome<S, ErrorResponse> {
|
||||||
|
fn from(e: ErrorResponse) -> Self {
|
||||||
|
Outcome::Failure(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl From<ErrorResponse> for (Status, ErrorResponse) {
|
||||||
|
fn from(e: ErrorResponse) -> Self {
|
||||||
|
(e.status, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: change for Display trait
|
||||||
|
pub fn make_500<E: std::fmt::Debug>(e: E) -> ErrorResponse {
|
||||||
|
error!("Making 500 for Error: {:?}", e);
|
||||||
|
|
||||||
|
ErrorResponse::new(Status::InternalServerError, "An unexpected error occured".into())
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
/*
|
|
||||||
pub mod class;
|
pub mod class;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod name;
|
pub mod name;
|
||||||
|
@ -17,7 +16,3 @@ pub use user::{LocalUser, UserInfo, Role, UserZone, User, CreateUserRequest};
|
||||||
pub use rdata::RData;
|
pub use rdata::RData;
|
||||||
pub use record::{Record, RecordList, ParseRecordList, RecordListParseError, UpdateRecordsRequest};
|
pub use record::{Record, RecordList, ParseRecordList, RecordListParseError, UpdateRecordsRequest};
|
||||||
pub use zone::{Zone, AddZoneMemberRequest, CreateZoneRequest};
|
pub use zone::{Zone, AddZoneMemberRequest, CreateZoneRequest};
|
||||||
*/
|
|
||||||
|
|
||||||
pub mod zone;
|
|
||||||
pub mod dns;
|
|
94
src/models/name.rs
Normal file
94
src/models/name.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
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<AbsoluteName, ProtoError> {
|
||||||
|
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<Self, Self::Error> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
258
src/models/rdata.rs
Normal file
258
src/models/rdata.rs
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::net::{Ipv6Addr, Ipv4Addr};
|
||||||
|
|
||||||
|
use base64::{Engine, engine::general_purpose};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use trust_dns_client::serialize::binary::BinEncoder;
|
||||||
|
use trust_dns_proto::error::ProtoError;
|
||||||
|
|
||||||
|
use crate::dns;
|
||||||
|
use super::name::SerdeName;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
#[serde(tag = "Type")]
|
||||||
|
#[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)
|
||||||
|
// TODO: TLSA
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<dns::RData> 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 TryFrom<RData> for dns::RData {
|
||||||
|
type Error = ProtoError;
|
||||||
|
|
||||||
|
fn try_from(rdata: RData) -> Result<Self, Self::Error> {
|
||||||
|
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!(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
122
src/models/record.rs
Normal file
122
src/models/record.rs
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<dns::Record> for Record {
|
||||||
|
fn from(record: dns::Record) -> Record {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Record> for dns::Record {
|
||||||
|
type Error = ProtoError;
|
||||||
|
|
||||||
|
fn try_from(record: Record) -> Result<Self, Self::Error> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub type RecordList = Vec<Record>;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateRecordsRequest {
|
||||||
|
pub old_records: RecordList,
|
||||||
|
pub new_records: RecordList,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RecordListParseError {
|
||||||
|
ParseError {
|
||||||
|
bad_records: Vec<Record>,
|
||||||
|
zone: dns::Name,
|
||||||
|
},
|
||||||
|
RecordNotInZone {
|
||||||
|
zone: dns::Name,
|
||||||
|
class: dns::DNSClass,
|
||||||
|
mismatched_class: Vec<Record>,
|
||||||
|
mismatched_zone: Vec<Record>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ParseRecordList {
|
||||||
|
fn try_into_dns_type(self, zone: dns::Name, class: dns::DNSClass) -> Result<Vec<dns::Record>, RecordListParseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseRecordList for RecordList {
|
||||||
|
fn try_into_dns_type(self, zone: dns::Name, class: dns::DNSClass) -> Result<Vec<dns::Record>, RecordListParseError> {
|
||||||
|
// TODO: What about relative names (also in cnames and stuff)
|
||||||
|
let mut bad_records = Vec::new();
|
||||||
|
let mut records: Vec<dns::Record> = Vec::new();
|
||||||
|
let mut mismatched_class: Vec<Record> = Vec::new();
|
||||||
|
let mut mismatched_zone: Vec<Record> = 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 !bad_records.is_empty() {
|
||||||
|
return Err(RecordListParseError::ParseError {
|
||||||
|
zone,
|
||||||
|
bad_records,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mismatched_class.is_empty() || !mismatched_zone.is_empty() {
|
||||||
|
return Err(RecordListParseError::RecordNotInZone {
|
||||||
|
zone,
|
||||||
|
class,
|
||||||
|
mismatched_zone,
|
||||||
|
mismatched_class
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(records)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
/*
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::result::Error as DieselError;
|
use diesel::result::Error as DieselError;
|
||||||
|
@ -235,4 +234,3 @@ impl LocalUser {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
93
src/models/zone.rs
Normal file
93
src/models/zone.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use crate::models::user::UserInfo;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::result::Error as DieselError;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
use crate::schema::*;
|
||||||
|
use super::name::AbsoluteName;
|
||||||
|
use super::user::UserZone;
|
||||||
|
use super::errors::UserError;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Queryable, Identifiable, Insertable)]
|
||||||
|
#[table_name = "zone"]
|
||||||
|
pub struct Zone {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct AddZoneMemberRequest {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, FromForm)]
|
||||||
|
pub struct CreateZoneRequest {
|
||||||
|
pub name: AbsoluteName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Should probably not be implemented here
|
||||||
|
// also, "UserError" seems like a misleading name
|
||||||
|
impl Zone {
|
||||||
|
pub fn get_all(conn: &diesel::SqliteConnection) -> Result<Vec<Zone>, UserError> {
|
||||||
|
use crate::schema::zone::dsl::*;
|
||||||
|
|
||||||
|
zone.get_results(conn)
|
||||||
|
.map_err(UserError::DbError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_name(conn: &diesel::SqliteConnection, zone_name: &str) -> Result<Zone, UserError> {
|
||||||
|
use crate::schema::zone::dsl::*;
|
||||||
|
|
||||||
|
zone.filter(name.eq(zone_name))
|
||||||
|
.get_result(conn)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
DieselError::NotFound => UserError::ZoneNotFound,
|
||||||
|
other => UserError::DbError(other)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_zone(conn: &diesel::SqliteConnection, zone_request: CreateZoneRequest) -> Result<Zone, UserError> {
|
||||||
|
use crate::schema::zone::dsl::*;
|
||||||
|
|
||||||
|
let new_zone = Zone {
|
||||||
|
id: Uuid::new_v4().to_simple().to_string(),
|
||||||
|
name: zone_request.name.to_utf8(),
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(zone)
|
||||||
|
.values(&new_zone)
|
||||||
|
.execute(conn)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserConflict,
|
||||||
|
other => UserError::DbError(other)
|
||||||
|
})?;
|
||||||
|
Ok(new_zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn add_member(&self, conn: &diesel::SqliteConnection, new_member: &UserInfo) -> Result<(), UserError> {
|
||||||
|
use crate::schema::user_zone::dsl::*;
|
||||||
|
|
||||||
|
let new_user_zone = UserZone {
|
||||||
|
zone_id: self.id.clone(),
|
||||||
|
user_id: new_member.id.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = diesel::insert_into(user_zone)
|
||||||
|
.values(new_user_zone)
|
||||||
|
.execute(conn);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
// If user has already access to the zone, safely ignore the conflit
|
||||||
|
// TODO: use 'on conflict do nothing' in postgres when we get there
|
||||||
|
Err(DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _)) => (),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
Ok(_) => ()
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
462
src/proto/dns.rs
462
src/proto/dns.rs
|
@ -1,462 +0,0 @@
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use domain::base::rdata::ComposeRecordData;
|
|
||||||
use domain::base::wire::{Composer, ParseError};
|
|
||||||
use domain::base::{iana::Class, Name, ParseRecordData, ParsedName, RecordData, Record, Rtype, ToName, Ttl};
|
|
||||||
use domain::rdata;
|
|
||||||
use domain::dep::octseq::{Parser, Octets};
|
|
||||||
|
|
||||||
use crate::resources::dns::internal;
|
|
||||||
use crate::errors::Error;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ProtoDnsError {
|
|
||||||
RDataUnknown { input: String, field: String, rtype: String },
|
|
||||||
NameParseError { input: String }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- A --------- */
|
|
||||||
|
|
||||||
impl From<domain::rdata::A> for internal::A {
|
|
||||||
fn from(record_data: domain::rdata::A) -> Self {
|
|
||||||
internal::A { address: record_data.addr() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::A> for domain::rdata::A {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(record_data: internal::A) -> Result<Self, Self::Error> {
|
|
||||||
Ok(domain::rdata::A::new(record_data.address))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- AAAA --------- */
|
|
||||||
|
|
||||||
impl From<domain::rdata::Aaaa> for internal::Aaaa {
|
|
||||||
fn from(record_data: domain::rdata::Aaaa) -> Self {
|
|
||||||
internal::Aaaa { address: record_data.addr() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::Aaaa> for domain::rdata::Aaaa {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(record_data: internal::Aaaa) -> Result<Self, Self::Error> {
|
|
||||||
Ok(domain::rdata::Aaaa::new(record_data.address))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- CNAME --------- */
|
|
||||||
|
|
||||||
impl<N: ToString> From<domain::rdata::Cname<N>> for internal::Cname {
|
|
||||||
fn from(record_data: domain::rdata::Cname<N>) -> Self {
|
|
||||||
internal::Cname { target: internal::Name::new(record_data.cname().to_string()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::Cname> for domain::rdata::Cname<Name<Vec<u8>>> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(record_data: internal::Cname) -> Result<domain::rdata::Cname<Name<Vec<u8>>>, Self::Error> {
|
|
||||||
let cname = record_data.target.to_string();
|
|
||||||
|
|
||||||
let cname: Name<_> = cname.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(ProtoDnsError::RDataUnknown {
|
|
||||||
input: cname,
|
|
||||||
field: "target".into(),
|
|
||||||
rtype: "CNAME".into(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(domain::rdata::Cname::new(cname))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- MX --------- */
|
|
||||||
|
|
||||||
impl<N: ToString> From<domain::rdata::Mx<N>> for internal::Mx {
|
|
||||||
fn from(record_data: domain::rdata::Mx<N>) -> Self {
|
|
||||||
internal::Mx {
|
|
||||||
preference: record_data.preference(),
|
|
||||||
mail_exchanger: internal::Name::new(record_data.exchange().to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::Mx> for domain::rdata::Mx<Name<Vec<u8>>> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(record_data: internal::Mx) -> Result<domain::rdata::Mx<Name<Vec<u8>>>, Self::Error> {
|
|
||||||
let mail_exchanger = record_data.mail_exchanger.to_string();
|
|
||||||
|
|
||||||
let mail_exchanger: Name<_> = mail_exchanger.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(ProtoDnsError::RDataUnknown {
|
|
||||||
input: mail_exchanger,
|
|
||||||
field: "mail_exchanger".into(),
|
|
||||||
rtype: "MX".into(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(domain::rdata::Mx::new(record_data.preference, mail_exchanger))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- NS --------- */
|
|
||||||
|
|
||||||
impl<N: ToString> From<domain::rdata::Ns<N>> for internal::Ns {
|
|
||||||
fn from(record_data: domain::rdata::Ns<N>) -> Self {
|
|
||||||
internal::Ns {
|
|
||||||
target: internal::Name::new(record_data.nsdname().to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::Ns> for domain::rdata::Ns<Name<Vec<u8>>> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(record_data: internal::Ns) -> Result<domain::rdata::Ns<Name<Vec<u8>>>, Self::Error> {
|
|
||||||
let target = record_data.target.to_string();
|
|
||||||
|
|
||||||
let target: Name<_> = target.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(ProtoDnsError::RDataUnknown {
|
|
||||||
input: target,
|
|
||||||
field: "target".into(),
|
|
||||||
rtype: "NS".into(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(domain::rdata::Ns::new(target))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- PTR --------- */
|
|
||||||
|
|
||||||
impl<N: ToString> From<domain::rdata::Ptr<N>> for internal::Ptr {
|
|
||||||
fn from(record_data: domain::rdata::Ptr<N>) -> Self {
|
|
||||||
internal::Ptr {
|
|
||||||
target: internal::Name::new(record_data.ptrdname().to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::Ptr> for domain::rdata::Ptr<Name<Vec<u8>>> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(record_data: internal::Ptr) -> Result<domain::rdata::Ptr<Name<Vec<u8>>>, Self::Error> {
|
|
||||||
let target = record_data.target.to_string();
|
|
||||||
|
|
||||||
let target: Name<_> = target.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(ProtoDnsError::RDataUnknown {
|
|
||||||
input: target,
|
|
||||||
field: "target".into(),
|
|
||||||
rtype: "PTR".into(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(domain::rdata::Ptr::new(target))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- SOA --------- */
|
|
||||||
|
|
||||||
impl<N: ToString> From<domain::rdata::Soa<N>> for internal::Soa {
|
|
||||||
fn from(record_rdata: domain::rdata::Soa<N>) -> Self {
|
|
||||||
internal::Soa {
|
|
||||||
primary_server: internal::Name::new(record_rdata.mname().to_string()),
|
|
||||||
maintainer: internal::Name::new(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 TryFrom<internal::Soa> for domain::rdata::Soa<Name<Vec<u8>>> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(record_data: internal::Soa) -> Result<domain::rdata::Soa<Name<Vec<u8>>>, Self::Error> {
|
|
||||||
let primary_server = record_data.primary_server.to_string();
|
|
||||||
let primary_server: Name<_> = primary_server.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(ProtoDnsError::RDataUnknown {
|
|
||||||
input: primary_server,
|
|
||||||
field: "primary_server".into(),
|
|
||||||
rtype: "SOA".into(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let maintainer = record_data.maintainer.to_string();
|
|
||||||
let maintainer: Name<_> = maintainer.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(ProtoDnsError::RDataUnknown {
|
|
||||||
input: maintainer,
|
|
||||||
field: "maintainer".into(),
|
|
||||||
rtype: "SOA".into(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(domain::rdata::Soa::new(
|
|
||||||
primary_server,
|
|
||||||
maintainer,
|
|
||||||
record_data.serial.into(),
|
|
||||||
Ttl::from_secs(record_data.refresh),
|
|
||||||
Ttl::from_secs(record_data.retry),
|
|
||||||
Ttl::from_secs(record_data.expire),
|
|
||||||
Ttl::from_secs(record_data.minimum),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- SRV --------- */
|
|
||||||
|
|
||||||
impl<N: ToString> From<domain::rdata::Srv<N>> for internal::Srv {
|
|
||||||
fn from(record_data: domain::rdata::Srv<N>) -> Self {
|
|
||||||
internal::Srv {
|
|
||||||
server: internal::Name::new(record_data.target().to_string()),
|
|
||||||
priority: record_data.priority(),
|
|
||||||
weight: record_data.weight(),
|
|
||||||
port: record_data.port(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::Srv> for domain::rdata::Srv<Name<Vec<u8>>> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(record_data: internal::Srv) -> Result<domain::rdata::Srv<Name<Vec<u8>>>, Self::Error> {
|
|
||||||
let server = record_data.server.to_string();
|
|
||||||
let server: Name<_> = server.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(ProtoDnsError::RDataUnknown {
|
|
||||||
input: server,
|
|
||||||
field: "server".into(),
|
|
||||||
rtype: "SRV".into(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
|
|
||||||
Ok(domain::rdata::Srv::new(
|
|
||||||
record_data.priority,
|
|
||||||
record_data.weight,
|
|
||||||
record_data.port,
|
|
||||||
server
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- TXT --------- */
|
|
||||||
|
|
||||||
impl<O: AsRef<[u8]>> From<domain::rdata::Txt<O>> for internal::Txt {
|
|
||||||
fn from(record_data: rdata::Txt<O>) -> Self {
|
|
||||||
let concatenated_text: Vec<_> = record_data.iter()
|
|
||||||
.flatten()
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
internal::Txt {
|
|
||||||
text: concatenated_text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::Txt> for domain::rdata::Txt<Vec<u8>> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(record_data: internal::Txt) -> Result<domain::rdata::Txt<Vec<u8>>, Self::Error> {
|
|
||||||
let txt: domain::rdata::Txt<_> = rdata::Txt::build_from_slice(&record_data.text).map_err(|e| {
|
|
||||||
let text = record_data.text.iter()
|
|
||||||
.fold(String::new(), |mut output, b| {
|
|
||||||
write!(output, "{:02X}", b).unwrap();
|
|
||||||
output
|
|
||||||
});
|
|
||||||
|
|
||||||
Error::from(ProtoDnsError::RDataUnknown {
|
|
||||||
input: format!("0x{}", text),
|
|
||||||
field: "text".into(),
|
|
||||||
rtype: "TXT".into(),
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
|
|
||||||
Ok(txt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- ParsedRData --------- */
|
|
||||||
|
|
||||||
pub enum ParsedRData<Name, Octs> {
|
|
||||||
A(domain::rdata::A),
|
|
||||||
Aaaa(domain::rdata::Aaaa),
|
|
||||||
Cname(domain::rdata::Cname<Name>),
|
|
||||||
Mx(domain::rdata::Mx<Name>),
|
|
||||||
Ns(domain::rdata::Ns<Name>),
|
|
||||||
Ptr(domain::rdata::Ptr<Name>),
|
|
||||||
Soa(domain::rdata::Soa<Name>),
|
|
||||||
Srv(domain::rdata::Srv<Name>),
|
|
||||||
Txt(domain::rdata::Txt<Octs>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Name: ToString, Octs: AsRef<[u8]>> From<ParsedRData<Name, Octs>> for internal::RData {
|
|
||||||
fn from(value: ParsedRData<Name, Octs>) -> Self {
|
|
||||||
match value {
|
|
||||||
ParsedRData::A(record_rdata) => internal::RData::A(record_rdata.into()),
|
|
||||||
ParsedRData::Aaaa(record_rdata) => internal::RData::Aaaa(record_rdata.into()),
|
|
||||||
ParsedRData::Cname(record_rdata) => internal::RData::Cname(record_rdata.into()),
|
|
||||||
ParsedRData::Mx(record_rdata) => internal::RData::Mx(record_rdata.into()),
|
|
||||||
ParsedRData::Ns(record_rdata) => internal::RData::Ns(record_rdata.into()),
|
|
||||||
ParsedRData::Ptr(record_rdata) => internal::RData::Ptr(record_rdata.into()),
|
|
||||||
ParsedRData::Soa(record_rdata) => internal::RData::Soa(record_rdata.into()),
|
|
||||||
ParsedRData::Srv(record_rdata) => internal::RData::Srv(record_rdata.into()),
|
|
||||||
ParsedRData::Txt(record_rdata) => internal::RData::Txt(record_rdata.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::RData> for ParsedRData<Name<Vec<u8>>, Vec<u8>> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(value: internal::RData) -> Result<Self, Self::Error> {
|
|
||||||
let rdata = match value {
|
|
||||||
internal::RData::A(record_rdata) => ParsedRData::A(record_rdata.try_into()?),
|
|
||||||
internal::RData::Aaaa(record_rdata) => ParsedRData::Aaaa(record_rdata.try_into()?),
|
|
||||||
internal::RData::Cname(record_rdata) => ParsedRData::Cname(record_rdata.try_into()?),
|
|
||||||
internal::RData::Mx(record_rdata) => ParsedRData::Mx(record_rdata.try_into()?),
|
|
||||||
internal::RData::Ns(record_rdata) => ParsedRData::Ns(record_rdata.try_into()?),
|
|
||||||
internal::RData::Ptr(record_rdata) => ParsedRData::Ptr(record_rdata.try_into()?),
|
|
||||||
internal::RData::Soa(record_rdata) => ParsedRData::Soa(record_rdata.try_into()?),
|
|
||||||
internal::RData::Srv(record_rdata) => ParsedRData::Srv(record_rdata.try_into()?),
|
|
||||||
internal::RData::Txt(record_rdata) => ParsedRData::Txt(record_rdata.try_into()?),
|
|
||||||
};
|
|
||||||
Ok(rdata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl<Name, Octs> ParsedRData<Name, Octs> {
|
|
||||||
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<Name, Octs> RecordData for ParsedRData<Name, Octs> {
|
|
||||||
fn rtype(&self) -> Rtype {
|
|
||||||
ParsedRData::rtype(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Octs: Octets + ?Sized> ParseRecordData<'a, Octs> for ParsedRData<ParsedName<Octs::Range<'a>>, Octs::Range<'a>> {
|
|
||||||
fn parse_rdata(
|
|
||||||
rtype: Rtype,
|
|
||||||
parser: &mut Parser<'a, Octs>,
|
|
||||||
) -> Result<Option<Self>, 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<Name: ToName, Octs: AsRef<[u8]>> ComposeRecordData for ParsedRData<Name, Octs> {
|
|
||||||
fn rdlen(&self, compress: bool) -> Option<u16> {
|
|
||||||
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<Target: Composer + ?Sized>(
|
|
||||||
&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<Target: Composer + ?Sized>(
|
|
||||||
&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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- Records --------- */
|
|
||||||
|
|
||||||
pub(crate) type RecordImpl = Record<
|
|
||||||
Name<Vec<u8>>,
|
|
||||||
ParsedRData<Name<Vec<u8>>,Vec<u8>>
|
|
||||||
>;
|
|
||||||
|
|
||||||
|
|
||||||
impl<Name: ToString, Oct: AsRef<[u8]>> From<Record<Name, ParsedRData<Name, Oct>>> for internal::Record {
|
|
||||||
fn from(value: Record<Name, ParsedRData<Name, Oct>>) -> Self {
|
|
||||||
internal::Record {
|
|
||||||
name: internal::Name::new(value.owner().to_string()),
|
|
||||||
ttl: value.ttl().as_secs(),
|
|
||||||
rdata: internal::RData::from(value.into_data()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::Record> for RecordImpl {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(value: internal::Record) -> Result<Self, Self::Error> {
|
|
||||||
let owner = value.name.to_string();
|
|
||||||
let owner = owner.parse::<Name<_>>().map_err(|e| {
|
|
||||||
Error::from(ProtoDnsError::NameParseError {
|
|
||||||
input: owner
|
|
||||||
}).with_cause(&e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let ttl = Ttl::from_secs(value.ttl);
|
|
||||||
let data = value.rdata.try_into()?;
|
|
||||||
Ok(Record::new(owner, Class::IN, ttl, data))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod dns;
|
|
4
src/resources/dns/external/mod.rs
vendored
4
src/resources/dns/external/mod.rs
vendored
|
@ -1,4 +0,0 @@
|
||||||
pub mod rdata;
|
|
||||||
pub mod record;
|
|
||||||
|
|
||||||
pub use record::*;
|
|
430
src/resources/dns/external/rdata.rs
vendored
430
src/resources/dns/external/rdata.rs
vendored
|
@ -1,430 +0,0 @@
|
||||||
use std::fmt::Write;
|
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
|
|
||||||
use domain::base::scan::Symbol;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::errors::Error;
|
|
||||||
use crate::validation;
|
|
||||||
|
|
||||||
use crate::macros::{append_errors, push_error};
|
|
||||||
use crate::resources::dns::internal;
|
|
||||||
|
|
||||||
pub enum RDataValidationError {
|
|
||||||
Ip4Address { input: String },
|
|
||||||
Ip6Address { input: String },
|
|
||||||
IpAddress { input: String },
|
|
||||||
Number { min: i128, max: i128 }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RData {
|
|
||||||
pub fn validate(self) -> Result<internal::RData, Vec<Error>> {
|
|
||||||
let rdata = match self {
|
|
||||||
RData::A(data) => internal::RData::A(data.validate()?),
|
|
||||||
RData::Aaaa(data) => internal::RData::Aaaa(data.validate()?),
|
|
||||||
RData::Cname(data) => internal::RData::Cname(data.validate()?),
|
|
||||||
RData::Mx(data) => internal::RData::Mx(data.validate()?),
|
|
||||||
RData::Ns(data) => internal::RData::Ns(data.validate()?),
|
|
||||||
RData::Ptr(data) => internal::RData::Ptr(data.validate()?),
|
|
||||||
RData::Soa(data) => internal::RData::Soa(data.validate()?),
|
|
||||||
RData::Srv(data) => internal::RData::Srv(data.validate()?),
|
|
||||||
RData::Txt(data) => internal::RData::Txt(data.validate()?),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(rdata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::RData> for RData {
|
|
||||||
fn from(value: internal::RData) -> Self {
|
|
||||||
match value {
|
|
||||||
internal::RData::A(data) => RData::A(data.into()),
|
|
||||||
internal::RData::Aaaa(data) => RData::Aaaa(data.into()),
|
|
||||||
internal::RData::Cname(data) => RData::Cname(data.into()),
|
|
||||||
internal::RData::Mx(data) => RData::Mx(data.into()),
|
|
||||||
internal::RData::Ns(data) => RData::Ns(data.into()),
|
|
||||||
internal::RData::Ptr(data) => RData::Ptr(data.into()),
|
|
||||||
internal::RData::Soa(data) => RData::Soa(data.into()),
|
|
||||||
internal::RData::Srv(data) => RData::Srv(data.into()),
|
|
||||||
internal::RData::Txt(data) => RData::Txt(data.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- A --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct A {
|
|
||||||
pub address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::A> for A {
|
|
||||||
fn from(value: internal::A) -> Self {
|
|
||||||
A {
|
|
||||||
address: value.address.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl A {
|
|
||||||
pub fn validate(self) -> Result<internal::A, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
// TODO: replace with custom validation
|
|
||||||
let address = push_error!(self.address.parse::<Ipv4Addr>().map_err(|e| {
|
|
||||||
Error::from(RDataValidationError::Ip4Address { input: self.address })
|
|
||||||
.with_cause(&e.to_string())
|
|
||||||
.with_path("/address")
|
|
||||||
}), errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::A {
|
|
||||||
address: address.unwrap()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- AAAA --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Aaaa {
|
|
||||||
pub address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::Aaaa> for Aaaa {
|
|
||||||
fn from(value: internal::Aaaa) -> Self {
|
|
||||||
Aaaa {
|
|
||||||
address: value.address.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Aaaa {
|
|
||||||
pub fn validate(self) -> Result<internal::Aaaa, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
// TODO: replace with custom validation
|
|
||||||
let address = push_error!(self.address.parse::<Ipv6Addr>().map_err(|e| {
|
|
||||||
Error::from(RDataValidationError::Ip6Address { input: self.address })
|
|
||||||
.with_cause(&e.to_string())
|
|
||||||
.with_path("/address")
|
|
||||||
}), errors);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::Aaaa {
|
|
||||||
address: address.unwrap()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- CNAME --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Cname {
|
|
||||||
pub target: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::Cname> for Cname {
|
|
||||||
fn from(value: internal::Cname) -> Self {
|
|
||||||
Cname {
|
|
||||||
target: value.target.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cname {
|
|
||||||
pub fn validate(self) -> Result<internal::Cname, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let cname = push_error!(
|
|
||||||
validation::normalize_domain(&self.target),
|
|
||||||
errors, "/target"
|
|
||||||
);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::Cname {
|
|
||||||
target: internal::Name::new(cname.unwrap())
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- MX --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Mx {
|
|
||||||
// TODO: Validate number
|
|
||||||
pub preference: u16,
|
|
||||||
pub mail_exchanger: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::Mx> for Mx {
|
|
||||||
fn from(value: internal::Mx) -> Self {
|
|
||||||
Mx {
|
|
||||||
preference: value.preference,
|
|
||||||
mail_exchanger: value.mail_exchanger.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mx {
|
|
||||||
pub fn validate(self) -> Result<internal::Mx, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let mail_exchanger = push_error!(
|
|
||||||
validation::normalize_domain(&self.mail_exchanger),
|
|
||||||
errors, "/mail_exchanger"
|
|
||||||
);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::Mx {
|
|
||||||
preference: self.preference,
|
|
||||||
mail_exchanger: internal::Name::new(mail_exchanger.unwrap()),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- NS --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Ns {
|
|
||||||
pub target: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::Ns> for Ns {
|
|
||||||
fn from(value: internal::Ns) -> Self {
|
|
||||||
Ns {
|
|
||||||
target: value.target.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ns {
|
|
||||||
pub fn validate(self) -> Result<internal::Ns, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let target = push_error!(
|
|
||||||
validation::normalize_domain(&self.target),
|
|
||||||
errors, "/target"
|
|
||||||
);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::Ns {
|
|
||||||
target: internal::Name::new(target.unwrap()),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- PTR --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Ptr {
|
|
||||||
pub target: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::Ptr> for Ptr {
|
|
||||||
fn from(value: internal::Ptr) -> Self {
|
|
||||||
Ptr {
|
|
||||||
target: value.target.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ptr {
|
|
||||||
pub fn validate(self) -> Result<internal::Ptr, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let target = push_error!(
|
|
||||||
validation::normalize_domain(&self.target),
|
|
||||||
errors, "/target"
|
|
||||||
);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::Ptr {
|
|
||||||
target: internal::Name::new(target.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<internal::Soa> for Soa {
|
|
||||||
fn from(value: internal::Soa) -> Self {
|
|
||||||
Soa {
|
|
||||||
primary_server: value.primary_server.to_string(),
|
|
||||||
maintainer: value.maintainer.to_string(),
|
|
||||||
refresh: value.refresh,
|
|
||||||
retry: value.retry,
|
|
||||||
expire: value.expire,
|
|
||||||
minimum: value.minimum,
|
|
||||||
serial: value.serial,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Soa {
|
|
||||||
pub fn validate(self) -> Result<internal::Soa, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let primary_server = push_error!(
|
|
||||||
validation::normalize_domain(&self.primary_server),
|
|
||||||
errors, "/primary_server"
|
|
||||||
);
|
|
||||||
|
|
||||||
let maintainer = push_error!(
|
|
||||||
validation::normalize_domain(&self.maintainer),
|
|
||||||
errors, "/maintainer"
|
|
||||||
);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::Soa {
|
|
||||||
primary_server: internal::Name::new(primary_server.unwrap()),
|
|
||||||
maintainer: internal::Name::new(maintainer.unwrap()),
|
|
||||||
refresh: self.refresh,
|
|
||||||
retry: self.retry,
|
|
||||||
expire: self.expire,
|
|
||||||
minimum: self.minimum,
|
|
||||||
serial: 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<internal::Srv> for Srv {
|
|
||||||
fn from(value: internal::Srv) -> Self {
|
|
||||||
Srv {
|
|
||||||
server: value.server.to_string(),
|
|
||||||
port: value.port,
|
|
||||||
priority: value.priority,
|
|
||||||
weight: value.weight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Srv {
|
|
||||||
pub fn validate(self) -> Result<internal::Srv, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let server = push_error!(
|
|
||||||
validation::normalize_domain(&self.server),
|
|
||||||
errors, "/server"
|
|
||||||
);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::Srv {
|
|
||||||
server: internal::Name::new(server.unwrap()),
|
|
||||||
priority: self.priority,
|
|
||||||
weight: self.weight,
|
|
||||||
port: self.port,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* --------- TXT --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Txt {
|
|
||||||
pub text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::Txt> for Txt {
|
|
||||||
fn from(value: internal::Txt) -> Self {
|
|
||||||
|
|
||||||
let mut concatenated_text = String::new();
|
|
||||||
for c in value.text.iter() {
|
|
||||||
// Escapes '\' and non printable chars
|
|
||||||
let c = Symbol::display_from_octet(*c);
|
|
||||||
write!(concatenated_text, "{}", c).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Txt {
|
|
||||||
text: concatenated_text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Txt {
|
|
||||||
pub fn validate(self) -> Result<internal::Txt, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let text = append_errors!(
|
|
||||||
validation::parse_txt_data(&self.text),
|
|
||||||
errors, "/text"
|
|
||||||
);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::Txt {
|
|
||||||
text: text.unwrap()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
119
src/resources/dns/external/record.rs
vendored
119
src/resources/dns/external/record.rs
vendored
|
@ -1,119 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::rdata::RData;
|
|
||||||
use crate::resources::dns::internal;
|
|
||||||
use crate::{errors::Error, validation};
|
|
||||||
use crate::macros::{append_errors, push_error};
|
|
||||||
|
|
||||||
pub enum RecordError {
|
|
||||||
Validation { suberrors: Vec<Error> },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum RecordValidationError {
|
|
||||||
NotInZone { name: String, zone: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Record {
|
|
||||||
pub name: String,
|
|
||||||
pub ttl: u32,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub rdata: RData
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::Record> for Record {
|
|
||||||
fn from(value: internal::Record) -> Self {
|
|
||||||
Record {
|
|
||||||
name: value.name.to_string(),
|
|
||||||
ttl: value.ttl,
|
|
||||||
rdata: value.rdata.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Record {
|
|
||||||
fn validate(self, zone_name: &internal::Name) -> Result<internal::Record, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let name = push_error!(validation::normalize_domain(&self.name), errors, "/name").map(internal::Name::new);
|
|
||||||
|
|
||||||
let name = name.and_then(|name| {
|
|
||||||
if !name.ends_with(zone_name) {
|
|
||||||
errors.push(
|
|
||||||
Error::from(RecordValidationError::NotInZone { name: self.name, zone: zone_name.to_string() })
|
|
||||||
.with_path("/name")
|
|
||||||
);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(name)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// TODO: validate ttl
|
|
||||||
let rdata = append_errors!(self.rdata.validate(), errors, "/rdata");
|
|
||||||
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::Record {
|
|
||||||
name: name.unwrap(),
|
|
||||||
ttl: self.ttl,
|
|
||||||
rdata: rdata.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct RecordList(pub Vec<Record>);
|
|
||||||
|
|
||||||
impl From<internal::RecordList> for RecordList {
|
|
||||||
fn from(value: internal::RecordList) -> Self {
|
|
||||||
let records = value.records.into_iter().map(Record::from).collect();
|
|
||||||
RecordList(records)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecordList {
|
|
||||||
fn validate(self, zone_name: &internal::Name) -> Result<internal::RecordList, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
let mut records = Vec::new();
|
|
||||||
|
|
||||||
for (index, record) in self.0.into_iter().enumerate() {
|
|
||||||
let record = append_errors!(record.validate(zone_name), errors, &format!("/{index}"));
|
|
||||||
|
|
||||||
if let Some(record) = record {
|
|
||||||
records.push(record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::RecordList { records })
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug,Deserialize)]
|
|
||||||
pub struct AddRecords {
|
|
||||||
pub new_records: RecordList
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AddRecords {
|
|
||||||
pub fn validate(self, zone_name: &internal::Name) -> Result<internal::AddRecords, Error> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
let records = append_errors!(self.new_records.validate(zone_name), errors, "/new_records");
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(internal::AddRecords {
|
|
||||||
new_records: records.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(Error::from(RecordError::Validation { suberrors: errors }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,207 +0,0 @@
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use crate::errors::Error;
|
|
||||||
use crate::macros::{push_error, append_errors, check_type};
|
|
||||||
use crate::resources::dns::external::rdata::RDataValidationError;
|
|
||||||
use crate::resources::dns::internal::base::{Name, Text};
|
|
||||||
use crate::validation;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum ValueType {
|
|
||||||
Object,
|
|
||||||
Array,
|
|
||||||
String,
|
|
||||||
Number,
|
|
||||||
Null,
|
|
||||||
Bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ValueType {
|
|
||||||
pub fn from_value(value: &Value) -> ValueType {
|
|
||||||
match value {
|
|
||||||
Value::Array(_) => ValueType::Array,
|
|
||||||
Value::Bool(_) => ValueType::Bool,
|
|
||||||
Value::Null => ValueType::Null,
|
|
||||||
Value::Number(_) => ValueType::Number,
|
|
||||||
Value::Object(_) => ValueType::Object,
|
|
||||||
Value::String(_) => ValueType::String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum InputDataError {
|
|
||||||
TypeError { expected: ValueType, found: ValueType },
|
|
||||||
MissingValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait FromValue: Sized {
|
|
||||||
fn from_value(value: Value) -> Result<Self, Vec<Error>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for Name {
|
|
||||||
fn from_value(value: Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let value = check_type!(value, String, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = push_error!(
|
|
||||||
validation::normalize_domain(&value.unwrap()),
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(Name::new(name.unwrap()))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for IpAddr {
|
|
||||||
fn from_value(value: Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let address = check_type!(value, String, errors);
|
|
||||||
|
|
||||||
let address = if let Some(address) = address {
|
|
||||||
// TODO: replace with custom validation
|
|
||||||
push_error!(address.parse::<IpAddr>().map_err(|e| {
|
|
||||||
Error::from(RDataValidationError::IpAddress { input: address })
|
|
||||||
.with_cause(&e.to_string())
|
|
||||||
}), errors)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(address.unwrap())
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for u32 {
|
|
||||||
fn from_value(value: Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let number = check_type!(value, Number, errors);
|
|
||||||
|
|
||||||
let address = if let Some(number) = number {
|
|
||||||
push_error!(
|
|
||||||
number.as_u64()
|
|
||||||
.ok_or(Error::from(RDataValidationError::Number { min: u32::MIN.into(), max: u32::MAX.into()}))
|
|
||||||
.and_then(|number| {
|
|
||||||
u32::try_from(number).map_err(|e| {
|
|
||||||
Error::from(RDataValidationError::Number { min: u32::MIN.into(), max: u32::MAX.into()})
|
|
||||||
.with_cause(&e.to_string())
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
errors
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(address.unwrap())
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for u16 {
|
|
||||||
fn from_value(value: Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let number = check_type!(value, Number, errors);
|
|
||||||
|
|
||||||
let address = if let Some(number) = number {
|
|
||||||
push_error!(
|
|
||||||
number.as_u64()
|
|
||||||
.ok_or(Error::from(RDataValidationError::Number { min: u16::MIN.into(), max: u16::MAX.into()}))
|
|
||||||
.and_then(|number| {
|
|
||||||
u16::try_from(number).map_err(|e| {
|
|
||||||
Error::from(RDataValidationError::Number { min: u16::MIN.into(), max: u16::MAX.into()})
|
|
||||||
.with_cause(&e.to_string())
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
errors
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(address.unwrap())
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: FromValue> FromValue for Vec<T> {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let array = check_type!(value, Array, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
let array = array.unwrap();
|
|
||||||
|
|
||||||
let mut list = Vec::new();
|
|
||||||
|
|
||||||
for (index, item) in array.into_iter().enumerate() {
|
|
||||||
let res = append_errors!(
|
|
||||||
T::from_value(item),
|
|
||||||
errors,
|
|
||||||
&format!("/{index}")
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(item) = res {
|
|
||||||
list.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(list)
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for Text {
|
|
||||||
fn from_value(value: Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let data = check_type!(value, String, errors);
|
|
||||||
|
|
||||||
let data = if let Some(data) = data {
|
|
||||||
append_errors!(validation::parse_txt_data(&data), errors)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(Text ::new(data.unwrap()))
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,271 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::validation;
|
|
||||||
use crate::errors::Error;
|
|
||||||
use crate::macros::{append_errors, check_type, get_object_value, push_error};
|
|
||||||
use crate::resources::dns::internal::{self, Name, Record};
|
|
||||||
|
|
||||||
use super::{rdata, FriendlyRType};
|
|
||||||
use super::FromValue;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum ConfigurationType {
|
|
||||||
Mail,
|
|
||||||
Web,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigurationType {
|
|
||||||
pub fn get_records(&self, input_data: serde_json::Value, default_ttl: u32, zone_name: Name) -> Result<Vec<Record>, Vec<Error>> {
|
|
||||||
match self {
|
|
||||||
ConfigurationType::Mail => {
|
|
||||||
NewSectionMail::from_value(input_data.clone())
|
|
||||||
.map(|section| section.internal(default_ttl, zone_name))
|
|
||||||
},
|
|
||||||
ConfigurationType::Web => {
|
|
||||||
NewSectionWeb::from_value(input_data.clone())
|
|
||||||
.map(|section| section.internal(default_ttl, zone_name))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct NewRecordQuery {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub config: Option<ConfigurationType>,
|
|
||||||
pub rtype: Option<FriendlyRType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NewRecordQuery {
|
|
||||||
pub fn validate(&mut self, zone_name: &str) -> Result<(), Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
if let Some(input_name) = &self.name {
|
|
||||||
if input_name.is_empty() {
|
|
||||||
self.name = Some(zone_name.to_string());
|
|
||||||
} else {
|
|
||||||
let new_name = format!("{input_name}.{zone_name}");
|
|
||||||
let new_name = push_error!(
|
|
||||||
validation::normalize_domain(&new_name),
|
|
||||||
errors,
|
|
||||||
"/name"
|
|
||||||
);
|
|
||||||
// Keep the old value if the name is invalid
|
|
||||||
if new_name.is_some() {
|
|
||||||
self.name = new_name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ToInternal {
|
|
||||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct NewRecord<T> {
|
|
||||||
ttl: Option<u32>,
|
|
||||||
data: Option<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: FromValue> FromValue for NewRecord<T> {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut object = object.unwrap();
|
|
||||||
|
|
||||||
let ttl = object.remove("ttl");
|
|
||||||
let ttl = if let Some(ttl) = ttl {
|
|
||||||
append_errors!(FromValue::from_value(ttl), errors, "/ttl")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = object.remove("data");
|
|
||||||
let data = if let Some(data) = data {
|
|
||||||
append_errors!(FromValue::from_value(data), errors, "/data")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(NewRecord {
|
|
||||||
ttl,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: ToInternal> ToInternal for NewRecord<T> {
|
|
||||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
|
||||||
let ttl = self.ttl.unwrap_or(ttl);
|
|
||||||
|
|
||||||
self.data
|
|
||||||
.map(|data| data.internal(ttl, node_name))
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct NewRequiredRecord<T> {
|
|
||||||
ttl: Option<u32>,
|
|
||||||
data: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: FromValue> FromValue for NewRequiredRecord<T> {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut object = object.unwrap();
|
|
||||||
|
|
||||||
let ttl = object.remove("ttl");
|
|
||||||
let ttl = if let Some(ttl) = ttl {
|
|
||||||
append_errors!(FromValue::from_value(ttl), errors, "/ttl")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = get_object_value!(object, "data", errors);
|
|
||||||
let data = if let Some(data) = data {
|
|
||||||
append_errors!(FromValue::from_value(data), errors, "/data")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(NewRequiredRecord {
|
|
||||||
ttl,
|
|
||||||
data: data.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: ToInternal> ToInternal for NewRequiredRecord<T> {
|
|
||||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
|
||||||
let ttl = self.ttl.unwrap_or(ttl);
|
|
||||||
|
|
||||||
self.data.internal(ttl, node_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct NewSectionWeb {
|
|
||||||
addresses: NewRequiredRecord<rdata::Addresses>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for NewSectionWeb {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut object = object.unwrap();
|
|
||||||
|
|
||||||
let addresses = get_object_value!(object, "addresses", errors);
|
|
||||||
let addresses = if let Some(addresses) = addresses {
|
|
||||||
append_errors!(FromValue::from_value(addresses), errors, "/addresses")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(NewSectionWeb {
|
|
||||||
addresses: addresses.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToInternal for NewSectionWeb {
|
|
||||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
|
||||||
self.addresses.internal(ttl, node_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct NewSectionMail {
|
|
||||||
mailservers: NewRequiredRecord<rdata::MailServers>,
|
|
||||||
spf: NewRecord<rdata::Spf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for NewSectionMail {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut object = object.unwrap();
|
|
||||||
|
|
||||||
let mailservers = get_object_value!(object, "mailservers", errors);
|
|
||||||
let mailservers = if let Some(mailservers) = mailservers {
|
|
||||||
append_errors!(FromValue::from_value(mailservers), errors, "/mailservers")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let spf = get_object_value!(object, "spf", errors);
|
|
||||||
let spf = if let Some(spf) = spf {
|
|
||||||
append_errors!(FromValue::from_value(spf), errors, "/spf")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(NewSectionMail {
|
|
||||||
mailservers: mailservers.unwrap(),
|
|
||||||
spf: spf.unwrap()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToInternal for NewSectionMail {
|
|
||||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
|
||||||
let mut records = self.mailservers.internal(ttl, node_name.clone());
|
|
||||||
records.extend(self.spf.internal(ttl, node_name));
|
|
||||||
records
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
pub mod rdata;
|
|
||||||
pub mod record;
|
|
||||||
pub mod base;
|
|
||||||
pub mod create;
|
|
||||||
|
|
||||||
pub use rdata::*;
|
|
||||||
pub use record::*;
|
|
||||||
pub use base::*;
|
|
||||||
pub use create::*;
|
|
|
@ -1,324 +0,0 @@
|
||||||
use std::hash::Hash;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::errors::Error;
|
|
||||||
use crate::resources::dns::internal::{self, Name, Text};
|
|
||||||
use crate::macros::{append_errors, check_type, get_object_value};
|
|
||||||
|
|
||||||
use super::{FromValue, ToInternal};
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub enum FriendlyRData {
|
|
||||||
Address(Address),
|
|
||||||
Alias(Alias),
|
|
||||||
MailServer(MailServer),
|
|
||||||
NameServer(NameServer),
|
|
||||||
Service(ServiceSingleTarget),
|
|
||||||
Spf(Spf),
|
|
||||||
TextData(TextData),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum FriendlyRDataAggregated {
|
|
||||||
Addresses(Addresses),
|
|
||||||
MailServers(MailServers),
|
|
||||||
NameServers(NameServers),
|
|
||||||
Service(Service),
|
|
||||||
Spf(Spf),
|
|
||||||
Texts(Texts),
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- RDATA --------- */
|
|
||||||
|
|
||||||
/* --------- Address --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Address {
|
|
||||||
pub address: IpAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for Address {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut object = object.unwrap();
|
|
||||||
|
|
||||||
let address = get_object_value!(object, "address", errors);
|
|
||||||
let address = if let Some(address) = address {
|
|
||||||
append_errors!(FromValue::from_value(address), errors, "/address")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(Address {
|
|
||||||
address: address.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Addresses {
|
|
||||||
pub addresses: Vec<Address>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for Addresses {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut object = object.unwrap();
|
|
||||||
|
|
||||||
let addresses = get_object_value!(object, "addresses", errors);
|
|
||||||
let addresses = if let Some(addresses) = addresses {
|
|
||||||
append_errors!(FromValue::from_value(addresses), errors, "/addresses")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(Addresses {
|
|
||||||
addresses: addresses.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToInternal for Addresses {
|
|
||||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
|
||||||
self.addresses.into_iter().map(|addr| {
|
|
||||||
internal::Record {
|
|
||||||
ttl,
|
|
||||||
name: node_name.clone(),
|
|
||||||
rdata: match addr.address {
|
|
||||||
IpAddr::V4(ip4) => internal::RData::A(internal::A::new(ip4)),
|
|
||||||
IpAddr::V6(ip6) => internal::RData::Aaaa(internal::Aaaa::new(ip6)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- Service --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct ServiceSingleTarget {
|
|
||||||
pub service_type: ServiceType,
|
|
||||||
pub service_target: ServiceTarget,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Service {
|
|
||||||
pub service_type: ServiceType,
|
|
||||||
pub service_targets: Vec<ServiceTarget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct ServiceTarget {
|
|
||||||
pub port: i64,
|
|
||||||
pub weight: i64,
|
|
||||||
pub priority: i64,
|
|
||||||
pub server: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Hash, Eq, PartialEq, Clone, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "lowercase", tag = "service_type")]
|
|
||||||
pub enum ServiceType {
|
|
||||||
Other { protocol: String, name: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- MailServer --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct MailServer {
|
|
||||||
pub preference: u16,
|
|
||||||
pub mail_exchanger: Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for MailServer {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut object = object.unwrap();
|
|
||||||
|
|
||||||
let preference = get_object_value!(object, "preference", errors);
|
|
||||||
let preference = if let Some(preference) = preference {
|
|
||||||
append_errors!(FromValue::from_value(preference), errors, "/preference")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mail_exchanger = get_object_value!(object, "mail_exchanger", errors);
|
|
||||||
let mail_exchanger = if let Some(mail_exchanger) = mail_exchanger {
|
|
||||||
append_errors!(FromValue::from_value(mail_exchanger), errors, "/mail_exchanger")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(MailServer {
|
|
||||||
preference: preference.unwrap(),
|
|
||||||
mail_exchanger: mail_exchanger.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct MailServers {
|
|
||||||
pub mailservers: Vec<MailServer>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for MailServers {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut object = object.unwrap();
|
|
||||||
|
|
||||||
let mailservers = get_object_value!(object, "mailservers", errors);
|
|
||||||
let mailservers = if let Some(mailservers) = mailservers {
|
|
||||||
append_errors!(FromValue::from_value(mailservers), errors, "/mailservers")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(MailServers {
|
|
||||||
mailservers: mailservers.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToInternal for MailServers {
|
|
||||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
|
||||||
self.mailservers.into_iter().map(|mailserver| {
|
|
||||||
internal::Record {
|
|
||||||
ttl,
|
|
||||||
name: node_name.clone(),
|
|
||||||
rdata: internal::RData::Mx(internal::Mx::new(mailserver.mail_exchanger, mailserver.preference)),
|
|
||||||
}
|
|
||||||
}).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* --------- NameServer --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct NameServer {
|
|
||||||
pub target: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct NameServers {
|
|
||||||
pub nameservers: Vec<NameServer>
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- TextData --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct TextData {
|
|
||||||
pub text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Texts {
|
|
||||||
pub texts: Vec<TextData>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- Spf --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Spf {
|
|
||||||
pub policy: Text
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromValue for Spf {
|
|
||||||
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
|
||||||
|
|
||||||
if !errors.is_empty() {
|
|
||||||
return Err(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut object = object.unwrap();
|
|
||||||
|
|
||||||
let policy = get_object_value!(object, "policy", errors);
|
|
||||||
let policy = if let Some(policy) = policy {
|
|
||||||
append_errors!(FromValue::from_value(policy), errors, "/policy")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(Spf {
|
|
||||||
policy: policy.unwrap(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl ToInternal for Spf {
|
|
||||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
|
||||||
vec![
|
|
||||||
internal::Record {
|
|
||||||
ttl,
|
|
||||||
name: node_name,
|
|
||||||
rdata: internal::RData::Txt(internal::Txt::new(self.policy))
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------- Alias --------- */
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Alias {
|
|
||||||
pub from: String,
|
|
||||||
pub target: String,
|
|
||||||
pub ttl: i64,
|
|
||||||
}
|
|
|
@ -1,249 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::hash::Hash;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize, Serializer};
|
|
||||||
|
|
||||||
use crate::resources::dns::internal;
|
|
||||||
|
|
||||||
use super::rdata::{
|
|
||||||
FriendlyRDataAggregated,
|
|
||||||
FriendlyRData,
|
|
||||||
ServiceType,
|
|
||||||
self
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all="lowercase")]
|
|
||||||
pub enum RecordSection {
|
|
||||||
Mail,
|
|
||||||
Web,
|
|
||||||
Services,
|
|
||||||
Miscellaneous,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq)]
|
|
||||||
#[serde(rename_all="lowercase")]
|
|
||||||
pub enum FriendlyRType {
|
|
||||||
Addresses,
|
|
||||||
Alias,
|
|
||||||
MailServers,
|
|
||||||
NameServers,
|
|
||||||
Service,
|
|
||||||
Spf,
|
|
||||||
Texts,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FriendlyRecord {
|
|
||||||
ttl: u32,
|
|
||||||
data: FriendlyRDataAggregated,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for FriendlyRecord {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct ExtendedRecord<'a> {
|
|
||||||
ttl: u32,
|
|
||||||
record_type: FriendlyRType,
|
|
||||||
record_section: RecordSection,
|
|
||||||
#[serde(flatten)]
|
|
||||||
data: &'a FriendlyRDataAggregated,
|
|
||||||
}
|
|
||||||
|
|
||||||
let extended_record = ExtendedRecord {
|
|
||||||
ttl: self.ttl,
|
|
||||||
data: &self.data,
|
|
||||||
record_type: self.record_type(),
|
|
||||||
record_section: self.record_section(),
|
|
||||||
};
|
|
||||||
|
|
||||||
extended_record.serialize(serializer)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FriendlyRecord {
|
|
||||||
pub fn new(ttl: u32, data: FriendlyRDataAggregated) -> Self {
|
|
||||||
FriendlyRecord {
|
|
||||||
ttl,
|
|
||||||
data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_type(&self) -> FriendlyRType {
|
|
||||||
match self.data {
|
|
||||||
FriendlyRDataAggregated::Addresses(_) => FriendlyRType::Addresses,
|
|
||||||
FriendlyRDataAggregated::MailServers(_) => FriendlyRType::MailServers,
|
|
||||||
FriendlyRDataAggregated::NameServers(_) => FriendlyRType::NameServers,
|
|
||||||
FriendlyRDataAggregated::Service(_) => FriendlyRType::Service,
|
|
||||||
FriendlyRDataAggregated::Spf(_) => FriendlyRType::Spf,
|
|
||||||
FriendlyRDataAggregated::Texts(_) => FriendlyRType::Texts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_section(&self) -> RecordSection {
|
|
||||||
match self.data {
|
|
||||||
FriendlyRDataAggregated::Addresses(_) => RecordSection::Web,
|
|
||||||
FriendlyRDataAggregated::MailServers(_) => RecordSection::Mail,
|
|
||||||
FriendlyRDataAggregated::NameServers(_) => RecordSection::Miscellaneous,
|
|
||||||
FriendlyRDataAggregated::Service(_) => RecordSection::Services,
|
|
||||||
FriendlyRDataAggregated::Spf(_) => RecordSection::Mail,
|
|
||||||
FriendlyRDataAggregated::Texts(_) => RecordSection::Miscellaneous,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct Node {
|
|
||||||
pub name: String,
|
|
||||||
pub records: Vec<FriendlyRecord>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node {
|
|
||||||
fn new(name: String) -> Self {
|
|
||||||
Node {
|
|
||||||
name,
|
|
||||||
records: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct FriendlyRecords {
|
|
||||||
records: Vec<Node>,
|
|
||||||
aliases: Vec<rdata::Alias>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FriendlyRecords {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
FriendlyRecords {
|
|
||||||
records: Vec::new(),
|
|
||||||
aliases: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<internal::RecordList> for FriendlyRecords {
|
|
||||||
fn from(value: internal::RecordList) -> Self {
|
|
||||||
|
|
||||||
let mut records = FriendlyRecords::new();
|
|
||||||
let mut name_mapping: HashMap<String, HashMap<FriendlyRType, FriendlyRecord>> = HashMap::new();
|
|
||||||
let mut service_mapping: HashMap<(String, ServiceType), FriendlyRecord> = HashMap::new();
|
|
||||||
|
|
||||||
for record in value.records {
|
|
||||||
let internal::Record { name, ttl, rdata } = record;
|
|
||||||
let name = name.to_string();
|
|
||||||
let rdata = rdata.friendly(&name, ttl);
|
|
||||||
|
|
||||||
if rdata.is_none() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (name, rdata) = rdata.unwrap();
|
|
||||||
|
|
||||||
if let FriendlyRData::Alias(alias) = rdata {
|
|
||||||
records.aliases.push(alias)
|
|
||||||
} else {
|
|
||||||
let node = name_mapping.entry(name.clone()).or_default();
|
|
||||||
|
|
||||||
match rdata {
|
|
||||||
FriendlyRData::Address(address) => {
|
|
||||||
let addresses = node.entry(FriendlyRType::Addresses).or_insert_with(|| {
|
|
||||||
FriendlyRecord::new(ttl, FriendlyRDataAggregated::Addresses(rdata::Addresses {
|
|
||||||
addresses: Vec::new()
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
match addresses.data {
|
|
||||||
FriendlyRDataAggregated::Addresses(ref mut addresses) => addresses.addresses.push(address),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
FriendlyRData::MailServer(mailserver) => {
|
|
||||||
let mailservers = node.entry(FriendlyRType::MailServers).or_insert_with(|| {
|
|
||||||
FriendlyRecord::new(ttl, FriendlyRDataAggregated::MailServers(rdata::MailServers {
|
|
||||||
mailservers: Vec::new()
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
match mailservers.data {
|
|
||||||
FriendlyRDataAggregated::MailServers(ref mut mailservers) => mailservers.mailservers.push(mailserver),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
FriendlyRData::Spf(spf) => {
|
|
||||||
node.insert(FriendlyRType::Spf, FriendlyRecord::new(ttl, FriendlyRDataAggregated::Spf(spf)));
|
|
||||||
},
|
|
||||||
FriendlyRData::Service(service_single) => {
|
|
||||||
let service = service_mapping.entry((name.clone(), service_single.service_type.clone()))
|
|
||||||
.or_insert_with(|| {
|
|
||||||
FriendlyRecord::new(ttl, FriendlyRDataAggregated::Service(rdata::Service {
|
|
||||||
service_type: service_single.service_type,
|
|
||||||
service_targets: Vec::new(),
|
|
||||||
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
match service.data {
|
|
||||||
FriendlyRDataAggregated::Service(ref mut service) => service.service_targets.push(service_single.service_target),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
FriendlyRData::NameServer(nameserver) => {
|
|
||||||
// TODO: NS -> Skip if NS for zone (authority), create Delegation section with glue + DS for others (how to check if record is glue?)
|
|
||||||
let nameservers = node.entry(FriendlyRType::NameServers).or_insert_with(|| {
|
|
||||||
FriendlyRecord::new(ttl, FriendlyRDataAggregated::NameServers(rdata::NameServers {
|
|
||||||
nameservers: Vec::new()
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
match nameservers.data {
|
|
||||||
FriendlyRDataAggregated::NameServers(ref mut nameservers) => nameservers.nameservers.push(nameserver),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
FriendlyRData::TextData(text) => {
|
|
||||||
let texts = node.entry(FriendlyRType::Texts).or_insert_with(|| {
|
|
||||||
FriendlyRecord::new(ttl, FriendlyRDataAggregated::Texts(rdata::Texts {
|
|
||||||
texts: Vec::new()
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
match texts.data {
|
|
||||||
FriendlyRDataAggregated::Texts(ref mut texts) => texts.texts.push(text),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
FriendlyRData::Alias(_) => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut nodes: HashMap<String, Node> = HashMap::new();
|
|
||||||
|
|
||||||
for ((name, _), service) in service_mapping {
|
|
||||||
let node = nodes.entry(name.clone())
|
|
||||||
.or_insert_with(|| Node::new(name));
|
|
||||||
node.records.push(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name, node_records) in name_mapping {
|
|
||||||
let node = nodes.entry(name.clone())
|
|
||||||
.or_insert_with(|| Node::new(name));
|
|
||||||
for (_, record) in node_records {
|
|
||||||
node.records.push(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
records.records = nodes.into_values().collect();
|
|
||||||
|
|
||||||
records.records.sort_by_key(|node| node.name.clone());
|
|
||||||
|
|
||||||
records
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
use domain::base::scan::Symbol;
|
|
||||||
use serde::{Serialize, Deserialize, Serializer};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
|
|
||||||
pub struct Name {
|
|
||||||
name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Name {
|
|
||||||
pub fn new(name: String) -> Self {
|
|
||||||
Name {
|
|
||||||
name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ends_with(&self, other: &Name) -> bool {
|
|
||||||
self.name == other.name || self.name.ends_with(&(String::from(".") + &other.name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Name {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Name {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for Name {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
let mut labels = self.name.split('.').rev();
|
|
||||||
let mut other_labels = other.name.split('.').rev();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match (labels.next(), other_labels.next()) {
|
|
||||||
(Some(label), Some(other_label)) => match label.cmp(other_label) {
|
|
||||||
std::cmp::Ordering::Equal => (),
|
|
||||||
res => return res,
|
|
||||||
},
|
|
||||||
(None, Some(_)) => return std::cmp::Ordering::Less,
|
|
||||||
(Some(_), None) => return std::cmp::Ordering::Greater,
|
|
||||||
(None, None) => return std::cmp::Ordering::Equal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for Name {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq)]
|
|
||||||
pub enum Rtype {
|
|
||||||
A,
|
|
||||||
Aaaa,
|
|
||||||
Cname,
|
|
||||||
Mx,
|
|
||||||
Ns,
|
|
||||||
Ptr,
|
|
||||||
Soa,
|
|
||||||
Srv,
|
|
||||||
Txt
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rtype {
|
|
||||||
pub fn value(&self) -> u16 {
|
|
||||||
match self {
|
|
||||||
Rtype::A => 1,
|
|
||||||
Rtype::Aaaa => 28,
|
|
||||||
Rtype::Cname => 5,
|
|
||||||
Rtype::Mx => 15,
|
|
||||||
Rtype::Ns => 2,
|
|
||||||
Rtype::Ptr => 12,
|
|
||||||
Rtype::Soa => 6,
|
|
||||||
Rtype::Srv => 33,
|
|
||||||
Rtype::Txt => 16,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for Rtype {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
self.value().cmp(&other.value())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for Rtype {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct Text {
|
|
||||||
pub data: Vec<u8>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Text {
|
|
||||||
pub fn new(data: Vec<u8>) -> Self {
|
|
||||||
Text {
|
|
||||||
data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bytes(self) -> Vec<u8> {
|
|
||||||
self.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Text {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Text {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
|
|
||||||
for c in &self.data {
|
|
||||||
// Escapes '\' and non printable chars
|
|
||||||
let c = Symbol::display_from_octet(*c);
|
|
||||||
write!(f, "{}", c)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
pub mod rdata;
|
|
||||||
pub mod record;
|
|
||||||
pub mod base;
|
|
||||||
|
|
||||||
pub use rdata::*;
|
|
||||||
pub use record::*;
|
|
||||||
pub use base::*;
|
|
|
@ -1,200 +0,0 @@
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
|
|
||||||
use super::{Name, Rtype, Text};
|
|
||||||
|
|
||||||
use crate::resources::dns::friendly;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum RData {
|
|
||||||
A(A),
|
|
||||||
Aaaa(Aaaa),
|
|
||||||
Cname(Cname),
|
|
||||||
Mx(Mx),
|
|
||||||
Ns(Ns),
|
|
||||||
Ptr(Ptr),
|
|
||||||
Soa(Soa),
|
|
||||||
Srv(Srv),
|
|
||||||
Txt(Txt),
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn friendly(self, owner: &str, ttl: u32) -> Option<(String, friendly::FriendlyRData)> {
|
|
||||||
match self {
|
|
||||||
RData::A(data) => data.friendly(owner),
|
|
||||||
RData::Aaaa(data) => data.friendly(owner),
|
|
||||||
RData::Cname(data) => data.friendly(owner, ttl),
|
|
||||||
RData::Mx(data) => data.friendly(owner),
|
|
||||||
RData::Ns(data) => data.friendly(owner),
|
|
||||||
RData::Srv(data) => data.friendly(owner),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct A {
|
|
||||||
pub address: Ipv4Addr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl A {
|
|
||||||
pub fn new(address: Ipv4Addr) -> Self {
|
|
||||||
A {
|
|
||||||
address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
|
|
||||||
Some((owner.to_string(), friendly::FriendlyRData::Address(friendly::Address {
|
|
||||||
address: self.address.into()
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Aaaa {
|
|
||||||
pub address: Ipv6Addr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Aaaa {
|
|
||||||
|
|
||||||
pub fn new(address: Ipv6Addr) -> Self {
|
|
||||||
Aaaa {
|
|
||||||
address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
|
|
||||||
Some((owner.to_string(), friendly::FriendlyRData::Address(friendly::Address {
|
|
||||||
address: self.address.into()
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Cname {
|
|
||||||
pub target: Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cname {
|
|
||||||
pub fn friendly(self, owner: &str, ttl: u32) -> Option<(String, friendly::FriendlyRData)> {
|
|
||||||
Some((owner.to_string(), friendly::FriendlyRData::Alias(friendly::Alias {
|
|
||||||
from: owner.into(),
|
|
||||||
target: self.target.to_string(),
|
|
||||||
ttl: ttl.into(),
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Mx {
|
|
||||||
pub preference: u16,
|
|
||||||
pub mail_exchanger: Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mx {
|
|
||||||
pub fn new(mail_exchanger: Name, preference: u16) -> Self {
|
|
||||||
Mx {
|
|
||||||
mail_exchanger,
|
|
||||||
preference,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
|
|
||||||
Some((owner.to_string(), friendly::FriendlyRData::MailServer(friendly::MailServer {
|
|
||||||
preference: self.preference,
|
|
||||||
mail_exchanger: self.mail_exchanger,
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Ns {
|
|
||||||
pub target: Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ns {
|
|
||||||
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
|
|
||||||
Some((owner.to_string(), friendly::FriendlyRData::NameServer(friendly::NameServer {
|
|
||||||
target: self.target.to_string(),
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Ptr {
|
|
||||||
pub target: Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Soa {
|
|
||||||
pub primary_server: Name,
|
|
||||||
pub maintainer: Name,
|
|
||||||
pub refresh: u32,
|
|
||||||
pub retry: u32,
|
|
||||||
pub expire: u32,
|
|
||||||
pub minimum: u32,
|
|
||||||
pub serial: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Srv {
|
|
||||||
pub server: Name,
|
|
||||||
pub port: u16,
|
|
||||||
pub priority: u16,
|
|
||||||
pub weight: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Srv {
|
|
||||||
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
|
|
||||||
let labels: Vec<_> = owner.splitn(3, '.').collect();
|
|
||||||
if labels.len() != 3 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let service_name = labels[0]. strip_prefix('_');
|
|
||||||
let protocol = labels[1]. strip_prefix('_');
|
|
||||||
if let (Some(service_name), Some(protocol)) = (service_name, protocol) {
|
|
||||||
Some((labels[2].to_string(), friendly::FriendlyRData::Service(friendly::ServiceSingleTarget {
|
|
||||||
service_type: friendly::ServiceType::Other {
|
|
||||||
name: service_name.into(),
|
|
||||||
protocol: protocol.into()
|
|
||||||
},
|
|
||||||
service_target: friendly::ServiceTarget {
|
|
||||||
port: self.port.into(),
|
|
||||||
weight: self.weight.into(),
|
|
||||||
priority: self.priority.into(),
|
|
||||||
server: self.server.to_string()
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Txt {
|
|
||||||
pub text: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Txt {
|
|
||||||
pub fn new(text: Text) -> Self {
|
|
||||||
Txt {
|
|
||||||
text: text.bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
use super::rdata::RData;
|
|
||||||
use super::Name;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Record {
|
|
||||||
pub name: Name,
|
|
||||||
pub ttl: u32,
|
|
||||||
pub rdata: RData
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct RecordList {
|
|
||||||
pub records: Vec<Record>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecordList {
|
|
||||||
pub fn new(records: &[Record]) -> Self {
|
|
||||||
RecordList { records: records.into() }
|
|
||||||
}
|
|
||||||
pub fn sort(&mut self) {
|
|
||||||
self.records.sort_by(|r1, r2| {
|
|
||||||
let key1 = (&r1.name, r1.rdata.rtype());
|
|
||||||
let key2 = (&r2.name, r2.rdata.rtype());
|
|
||||||
key1.cmp(&key2)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct AddRecords {
|
|
||||||
pub new_records: RecordList
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod external;
|
|
||||||
pub mod internal;
|
|
||||||
pub mod friendly;
|
|
|
@ -1,239 +0,0 @@
|
||||||
use async_trait::async_trait;
|
|
||||||
use axum::response::Response;
|
|
||||||
use domain::base::record;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use rusqlite::Error as RusqliteError;
|
|
||||||
|
|
||||||
|
|
||||||
use crate::database::{BoxedDb, sqlite::SqliteDB};
|
|
||||||
use crate::dns::{BoxedZoneDriver, BoxedRecordDriver, DnsDriverError};
|
|
||||||
use crate::errors::Error;
|
|
||||||
use crate::macros::push_error;
|
|
||||||
use crate::resources::dns::internal::{self, RecordList};
|
|
||||||
use crate::validation;
|
|
||||||
|
|
||||||
pub enum ZoneError {
|
|
||||||
ZoneConflict { name: String },
|
|
||||||
NotFound { name: String },
|
|
||||||
NotExistsNs { name: String },
|
|
||||||
Validation { suberrors: Vec<Error> },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct Zone {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Zone {
|
|
||||||
pub async fn create(create_zone: CreateZoneRequest, zone_driver: BoxedZoneDriver, db: BoxedDb) -> Result<Self, Error> {
|
|
||||||
let create_zone = create_zone.validate()?;
|
|
||||||
|
|
||||||
zone_driver.zone_exists(&create_zone.name)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
match e {
|
|
||||||
DnsDriverError::ZoneNotFound { name } => {
|
|
||||||
Error::from(ZoneError::NotExistsNs { name })
|
|
||||||
.with_path("/name")
|
|
||||||
},
|
|
||||||
e => Error::from(e)
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
db.create_zone(create_zone).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_records(&self, record_driver: BoxedRecordDriver) ->Result<RecordList, Error> {
|
|
||||||
let mut records = record_driver.get_records(&self.name).await?;
|
|
||||||
|
|
||||||
records.sort();
|
|
||||||
|
|
||||||
Ok(records)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_records(&self, record_driver: BoxedRecordDriver, records: internal::RecordList) -> Result<(), Error> {
|
|
||||||
record_driver.add_records(&self.name, records).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct CreateZoneRequest {
|
|
||||||
pub name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CreateZone {
|
|
||||||
pub name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CreateZoneRequest {
|
|
||||||
pub fn validate(self) -> Result<CreateZone, Error> {
|
|
||||||
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let name = push_error!(validation::normalize_domain(&self.name), errors, "/name");
|
|
||||||
name.ok_or(Error::from(ZoneError::Validation { suberrors: errors }))
|
|
||||||
.map(|name| {
|
|
||||||
CreateZone { name }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait ZoneModel: Send + Sync {
|
|
||||||
async fn create_zone(&self, create_zone: CreateZone) -> Result<Zone, Error>;
|
|
||||||
async fn get_zone_by_name(&self, zone_name: &str) -> Result<Zone, Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ZoneModel for SqliteDB {
|
|
||||||
async fn create_zone(&self, create_zone: CreateZone) -> Result<Zone, Error> {
|
|
||||||
let pool = self.pool.clone();
|
|
||||||
|
|
||||||
let conn = pool.get().await?;
|
|
||||||
|
|
||||||
tokio::task::block_in_place(move || {
|
|
||||||
let mut stmt = conn.prepare("insert into zones (name) values (?1) returning *")?;
|
|
||||||
let zone = stmt.query_row((&create_zone.name,), |row| {
|
|
||||||
Ok(Zone {
|
|
||||||
name: row.get(0)?
|
|
||||||
})
|
|
||||||
}).map_err(|e| {
|
|
||||||
match e {
|
|
||||||
/* SQLITE_CONSTRAINT_PRIMARYKEY */
|
|
||||||
RusqliteError::SqliteFailure(e, _) if e.extended_code == 1555 => {
|
|
||||||
Error::from(ZoneError::ZoneConflict { name: create_zone.name })
|
|
||||||
.with_path("/name")
|
|
||||||
},
|
|
||||||
e => Error::new("internal:zone:create", "Failed to create zone")
|
|
||||||
.with_cause(&e.to_string())
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(zone)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_zone_by_name(&self, zone_name: &str) -> Result<Zone, Error> {
|
|
||||||
let pool = self.pool.clone();
|
|
||||||
|
|
||||||
let conn = pool.get().await?;
|
|
||||||
|
|
||||||
tokio::task::block_in_place(move || {
|
|
||||||
let mut stmt = conn.prepare("select * from zones where name = ?1")?;
|
|
||||||
let zone = stmt.query_row((zone_name,), |row| {
|
|
||||||
Ok(Zone {
|
|
||||||
name: row.get(0)?
|
|
||||||
})
|
|
||||||
}).map_err(|e| {
|
|
||||||
match e {
|
|
||||||
RusqliteError::QueryReturnedNoRows => {
|
|
||||||
Error::from(ZoneError::NotFound { name: zone_name.to_string() })
|
|
||||||
},
|
|
||||||
e => Error::new("internal:zone:get_by_name", "Failed to fetch zone by name")
|
|
||||||
.with_cause(&e.to_string())
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(zone)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
use crate::models::user::UserInfo;
|
|
||||||
|
|
||||||
use uuid::Uuid;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use diesel::result::Error as DieselError;
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
use crate::schema::*;
|
|
||||||
use super::name::AbsoluteName;
|
|
||||||
use super::user::UserZone;
|
|
||||||
use super::errors::UserError;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Queryable, Identifiable, Insertable)]
|
|
||||||
#[table_name = "zone"]
|
|
||||||
pub struct Zone {
|
|
||||||
#[serde(skip)]
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct AddZoneMemberRequest {
|
|
||||||
pub id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, FromForm)]
|
|
||||||
pub struct CreateZoneRequest {
|
|
||||||
pub name: AbsoluteName,
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: Should probably not be implemented here
|
|
||||||
// also, "UserError" seems like a misleading name
|
|
||||||
impl Zone {
|
|
||||||
pub fn get_all(conn: &diesel::SqliteConnection) -> Result<Vec<Zone>, UserError> {
|
|
||||||
use crate::schema::zone::dsl::*;
|
|
||||||
|
|
||||||
zone.get_results(conn)
|
|
||||||
.map_err(UserError::DbError)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_by_name(conn: &diesel::SqliteConnection, zone_name: &str) -> Result<Zone, UserError> {
|
|
||||||
use crate::schema::zone::dsl::*;
|
|
||||||
|
|
||||||
zone.filter(name.eq(zone_name))
|
|
||||||
.get_result(conn)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
DieselError::NotFound => UserError::ZoneNotFound,
|
|
||||||
other => UserError::DbError(other)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_zone(conn: &diesel::SqliteConnection, zone_request: CreateZoneRequest) -> Result<Zone, UserError> {
|
|
||||||
use crate::schema::zone::dsl::*;
|
|
||||||
|
|
||||||
let new_zone = Zone {
|
|
||||||
id: Uuid::new_v4().to_simple().to_string(),
|
|
||||||
name: zone_request.name.to_utf8(),
|
|
||||||
};
|
|
||||||
|
|
||||||
diesel::insert_into(zone)
|
|
||||||
.values(&new_zone)
|
|
||||||
.execute(conn)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserConflict,
|
|
||||||
other => UserError::DbError(other)
|
|
||||||
})?;
|
|
||||||
Ok(new_zone)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn add_member(&self, conn: &diesel::SqliteConnection, new_member: &UserInfo) -> Result<(), UserError> {
|
|
||||||
use crate::schema::user_zone::dsl::*;
|
|
||||||
|
|
||||||
let new_user_zone = UserZone {
|
|
||||||
zone_id: self.id.clone(),
|
|
||||||
user_id: new_member.id.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = diesel::insert_into(user_zone)
|
|
||||||
.values(new_user_zone)
|
|
||||||
.execute(conn);
|
|
||||||
|
|
||||||
match res {
|
|
||||||
// If user has already access to the zone, safely ignore the conflit
|
|
||||||
// TODO: use 'on conflict do nothing' in postgres when we get there
|
|
||||||
Err(DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _)) => (),
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
Ok(_) => ()
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -1,5 +1,5 @@
|
||||||
//pub mod users;
|
pub mod users;
|
||||||
pub mod zones;
|
pub mod zones;
|
||||||
|
|
||||||
//pub use users::*;
|
pub use users::*;
|
||||||
//pub use zones::*;
|
pub use zones::*;
|
||||||
|
|
|
@ -1,48 +1,3 @@
|
||||||
use axum::extract::{Path, State};
|
|
||||||
use axum::Json;
|
|
||||||
|
|
||||||
use crate::AppState;
|
|
||||||
use crate::errors::Error;
|
|
||||||
use crate::resources::zone::{CreateZoneRequest, Zone};
|
|
||||||
use crate::resources::dns::external;
|
|
||||||
use crate::resources::dns::internal;
|
|
||||||
|
|
||||||
|
|
||||||
pub async fn create_zone(
|
|
||||||
State(app): State<AppState>,
|
|
||||||
Json(create_zone): Json<CreateZoneRequest>,
|
|
||||||
) -> Result<Json<Zone>, Error>
|
|
||||||
{
|
|
||||||
Zone::create(create_zone, app.zone, app.db).await.map(Json)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_zone_records(
|
|
||||||
Path(zone_name): Path<String>,
|
|
||||||
State(app): State<AppState>,
|
|
||||||
) -> Result<Json<external::RecordList>, Error>
|
|
||||||
{
|
|
||||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
|
||||||
zone.get_records(app.records)
|
|
||||||
.await
|
|
||||||
.map(|records| Json(records.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_zone_records(
|
|
||||||
Path(zone_name): Path<String>,
|
|
||||||
State(app): State<AppState>,
|
|
||||||
Json(add_records): Json<external::AddRecords>,
|
|
||||||
) -> Result<Json<external::RecordList>, Error>
|
|
||||||
{
|
|
||||||
|
|
||||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
|
||||||
let name = internal::Name::new(zone.name.clone());
|
|
||||||
let add_records = add_records.validate(&name)?;
|
|
||||||
app.records.add_records(&zone.name, add_records.new_records.clone()).await?;
|
|
||||||
|
|
||||||
Ok(Json(add_records.new_records.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
|
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
|
@ -223,4 +178,3 @@ pub async fn add_member_to_zone<'r>(
|
||||||
|
|
||||||
Ok(Status::Created) // TODO: change this?
|
Ok(Status::Created) // TODO: change this?
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
//pub mod auth;
|
pub mod auth;
|
||||||
pub mod zones;
|
pub mod zones;
|
||||||
|
|
||||||
|
pub use auth::*;
|
||||||
|
pub use zones::*;
|
||||||
|
|
|
@ -1,121 +1,128 @@
|
||||||
use axum::extract::{Query, Path, State, OriginalUri};
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::response::{IntoResponse, Response};
|
|
||||||
use axum::Extension;
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use unic_langid::LanguageIdentifier;
|
use serde::Serialize;
|
||||||
|
use rocket::http::{Status};
|
||||||
|
use rocket::http::uri::Origin;
|
||||||
|
use rocket::form::Form;
|
||||||
|
|
||||||
use crate::form::Node;
|
|
||||||
use crate::macros::append_errors;
|
|
||||||
use crate::AppState;
|
|
||||||
use crate::errors::{Error, error_map};
|
|
||||||
use crate::template::Template;
|
use crate::template::Template;
|
||||||
use crate::resources::dns::friendly::{self, NewRecordQuery};
|
use crate::models;
|
||||||
use crate::resources::dns::internal;
|
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/<zone>/records")]
|
||||||
|
pub async fn get_zone_records_page(user_info: models::UserInfo, zone: models::AbsoluteName, conn: DbConn, origin: &Origin<'_>) -> Result<Template<'static, Value>, 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_records_page(
|
|
||||||
Path(zone_name): Path<String>,
|
|
||||||
State(app): State<AppState>,
|
|
||||||
OriginalUri(url): OriginalUri,
|
|
||||||
Extension(lang): Extension<LanguageIdentifier>,
|
|
||||||
) -> Result<Template<'static, Value>, Error> {
|
|
||||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
|
||||||
let records = zone.get_records(app.records).await?;
|
|
||||||
let records = friendly::FriendlyRecords::from(records.clone());
|
|
||||||
|
|
||||||
Ok(Template::new(
|
Ok(Template::new(
|
||||||
"pages/records.html",
|
"pages/zone/records.html",
|
||||||
app.template_engine,
|
|
||||||
json!({
|
json!({
|
||||||
"current_zone": zone.name,
|
"current_zone": zone.to_utf8(),
|
||||||
"records": records,
|
"zones": zones,
|
||||||
"url": url.to_string(),
|
"nav_page": origin.clone().into_normalized().path().as_str(),
|
||||||
"lang": lang.to_string(),
|
"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<Template<'static, Value>, 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"],
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn get_new_record_page(
|
#[get("/zones/new")]
|
||||||
Path(zone_name): Path<String>,
|
pub async fn get_create_zone_page(
|
||||||
State(app): State<AppState>,
|
conn: DbConn,
|
||||||
Query(mut params): Query<NewRecordQuery>,
|
user_info: models::UserInfo,
|
||||||
OriginalUri(url): OriginalUri,
|
origin: &Origin<'_>
|
||||||
Extension(lang): Extension<LanguageIdentifier>,
|
) -> Result<Template<'static, Value>, Status> {
|
||||||
) -> Result<Template<'static, Value>, Error> {
|
|
||||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
|
||||||
|
|
||||||
let mut errors = Vec::new();
|
user_info
|
||||||
append_errors!(params.validate(&zone.name), errors);
|
.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(
|
Ok(Template::new(
|
||||||
"pages/new_record.html",
|
"pages/zones/new.html",
|
||||||
app.template_engine,
|
|
||||||
json!({
|
json!({
|
||||||
"current_zone": zone.name,
|
"zone": None::<models::Zone>,
|
||||||
"new_record_name": params.name,
|
"zones": zones,
|
||||||
"errors": error_map(errors),
|
"error": None::<String>,
|
||||||
"config": params.config,
|
"nav_page": origin.clone().into_normalized().path().as_str(),
|
||||||
"rtype": params.rtype,
|
"nav_sections": vec!["zones", "_new-zone"],
|
||||||
"url": url.to_string(),
|
|
||||||
"lang": lang.to_string(),
|
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_new_record(
|
#[post("/zones/new", data = "<zone_request>")]
|
||||||
Path(zone_name): Path<String>,
|
pub async fn post_create_zone_page(
|
||||||
State(app): State<AppState>,
|
conn: DbConn,
|
||||||
Query(mut params): Query<NewRecordQuery>,
|
dns_api: Box<dyn ZoneConnector>,
|
||||||
OriginalUri(url): OriginalUri,
|
user_info: models::UserInfo,
|
||||||
Extension(lang): Extension<LanguageIdentifier>,
|
zone_request: Form<models::CreateZoneRequest>,
|
||||||
form: Node,
|
origin: &Origin<'_>
|
||||||
) -> Result<Response, Error> {
|
) -> Result<Template<'static, Value>, Status> {
|
||||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
user_info
|
||||||
let mut errors = Vec::new();
|
.check_admin()
|
||||||
append_errors!(params.validate(&zone.name), errors);
|
.map_err(|e| models::ErrorResponse::from(e).status)?;
|
||||||
|
|
||||||
if !errors.is_empty() || params.name.is_none() || !(params.config.is_none() ^ params.config.is_some()) {
|
let zone = controllers::create_zone(
|
||||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
&conn,
|
||||||
}
|
dns_api,
|
||||||
|
user_info.clone(),
|
||||||
|
zone_request.into_inner()
|
||||||
|
).await.map_err(|e| e.status)?;
|
||||||
|
|
||||||
let name = params.name.clone().map(internal::Name::new).unwrap();
|
let zones = controllers::get_zones(
|
||||||
let input_data = form.into_json_value();
|
&conn,
|
||||||
|
user_info
|
||||||
|
).await.map_err(|e| e.status)?;
|
||||||
|
|
||||||
let new_records = {
|
|
||||||
let new_records = if let Some(config_type) = params.config.clone() {
|
|
||||||
config_type.get_records(input_data.clone(), 3600, name)
|
|
||||||
} else if let Some(_rtype) = params.rtype {
|
|
||||||
unimplemented!()
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
|
|
||||||
append_errors!(new_records, errors)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(new_records) = new_records {
|
|
||||||
zone.add_records(
|
|
||||||
app.records,
|
|
||||||
internal::RecordList::new(&new_records)
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
Ok(().into_response())
|
|
||||||
} else {
|
|
||||||
Ok(Template::new(
|
Ok(Template::new(
|
||||||
"pages/new_record.html",
|
"pages/zones/new.html",
|
||||||
app.template_engine,
|
|
||||||
json!({
|
json!({
|
||||||
"current_zone": zone.name,
|
"zone": Some(zone),
|
||||||
"new_record_name": params.name,
|
"zones": zones,
|
||||||
"input_data": input_data,
|
"error": None::<String>,
|
||||||
"errors": error_map(errors),
|
"nav_page": origin.clone().into_normalized().path().as_str(),
|
||||||
"config": params.config,
|
"nav_sections": vec!["zones", "_new-zone"],
|
||||||
"rtype": params.rtype,
|
|
||||||
"url": url.to_string(),
|
|
||||||
"lang": lang.to_string(),
|
|
||||||
})
|
})
|
||||||
).into_response())
|
))
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,77 +1,63 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use axum::response::{Html, IntoResponse};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use rocket::request::Request;
|
||||||
|
use rocket::response::{self, Responder};
|
||||||
|
use rocket::http::{Status, ContentType};
|
||||||
|
|
||||||
use tera::{Tera, Context};
|
use tera::{Tera, Context};
|
||||||
|
|
||||||
use crate::{errors::Error, localization::Localization};
|
|
||||||
|
|
||||||
|
pub struct TemplateState {
|
||||||
#[derive(Clone)]
|
tera: Tera,
|
||||||
pub struct TemplateEngine {
|
|
||||||
pub tera: Arc<Tera>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl TemplateState {
|
||||||
pub enum TemplateError {
|
pub fn new(template_directory: &Path) -> Self {
|
||||||
SerializationError { reason: Box<dyn std::error::Error> },
|
|
||||||
RenderError { name: String, reason: Box<dyn std::error::Error> },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TemplateEngine {
|
|
||||||
pub fn new(template_directory: &Path, localization: Localization) -> Self {
|
|
||||||
let template_glob = template_directory.join("**").join("*");
|
let template_glob = template_directory.join("**").join("*");
|
||||||
match Tera::new(template_glob.to_str().expect("valid glob path string")) {
|
match Tera::new(template_glob.to_str().expect("valid glob path string")) {
|
||||||
Ok(mut tera) => {
|
Ok(tera) => TemplateState { tera },
|
||||||
tera.register_function("tr", localization);
|
|
||||||
TemplateEngine { tera: Arc::new(tera) }
|
|
||||||
},
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Loading templates failed: {}", e);
|
println!("Loading templates failed: {}", e);
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render<S: Serialize>(&self, name: &str, context: S) -> Result<String, TemplateError> {
|
|
||||||
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<'n, S: Serialize> {
|
pub struct Template<'t, S: Serialize> {
|
||||||
pub name: &'n str,
|
pub name: &'t str,
|
||||||
pub engine: TemplateEngine,
|
|
||||||
pub context: S,
|
pub context: S,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'n, S: Serialize> Template<'n, S> {
|
impl<'r, S: Serialize> Template<'r, S> {
|
||||||
pub fn new(name: &'n str, engine: TemplateEngine, context: S) -> Self {
|
pub fn new(name: &'r str, context: S) -> Self {
|
||||||
Template {
|
Template {
|
||||||
name,
|
name,
|
||||||
engine,
|
context
|
||||||
context,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: Serialize> IntoResponse for Template<'_, S> {
|
fn render(self, tera: &Tera) -> Result<(ContentType, String), Status> {
|
||||||
fn into_response(self) -> axum::response::Response {
|
let context = Context::from_serialize(self.context).map_err(|e| {
|
||||||
let res = self.engine.render(self.name, self.context);
|
error!("Failed to serialize context: {}", e);
|
||||||
|
Status::InternalServerError
|
||||||
|
})?;
|
||||||
|
|
||||||
match res {
|
let content = tera.render(self.name, &context).map_err(|e| {
|
||||||
Ok(content) => Html(content).into_response(),
|
error!("Failed to render template `{}`: {}", self.name, e);
|
||||||
Err(err) => Error::from(err).into_response(),
|
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::<TemplateState>().ok_or(Status::InternalServerError)?;
|
||||||
|
|
||||||
|
self.render(&template_state.tera).respond_to(request)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
use crate::errors::Error;
|
|
||||||
|
|
||||||
pub enum DomainValidationError {
|
|
||||||
EmptyDomain,
|
|
||||||
DomainTooLong { length: usize },
|
|
||||||
CharactersNotPermitted { label: String },
|
|
||||||
LabelToolLong { length: usize, label: String },
|
|
||||||
EmptyLabel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Not complete but probably good enough
|
|
||||||
/// https://doc.zonemaster.fr/v2024.1/specifications/tests/RequirementsAndNormalizationOfDomainNames.html
|
|
||||||
/// TODO: No support of dots in labels, how to handle RNAME in SOA?
|
|
||||||
pub fn normalize_domain(domain_name: &str) -> Result<String, Error> {
|
|
||||||
if domain_name.is_empty() {
|
|
||||||
return Err(Error::from(DomainValidationError::EmptyDomain))
|
|
||||||
}
|
|
||||||
|
|
||||||
let domain = domain_name.strip_suffix('.').unwrap_or(domain_name).to_lowercase();
|
|
||||||
|
|
||||||
if domain.len() > 255 {
|
|
||||||
Err(Error::from(DomainValidationError::DomainTooLong { length: domain.len() }))
|
|
||||||
} else {
|
|
||||||
let labels = domain.split('.').collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if labels.iter().any(|l| l.is_empty()) {
|
|
||||||
return Err(
|
|
||||||
Error::from(DomainValidationError::EmptyLabel)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for label in labels {
|
|
||||||
if !label.chars().all(|c| {
|
|
||||||
// allow for '/' for reverse zone
|
|
||||||
c.is_ascii_alphanumeric() || c == '-' || c == '/' || c == '_'
|
|
||||||
}) {
|
|
||||||
return Err(
|
|
||||||
Error::from(DomainValidationError::CharactersNotPermitted { label: label.into() })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if label.len() > 63 {
|
|
||||||
return Err(Error::from(DomainValidationError::LabelToolLong {
|
|
||||||
label: label.into(),
|
|
||||||
length: label.len()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
pub enum TxtParseError {
|
|
||||||
MissingEscape,
|
|
||||||
NonAscii { character: String },
|
|
||||||
BadEscapeDigitsTooShort { sequence: String },
|
|
||||||
BadEscapeDigitsNotDigits { sequence: String },
|
|
||||||
BadEscapeDigitIndexTooHigh { sequence: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_txt_data(text: &str) -> Result<Vec<u8>, Vec<Error>> {
|
|
||||||
let mut chars = text.chars();
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
let mut data = Vec::new();
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn printable(ch: char) -> bool {
|
|
||||||
ch.is_ascii() && ('\u{20}'..='\u{7E}').contains(&ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(ch) = chars.next() {
|
|
||||||
if ch == '\\' {
|
|
||||||
match chars.next() {
|
|
||||||
Some(ch) => {
|
|
||||||
if ch.is_ascii_digit() {
|
|
||||||
let mut digits: Vec<_> = chars.by_ref().take(2).collect();
|
|
||||||
digits.insert(0, ch);
|
|
||||||
if digits.len() < 3 {
|
|
||||||
errors.push(Error::from(TxtParseError::BadEscapeDigitsTooShort { sequence: String::from_iter(digits) }))
|
|
||||||
} else if !digits.iter().all(|c| c.is_ascii_digit()) {
|
|
||||||
errors.push(Error::from(TxtParseError::BadEscapeDigitsNotDigits { sequence: String::from_iter(digits) }))
|
|
||||||
} else {
|
|
||||||
let index = {
|
|
||||||
digits[0].to_digit(10).unwrap() * 100 +
|
|
||||||
digits[1].to_digit(10).unwrap() * 10 +
|
|
||||||
digits[2].to_digit(10).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
if index > 255 {
|
|
||||||
errors.push(Error::from(TxtParseError::BadEscapeDigitIndexTooHigh { sequence: String::from_iter(digits) }))
|
|
||||||
}
|
|
||||||
|
|
||||||
data.push(index as u8)
|
|
||||||
}
|
|
||||||
} else if printable(ch) {
|
|
||||||
data.push(ch as u8)
|
|
||||||
} else {
|
|
||||||
errors.push(Error::from(TxtParseError::NonAscii { character: ch.into() }))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
errors.push(Error::from(TxtParseError::MissingEscape))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if printable(ch) {
|
|
||||||
data.push(ch as u8);
|
|
||||||
} else {
|
|
||||||
errors.push(Error::from(TxtParseError::NonAscii { character: ch.into() }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: check txt data max length?
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
Ok(data)
|
|
||||||
} else {
|
|
||||||
Err(errors)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +1,44 @@
|
||||||
{% extends "bases/base.html" %}
|
{% extends "bases/base.html" %}
|
||||||
|
{% import "macros.html" as macros %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<nav aria-label="Principal" class="main">
|
<nav aria-label="Principal" class="main">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="#">Account</a></li>
|
<li><a href="/profile">Mon profil</a></li>
|
||||||
<li><a href="#">Zones</a></li>
|
<li>
|
||||||
<li><a href="#">Admin</a></li>
|
{{ macros::nav_link(
|
||||||
|
content="Mes zones",
|
||||||
|
href="/zones",
|
||||||
|
current_page=nav_page,
|
||||||
|
section="zones",
|
||||||
|
current_sections=nav_sections,
|
||||||
|
) }}
|
||||||
|
<ul>
|
||||||
|
{% for zone in zones %}
|
||||||
|
<li>
|
||||||
|
{{ macros::nav_link(
|
||||||
|
content=zone.name,
|
||||||
|
href="/zone/" ~ zone.name,
|
||||||
|
current_page=nav_page,
|
||||||
|
section=zone.name,
|
||||||
|
current_sections=nav_sections,
|
||||||
|
) }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li>
|
||||||
|
{{ macros::nav_link(
|
||||||
|
safe_content='<img alt="" src="/images/plus.svg"> Ajouter une zone',
|
||||||
|
href="/zones/new",
|
||||||
|
current_page=nav_page,
|
||||||
|
section="_new-zone",
|
||||||
|
current_sections=nav_sections,
|
||||||
|
) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<main>
|
<main>
|
||||||
{% block main %}{% endblock main %}
|
{% block main %}{% endblock main %}
|
||||||
</main>
|
</main>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="/assets/scripts/add-form-row.js"></script>
|
|
||||||
{% endblock scripts %}
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ lang }}">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}{% endblock title %}Nomilo</title>
|
<title>{% block title %}{% endblock title %}Nomilo</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/assets/styles/main.css">
|
<link rel="stylesheet" type="text/css" href="/styles/main.css">
|
||||||
{% block styles %}{% endblock styles %}
|
{% block styles %}{% endblock styles %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
11
templates/macros.html
Normal file
11
templates/macros.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% macro nav_link(content, href, current_page, content='', safe_content='', section=False, current_sections=False, props='') %}
|
||||||
|
<a
|
||||||
|
href="{{ href }}"
|
||||||
|
{{ props }}
|
||||||
|
{% if current_page == href %}
|
||||||
|
aria-current="page"
|
||||||
|
{% elif section and section in current_sections %}
|
||||||
|
aria-current="location"
|
||||||
|
{% endif %}
|
||||||
|
>{{ content }}{{ safe_content | safe }}</a>
|
||||||
|
{% endmacro nav_link %}
|
|
@ -1,93 +0,0 @@
|
||||||
{% macro rrset(record, zone, lang) %}
|
|
||||||
<li class="rrset">
|
|
||||||
<div class="rtype">
|
|
||||||
{% if record.record_type == "service" %}
|
|
||||||
{% if record.service.service_type.service_type == "other" %}
|
|
||||||
{{ record.service.service_type.name }}/{{ record.service.service_type.protocol }}
|
|
||||||
{% else %}
|
|
||||||
{{ record.srvice.service_type.service_type }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{{ tr(msg="zone-content-record-type-" ~ record.record_type, attr="type-name", lang=lang) }}
|
|
||||||
{% endif %}
|
|
||||||
<div class="action">
|
|
||||||
<a class="button icon" href="#">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
|
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% if record.record_type == "addresses" %}
|
|
||||||
{% for address in record.addresses.addresses %}
|
|
||||||
<li>
|
|
||||||
<div class="rdata">
|
|
||||||
<div class="rdata-main">
|
|
||||||
<span class="pill">
|
|
||||||
{{ address.address }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% elif record.record_type == "mailservers" %}
|
|
||||||
{% for mailserver in record.mailservers.mailservers %}
|
|
||||||
<li>
|
|
||||||
<div class="rdata-main">
|
|
||||||
<span class="pill">
|
|
||||||
{{ mailserver.mail_exchanger }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="rdata-complementary">
|
|
||||||
<span class="pill">
|
|
||||||
{{ tr(
|
|
||||||
msg="zone-content-record-type-mailservers",
|
|
||||||
attr="data-preference",
|
|
||||||
preference=mailserver.preference,
|
|
||||||
lang=lang) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% elif record.record_type == "nameservers" %}
|
|
||||||
{% for nameserver in record.nameservers.nameservers %}
|
|
||||||
<li>
|
|
||||||
<div class="rdata-main">
|
|
||||||
<span class="pill">
|
|
||||||
{{ nameserver.target }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% elif record.record_type == "service" %}
|
|
||||||
{% for service_target in record.service.service_targets %}
|
|
||||||
<li>
|
|
||||||
<div class="rdata-main">
|
|
||||||
<span class="pill">
|
|
||||||
{{ service_target.server ~ ":" ~ service_target.port }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="rdata-complementary">
|
|
||||||
<span class="pill">
|
|
||||||
{{ tr(
|
|
||||||
msg="zone-content-record-type-service",
|
|
||||||
attr="data-priority",
|
|
||||||
priority=service_target.priority,
|
|
||||||
lang=lang) }}
|
|
||||||
</span>
|
|
||||||
<span class="pill">
|
|
||||||
{{ tr(
|
|
||||||
msg="zone-content-record-type-service",
|
|
||||||
attr="data-weight",
|
|
||||||
weight=service_target.weight,
|
|
||||||
lang=lang) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{% endmacro rrset %}
|
|
25
templates/pages/login.html
Normal file
25
templates/pages/login.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{% extends "bases/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Se connecter ⋅ {% endblock title %}
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="/styles/login.css">
|
||||||
|
{% endblock styles %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<h1>Se connecter</h1>
|
||||||
|
{% if error %}
|
||||||
|
<p class="feedback error" role="alert">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<label for="email">Adresse e-mail</label>
|
||||||
|
<input type="email" id="email" name="email">
|
||||||
|
<label for="password">Mot de passe</label>
|
||||||
|
<input type="password" id="password" name="password">
|
||||||
|
<input type="submit" value="Se connecter">
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{% endblock content %}
|
|
@ -1,20 +0,0 @@
|
||||||
{% extends "bases/app.html" %}
|
|
||||||
{% import "macros/new_rrset.html" as new_rrset %}
|
|
||||||
{% block title %}{{ tr(msg="new-record-title", lang=lang) }} – {{ current_zone }} ‑ {% endblock title %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<h1>{{ tr(msg="record-creation-process-heading", zone=current_zone, lang=lang) }}</h1>
|
|
||||||
|
|
||||||
{{ errors | json_encode(pretty=true) }}
|
|
||||||
|
|
||||||
{% set domain_error = errors | get(key="/name", default="") %}
|
|
||||||
|
|
||||||
{% if not new_record_name or (new_record_name and domain_error) %}
|
|
||||||
{% include "pages/new_record/choose_name.html" %}
|
|
||||||
{% elif not config and not rtype %}
|
|
||||||
{% include "pages/new_record/choose_record.html" %}
|
|
||||||
{% else %}
|
|
||||||
{% include "pages/new_record/configure_record.html" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,28 +0,0 @@
|
||||||
<h2>{{ tr(msg="record-choose-name-heading", lang=lang) }}</h2>
|
|
||||||
<form action="" method="GET">
|
|
||||||
<div class="form-input">
|
|
||||||
<label for="name">{{ tr(msg="record-input-name", attr="input-label", lang=lang) }}</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
{% if domain_error %}aria-invalid="true"{% endif %}
|
|
||||||
aria-describedby="{% if domain_error %}name-error {% endif %}subdomain-help"
|
|
||||||
value="{{ new_record_name | default(value="") }}"
|
|
||||||
>
|
|
||||||
<span>.{{ current_zone }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if domain_error %}
|
|
||||||
<p class="error" id="name-error">
|
|
||||||
{{ tr(
|
|
||||||
msg="record-input-name",
|
|
||||||
attr="error-" ~ domain_error.code | replace(from=":", to="-"),
|
|
||||||
extra_args=domain_error | get(key="details", default=""),
|
|
||||||
lang=lang) }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
<p id="name-help">{{ tr(msg="record-input-name", attr="help-description", zone=current_zone, lang=lang) }}</p>
|
|
||||||
<button type="submit">{{ tr(msg="button-create-record-next-step", lang=lang) }}</button>
|
|
||||||
</form>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<h2>Configure the domain {{ new_record_name }}...</h2>
|
|
||||||
<ul>
|
|
||||||
<li><a href="{{ url }}&config=web">Web site</a></li>
|
|
||||||
<li><a href="{{ url }}&config=mail">E-mails</a></li>
|
|
||||||
</ul>
|
|
||||||
<h2>...or create a new record for the domain {{ new_record_name }}</h2>
|
|
||||||
<h3>General</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="{{ url }}&rtype=address">Address (A or AAAA)</a></li>
|
|
||||||
<li><a href="{{ url }}&rtype=alias">Alias (CNAME)</a></li>
|
|
||||||
<li><a href="{{ url }}&rtype=text">Text (TXT)</a></li>
|
|
||||||
<li><a href="{{ url }}&rtype=service">Service (SRV)</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>E-mails</h3>
|
|
||||||
<ul>sdv
|
|
||||||
<li><a href="{{ url }}&rtype=service">Mail servers (MX)</a></li>
|
|
||||||
<li><a href="{{ url }}&rtype=spf">Sender policy (SPF)</a></li>
|
|
||||||
<li><a href="{{ url }}&rtype=dkim">Cryptographic signature (DKIM)</a></li>
|
|
||||||
<li><a href="{{ url }}&rtype=dmarc">Error reporting (DMARC)</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Security</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="{{ url }}&rtype=dane">Domain authentication (TLSA)</a></li>
|
|
||||||
<li><a href="{{ url }}&rtype=sshfp">SSH keys fingerprint (SSHFP)</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>DNS Delegation</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="{{ url }}&rtype=nameserver">Nameserver (NS)</a></li>
|
|
||||||
<li><a href="{{ url }}&rtype=ds">Delegation signer (DS)</a></li>
|
|
||||||
</ul>
|
|
|
@ -1,264 +0,0 @@
|
||||||
{% if config == "web" %}
|
|
||||||
|
|
||||||
<h2>{{ tr(msg="record-config-web-heading", name=new_record_name, lang=lang) }}</h2>
|
|
||||||
|
|
||||||
<form method="post" action="">
|
|
||||||
<h3>{{ tr(msg="record-config-section-web", attr="servers", lang=lang) }}</h3>
|
|
||||||
|
|
||||||
<input name="addresses[_exist]" type="hidden" value="true">
|
|
||||||
|
|
||||||
{% set ttl_error = errors | get(key="/addresses/ttl", default="") %}
|
|
||||||
<div class="form-input">
|
|
||||||
<label for="addresses-ttl">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="input-label", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
inputmode="numeric"
|
|
||||||
name="addresses[ttl]"
|
|
||||||
id="addresses-ttl"
|
|
||||||
aria-describedby="ttl-help{% if ttl_error %} ttl-error{% endif %}"
|
|
||||||
value="{{ input_data.addresses.ttl | default(value="") }}"
|
|
||||||
>
|
|
||||||
{% if ttl_error %}
|
|
||||||
<p class="error" id="ttl-error">
|
|
||||||
{{ tr(
|
|
||||||
msg="record-input-ttl",
|
|
||||||
attr="error-" ~ ttl_error.code | replace(from=":", to="-"),
|
|
||||||
lang=lang) }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
<p class="help" id="ttl-help">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="help", lang=lang) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% set global_address_error = errors | get(key="/addresses/data", default="") %}
|
|
||||||
{% for address in input_data.addresses.data.addresses | default(value=[""]) %}
|
|
||||||
{% set address_error = errors | get(key="/addresses/data/addresses/" ~ loop.index0 ~ "/address", default="") %}
|
|
||||||
<div class="form-input" data-new-item-template="address">
|
|
||||||
<label
|
|
||||||
for="address-{{ loop.index0 }}"
|
|
||||||
data-new-item-template-attr="for"
|
|
||||||
data-template-for="address-{i}"
|
|
||||||
data-new-item-template-content="{{ tr(msg="record-input-addresses", attr="input-label", index="{i}", lang=lang) }}"
|
|
||||||
>
|
|
||||||
{{ tr(msg="record-input-addresses", attr="input-label", index=loop.index, lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="addresses[data][addresses][{{ loop.index0 }}][address]"
|
|
||||||
id="address-{{ loop.index0 }}"
|
|
||||||
data-new-item-template-attr="name id"
|
|
||||||
data-template-name="addresses[data][addresses][{i}][address]"
|
|
||||||
data-template-id="address-{i}"
|
|
||||||
aria-describedby="{% if address_error %}address-{{ loop.index0 }}-error{% endif %}"
|
|
||||||
{% if address_error %}aria-invalid="true"{% endif %}
|
|
||||||
value="{{ address.address | default(value="") }}"
|
|
||||||
>
|
|
||||||
{% if global_address_error or address_error %}
|
|
||||||
<p class="error" id="address-{{ loop.index0 }}-error" data-new-item-skip>
|
|
||||||
{% if global_address_error %}
|
|
||||||
{{ tr(
|
|
||||||
msg="record-input-addresses",
|
|
||||||
attr="error-" ~ global_address_error.code | replace(from=":", to="-"),
|
|
||||||
lang=lang) }}
|
|
||||||
{% else %}
|
|
||||||
{{ tr(
|
|
||||||
msg="record-input-addresses",
|
|
||||||
attr="error-" ~ address_error.code | replace(from=":", to="-"),
|
|
||||||
lang=lang) }}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="form-new-item" type="button" data-new-item="address">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16" aria-hidden="true">
|
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
|
|
||||||
</svg>
|
|
||||||
{{ tr(msg="button-add-address", lang=lang) }}
|
|
||||||
</button>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="form-action">
|
|
||||||
<button type="submit">{{ tr(msg="button-save-configuration", lang=lang) }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% elif config == "mail" %}
|
|
||||||
|
|
||||||
<h2>{{ tr(msg="record-config-mail-heading", name=new_record_name, lang=lang) }}</h2>
|
|
||||||
|
|
||||||
<form method="post" action="">
|
|
||||||
<h3>{{ tr(msg="record-config-section-mail", attr="servers", lang=lang) }}</h3>
|
|
||||||
<input name="mailservers[_exist]" type="hidden" value="true">
|
|
||||||
|
|
||||||
<div class="form-input">
|
|
||||||
<label for="mailservers-ttl">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="input-label", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input name="mailservers[ttl]" id="mailservers-ttl" type="text" value="{{ input_data.mailservers.ttl | default(value="") }}" aria-describedby="mailservers-ttl">
|
|
||||||
<p class="help" id="mailservers-ttl-help">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="help", lang=lang) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for mailserver in input_data.mailservers.data.mailservers | default(value=[""]) %}
|
|
||||||
<fieldset data-new-item-template="mailserver">
|
|
||||||
<legend data-new-item-template-content="{{ tr(msg="record-input-mailservers", attr="legend", index="{i}", lang=lang) }}">
|
|
||||||
{{ tr(msg="record-input-mailservers", attr="legend", index=loop.index, lang=lang) }}
|
|
||||||
</legend>
|
|
||||||
<div class="form-input">
|
|
||||||
<label
|
|
||||||
for="mailserver-mail_exchanger-{{ loop.index0 }}"
|
|
||||||
data-new-item-template-attr="for"
|
|
||||||
data-template-for="mailserver-mail_exchanger-{i}"
|
|
||||||
>
|
|
||||||
{{ tr(msg="record-input-mailservers", attr="input-label-server-name", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
name="mailservers[data][mailservers][{{ loop.index0 }}][mail_exchanger]"
|
|
||||||
id="mailserver-mail_exchanger-{{ loop.index0 }}"
|
|
||||||
data-new-item-template-attr="name id"
|
|
||||||
data-template-name="mailservers[data][mailservers][{i}][mail_exchanger]"
|
|
||||||
data-template-id="mailserver-mail_exchanger-{i}"
|
|
||||||
type="text"
|
|
||||||
value="{{ mailserver.mail_exchanger | default(value="") }}"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label
|
|
||||||
for="mailserver-preference-{{ loop.index0 }}"
|
|
||||||
data-new-item-template-attr="for"
|
|
||||||
data-template-for="mailserver-preference-{i}"
|
|
||||||
>
|
|
||||||
{{ tr(msg="record-input-mailservers", attr="input-label-preference", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
name="mailservers[data][mailservers][{{ loop.index0 }}][preference]"
|
|
||||||
id="mailserver-preference-{{ loop.index0 }}"
|
|
||||||
data-new-item-template-attr="name id"
|
|
||||||
data-template-name="mailservers[data][mailservers][{i}][preference]"
|
|
||||||
data-template-id="mailserver-preference-{i}"
|
|
||||||
type="text"
|
|
||||||
value="{{ mailserver.preference | default(value="") }}"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<button class="form-new-item" type="button" data-new-item="mailserver">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16" aria-hidden="true">
|
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
|
|
||||||
</svg>
|
|
||||||
{{ tr(msg="button-add-mailserver", lang=lang) }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<h3>{{ tr(msg="record-config-section-mail", attr="security", lang=lang) }}</h3>
|
|
||||||
<input name="spf[_exist]" type="hidden" value="true">
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<legend>{{ tr(msg="record-input-spf", attr="legend", lang=lang) }}</legend>
|
|
||||||
<div class="form-input">
|
|
||||||
<label for="spf-policy">
|
|
||||||
{{ tr(msg="record-input-spf", attr="input-label", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input name="spf[data][policy]" id="spf-policy" type="text" value="{{ input_data.spf.data.policy | default(value="") }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label for="spf-ttl">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="input-label", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input name="spf[ttl]" id="spf-ttl" type="text" value="{{ input_data.spf.ttl | default(value="") }}" aria-describedby="spf-ttl-help">
|
|
||||||
<p class="help" id="spf-ttl-help">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="help", lang=lang) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<legend>{{ tr(msg="record-input-dmarc", attr="legend", lang=lang) }}</legend>
|
|
||||||
<div class="form-input">
|
|
||||||
<label for="dmarc">
|
|
||||||
{{ tr(msg="record-input-dmarc", attr="input-label", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input name="dmarc" id="dmarc" type="text">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label for="dmarc-ttl">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="input-label", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input name="dmarc[ttl]" id="dmarc-ttl" type="text" value="{{ input_data.dmarc.ttl | default(value="") }}" aria-describedby="dmac-ttl-help">
|
|
||||||
<p class="help" id="dmarc-ttl-help">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="help", lang=lang) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<h3>{{ tr(msg="record-config-section-mail", attr="dkim", lang=lang) }}</h3>
|
|
||||||
|
|
||||||
<div class="form-input">
|
|
||||||
<label for="dkim-ttl">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="input-label", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input name="dkim[ttl]" id="dkim-ttl" type="text" value="{{ input_data.dkim.ttl | default(value="") }}" aria-describedby="dkim-ttl-help">
|
|
||||||
<p class="help" id="dkim-ttl-help">
|
|
||||||
{{ tr(msg="record-input-ttl", attr="help", lang=lang) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<legend>
|
|
||||||
{{ tr(msg="record-input-dkim", attr="legend", index=1, lang=lang) }}
|
|
||||||
</legend>
|
|
||||||
|
|
||||||
<div class="form-input">
|
|
||||||
<label for="dkim-selector">
|
|
||||||
{{ tr(msg="record-input-dkim", attr="input-label-selector", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input name="dkim-selector" id="dkim-selector" type="text">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label for="dkim-key">
|
|
||||||
{{ tr(msg="record-input-dkim", attr="input-label-signing-key", lang=lang) }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<textarea name="dkim-key" id="dkim-key"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<button class="form-new-item" type="button" data-new-item="dmark-key">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16" aria-hidden="true">
|
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
|
|
||||||
</svg>
|
|
||||||
{{ tr(msg="button-add-dkim-key", lang=lang) }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="form-action">
|
|
||||||
<button type="submit">{{ tr(msg="button-save-configuration", lang=lang) }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
|
@ -1,102 +0,0 @@
|
||||||
{% import "macros/display_rrset.html" as rrset %}
|
|
||||||
{% extends "bases/app.html" %}
|
|
||||||
|
|
||||||
{% block title %}{{ tr(msg="zone-content-title", lang=lang) }} – {{ current_zone }} – {% endblock title %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<h1>{{ tr(msg="zone-header", lang=lang, zone_name="<strong>" ~ current_zone ~ "</strong>") | safe }}</h1>
|
|
||||||
<svg width="0" height="0" aria-hidden="true" style="position: absolute;">
|
|
||||||
<defs>
|
|
||||||
<clipPath id="corner-folder-tab-right" clipPathUnits="objectBoundingBox">
|
|
||||||
<path d="m 0,0 c .25,0 0.75,1 1,1 l -1,0 z"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>{{ tr(msg="zone-content-records-header", lang=lang) }}</h2>
|
|
||||||
{% for node in records.records %}
|
|
||||||
<article class="domain">
|
|
||||||
<header>
|
|
||||||
<h3 class="folder-tab">{{ node.name }}</h3>
|
|
||||||
<span class="sep"></span>
|
|
||||||
<a href="{{ url }}/new?name={{ node.name | trim_end_matches(pat=current_zone) | trim_end_matches(pat=".") }}" class="button">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16" aria-hidden="true">
|
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
|
|
||||||
</svg>
|
|
||||||
{{ tr(msg="zone-content-new-record-button", lang=lang) }}
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
{% set sections = node.records | group_by(attribute="record_section") %}
|
|
||||||
<div class="records">
|
|
||||||
{% if sections.web %}
|
|
||||||
{% set records = sections.web | group_by(attribute="record_type") %}
|
|
||||||
<h4>{{ tr(msg="zone-content-section-web-header", lang=lang) }}</h4>
|
|
||||||
<ul>
|
|
||||||
{% if records.addresses %}
|
|
||||||
{{ rrset::rrset(
|
|
||||||
record=records.addresses.0,
|
|
||||||
zone=current_zone,
|
|
||||||
lang=lang) }}
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if sections.mail %}
|
|
||||||
{% set records = sections.mail | group_by(attribute="record_type") %}
|
|
||||||
<h4>{{ tr(msg="zone-content-section-mail-header", lang=lang) }}</h4>
|
|
||||||
<ul>
|
|
||||||
{% if records.mailservers %}
|
|
||||||
{{ rrset::rrset(
|
|
||||||
record=records.mailservers.0,
|
|
||||||
zone=current_zone,
|
|
||||||
lang=lang) }}
|
|
||||||
{% endif %}
|
|
||||||
{% if records.spf %}
|
|
||||||
{{ rrset::rrset(
|
|
||||||
record=records.spf.0,
|
|
||||||
zone=current_zone,
|
|
||||||
lang=lang) }}
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if sections.services %}
|
|
||||||
<h4>{{ tr(msg="zone-content-section-services-header", lang=lang) }}</h4>
|
|
||||||
<ul>
|
|
||||||
{% for service in sections.services %}
|
|
||||||
{{ rrset::rrset(
|
|
||||||
record=service,
|
|
||||||
zone=current_zone,
|
|
||||||
lang=lang) }}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if sections.miscellaneous %}
|
|
||||||
<h4>{{ tr(msg="zone-content-section-general-header", lang=lang) }}</h4>
|
|
||||||
<ul>
|
|
||||||
{% for record in sections.miscellaneous %}
|
|
||||||
{{ rrset::rrset(
|
|
||||||
record=record,
|
|
||||||
zone=current_zone,
|
|
||||||
lang=lang) }}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>{{ tr(msg="zone-content-aliases-header", lang=lang) }}</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for alias in records.aliases %}
|
|
||||||
<li>{{ alias.from }} → {{ alias.target }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
{% endblock main %}
|
|
43
templates/pages/zone/records.html
Normal file
43
templates/pages/zone/records.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends "bases/app.html" %}
|
||||||
|
{% import "macros.html" as macros %}
|
||||||
|
|
||||||
|
{% block title %}{{ current_zone }} ⋅ Records ⋅ {% endblock title %}
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="/styles/zone.css">
|
||||||
|
{% endblock styles %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<h1>Gestion de la zone {{ current_zone }}</h1>
|
||||||
|
<nav class="secondary" aria-label="Secondaire">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
{{ macros::nav_link(
|
||||||
|
content="Enregistrements",
|
||||||
|
href="/zone/" ~ current_zone ~ "/records",
|
||||||
|
current_page=nav_page,
|
||||||
|
) }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ macros::nav_link(
|
||||||
|
content="Membres",
|
||||||
|
href="/zone/" ~ current_zone ~ "/members",
|
||||||
|
current_page=nav_page,
|
||||||
|
) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<section>
|
||||||
|
<zone-content>
|
||||||
|
</zone-content>
|
||||||
|
</section>
|
||||||
|
{% endblock main %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="module">
|
||||||
|
const zoneName = '{{ current_zone }}';
|
||||||
|
|
||||||
|
import initRecordsComponent from '/scripts/records.js';
|
||||||
|
|
||||||
|
initRecordsComponent(document.querySelector('zone-content'), { zone: zoneName });
|
||||||
|
</script>
|
||||||
|
{% endblock scripts %}
|
3
templates/pages/zones.html
Normal file
3
templates/pages/zones.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "bases/app.html" %}
|
||||||
|
|
||||||
|
{% block title %}Zones ⋅ {% endblock title %}
|
18
templates/pages/zones/new.html
Normal file
18
templates/pages/zones/new.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "bases/app.html" %}
|
||||||
|
|
||||||
|
{% block title %}Ajouter une zone ⋅ {% endblock title %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<h1>Ajouter une zone</h1>
|
||||||
|
<form method="POST" action="/zones/new">
|
||||||
|
{% if error %}
|
||||||
|
<p class="feedback error" role="alert">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<label for="zone_name">Nom de la zone</label>
|
||||||
|
<input type="text" id="zone_name" name="name">
|
||||||
|
<input type="submit" value="Créer la zone">
|
||||||
|
</form>
|
||||||
|
{% endblock main %}
|
Loading…
Reference in a new issue