DNS server (add NS record)

This commit is contained in:
Face
2025-08-21 12:27:44 +03:00
parent 48820d48b5
commit 0a38af1b66
27 changed files with 2313 additions and 365 deletions

View File

@@ -2,16 +2,11 @@ use serenity::async_trait;
use serenity::all::*;
use sqlx::PgPool;
pub struct DiscordBot {
pub pool: PgPool,
}
#[derive(Debug)]
pub struct DomainRegistration {
pub id: i32,
pub domain_name: String,
pub tld: String,
pub ip: String,
pub user_id: i32,
pub username: String,
}
@@ -40,31 +35,61 @@ impl EventHandler for BotHandler {
}
};
// Update domain status to approved
match sqlx::query("UPDATE domains SET status = 'approved' WHERE id = $1")
.bind(domain_id)
.execute(&self.pool)
.await
{
Ok(_) => {
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("✅ Domain approved!")
.ephemeral(true)
);
if let Err(e) = component.create_response(&ctx.http, response).await {
log::error!("Error responding to interaction: {}", e);
// Get domain info for the updated embed
let domain: Option<(String, String, String)> = sqlx::query_as(
"SELECT d.name, d.tld, u.username FROM domains d JOIN users u ON d.user_id = u.id WHERE d.id = $1"
)
.bind(domain_id)
.fetch_optional(&self.pool)
.await
.unwrap_or(None);
if let Some((name, tld, username)) = domain {
// Update domain status to approved
match sqlx::query("UPDATE domains SET status = 'approved' WHERE id = $1")
.bind(domain_id)
.execute(&self.pool)
.await
{
Ok(_) => {
// First, send ephemeral confirmation
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("✅ Domain approved!")
.ephemeral(true)
);
if let Err(e) = component.create_response(&ctx.http, response).await {
log::error!("Error responding to interaction: {}", e);
return;
}
// Then edit the original message with green color and no buttons
let updated_embed = CreateEmbed::new()
.title("✅ Domain Registration - APPROVED")
.field("Domain", format!("{}.{}", name, tld), true)
.field("User", username, true)
.field("Status", "Approved", true)
.color(0x00ff00); // Green color
let edit_message = EditMessage::new()
.embed(updated_embed)
.components(vec![]); // Remove buttons
let mut message = component.message.clone();
if let Err(e) = message.edit(&ctx.http, edit_message).await {
log::error!("Error updating original message: {}", e);
}
}
Err(e) => {
log::error!("Error approving domain: {}", e);
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("❌ Error approving domain")
.ephemeral(true)
);
let _ = component.create_response(&ctx.http, response).await;
}
}
Err(e) => {
log::error!("Error approving domain: {}", e);
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("❌ Error approving domain")
.ephemeral(true)
);
let _ = component.create_response(&ctx.http, response).await;
}
}
} else if custom_id.starts_with("deny_") {
@@ -116,32 +141,66 @@ impl EventHandler for BotHandler {
})
.unwrap_or("No reason provided");
// Update domain status to denied with reason
match sqlx::query("UPDATE domains SET status = 'denied', denial_reason = $1 WHERE id = $2")
.bind(reason)
.bind(domain_id)
.execute(&self.pool)
.await
{
Ok(_) => {
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("❌ Domain denied!")
.ephemeral(true)
);
if let Err(e) = modal_submit.create_response(&ctx.http, response).await {
log::error!("Error responding to modal: {}", e);
// Get domain info for the updated embed
let domain: Option<(String, String, String)> = sqlx::query_as(
"SELECT d.name, d.tld, u.username FROM domains d JOIN users u ON d.user_id = u.id WHERE d.id = $1"
)
.bind(domain_id)
.fetch_optional(&self.pool)
.await
.unwrap_or(None);
if let Some((name, tld, username)) = domain {
// Update domain status to denied with reason
match sqlx::query("UPDATE domains SET status = 'denied', denial_reason = $1 WHERE id = $2")
.bind(reason)
.bind(domain_id)
.execute(&self.pool)
.await
{
Ok(_) => {
// First, send ephemeral confirmation
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("❌ Domain denied!")
.ephemeral(true)
);
if let Err(e) = modal_submit.create_response(&ctx.http, response).await {
log::error!("Error responding to modal: {}", e);
return;
}
// Then edit the original message with red color and no buttons
let updated_embed = CreateEmbed::new()
.title("❌ Domain Registration - DENIED")
.field("Domain", format!("{}.{}", name, tld), true)
.field("User", username, true)
.field("Status", "Denied", true)
.field("Reason", reason, false)
.color(0xff0000); // Red color
let edit_message = EditMessage::new()
.embed(updated_embed)
.components(vec![]); // Remove buttons
if let Some(mut message) = modal_submit.message.clone() {
if let Err(e) = message.edit(&ctx.http, edit_message).await {
log::error!("Error updating original message: {}", e);
}
} else {
log::error!("Original message not found for editing");
}
}
Err(e) => {
log::error!("Error denying domain: {}", e);
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("❌ Error denying domain")
.ephemeral(true)
);
let _ = modal_submit.create_response(&ctx.http, response).await;
}
}
Err(e) => {
log::error!("Error denying domain: {}", e);
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("❌ Error denying domain")
.ephemeral(true)
);
let _ = modal_submit.create_response(&ctx.http, response).await;
}
}
}
@@ -162,12 +221,12 @@ pub async fn send_domain_approval_request(
let http = serenity::http::Http::new(bot_token);
let embed = CreateEmbed::new()
.title("New Domain Registration")
.title("Domain request")
.field("Domain", format!("{}.{}", registration.domain_name, registration.tld), true)
.field("IP", &registration.ip, true)
.field("User", &registration.username, true)
.field("User ID", registration.user_id.to_string(), true)
.color(0x00ff00);
.field("Status", "Pending Review", true)
.color(0x808080); // Gray color for pending
let approve_button = CreateButton::new(format!("approve_{}", registration.id))
.style(ButtonStyle::Success)

View File

@@ -95,6 +95,8 @@ enum HandlerType {
DeleteDomain,
GetUserDomains,
CreateDomainRecord,
ResolveDomain,
ResolveFullDomain,
}
impl GurtHandler for AppHandler {
@@ -163,6 +165,8 @@ impl GurtHandler for AppHandler {
handle_authenticated!(ctx, app_state, routes::delete_domain)
}
},
HandlerType::ResolveDomain => routes::resolve_domain(&ctx, app_state).await,
HandlerType::ResolveFullDomain => routes::resolve_full_domain(&ctx, app_state).await,
};
let duration = start_time.elapsed();
@@ -231,7 +235,9 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
.route(Route::get("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetDomain })
.route(Route::post("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::CreateDomainRecord })
.route(Route::put("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::UpdateDomain })
.route(Route::delete("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::DeleteDomain });
.route(Route::delete("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::DeleteDomain })
.route(Route::post("/resolve"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::ResolveDomain })
.route(Route::post("/resolve-full"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::ResolveFullDomain });
log::info!("GURT server listening on {}", config.get_address());
server.listen(&config.get_address()).await.map_err(|e| {

View File

@@ -1,14 +1,3 @@
use gurt::prelude::*;
use std::net::IpAddr;
pub fn validate_ip(domain: &super::models::Domain) -> Result<()> {
if domain.ip.parse::<IpAddr>().is_err() {
return Err(GurtError::invalid_message("Invalid IP address"));
}
Ok(())
}
pub fn deserialize_lowercase<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
where

View File

@@ -6,11 +6,12 @@ use sqlx::{FromRow, types::chrono::{DateTime, Utc}};
pub struct Domain {
#[serde(skip_deserializing)]
pub(crate) id: Option<i32>,
pub(crate) ip: String,
#[serde(deserialize_with = "deserialize_lowercase")]
pub(crate) tld: String,
#[serde(deserialize_with = "deserialize_lowercase")]
pub(crate) name: String,
#[serde(deserialize_with = "deserialize_lowercase")]
pub(crate) tld: String,
#[serde(skip_deserializing)]
pub(crate) ip: Option<String>,
#[serde(skip_deserializing)]
pub(crate) user_id: Option<i32>,
#[serde(skip_deserializing)]
@@ -70,7 +71,6 @@ pub struct DomainInviteCode {
#[derive(Debug, Serialize)]
pub(crate) struct ResponseDomain {
pub(crate) tld: String,
pub(crate) ip: String,
pub(crate) name: String,
pub(crate) records: Option<Vec<ResponseDnsRecord>>,
}
@@ -87,8 +87,16 @@ pub(crate) struct ResponseDnsRecord {
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct UpdateDomain {
pub(crate) ip: String,
pub(crate) struct DnsResolutionRequest {
pub(crate) name: String,
pub(crate) tld: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct DnsResolutionResponse {
pub(crate) name: String,
pub(crate) tld: String,
pub(crate) records: Vec<ResponseDnsRecord>,
}
#[derive(Serialize)]
@@ -108,7 +116,6 @@ pub(crate) struct DomainList {
pub(crate) struct UserDomain {
pub(crate) name: String,
pub(crate) tld: String,
pub(crate) ip: String,
pub(crate) status: String,
pub(crate) denial_reason: Option<String>,
}

View File

@@ -1,5 +1,6 @@
use super::{models::*, AppState, helpers::validate_ip};
use super::{models::*, AppState};
use crate::auth::Claims;
use crate::discord_bot::{send_domain_approval_request, DomainRegistration};
use gurt::prelude::*;
use std::{env, collections::HashMap};
@@ -23,8 +24,6 @@ pub(crate) async fn index(_app_state: AppState) -> Result<GurtResponse> {
}
pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -> Result<Domain> {
validate_ip(&domain)?;
if !app.config.tld_list().contains(&domain.tld.as_str())
|| !domain.name.chars().all(|c| c.is_alphabetic() || c == '-')
|| domain.name.len() > 24
@@ -51,17 +50,26 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
return Err(GurtError::invalid_message("Domain already exists"));
}
sqlx::query(
"INSERT INTO domains (name, tld, ip, user_id, status) VALUES ($1, $2, $3, $4, 'pending')"
let user: (String,) = sqlx::query_as("SELECT username FROM users WHERE id = $1")
.bind(user_id)
.fetch_one(&app.db)
.await
.map_err(|_| GurtError::invalid_message("User not found"))?;
let username = user.0;
let domain_row: (i32,) = sqlx::query_as(
"INSERT INTO domains (name, tld, user_id, status) VALUES ($1, $2, $3, 'pending') RETURNING id"
)
.bind(&domain.name)
.bind(&domain.tld)
.bind(&domain.ip)
.bind(user_id)
.execute(&app.db)
.fetch_one(&app.db)
.await
.map_err(|_| GurtError::invalid_message("Failed to create domain"))?;
let domain_id = domain_row.0;
// Decrease user's registrations remaining
sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1")
.bind(user_id)
@@ -69,6 +77,29 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
.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 {
let domain_registration = DomainRegistration {
id: domain_id,
domain_name: domain.name.clone(),
tld: domain.tld.clone(),
user_id,
username: username.clone(),
};
let channel_id = app.config.discord.channel_id;
let bot_token = app.config.discord.bot_token.clone();
tokio::spawn(async move {
if let Err(e) = send_domain_approval_request(
channel_id,
domain_registration,
&bot_token,
).await {
log::error!("Failed to send Discord notification: {}", e);
}
});
}
Ok(domain)
}
@@ -177,7 +208,6 @@ pub(crate) async fn get_domains(ctx: &ServerContext, app_state: AppState) -> Res
ResponseDomain {
name: domain.name,
tld: domain.tld,
ip: domain.ip,
records: None,
}
}).collect();
@@ -226,56 +256,8 @@ pub(crate) async fn check_domain(ctx: &ServerContext, app_state: AppState) -> Re
Ok(GurtResponse::ok().with_json_body(&domain_list)?)
}
pub(crate) async fn update_domain(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 4 {
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{name}/{tld}"));
}
let name = path_parts[2];
let tld = path_parts[3];
let update_data: UpdateDomain = serde_json::from_slice(ctx.body())
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
// Verify user owns this domain
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND user_id = $3"
)
.bind(name)
.bind(tld)
.bind(claims.user_id)
.fetch_optional(&app_state.db)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
let domain = match domain {
Some(d) => d,
None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied"))
};
// Validate IP
validate_ip(&Domain {
id: domain.id,
name: domain.name.clone(),
tld: domain.tld.clone(),
ip: update_data.ip.clone(),
user_id: domain.user_id,
status: domain.status,
denial_reason: domain.denial_reason,
created_at: domain.created_at,
})?;
sqlx::query("UPDATE domains SET ip = $1 WHERE name = $2 AND tld = $3 AND user_id = $4")
.bind(&update_data.ip)
.bind(name)
.bind(tld)
.bind(claims.user_id)
.execute(&app_state.db)
.await
.map_err(|_| GurtError::invalid_message("Failed to update domain"))?;
Ok(GurtResponse::ok().with_string_body("Domain updated successfully"))
pub(crate) async fn update_domain(_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> {
@@ -349,7 +331,6 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
UserDomain {
name: domain.name,
tld: domain.tld,
ip: domain.ip,
status: domain.status.unwrap_or_else(|| "pending".to_string()),
denial_reason: domain.denial_reason,
}
@@ -464,13 +445,37 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
return Ok(GurtResponse::bad_request().with_string_body("Record type is required"));
}
let valid_types = ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV"];
let valid_types = ["A", "AAAA", "CNAME", "TXT", "NS"];
if !valid_types.contains(&record_data.record_type.as_str()) {
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type"));
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type. Only A, AAAA, CNAME, TXT, and NS records are supported."));
}
let record_name = record_data.name.unwrap_or_else(|| "@".to_string());
let ttl = record_data.ttl.unwrap_or(3600);
match record_data.record_type.as_str() {
"A" => {
if !record_data.value.parse::<std::net::Ipv4Addr>().is_ok() {
return Ok(GurtResponse::bad_request().with_string_body("Invalid IPv4 address for A record"));
}
},
"AAAA" => {
if !record_data.value.parse::<std::net::Ipv6Addr>().is_ok() {
return Ok(GurtResponse::bad_request().with_string_body("Invalid IPv6 address for AAAA record"));
}
},
"CNAME" | "NS" => {
if record_data.value.is_empty() || !record_data.value.contains('.') {
return Ok(GurtResponse::bad_request().with_string_body("CNAME and NS records must contain a valid domain name"));
}
},
"TXT" => {
// TXT records can contain any text
},
_ => {
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type"));
}
}
let record_id: (i32,) = sqlx::query_as(
"INSERT INTO dns_records (domain_id, record_type, name, value, ttl, priority) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id"
@@ -550,6 +555,212 @@ pub(crate) async fn delete_domain_record(ctx: &ServerContext, app_state: AppStat
Ok(GurtResponse::ok().with_string_body("DNS record deleted successfully"))
}
pub(crate) async fn resolve_domain(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> {
let resolution_request: DnsResolutionRequest = serde_json::from_slice(ctx.body())
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
let full_domain = format!("{}.{}", resolution_request.name, resolution_request.tld);
// Try to resolve with enhanced subdomain and delegation support
match resolve_dns_with_delegation(&full_domain, &app_state).await {
Ok(response) => Ok(GurtResponse::ok().with_json_body(&response)?),
Err(_) => Ok(GurtResponse::not_found().with_json_body(&Error {
msg: "Domain not found",
error: "Domain not found, not approved, or delegation failed".into(),
})?),
}
}
async fn resolve_dns_with_delegation(query_name: &str, app_state: &AppState) -> Result<DnsResolutionResponse> {
// Parse the query domain
let parts: Vec<&str> = query_name.split('.').collect();
if parts.len() < 2 {
return Err(GurtError::invalid_message("Invalid domain format"));
}
let tld = parts.last().unwrap();
// Try to find exact match first
if let Some(response) = try_exact_match(query_name, tld, app_state).await? {
return Ok(response);
}
// Try to find delegation by checking parent domains
if let Some(response) = try_delegation_match(query_name, tld, app_state).await? {
return Ok(response);
}
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>> {
let parts: Vec<&str> = query_name.split('.').collect();
if parts.len() < 2 {
return Ok(None);
}
// For a query like "api.blog.example.com", try different combinations
for i in (1..parts.len()).rev() {
let domain_name = parts[parts.len() - i - 1];
let subdomain_parts = &parts[0..parts.len() - i - 1];
let subdomain = if subdomain_parts.is_empty() {
"@".to_string()
} else {
subdomain_parts.join(".")
};
// Look for the domain in our database
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
)
.bind(domain_name)
.bind(tld)
.fetch_optional(&app_state.db)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
if let Some(domain) = domain {
// Look for specific records for this subdomain
let 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 name = $2 ORDER BY created_at ASC"
)
.bind(domain.id.unwrap())
.bind(&subdomain)
.fetch_all(&app_state.db)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
if !records.is_empty() {
let response_records: Vec<ResponseDnsRecord> = records.into_iter().map(|record| {
ResponseDnsRecord {
id: record.id.unwrap(),
record_type: record.record_type,
name: record.name,
value: record.value,
ttl: record.ttl.unwrap_or(3600),
priority: record.priority,
}
}).collect();
return Ok(Some(DnsResolutionResponse {
name: query_name.to_string(),
tld: tld.to_string(),
records: response_records,
}));
}
}
}
Ok(None)
}
async fn try_delegation_match(query_name: &str, tld: &str, app_state: &AppState) -> Result<Option<DnsResolutionResponse>> {
let parts: Vec<&str> = query_name.split('.').collect();
// Try to find NS records for parent domains
for i in (1..parts.len()).rev() {
let domain_name = parts[parts.len() - i - 1];
let subdomain_parts = &parts[0..parts.len() - i - 1];
let subdomain = if subdomain_parts.is_empty() {
"@".to_string()
} else {
subdomain_parts.join(".")
};
// Look for the domain
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
)
.bind(domain_name)
.bind(tld)
.fetch_optional(&app_state.db)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
if let Some(domain) = domain {
// Look for NS records that match this subdomain or parent
let ns_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 = 'NS' AND (name = $2 OR name = $3) ORDER BY created_at ASC"
)
.bind(domain.id.unwrap())
.bind(&subdomain)
.bind("@")
.fetch_all(&app_state.db)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
if !ns_records.is_empty() {
// Also look for glue records (A/AAAA records for the NS hosts)
let mut all_records = ns_records;
// Get glue records for NS entries that point to subdomains of this zone
for ns_record in &all_records.clone() {
let ns_host = &ns_record.value;
if ns_host.ends_with(&format!(".{}.{}", domain_name, tld)) ||
ns_host == &format!("{}.{}", domain_name, tld) {
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"
)
.bind(domain.id.unwrap())
.bind(ns_host)
.fetch_all(&app_state.db)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
all_records.extend(glue_records);
}
}
let response_records: Vec<ResponseDnsRecord> = all_records.into_iter().map(|record| {
ResponseDnsRecord {
id: record.id.unwrap(),
record_type: record.record_type,
name: record.name,
value: record.value,
ttl: record.ttl.unwrap_or(3600),
priority: record.priority,
}
}).collect();
return Ok(Some(DnsResolutionResponse {
name: query_name.to_string(),
tld: tld.to_string(),
records: response_records,
}));
}
}
}
Ok(None)
}
pub(crate) async fn resolve_full_domain(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> {
#[derive(serde::Deserialize)]
struct FullDomainRequest {
domain: String,
record_type: Option<String>,
}
let request: FullDomainRequest = serde_json::from_slice(ctx.body())
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
// Try to resolve with enhanced subdomain and delegation support
match resolve_dns_with_delegation(&request.domain, &app_state).await {
Ok(mut response) => {
// Filter by record type if specified
if let Some(record_type) = request.record_type {
response.records.retain(|r| r.record_type == record_type);
}
Ok(GurtResponse::ok().with_json_body(&response)?)
}
Err(_) => Ok(GurtResponse::not_found().with_json_body(&Error {
msg: "Domain not found",
error: "Domain not found, not approved, or delegation failed".into(),
})?),
}
}
#[derive(serde::Serialize)]
struct Error {
msg: &'static str,

View File

@@ -1,6 +1,5 @@
mod config;
mod gurt_server;
mod secret;
mod auth;
mod discord_bot;

View File

@@ -1,26 +0,0 @@
use rand::{rngs::StdRng, Rng, SeedableRng};
pub fn generate(size: usize) -> String {
const ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let alphabet_len = ALPHABET.len();
let mask = alphabet_len.next_power_of_two() - 1;
let step = 8 * size / 5;
let mut id = String::with_capacity(size);
let mut rng = StdRng::from_entropy();
while id.len() < size {
let bytes: Vec<u8> = (0..step).map(|_| rng.gen::<u8>()).collect::<Vec<u8>>();
id.extend(
bytes
.iter()
.map(|&byte| (byte as usize) & mask)
.filter_map(|index| ALPHABET.get(index).copied())
.take(size - id.len())
.map(char::from),
);
}
id
}