add timestamp to random code

This commit is contained in:
Face
2025-08-22 18:28:24 +03:00
parent 17cfa07e86
commit c4e818d476

View File

@@ -2,7 +2,9 @@ use super::{models::*, AppState};
use crate::auth::Claims; use crate::auth::Claims;
use crate::discord_bot::{send_domain_approval_request, DomainRegistration}; use crate::discord_bot::{send_domain_approval_request, DomainRegistration};
use gurt::prelude::*; use gurt::prelude::*;
use std::{env, collections::HashMap}; use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{collections::HashMap, env};
const VALID_DNS_RECORD_TYPES: &[&str] = &["A", "AAAA", "CNAME", "TXT"]; const VALID_DNS_RECORD_TYPES: &[&str] = &["A", "AAAA", "CNAME", "TXT"];
@@ -31,22 +33,31 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
|| domain.name.len() > 24 || domain.name.len() > 24
|| domain.name.is_empty() || domain.name.is_empty()
|| domain.name.starts_with('-') || domain.name.starts_with('-')
|| domain.name.ends_with('-') { || domain.name.ends_with('-')
return Err(GurtError::invalid_message("Invalid name, non-existent TLD, or name too long (24 chars).")); {
return Err(GurtError::invalid_message(
"Invalid name, non-existent TLD, or name too long (24 chars).",
));
} }
if app.config.offen_words().iter().any(|word| domain.name.contains(word)) { if app
return Err(GurtError::invalid_message("The given domain name is offensive.")); .config
.offen_words()
.iter()
.any(|word| domain.name.contains(word))
{
return Err(GurtError::invalid_message(
"The given domain name is offensive.",
));
} }
let existing_count: i64 = sqlx::query_scalar( let existing_count: i64 =
"SELECT COUNT(*) FROM domains WHERE name = $1 AND tld = $2" sqlx::query_scalar("SELECT COUNT(*) FROM domains WHERE name = $1 AND tld = $2")
) .bind(&domain.name)
.bind(&domain.name) .bind(&domain.tld)
.bind(&domain.tld) .fetch_one(&app.db)
.fetch_one(&app.db) .await
.await .map_err(|_| GurtError::invalid_message("Database error"))?;
.map_err(|_| GurtError::invalid_message("Database error"))?;
if existing_count > 0 { if existing_count > 0 {
return Err(GurtError::invalid_message("Domain already exists")); return Err(GurtError::invalid_message("Domain already exists"));
@@ -73,11 +84,13 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
let domain_id = domain_row.0; let domain_id = domain_row.0;
// Decrease user's registrations remaining // Decrease user's registrations remaining
sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1") sqlx::query(
.bind(user_id) "UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1",
.execute(&app.db) )
.await .bind(user_id)
.map_err(|_| GurtError::invalid_message("Failed to update user registrations"))?; .execute(&app.db)
.await
.map_err(|_| GurtError::invalid_message("Failed to update user registrations"))?;
if !app.config.discord.bot_token.is_empty() && app.config.discord.channel_id != 0 { if !app.config.discord.bot_token.is_empty() && app.config.discord.channel_id != 0 {
let domain_registration = DomainRegistration { let domain_registration = DomainRegistration {
@@ -92,11 +105,9 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
let bot_token = app.config.discord.bot_token.clone(); let bot_token = app.config.discord.bot_token.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = send_domain_approval_request( if let Err(e) =
channel_id, send_domain_approval_request(channel_id, domain_registration, &bot_token).await
domain_registration, {
&bot_token,
).await {
log::error!("Failed to send Discord notification: {}", e); log::error!("Failed to send Discord notification: {}", e);
} }
}); });
@@ -105,7 +116,11 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
Ok(domain) Ok(domain)
} }
pub(crate) async fn create_domain(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> { pub(crate) async fn create_domain(
ctx: &ServerContext,
app_state: AppState,
claims: Claims,
) -> Result<GurtResponse> {
// Check if user has registrations remaining // Check if user has registrations remaining
let user: (i32,) = sqlx::query_as("SELECT registrations_remaining FROM users WHERE id = $1") let user: (i32,) = sqlx::query_as("SELECT registrations_remaining FROM users WHERE id = $1")
.bind(claims.user_id) .bind(claims.user_id)
@@ -124,29 +139,31 @@ pub(crate) async fn create_domain(ctx: &ServerContext, app_state: AppState, clai
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?; .map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
match create_logic(domain.clone(), claims.user_id, &app_state).await { match create_logic(domain.clone(), claims.user_id, &app_state).await {
Ok(created_domain) => { Ok(created_domain) => Ok(GurtResponse::ok().with_json_body(&created_domain)?),
Ok(GurtResponse::ok().with_json_body(&created_domain)?) Err(e) => Ok(GurtResponse::bad_request().with_json_body(&Error {
} msg: "Failed to create domain",
Err(e) => { error: e.to_string(),
Ok(GurtResponse::bad_request().with_json_body(&Error { })?),
msg: "Failed to create domain",
error: e.to_string(),
})?)
}
} }
} }
pub(crate) async fn get_domain(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> { pub(crate) async fn get_domain(
ctx: &ServerContext,
app_state: AppState,
claims: Claims,
) -> Result<GurtResponse> {
let path_parts: Vec<&str> = ctx.path().split('/').collect(); let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 3 { if path_parts.len() < 3 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid path format. Expected /domain/{domainName}"));
} }
let domain_name = path_parts[2]; let domain_name = path_parts[2];
let domain_parts: Vec<&str> = domain_name.split('.').collect(); let domain_parts: Vec<&str> = domain_name.split('.').collect();
if domain_parts.len() < 2 { if domain_parts.len() < 2 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid domain format. Expected name.tld"));
} }
let name = domain_parts[0]; let name = domain_parts[0];
@@ -171,7 +188,7 @@ pub(crate) async fn get_domain(ctx: &ServerContext, app_state: AppState, claims:
}; };
Ok(GurtResponse::ok().with_json_body(&response_domain)?) Ok(GurtResponse::ok().with_json_body(&response_domain)?)
} }
None => Ok(GurtResponse::not_found().with_string_body("Domain not found")) None => Ok(GurtResponse::not_found().with_string_body("Domain not found")),
} }
} }
@@ -185,12 +202,14 @@ pub(crate) async fn get_domains(ctx: &ServerContext, app_state: AppState) -> Res
HashMap::new() HashMap::new()
}; };
let page = query_params.get("page") let page = query_params
.get("page")
.and_then(|p| p.parse::<u32>().ok()) .and_then(|p| p.parse::<u32>().ok())
.unwrap_or(1) .unwrap_or(1)
.max(1); // Ensure page is at least 1 .max(1); // Ensure page is at least 1
let page_size = query_params.get("limit") let page_size = query_params
.get("limit")
.and_then(|l| l.parse::<u32>().ok()) .and_then(|l| l.parse::<u32>().ok())
.unwrap_or(100) .unwrap_or(100)
.clamp(1, 1000); // Limit between 1 and 1000 .clamp(1, 1000); // Limit between 1 and 1000
@@ -206,13 +225,14 @@ pub(crate) async fn get_domains(ctx: &ServerContext, app_state: AppState) -> Res
.await .await
.map_err(|_| GurtError::invalid_message("Database error"))?; .map_err(|_| GurtError::invalid_message("Database error"))?;
let response_domains: Vec<ResponseDomain> = domains.into_iter().map(|domain| { let response_domains: Vec<ResponseDomain> = domains
ResponseDomain { .into_iter()
.map(|domain| ResponseDomain {
name: domain.name, name: domain.name,
tld: domain.tld, tld: domain.tld,
records: None, records: None,
} })
}).collect(); .collect();
let response = PaginationResponse { let response = PaginationResponse {
domains: response_domains, domains: response_domains,
@@ -233,12 +253,15 @@ pub(crate) async fn check_domain(ctx: &ServerContext, app_state: AppState) -> Re
let query_string = &path[query_start + 1..]; let query_string = &path[query_start + 1..];
parse_query_string(query_string) parse_query_string(query_string)
} else { } else {
return Ok(GurtResponse::bad_request().with_string_body("Missing query parameters. Expected ?name=<name>&tld=<tld>")); return Ok(GurtResponse::bad_request()
.with_string_body("Missing query parameters. Expected ?name=<name>&tld=<tld>"));
}; };
let name = query_params.get("name") let name = query_params
.get("name")
.ok_or_else(|| GurtError::invalid_message("Missing 'name' parameter"))?; .ok_or_else(|| GurtError::invalid_message("Missing 'name' parameter"))?;
let tld = query_params.get("tld") let tld = query_params
.get("tld")
.ok_or_else(|| GurtError::invalid_message("Missing 'tld' parameter"))?; .ok_or_else(|| GurtError::invalid_message("Missing 'tld' parameter"))?;
let domain: Option<Domain> = sqlx::query_as::<_, Domain>( let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
@@ -258,14 +281,24 @@ pub(crate) async fn check_domain(ctx: &ServerContext, app_state: AppState) -> Re
Ok(GurtResponse::ok().with_json_body(&domain_list)?) Ok(GurtResponse::ok().with_json_body(&domain_list)?)
} }
pub(crate) async fn update_domain(_ctx: &ServerContext, _app_state: AppState, _claims: Claims) -> Result<GurtResponse> { pub(crate) async fn update_domain(
return Ok(GurtResponse::bad_request().with_string_body("Domain updates are no longer supported. Use DNS records instead.")); _ctx: &ServerContext,
_app_state: AppState,
_claims: Claims,
) -> Result<GurtResponse> {
return Ok(GurtResponse::bad_request()
.with_string_body("Domain updates are no longer supported. Use DNS records instead."));
} }
pub(crate) async fn delete_domain(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> { pub(crate) async fn delete_domain(
ctx: &ServerContext,
app_state: AppState,
claims: Claims,
) -> Result<GurtResponse> {
let path_parts: Vec<&str> = ctx.path().split('/').collect(); let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 4 { if path_parts.len() < 4 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{name}/{tld}")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid path format. Expected /domain/{name}/{tld}"));
} }
let name = path_parts[2]; let name = path_parts[2];
@@ -297,7 +330,11 @@ pub(crate) async fn delete_domain(ctx: &ServerContext, app_state: AppState, clai
Ok(GurtResponse::ok().with_string_body("Domain deleted successfully")) Ok(GurtResponse::ok().with_string_body("Domain deleted successfully"))
} }
pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> { pub(crate) async fn get_user_domains(
ctx: &ServerContext,
app_state: AppState,
claims: Claims,
) -> Result<GurtResponse> {
// Parse pagination from query parameters // Parse pagination from query parameters
let path = ctx.path(); let path = ctx.path();
let query_params = if let Some(query_start) = path.find('?') { let query_params = if let Some(query_start) = path.find('?') {
@@ -307,12 +344,14 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
HashMap::new() HashMap::new()
}; };
let page = query_params.get("page") let page = query_params
.get("page")
.and_then(|p| p.parse::<u32>().ok()) .and_then(|p| p.parse::<u32>().ok())
.unwrap_or(1) .unwrap_or(1)
.max(1); .max(1);
let page_size = query_params.get("limit") let page_size = query_params
.get("limit")
.and_then(|l| l.parse::<u32>().ok()) .and_then(|l| l.parse::<u32>().ok())
.unwrap_or(100) .unwrap_or(100)
.clamp(1, 1000); .clamp(1, 1000);
@@ -329,14 +368,15 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
.await .await
.map_err(|_| GurtError::invalid_message("Database error"))?; .map_err(|_| GurtError::invalid_message("Database error"))?;
let response_domains: Vec<UserDomain> = domains.into_iter().map(|domain| { let response_domains: Vec<UserDomain> = domains
UserDomain { .into_iter()
.map(|domain| UserDomain {
name: domain.name, name: domain.name,
tld: domain.tld, tld: domain.tld,
status: domain.status.unwrap_or_else(|| "pending".to_string()), status: domain.status.unwrap_or_else(|| "pending".to_string()),
denial_reason: domain.denial_reason, denial_reason: domain.denial_reason,
} })
}).collect(); .collect();
let response = UserDomainResponse { let response = UserDomainResponse {
domains: response_domains, domains: response_domains,
@@ -347,17 +387,23 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
Ok(GurtResponse::ok().with_json_body(&response)?) Ok(GurtResponse::ok().with_json_body(&response)?)
} }
pub(crate) async fn get_domain_records(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> { pub(crate) async fn get_domain_records(
ctx: &ServerContext,
app_state: AppState,
claims: Claims,
) -> Result<GurtResponse> {
let path_parts: Vec<&str> = ctx.path().split('/').collect(); let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 4 { if path_parts.len() < 4 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}/records")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid path format. Expected /domain/{domainName}/records"));
} }
let domain_name = path_parts[2]; let domain_name = path_parts[2];
let domain_parts: Vec<&str> = domain_name.split('.').collect(); let domain_parts: Vec<&str> = domain_name.split('.').collect();
if domain_parts.len() < 2 { if domain_parts.len() < 2 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid domain format. Expected name.tld"));
} }
let name = domain_parts[0]; let name = domain_parts[0];
@@ -375,7 +421,11 @@ pub(crate) async fn get_domain_records(ctx: &ServerContext, app_state: AppState,
let domain = match domain { let domain = match domain {
Some(d) => d, Some(d) => d,
None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied")) None => {
return Ok(
GurtResponse::not_found().with_string_body("Domain not found or access denied")
)
}
}; };
let records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>( let records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>(
@@ -386,31 +436,38 @@ pub(crate) async fn get_domain_records(ctx: &ServerContext, app_state: AppState,
.await .await
.map_err(|_| GurtError::invalid_message("Database error"))?; .map_err(|_| GurtError::invalid_message("Database error"))?;
let response_records: Vec<ResponseDnsRecord> = records.into_iter().map(|record| { let response_records: Vec<ResponseDnsRecord> = records
ResponseDnsRecord { .into_iter()
.map(|record| ResponseDnsRecord {
id: record.id.unwrap(), id: record.id.unwrap(),
record_type: record.record_type, record_type: record.record_type,
name: record.name, name: record.name,
value: record.value, value: record.value,
ttl: record.ttl, ttl: record.ttl,
priority: record.priority, priority: record.priority,
} })
}).collect(); .collect();
Ok(GurtResponse::ok().with_json_body(&response_records)?) Ok(GurtResponse::ok().with_json_body(&response_records)?)
} }
pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> { pub(crate) async fn create_domain_record(
ctx: &ServerContext,
app_state: AppState,
claims: Claims,
) -> Result<GurtResponse> {
let path_parts: Vec<&str> = ctx.path().split('/').collect(); let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 4 { if path_parts.len() < 4 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}/records")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid path format. Expected /domain/{domainName}/records"));
} }
let domain_name = path_parts[2]; let domain_name = path_parts[2];
let domain_parts: Vec<&str> = domain_name.split('.').collect(); let domain_parts: Vec<&str> = domain_name.split('.').collect();
if domain_parts.len() < 2 { if domain_parts.len() < 2 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid domain format. Expected name.tld"));
} }
let name = domain_parts[0]; let name = domain_parts[0];
@@ -428,7 +485,11 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
let domain = match domain { let domain = match domain {
Some(d) => d, Some(d) => d,
None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied")) None => {
return Ok(
GurtResponse::not_found().with_string_body("Domain not found or access denied")
)
}
}; };
let record_data: CreateDnsRecord = { let record_data: CreateDnsRecord = {
@@ -436,11 +497,10 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
let body_str = std::str::from_utf8(body_bytes).unwrap_or("<invalid utf8>"); let body_str = std::str::from_utf8(body_bytes).unwrap_or("<invalid utf8>");
log::info!("Received JSON body: {}", body_str); log::info!("Received JSON body: {}", body_str);
serde_json::from_slice(body_bytes) serde_json::from_slice(body_bytes).map_err(|e| {
.map_err(|e| { log::error!("JSON parsing error: {} for body: {}", e, body_str);
log::error!("JSON parsing error: {} for body: {}", e, body_str); GurtError::invalid_message("Invalid JSON")
GurtError::invalid_message("Invalid JSON") })?
})?
}; };
if record_data.record_type.is_empty() { if record_data.record_type.is_empty() {
@@ -448,7 +508,9 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
} }
if !VALID_DNS_RECORD_TYPES.contains(&record_data.record_type.as_str()) { if !VALID_DNS_RECORD_TYPES.contains(&record_data.record_type.as_str()) {
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type. Only A, AAAA, CNAME, and TXT records are supported.")); return Ok(GurtResponse::bad_request().with_string_body(
"Invalid record type. Only A, AAAA, CNAME, and TXT records are supported.",
));
} }
let record_name = record_data.name.unwrap_or_else(|| "@".to_string()); let record_name = record_data.name.unwrap_or_else(|| "@".to_string());
@@ -457,22 +519,25 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
match record_data.record_type.as_str() { match record_data.record_type.as_str() {
"A" => { "A" => {
if !record_data.value.parse::<std::net::Ipv4Addr>().is_ok() { if !record_data.value.parse::<std::net::Ipv4Addr>().is_ok() {
return Ok(GurtResponse::bad_request().with_string_body("Invalid IPv4 address for A record")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid IPv4 address for A record"));
} }
}, }
"AAAA" => { "AAAA" => {
if !record_data.value.parse::<std::net::Ipv6Addr>().is_ok() { if !record_data.value.parse::<std::net::Ipv6Addr>().is_ok() {
return Ok(GurtResponse::bad_request().with_string_body("Invalid IPv6 address for AAAA record")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid IPv6 address for AAAA record"));
} }
}, }
"CNAME" => { "CNAME" => {
if record_data.value.is_empty() || !record_data.value.contains('.') { if record_data.value.is_empty() || !record_data.value.contains('.') {
return Ok(GurtResponse::bad_request().with_string_body("CNAME records must contain a valid domain name")); return Ok(GurtResponse::bad_request()
.with_string_body("CNAME records must contain a valid domain name"));
} }
}, }
"TXT" => { "TXT" => {
// TXT records can contain any text // TXT records can contain any text
}, }
_ => { _ => {
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type")); return Ok(GurtResponse::bad_request().with_string_body("Invalid record type"));
} }
@@ -506,21 +571,29 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
Ok(GurtResponse::ok().with_json_body(&response_record)?) Ok(GurtResponse::ok().with_json_body(&response_record)?)
} }
pub(crate) async fn delete_domain_record(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> { pub(crate) async fn delete_domain_record(
ctx: &ServerContext,
app_state: AppState,
claims: Claims,
) -> Result<GurtResponse> {
let path_parts: Vec<&str> = ctx.path().split('/').collect(); let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 5 { if path_parts.len() < 5 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}/records/{recordId}")); return Ok(GurtResponse::bad_request().with_string_body(
"Invalid path format. Expected /domain/{domainName}/records/{recordId}",
));
} }
let domain_name = path_parts[2]; let domain_name = path_parts[2];
let record_id_str = path_parts[4]; let record_id_str = path_parts[4];
let record_id: i32 = record_id_str.parse() let record_id: i32 = record_id_str
.parse()
.map_err(|_| GurtError::invalid_message("Invalid record ID"))?; .map_err(|_| GurtError::invalid_message("Invalid record ID"))?;
let domain_parts: Vec<&str> = domain_name.split('.').collect(); let domain_parts: Vec<&str> = domain_name.split('.').collect();
if domain_parts.len() < 2 { if domain_parts.len() < 2 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld")); return Ok(GurtResponse::bad_request()
.with_string_body("Invalid domain format. Expected name.tld"));
} }
let name = domain_parts[0]; let name = domain_parts[0];
@@ -538,7 +611,11 @@ pub(crate) async fn delete_domain_record(ctx: &ServerContext, app_state: AppStat
let domain = match domain { let domain = match domain {
Some(d) => d, Some(d) => d,
None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied")) None => {
return Ok(
GurtResponse::not_found().with_string_body("Domain not found or access denied")
)
}
}; };
let rows_affected = sqlx::query("DELETE FROM dns_records WHERE id = $1 AND domain_id = $2") let rows_affected = sqlx::query("DELETE FROM dns_records WHERE id = $1 AND domain_id = $2")
@@ -556,7 +633,10 @@ pub(crate) async fn delete_domain_record(ctx: &ServerContext, app_state: AppStat
Ok(GurtResponse::ok().with_string_body("DNS record deleted successfully")) Ok(GurtResponse::ok().with_string_body("DNS record deleted successfully"))
} }
pub(crate) async fn resolve_domain(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> { pub(crate) async fn resolve_domain(
ctx: &ServerContext,
app_state: AppState,
) -> Result<GurtResponse> {
let resolution_request: DnsResolutionRequest = serde_json::from_slice(ctx.body()) let resolution_request: DnsResolutionRequest = serde_json::from_slice(ctx.body())
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?; .map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
@@ -572,7 +652,10 @@ pub(crate) async fn resolve_domain(ctx: &ServerContext, app_state: AppState) ->
} }
} }
async fn resolve_dns_with_delegation(query_name: &str, app_state: &AppState) -> Result<DnsResolutionResponse> { async fn resolve_dns_with_delegation(
query_name: &str,
app_state: &AppState,
) -> Result<DnsResolutionResponse> {
// Parse the query domain // Parse the query domain
let parts: Vec<&str> = query_name.split('.').collect(); let parts: Vec<&str> = query_name.split('.').collect();
if parts.len() < 2 { if parts.len() < 2 {
@@ -591,10 +674,16 @@ async fn resolve_dns_with_delegation(query_name: &str, app_state: &AppState) ->
return Ok(response); return Ok(response);
} }
Err(GurtError::invalid_message("No matching records or delegation found")) Err(GurtError::invalid_message(
"No matching records or delegation found",
))
} }
async fn try_exact_match(query_name: &str, tld: &str, app_state: &AppState) -> Result<Option<DnsResolutionResponse>> { async fn try_exact_match(
query_name: &str,
tld: &str,
app_state: &AppState,
) -> Result<Option<DnsResolutionResponse>> {
let parts: Vec<&str> = query_name.split('.').collect(); let parts: Vec<&str> = query_name.split('.').collect();
if parts.len() < 2 { if parts.len() < 2 {
return Ok(None); return Ok(None);
@@ -632,16 +721,17 @@ async fn try_exact_match(query_name: &str, tld: &str, app_state: &AppState) -> R
.map_err(|_| GurtError::invalid_message("Database error"))?; .map_err(|_| GurtError::invalid_message("Database error"))?;
if !records.is_empty() { if !records.is_empty() {
let response_records: Vec<ResponseDnsRecord> = records.into_iter().map(|record| { let response_records: Vec<ResponseDnsRecord> = records
ResponseDnsRecord { .into_iter()
.map(|record| ResponseDnsRecord {
id: record.id.unwrap(), id: record.id.unwrap(),
record_type: record.record_type, record_type: record.record_type,
name: record.name, name: record.name,
value: record.value, value: record.value,
ttl: record.ttl, ttl: record.ttl,
priority: record.priority, priority: record.priority,
} })
}).collect(); .collect();
return Ok(Some(DnsResolutionResponse { return Ok(Some(DnsResolutionResponse {
name: query_name.to_string(), name: query_name.to_string(),
@@ -655,7 +745,11 @@ async fn try_exact_match(query_name: &str, tld: &str, app_state: &AppState) -> R
Ok(None) Ok(None)
} }
async fn try_delegation_match(query_name: &str, tld: &str, app_state: &AppState) -> Result<Option<DnsResolutionResponse>> { async fn try_delegation_match(
query_name: &str,
tld: &str,
app_state: &AppState,
) -> Result<Option<DnsResolutionResponse>> {
let parts: Vec<&str> = query_name.split('.').collect(); let parts: Vec<&str> = query_name.split('.').collect();
// Try to find NS records for parent domains // Try to find NS records for parent domains
@@ -697,9 +791,9 @@ async fn try_delegation_match(query_name: &str, tld: &str, app_state: &AppState)
// Get glue records for NS entries that point to subdomains of this zone // Get glue records for NS entries that point to subdomains of this zone
for ns_record in &all_records.clone() { for ns_record in &all_records.clone() {
let ns_host = &ns_record.value; let ns_host = &ns_record.value;
if ns_host.ends_with(&format!(".{}.{}", domain_name, tld)) || if ns_host.ends_with(&format!(".{}.{}", domain_name, tld))
ns_host == &format!("{}.{}", domain_name, tld) { || ns_host == &format!("{}.{}", domain_name, tld)
{
let glue_records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>( let glue_records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>(
"SELECT id, domain_id, record_type, name, value, ttl, priority, created_at FROM dns_records WHERE domain_id = $1 AND (record_type = 'A' OR record_type = 'AAAA') AND value = $2" "SELECT id, domain_id, record_type, name, value, ttl, priority, created_at FROM dns_records WHERE domain_id = $1 AND (record_type = 'A' OR record_type = 'AAAA') AND value = $2"
) )
@@ -713,16 +807,17 @@ async fn try_delegation_match(query_name: &str, tld: &str, app_state: &AppState)
} }
} }
let response_records: Vec<ResponseDnsRecord> = all_records.into_iter().map(|record| { let response_records: Vec<ResponseDnsRecord> = all_records
ResponseDnsRecord { .into_iter()
.map(|record| ResponseDnsRecord {
id: record.id.unwrap(), id: record.id.unwrap(),
record_type: record.record_type, record_type: record.record_type,
name: record.name, name: record.name,
value: record.value, value: record.value,
ttl: record.ttl, ttl: record.ttl,
priority: record.priority, priority: record.priority,
} })
}).collect(); .collect();
return Ok(Some(DnsResolutionResponse { return Ok(Some(DnsResolutionResponse {
name: query_name.to_string(), name: query_name.to_string(),
@@ -736,7 +831,10 @@ async fn try_delegation_match(query_name: &str, tld: &str, app_state: &AppState)
Ok(None) Ok(None)
} }
pub(crate) async fn resolve_full_domain(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> { pub(crate) async fn resolve_full_domain(
ctx: &ServerContext,
app_state: AppState,
) -> Result<GurtResponse> {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct FullDomainRequest { struct FullDomainRequest {
domain: String, domain: String,
@@ -763,7 +861,10 @@ pub(crate) async fn resolve_full_domain(ctx: &ServerContext, app_state: AppState
} }
// Certificate Authority endpoints // Certificate Authority endpoints
pub(crate) async fn verify_domain_ownership(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> { pub(crate) async fn verify_domain_ownership(
ctx: &ServerContext,
app_state: AppState,
) -> Result<GurtResponse> {
let path_parts: Vec<&str> = ctx.path().split('/').collect(); let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 3 { if path_parts.len() < 3 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format")); return Ok(GurtResponse::bad_request().with_string_body("Invalid path format"));
@@ -796,7 +897,10 @@ pub(crate) async fn verify_domain_ownership(ctx: &ServerContext, app_state: AppS
}))?) }))?)
} }
pub(crate) async fn request_certificate(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> { pub(crate) async fn request_certificate(
ctx: &ServerContext,
app_state: AppState,
) -> Result<GurtResponse> {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct CertRequest { struct CertRequest {
domain: String, domain: String,
@@ -824,7 +928,8 @@ pub(crate) async fn request_certificate(ctx: &ServerContext, app_state: AppState
.map_err(|_| GurtError::invalid_message("Database error"))?; .map_err(|_| GurtError::invalid_message("Database error"))?;
if domain_record.is_none() { if domain_record.is_none() {
return Ok(GurtResponse::bad_request().with_string_body("Domain does not exist or is not approved")); return Ok(GurtResponse::bad_request()
.with_string_body("Domain does not exist or is not approved"));
} }
let token = uuid::Uuid::new_v4().to_string(); let token = uuid::Uuid::new_v4().to_string();
@@ -853,7 +958,10 @@ pub(crate) async fn request_certificate(ctx: &ServerContext, app_state: AppState
Ok(GurtResponse::ok().with_json_body(&challenge)?) Ok(GurtResponse::ok().with_json_body(&challenge)?)
} }
pub(crate) async fn get_certificate(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> { pub(crate) async fn get_certificate(
ctx: &ServerContext,
app_state: AppState,
) -> Result<GurtResponse> {
let path_parts: Vec<&str> = ctx.path().split('/').collect(); let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 4 { if path_parts.len() < 4 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format")); return Ok(GurtResponse::bad_request().with_string_body("Invalid path format"));
@@ -871,12 +979,16 @@ pub(crate) async fn get_certificate(ctx: &ServerContext, app_state: AppState) ->
let (domain, _challenge_type, verification_data, csr_pem, expires_at) = match challenge { let (domain, _challenge_type, verification_data, csr_pem, expires_at) = match challenge {
Some(c) => c, Some(c) => c,
None => return Ok(GurtResponse::not_found().with_string_body("Challenge not found")) None => return Ok(GurtResponse::not_found().with_string_body("Challenge not found")),
}; };
let csr_pem = match csr_pem { let csr_pem = match csr_pem {
Some(csr) => csr, Some(csr) => csr,
None => return Ok(GurtResponse::bad_request().with_string_body("CSR not found for this challenge")) None => {
return Ok(
GurtResponse::bad_request().with_string_body("CSR not found for this challenge")
)
}
}; };
if chrono::Utc::now() > expires_at { if chrono::Utc::now() > expires_at {
@@ -904,7 +1016,11 @@ pub(crate) async fn get_certificate(ctx: &ServerContext, app_state: AppState) ->
let domain_record = match domain_record { let domain_record = match domain_record {
Some(d) => d, Some(d) => d,
None => return Ok(GurtResponse::bad_request().with_string_body("Domain not found or not approved")) None => {
return Ok(
GurtResponse::bad_request().with_string_body("Domain not found or not approved")
)
}
}; };
let txt_records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>( let txt_records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>(
@@ -918,10 +1034,12 @@ pub(crate) async fn get_certificate(ctx: &ServerContext, app_state: AppState) ->
.map_err(|_| GurtError::invalid_message("Database error"))?; .map_err(|_| GurtError::invalid_message("Database error"))?;
if txt_records.is_empty() { if txt_records.is_empty() {
return Ok(GurtResponse::new(gurt::GurtStatusCode::Accepted).with_string_body("Challenge not completed yet")); return Ok(GurtResponse::new(gurt::GurtStatusCode::Accepted)
.with_string_body("Challenge not completed yet"));
} }
let ca_cert = super::ca::get_or_create_ca(&app_state.db).await let ca_cert = super::ca::get_or_create_ca(&app_state.db)
.await
.map_err(|e| { .map_err(|e| {
log::error!("Failed to get CA certificate: {}", e); log::error!("Failed to get CA certificate: {}", e);
GurtError::invalid_message("CA certificate error") GurtError::invalid_message("CA certificate error")
@@ -931,8 +1049,9 @@ pub(crate) async fn get_certificate(ctx: &ServerContext, app_state: AppState) ->
&csr_pem, &csr_pem,
&ca_cert.ca_cert_pem, &ca_cert.ca_cert_pem,
&ca_cert.ca_key_pem, &ca_cert.ca_key_pem,
&domain &domain,
).map_err(|e| { )
.map_err(|e| {
log::error!("Failed to sign certificate: {}", e); log::error!("Failed to sign certificate: {}", e);
GurtError::invalid_message("Certificate signing failed") GurtError::invalid_message("Certificate signing failed")
})?; })?;
@@ -953,8 +1072,12 @@ pub(crate) async fn get_certificate(ctx: &ServerContext, app_state: AppState) ->
Ok(GurtResponse::ok().with_json_body(&certificate)?) Ok(GurtResponse::ok().with_json_body(&certificate)?)
} }
pub(crate) async fn get_ca_certificate(_ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> { pub(crate) async fn get_ca_certificate(
let ca_cert = super::ca::get_or_create_ca(&app_state.db).await _ctx: &ServerContext,
app_state: AppState,
) -> Result<GurtResponse> {
let ca_cert = super::ca::get_or_create_ca(&app_state.db)
.await
.map_err(|e| { .map_err(|e| {
log::error!("Failed to get CA certificate: {}", e); log::error!("Failed to get CA certificate: {}", e);
GurtError::invalid_message("CA certificate error") GurtError::invalid_message("CA certificate error")
@@ -962,14 +1085,22 @@ pub(crate) async fn get_ca_certificate(_ctx: &ServerContext, app_state: AppState
Ok(GurtResponse::ok() Ok(GurtResponse::ok()
.with_header("Content-Type", "application/x-pem-file") .with_header("Content-Type", "application/x-pem-file")
.with_header("Content-Disposition", "attachment; filename=\"gurted-ca.crt\"") .with_header(
"Content-Disposition",
"attachment; filename=\"gurted-ca.crt\"",
)
.with_string_body(ca_cert.ca_cert_pem)) .with_string_body(ca_cert.ca_cert_pem))
} }
fn generate_challenge_data(domain: &str, token: &str) -> Result<String> { fn generate_challenge_data(domain: &str, token: &str) -> Result<String> {
use sha2::{Sha256, Digest}; let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| GurtError::invalid_message("System time error"))?
.as_nanos();
let data = format!("{}:{}", domain, token); let entropy = uuid::Uuid::new_v4().to_string();
let data = format!("{}:{}:{}:{}", domain, token, timestamp, entropy);
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(data.as_bytes()); hasher.update(data.as_bytes());
let hash = hasher.finalize(); let hash = hasher.finalize();