diff --git a/Cargo.lock b/Cargo.lock index acd3f31..6b47eee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1308,6 +1308,7 @@ dependencies = [ "futures 0.3.14", "log", "nest", + "regex", "rocket", "rocket_contrib", "serde 1.0.125", diff --git a/Cargo.toml b/Cargo.toml index bf5e96f..772d13d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/constants.rs b/src/constants.rs index f7b62f9..c77125a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -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"; \ No newline at end of file +pub const DOMAIN_REGEX: &str = r"^.*\.dyn\.commointernet\.net$"; \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs index fdb3808..d3ab7fc 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -7,6 +7,7 @@ pub enum PeachDynError { GenerateTsigParseError(std::string::FromUtf8Error), DomainAlreadyExistsError(String), BindConfigurationError(String), + InvalidDomainError(String) } impl From for PeachDynError { diff --git a/src/generate_zone.rs b/src/generate_zone.rs index c9436e6..a5108d5 100644 --- a/src/generate_zone.rs +++ b/src/generate_zone.rs @@ -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 { 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 { - // 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 { 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 { ", 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 { 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 diff --git a/src/main.rs b/src/main.rs index 5ece8de..34a8770 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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() diff --git a/src/routes.rs b/src/routes.rs index d94e2c4..8264dd3 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -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) -> Json { 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 = "")] pub async fn check_available(data: Json) -> Json { 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))) + } }