Add domain validation and cleanup

This commit is contained in:
notplants 2021-05-18 18:49:01 +02:00
parent c2431b6225
commit b6ecf6eda3
7 changed files with 66 additions and 43 deletions

1
Cargo.lock generated
View File

@ -1308,6 +1308,7 @@ dependencies = [
"futures 0.3.14",
"log",
"nest",
"regex",
"rocket",
"rocket_contrib",
"serde 1.0.125",

View File

@ -22,6 +22,7 @@ rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", branch = "ma
serde = "1.0.125"
dotenv = "0.15.0"
tera = "1"
regex = "1"
[[bin]]
name = "main"

View File

@ -1,5 +1,4 @@
// this is the base domain which peach-dyndns creates subdomains under
// this regex is used to validate that domains are in the correct format
// e.g. blue.dyn.peachcloud.org
pub const BASE_DOMAIN: &str = "dyn.commoninternet.net";
pub const DOMAIN_REGEX: &str = r"^.*\.dyn\.commointernet\.net$";

View File

@ -7,6 +7,7 @@ pub enum PeachDynError {
GenerateTsigParseError(std::string::FromUtf8Error),
DomainAlreadyExistsError(String),
BindConfigurationError(String),
InvalidDomainError(String)
}
impl From<std::io::Error> for PeachDynError {

View File

@ -1,8 +1,6 @@
/* For each subdomain,
- generate a new ddns key (tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net) and append it to /etc/bind/dyn.commoninternet.net.keys
- add a zone section to /etc/bind/named.conf.local, associating the key with the subdomain
- add a minimal zone file to /var/lib/bind/subdomain.dyn.commoninternet.net
- reload bind and return the secret key to the client
/*
* Functions for generating bind9 configurations to enable dynamic dns for a subdomain via TSIG authentication
* which is unique to that subdomain
*/
use std::fs::File;
use std::fs::OpenOptions;
@ -10,7 +8,7 @@ use std::io::Write;
use std::process::Command;
use tera::{Tera, Context};
use crate::errors::PeachDynError;
use crate::constants::BASE_DOMAIN;
use crate::constants::DOMAIN_REGEX;
/// function to generate the text of a TSIG key file
@ -24,6 +22,13 @@ pub fn generate_tsig_key(full_domain: &str) -> Result<String, PeachDynError> {
Ok(key_file_text)
}
// function returns true if domain is of the format something.dyn.peachcloud.org
pub fn validate_domain(domain: &str) -> bool {
use regex::Regex;
let re = Regex::new(DOMAIN_REGEX).unwrap();
re.is_match(domain)
}
/// function which helps us guarantee that a given domain is not already being used by bind
/// it checks three places for the domain, and only returns true if it is not found in all three places
/// - no already extant tsig key for the given domain
@ -54,13 +59,20 @@ pub fn check_domain_available(full_domain: &str) -> bool {
/// function which generates all necessary bind configuration to serve the given
/// subdomain using dynamic DNS authenticated via a new TSIG key which is unique to that subdomain
/// - thus only the possessor of that key can use nsupdate to modify the records
/// thus only the possessor of that key can use nsupdate to modify the records
/// for that subodmain
/// - generate a new ddns key (tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net) and append it to /etc/bind/dyn.commoninternet.net.keys
/// - add a zone section to /etc/bind/named.conf.local, associating the key with the subdomain
/// - add a minimal zone file to /var/lib/bind/subdomain.dyn.commoninternet.net
/// - reload bind and return the secret key to the client
pub fn generate_zone(full_domain: &str) -> Result<String, PeachDynError> {
// TODO: confirm that domain matches correct format
// first safety check domain is in correct format
if !validate_domain(full_domain) {
return Err(PeachDynError::InvalidDomainError(full_domain.to_string()));
}
// first safety check if the domain is available
// safety check if the domain is available
let is_available = check_domain_available(full_domain);
if !is_available {
return Err(PeachDynError::DomainAlreadyExistsError(full_domain.to_string()));
@ -74,7 +86,7 @@ pub fn generate_zone(full_domain: &str) -> Result<String, PeachDynError> {
let mut file = OpenOptions::new().append(true).open(key_file_path)
.expect(&format!("failed to open {}", key_file_path));
if let Err(e) = writeln!(file, "{}", key_file_text) {
eprintln!("Couldn't write to file: {}", e);
error!("Couldn't write to file: {}", e);
}
// append zone section to /etc/bind/named.conf.local
@ -95,15 +107,13 @@ pub fn generate_zone(full_domain: &str) -> Result<String, PeachDynError> {
",
full_domain = full_domain
);
if let Err(e) = writeln!(file, "{}", zone_section_text) {
eprintln!("Couldn't write to file: {}", e);
}
writeln!(file, "{}", zone_section_text).expect(&format!("Couldn't write to file: {}", bind_conf_path));
// use tera to render the zone file
let tera = match Tera::new("templates/*.tera") {
Ok(t) => t,
Err(e) => {
println!("Parsing error(s): {}", e);
info!("Parsing error(s): {}", e);
::std::process::exit(1);
}
};
@ -115,9 +125,7 @@ pub fn generate_zone(full_domain: &str) -> Result<String, PeachDynError> {
let zone_file_path = format!("/var/lib/bind/{}", full_domain);
let mut file = File::create(&zone_file_path)
.expect(&format!("failed to create {}", zone_file_path));
if let Err(e) = writeln!(file, "{}", result) {
eprintln!("Couldn't write to file: {}", e)
};
writeln!(file, "{}", result).expect(&format!("Couldn't write to file: {}", zone_file_path));
// restart bind
// we use the /etc/sudoers.d/bindctl to allow peach-dyndns user to restart bind as sudo without entering a password

View File

@ -4,8 +4,6 @@
extern crate rocket;
use crate::routes::{index, register_domain, check_available};
use std::io;
use tokio::task;
mod cli;
mod routes;
@ -15,6 +13,8 @@ mod generate_zone;
#[tokio::main]
async fn main() {
let _args = cli::args().expect("error parsing args");
let rocket_result = rocket::build()
.mount("/", routes![index, register_domain, check_available])
.launch()

View File

@ -5,7 +5,7 @@
* /user/register sends an email verification to create a new account) NOT IMPLEMENTED
* /user/verify (for clicking the link in the email) NOT IMPLEMENTED
*/
use crate::generate_zone::{check_domain_available, generate_zone};
use crate::generate_zone::{check_domain_available, generate_zone, validate_domain};
use rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize};
@ -41,24 +41,32 @@ pub struct RegisterDomainPost {
pub async fn register_domain(data: Json<RegisterDomainPost>) -> Json<JsonResponse> {
info!("++ post request to register new domain: {:?}", data);
// TODO: grab/create a mutex, so that only one rocket thread is calling register_domain at a time
// TODO: first confirm domain is in the right format ("*.dyn.peachcloud.org")
let is_domain_available = check_domain_available(&data.domain);
if !is_domain_available{
// check if its a valid domain
if !validate_domain(&data.domain) {
let status = "error".to_string();
let msg = "can't register a domain that is already registered".to_string();
let msg = "domain is not in a valid format".to_string();
Json(build_json_response(status, None, Some(msg)))
} else {
let result = generate_zone(&data.domain);
match result {
Ok(key_file_text) => {
let status = "success".to_string();
let msg = key_file_text.to_string();
Json(build_json_response(status, None, Some(msg)))
}
Err(_err) => {
let status = "error".to_string();
let msg = "there was an error creating the zone file".to_string();
Json(build_json_response(status, None, Some(msg)))
// check if the domain is available
let is_domain_available = check_domain_available(&data.domain);
if !is_domain_available {
let status = "error".to_string();
let msg = "can't register a domain that is already registered".to_string();
Json(build_json_response(status, None, Some(msg)))
} else {
// generate configs for the zone
let result = generate_zone(&data.domain);
match result {
Ok(key_file_text) => {
let status = "success".to_string();
let msg = key_file_text.to_string();
Json(build_json_response(status, None, Some(msg)))
}
Err(_err) => {
let status = "error".to_string();
let msg = "there was an error creating the zone file".to_string();
Json(build_json_response(status, None, Some(msg)))
}
}
}
}
@ -73,9 +81,14 @@ pub struct CheckAvailableDomainPost {
#[post("/domain/check-available", data = "<data>")]
pub async fn check_available(data: Json<CheckAvailableDomainPost>) -> Json<JsonResponse> {
info!("post request to check if domain is available {:?}", data);
// TODO: validate that domain is in correct format
let status = "success".to_string();
let is_available = check_domain_available(&data.domain);
let msg = is_available.to_string();
Json(build_json_response(status, None, Some(msg)))
if !validate_domain(&data.domain) {
let status = "error".to_string();
let msg = "domain is not in a valid format".to_string();
Json(build_json_response(status, None, Some(msg)))
} else {
let status = "success".to_string();
let is_available = check_domain_available(&data.domain);
let msg = is_available.to_string();
Json(build_json_response(status, None, Some(msg)))
}
}