From 0a38af1b66691c614f0e54d613daa2825a9d6d99 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:27:44 +0300 Subject: [PATCH 01/17] DNS server (add NS record) --- .gitignore | 3 +- dns/frontend/domain.html | 8 + dns/frontend/domain.lua | 106 +- dns/frontend/index.html | 48 +- dns/frontend/register.html | 5 +- dns/frontend/register.lua | 17 +- dns/migrations/002_remove_ip_requirement.sql | 7 + dns/migrations/003_add_ns_records.sql | 10 + dns/migrations/004_fix_record_types.sql | 8 + dns/src/discord_bot.rs | 173 ++- dns/src/gurt_server.rs | 8 +- dns/src/gurt_server/helpers.rs | 11 - dns/src/gurt_server/models.rs | 21 +- dns/src/gurt_server/routes.rs | 333 ++++- dns/src/main.rs | 1 - dns/src/secret.rs | 26 - docs/docs/dns-system.md | 253 +++- docs/docusaurus.config.ts | 6 +- docs/package-lock.json | 1196 +++++++++++++++++ docs/package.json | 1 + flumi/Scenes/Tags/select.tscn | 2 +- flumi/Scenes/main.tscn | 23 +- flumi/Scripts/GurtProtocol.gd | 283 +++- flumi/Scripts/Tags/p.gd | 2 +- .../gurt-protocol/bin/windows/gurt_godot.dll | Bin 4695552 -> 4697088 bytes .../gurt-protocol/bin/windows/~gurt_godot.dll | Bin 4695552 -> 4697088 bytes protocol/library/src/server.rs | 127 +- 27 files changed, 2313 insertions(+), 365 deletions(-) create mode 100644 dns/migrations/002_remove_ip_requirement.sql create mode 100644 dns/migrations/003_add_ns_records.sql create mode 100644 dns/migrations/004_fix_record_types.sql delete mode 100644 dns/src/secret.rs diff --git a/.gitignore b/.gitignore index 3e2ae94..92a146e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *target* -*.pem \ No newline at end of file +*.pem +*gurty.toml \ No newline at end of file diff --git a/dns/frontend/domain.html b/dns/frontend/domain.html index 5a422c7..1270cc2 100644 --- a/dns/frontend/domain.html +++ b/dns/frontend/domain.html @@ -116,6 +116,7 @@ +
Value:
+TTL:
diff --git a/dns/frontend/domain.lua b/dns/frontend/domain.lua index 3700a76..19c49ed 100644 --- a/dns/frontend/domain.lua +++ b/dns/frontend/domain.lua @@ -92,10 +92,16 @@ renderRecords = function(appendOnly) if #records == 0 then local emptyMessage = gurt.create('div', { text = 'No DNS records found. Add your first record below!', - style = 'text-center text-[#6b7280] py-8' + style = 'text-center text-[#6b7280] py-8', + id = '404' }) recordsList:append(emptyMessage) return + else + local err = gurt.select('#404') + if err then + gurt.select('#404'):remove() + end end -- Create header only if not appending or if list is empty @@ -222,6 +228,11 @@ end local function addRecord(type, name, value, ttl) hideError('record-error') print('Adding DNS record: ' .. type .. ' ' .. name .. ' ' .. value) + print('Network request details:') + print(' URL: gurt://localhost:8877/domain/' .. domainName .. '/records') + print(' Method: POST') + print(' Auth token: ' .. (authToken and 'present' or 'missing')) + print(' Domain name: ' .. (domainName or 'nil')) local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', { method = 'POST', @@ -237,30 +248,51 @@ local function addRecord(type, name, value, ttl) }) }) - if response:ok() then - print('DNS record added successfully') + print('Response received: ' .. tostring(response)) + + if response then + print('Response status: ' .. tostring(response.status)) + print('Response ok: ' .. tostring(response:ok())) - -- Clear form - gurt.select('#record-name').value = '' - gurt.select('#record-value').value = '' - gurt.select('#record-ttl').value = '3600' - - -- Add the new record to existing records array - local newRecord = response:json() - if newRecord and newRecord.id then - -- Server returned the created record, add it to our local array - table.insert(records, newRecord) - -- Render only the new record - renderRecords(true) + if response:ok() then + print('DNS record added successfully') + + -- Clear form + gurt.select('#record-name').value = '' + gurt.select('#record-value').value = '' + gurt.select('#record-ttl').value = '3600' + + -- Add the new record to existing records array + local newRecord = response:json() + if newRecord and newRecord.id then + -- Server returned the created record, add it to our local array + table.insert(records, newRecord) + + -- Check if we had no records before (showing empty message) + local wasEmpty = (#records == 1) -- If this is the first record + + if wasEmpty then + -- Full re-render to replace empty message with proper table + renderRecords(false) + else + -- Just append the new record to existing table + renderRecords(true) + end + else + -- Server didn't return record details, reload to get the actual data + loadRecords() + end else - -- Server didn't return record details, reload to get the actual data - loadRecords() + local error = response:text() + showError('record-error', 'Failed to add record: ' .. error) + print('Failed to add DNS record: ' .. error) end else - local error = response:text() - showError('record-error', 'Failed to add record: ' .. error) - print('Failed to add DNS record: ' .. error) + print('No response received from server') + showError('record-error', 'No response from server - connection failed') + print('Failed to add DNS record: No response') end + end local function logout() @@ -273,9 +305,42 @@ local function goBack() --gurt.location.goto("/dashboard.html") end +-- Function to update help text based on record type +local function updateHelpText() + local recordType = gurt.select('#record-type').value + + -- Hide all help texts + local helpTypes = {'A', 'AAAA', 'CNAME', 'TXT', 'NS'} + for _, helpType in ipairs(helpTypes) do + local helpElement = gurt.select('#help-' .. helpType) + if helpElement then + helpElement.classList:add('hidden') + end + end + + -- Show the relevant help text + local currentHelp = gurt.select('#help-' .. recordType) + if currentHelp then + currentHelp.classList:remove('hidden') + end + + -- Update placeholder text based on record type + local valueInput = gurt.select('#record-value') + if recordType == 'A' then + valueInput.placeholder = '192.168.1.1' + elseif recordType == 'AAAA' then + valueInput.placeholder = '2001:db8::1' + elseif recordType == 'CNAME' or recordType == 'NS' then + valueInput.placeholder = 'example.com' + elseif recordType == 'TXT' then + valueInput.placeholder = 'Any text content' + end +end + -- Event handlers gurt.select('#logout-btn'):on('click', logout) gurt.select('#back-btn'):on('click', goBack) +gurt.select('#record-type'):on('change', updateHelpText) gurt.select('#add-record-btn'):on('click', function() local recordType = gurt.select('#record-type').value @@ -297,4 +362,5 @@ end) -- Initialize print('Domain management page initialized') +updateHelpText() -- Set initial help text checkAuth() diff --git a/dns/frontend/index.html b/dns/frontend/index.html index 161e15e..49cf136 100644 --- a/dns/frontend/index.html +++ b/dns/frontend/index.html @@ -7,35 +7,33 @@ diff --git a/dns/frontend/register.html b/dns/frontend/register.html index 8ee9a88..95c035c 100644 --- a/dns/frontend/register.html +++ b/dns/frontend/register.html @@ -105,10 +105,7 @@Loading TLDs...
IP Address:
- -Note: After registration is approved, you can add DNS records (A, AAAA, CNAME, TXT) to configure where your domain points.
diff --git a/dns/frontend/register.lua b/dns/frontend/register.lua index 55f6448..9a6d90c 100644 --- a/dns/frontend/register.lua +++ b/dns/frontend/register.lua @@ -122,7 +122,7 @@ local function goToDashboard() gurt.location.goto("/dashboard.html") end -local function submitDomain(name, tld, ip) +local function submitDomain(name, tld) hideError('domain-error') print('Submitting domain: ' .. name .. '.' .. tld) @@ -132,7 +132,7 @@ local function submitDomain(name, tld, ip) ['Content-Type'] = 'application/json', Authorization = 'Bearer ' .. authToken }, - body = JSON.stringify({ name = name, tld = tld, ip = ip }) + body = JSON.stringify({ name = name, tld = tld }) }) if response:ok() then @@ -144,7 +144,6 @@ local function submitDomain(name, tld, ip) -- Clear form gurt.select('#domain-name').text = '' - gurt.select('#domain-ip').text = '' -- Redirect to dashboard gurt.location.goto('/dashboard.html') @@ -212,12 +211,10 @@ gurt.select('#dashboard-btn'):on('click', goToDashboard) gurt.select('#submit-domain-btn'):on('click', function() local name = gurt.select('#domain-name').value - local ip = gurt.select('#domain-ip').value local selectedTLD = gurt.select('.tld-selected') print('Submit domain button clicked') print('Input name:', name) - print('Input IP:', ip) print('Selected TLD element:', selectedTLD) if not name or name == '' then @@ -226,12 +223,6 @@ gurt.select('#submit-domain-btn'):on('click', function() return end - if not ip or ip == '' then - print('Validation failed: IP address is required') - showError('domain-error', 'IP address is required') - return - end - if not selectedTLD then print('Validation failed: No TLD selected') showError('domain-error', 'Please select a TLD') @@ -239,8 +230,8 @@ gurt.select('#submit-domain-btn'):on('click', function() end local tld = selectedTLD:getAttribute('data-tld') - print('Submitting domain with name:', name, 'tld:', tld, 'ip:', ip) - submitDomain(name, tld, ip) + print('Submitting domain with name:', name, 'tld:', tld) + submitDomain(name, tld) end) gurt.select('#create-invite-btn'):on('click', createInvite) diff --git a/dns/migrations/002_remove_ip_requirement.sql b/dns/migrations/002_remove_ip_requirement.sql new file mode 100644 index 0000000..cb83098 --- /dev/null +++ b/dns/migrations/002_remove_ip_requirement.sql @@ -0,0 +1,7 @@ +-- Make IP column optional for domains +ALTER TABLE domains ALTER COLUMN ip DROP NOT NULL; + +-- Update DNS records constraint to only allow A, AAAA, CNAME, TXT +ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check; +ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check + CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT')); \ No newline at end of file diff --git a/dns/migrations/003_add_ns_records.sql b/dns/migrations/003_add_ns_records.sql new file mode 100644 index 0000000..fce2c17 --- /dev/null +++ b/dns/migrations/003_add_ns_records.sql @@ -0,0 +1,10 @@ +-- Re-add NS record support and extend record types +ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check; +ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check + CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT', 'NS', 'MX')); + +-- Add index for efficient NS record lookups during delegation +CREATE INDEX IF NOT EXISTS idx_dns_records_ns_lookup ON dns_records(record_type, name) WHERE record_type = 'NS'; + +-- Add index for subdomain resolution optimization +CREATE INDEX IF NOT EXISTS idx_dns_records_subdomain_lookup ON dns_records(domain_id, name, record_type); \ No newline at end of file diff --git a/dns/migrations/004_fix_record_types.sql b/dns/migrations/004_fix_record_types.sql new file mode 100644 index 0000000..55b863d --- /dev/null +++ b/dns/migrations/004_fix_record_types.sql @@ -0,0 +1,8 @@ +-- Fix record types to remove MX and ensure NS is supported +ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check; +ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check + CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT', 'NS')); + +-- Add indexes for efficient DNS lookups if they don't exist +CREATE INDEX IF NOT EXISTS idx_dns_records_ns_lookup ON dns_records(record_type, name) WHERE record_type = 'NS'; +CREATE INDEX IF NOT EXISTS idx_dns_records_subdomain_lookup ON dns_records(domain_id, name, record_type); \ No newline at end of file diff --git a/dns/src/discord_bot.rs b/dns/src/discord_bot.rs index 06e2454..086b75f 100644 --- a/dns/src/discord_bot.rs +++ b/dns/src/discord_bot.rs @@ -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", ®istration.ip, true) .field("User", ®istration.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) diff --git a/dns/src/gurt_server.rs b/dns/src/gurt_server.rs index ae1635c..165324a 100644 --- a/dns/src/gurt_server.rs +++ b/dns/src/gurt_server.rs @@ -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| { diff --git a/dns/src/gurt_server/helpers.rs b/dns/src/gurt_server/helpers.rs index 908fd5c..c84f12d 100644 --- a/dns/src/gurt_server/helpers.rs +++ b/dns/src/gurt_server/helpers.rs @@ -1,14 +1,3 @@ -use gurt::prelude::*; - -use std::net::IpAddr; - -pub fn validate_ip(domain: &super::models::Domain) -> Result<()> { - if domain.ip.parse::