Add domain validation and cleanup
This commit is contained in:
parent
c2431b6225
commit
b6ecf6eda3
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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$";
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user