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", "futures 0.3.14",
"log", "log",
"nest", "nest",
"regex",
"rocket", "rocket",
"rocket_contrib", "rocket_contrib",
"serde 1.0.125", "serde 1.0.125",

View File

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

View File

@ -1,5 +1,4 @@
// this regex is used to validate that domains are in the correct format
// this is the base domain which peach-dyndns creates subdomains under
// e.g. blue.dyn.peachcloud.org // 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), GenerateTsigParseError(std::string::FromUtf8Error),
DomainAlreadyExistsError(String), DomainAlreadyExistsError(String),
BindConfigurationError(String), BindConfigurationError(String),
InvalidDomainError(String)
} }
impl From<std::io::Error> for PeachDynError { 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 * Functions for generating bind9 configurations to enable dynamic dns for a subdomain via TSIG authentication
- add a zone section to /etc/bind/named.conf.local, associating the key with the subdomain * which is unique to that subdomain
- add a minimal zone file to /var/lib/bind/subdomain.dyn.commoninternet.net
- reload bind and return the secret key to the client
*/ */
use std::fs::File; use std::fs::File;
use std::fs::OpenOptions; use std::fs::OpenOptions;
@ -10,7 +8,7 @@ use std::io::Write;
use std::process::Command; use std::process::Command;
use tera::{Tera, Context}; use tera::{Tera, Context};
use crate::errors::PeachDynError; use crate::errors::PeachDynError;
use crate::constants::BASE_DOMAIN; use crate::constants::DOMAIN_REGEX;
/// function to generate the text of a TSIG key file /// 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) 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 /// 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 /// 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 /// - 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 /// 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 /// 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 /// 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> { 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); let is_available = check_domain_available(full_domain);
if !is_available { if !is_available {
return Err(PeachDynError::DomainAlreadyExistsError(full_domain.to_string())); 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) let mut file = OpenOptions::new().append(true).open(key_file_path)
.expect(&format!("failed to open {}", key_file_path)); .expect(&format!("failed to open {}", key_file_path));
if let Err(e) = writeln!(file, "{}", key_file_text) { 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 // 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 full_domain = full_domain
); );
if let Err(e) = writeln!(file, "{}", zone_section_text) { writeln!(file, "{}", zone_section_text).expect(&format!("Couldn't write to file: {}", bind_conf_path));
eprintln!("Couldn't write to file: {}", e);
}
// use tera to render the zone file // use tera to render the zone file
let tera = match Tera::new("templates/*.tera") { let tera = match Tera::new("templates/*.tera") {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
println!("Parsing error(s): {}", e); info!("Parsing error(s): {}", e);
::std::process::exit(1); ::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 zone_file_path = format!("/var/lib/bind/{}", full_domain);
let mut file = File::create(&zone_file_path) let mut file = File::create(&zone_file_path)
.expect(&format!("failed to create {}", zone_file_path)); .expect(&format!("failed to create {}", zone_file_path));
if let Err(e) = writeln!(file, "{}", result) { writeln!(file, "{}", result).expect(&format!("Couldn't write to file: {}", zone_file_path));
eprintln!("Couldn't write to file: {}", e)
};
// restart bind // restart bind
// we use the /etc/sudoers.d/bindctl to allow peach-dyndns user to restart bind as sudo without entering a password // 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; extern crate rocket;
use crate::routes::{index, register_domain, check_available}; use crate::routes::{index, register_domain, check_available};
use std::io;
use tokio::task;
mod cli; mod cli;
mod routes; mod routes;
@ -15,6 +13,8 @@ mod generate_zone;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let _args = cli::args().expect("error parsing args");
let rocket_result = rocket::build() let rocket_result = rocket::build()
.mount("/", routes![index, register_domain, check_available]) .mount("/", routes![index, register_domain, check_available])
.launch() .launch()

View File

@ -5,7 +5,7 @@
* /user/register sends an email verification to create a new account) NOT IMPLEMENTED * /user/register sends an email verification to create a new account) NOT IMPLEMENTED
* /user/verify (for clicking the link in the email) 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 rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -41,13 +41,20 @@ pub struct RegisterDomainPost {
pub async fn register_domain(data: Json<RegisterDomainPost>) -> Json<JsonResponse> { pub async fn register_domain(data: Json<RegisterDomainPost>) -> Json<JsonResponse> {
info!("++ post request to register new domain: {:?}", data); 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: 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") // check if its a valid domain
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 {
// check if the domain is available
let is_domain_available = check_domain_available(&data.domain); let is_domain_available = check_domain_available(&data.domain);
if !is_domain_available{ if !is_domain_available {
let status = "error".to_string(); let status = "error".to_string();
let msg = "can't register a domain that is already registered".to_string(); let msg = "can't register a domain that is already registered".to_string();
Json(build_json_response(status, None, Some(msg))) Json(build_json_response(status, None, Some(msg)))
} else { } else {
// generate configs for the zone
let result = generate_zone(&data.domain); let result = generate_zone(&data.domain);
match result { match result {
Ok(key_file_text) => { Ok(key_file_text) => {
@ -62,6 +69,7 @@ pub async fn register_domain(data: Json<RegisterDomainPost>) -> Json<JsonRespons
} }
} }
} }
}
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -73,9 +81,14 @@ pub struct CheckAvailableDomainPost {
#[post("/domain/check-available", data = "<data>")] #[post("/domain/check-available", data = "<data>")]
pub async fn check_available(data: Json<CheckAvailableDomainPost>) -> Json<JsonResponse> { pub async fn check_available(data: Json<CheckAvailableDomainPost>) -> Json<JsonResponse> {
info!("post request to check if domain is available {:?}", data); info!("post request to check if domain is available {:?}", data);
// TODO: validate that domain is in correct format 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 status = "success".to_string();
let is_available = check_domain_available(&data.domain); let is_available = check_domain_available(&data.domain);
let msg = is_available.to_string(); let msg = is_available.to_string();
Json(build_json_response(status, None, Some(msg))) Json(build_json_response(status, None, Some(msg)))
}
} }