DNS server (add NS record)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
*target*
|
||||
*.pem
|
||||
*.pem
|
||||
*gurty.toml
|
||||
@@ -116,6 +116,7 @@
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="NS">NS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="form-group">
|
||||
@@ -125,6 +126,13 @@
|
||||
<div style="form-group">
|
||||
<p>Value:</p>
|
||||
<input id="record-value" type="text" style="form-input" placeholder="192.168.1.1, example.com" />
|
||||
<div id="record-help" style="text-[#6b7280] text-sm mt-1">
|
||||
<span id="help-A">Enter an IPv4 address (e.g., 192.168.1.1)</span>
|
||||
<span id="help-AAAA" style="hidden">Enter an IPv6 address (e.g., 2001:db8::1)</span>
|
||||
<span id="help-CNAME" style="hidden">Enter a domain name (e.g., example.com)</span>
|
||||
<span id="help-TXT" style="hidden">Enter any text content</span>
|
||||
<span id="help-NS" style="hidden">Enter a nameserver domain (e.g., ns1.example.com)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="form-group">
|
||||
<p>TTL:</p>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -7,35 +7,33 @@
|
||||
<font name="roboto" src="https://fonts.gstatic.com/s/roboto/v48/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2" />
|
||||
|
||||
<style>
|
||||
body { bg-[#2a2a2a] text-[#ffffff] font-roboto flex items-center justify-center p-4 }
|
||||
.login-card { p-8 }
|
||||
h1 { text-3xl font-bold text-center mb-6 text-[#ffffff] }
|
||||
body {
|
||||
bg-[#171616] font-sans text-white
|
||||
}
|
||||
|
||||
.login-card {
|
||||
bg-[#262626] p-8 rounded-lg shadow-lg max-w-md mx-auto my-auto h-full
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-3xl font-bold text-center mb-6
|
||||
}
|
||||
|
||||
input {
|
||||
bg-[#3b3b3b]
|
||||
border-none
|
||||
rounded-md
|
||||
p-3
|
||||
w-full
|
||||
text-[#ffffff]
|
||||
placeholder:text-[#999999]
|
||||
outline-none
|
||||
focus:ring-2
|
||||
focus:ring-[#5b5b5b]
|
||||
mb-4
|
||||
w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white mb-4 placeholder:text-[#999999] outline-none active:border-red-500
|
||||
}
|
||||
|
||||
button {
|
||||
bg-[#4ade80]
|
||||
text-[#1b1b1b]
|
||||
font-bold
|
||||
p-3
|
||||
rounded-md
|
||||
w-full
|
||||
hover:bg-[#22c55e]
|
||||
active:bg-[#15803d]
|
||||
cursor-pointer
|
||||
bg-[#dc2626] text-white font-medium p-3 rounded-lg w-full cursor-pointer transition-colors hover:bg-[#b91c1c] active:bg-[#991b1b]
|
||||
}
|
||||
|
||||
a {
|
||||
text-[#ef4444] hover:text-[#dc2626] cursor-pointer
|
||||
}
|
||||
|
||||
#log-output {
|
||||
text-[#fca5a5] p-4 rounded-md mt-4 font-mono max-h-40
|
||||
}
|
||||
a { text-[#4ade80] hover:text-[#22c55e] cursor-pointer }
|
||||
#log-output { text-white p-4 rounded-md mt-4 font-mono max-h-40 }
|
||||
</style>
|
||||
|
||||
<script src="script.lua" />
|
||||
|
||||
@@ -105,10 +105,7 @@
|
||||
<p id="tld-loading">Loading TLDs...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="form-group">
|
||||
<p>IP Address:</p>
|
||||
<input id="domain-ip" type="text" style="form-input" placeholder="192.168.1.100" />
|
||||
</div>
|
||||
<p style="text-[#6b7280] text-sm mb-4">Note: After registration is approved, you can add DNS records (A, AAAA, CNAME, TXT) to configure where your domain points.</p>
|
||||
<div id="domain-error" style="error-text hidden mb-2"></div>
|
||||
<button id="submit-domain-btn" style="success-btn">Submit for Approval</button>
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
|
||||
7
dns/migrations/002_remove_ip_requirement.sql
Normal file
7
dns/migrations/002_remove_ip_requirement.sql
Normal file
@@ -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'));
|
||||
10
dns/migrations/003_add_ns_records.sql
Normal file
10
dns/migrations/003_add_ns_records.sql
Normal file
@@ -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);
|
||||
8
dns/migrations/004_fix_record_types.sql
Normal file
8
dns/migrations/004_fix_record_types.sql
Normal file
@@ -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);
|
||||
@@ -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)
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
mod config;
|
||||
mod gurt_server;
|
||||
mod secret;
|
||||
mod auth;
|
||||
mod discord_bot;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,12 +2,255 @@
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# DNS
|
||||
# DNS System
|
||||
|
||||
The Gurted ecosystem features a custom DNS system that enables domain resolution for the gurt:// protocol. Unlike traditional DNS, Gurted DNS is designed specifically for the decentralized web ecosystem, providing:
|
||||
|
||||
- domain registration
|
||||
- approval workflows (to ensure a free-of-charge, spam-free experience)
|
||||
- archivable domain records
|
||||
- Domain registration with approval workflows
|
||||
- DNS record management (A, AAAA, CNAME, TXT, NS)
|
||||
- Subdomain support with delegation
|
||||
- CNAME chain resolution
|
||||
- Nameserver delegation with glue records
|
||||
|
||||
TODO: complete
|
||||
DNS queries in Gurted are transmitted using the GURT protocol, which, similar to DNS over HTTPS (DoH), encrypts DNS resolution under TLS. This ensures that your DNS requests are private and secure, making it impossible for your internet provider to see which sites you visit unless they map the IP.
|
||||
|
||||
## Record Types
|
||||
|
||||
### A Records
|
||||
- **Purpose**: Map domain names to IPv4 addresses
|
||||
- **Format**: `example.web → 192.168.1.1`
|
||||
|
||||
### AAAA Records
|
||||
- **Purpose**: Map domain names to IPv6 addresses
|
||||
- **Format**: `example.web → 2001:db8::1`
|
||||
|
||||
### CNAME Records
|
||||
- **Purpose**: Create aliases that point to other domain names
|
||||
- **Format**: `www.example.web → example.web`
|
||||
- **Chain Resolution**: Supports up to 5 levels of CNAME chaining
|
||||
|
||||
### TXT Records
|
||||
- **Purpose**: Store arbitrary text data
|
||||
- **Format**: `example.web → "v=spf1 include:_spf.example.web ~all"`
|
||||
|
||||
### NS Records
|
||||
- **Purpose**: Delegate subdomains to other nameservers
|
||||
- **Format**: `subdomain.example.web → ns1.provider.web`
|
||||
- **Glue Records**: Automatically handled for in-zone nameservers
|
||||
|
||||
## DNS Resolution Flow
|
||||
|
||||
### 1. Basic Domain Resolution
|
||||
|
||||
Flumi follows a straightforward resolution process for domains like `example.web`:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Browser requests example.web] --> B[Parse domain]
|
||||
B --> C[Check DNS cache]
|
||||
C --> D{Cache hit?}
|
||||
D -->|Yes| E[Return cached IP]
|
||||
D -->|No| F[Query DNS server]
|
||||
F --> G[Return A record]
|
||||
G --> H[Cache result]
|
||||
H --> I[Connect to IP]
|
||||
```
|
||||
|
||||
### 2. Subdomain Resolution
|
||||
|
||||
For queries like `api.blog.example.web`:
|
||||
|
||||
1. **Exact Match Check**: Look for specific records for `api.blog.example.web`
|
||||
2. **Parent Domain Check**: If not found, check parent domains (`blog.example.web`, then `example.web`)
|
||||
3. **Delegation Check**: Look for NS records that might delegate the subdomain
|
||||
|
||||
### 3. CNAME Chain Resolution
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Query www.example.web] --> B{Record type?}
|
||||
B -->|A/AAAA| C[Return IP directly]
|
||||
B -->|CNAME| D[Follow CNAME target]
|
||||
D --> E[Query target domain]
|
||||
E --> F{Target type?}
|
||||
F -->|A/AAAA| G[Return final IP]
|
||||
F -->|CNAME| H[Continue chain]
|
||||
H --> I{Max depth reached?}
|
||||
I -->|No| E
|
||||
I -->|Yes| J[Error: Chain too deep]
|
||||
```
|
||||
|
||||
### 4. NS Delegation
|
||||
|
||||
When a domain has NS records:
|
||||
|
||||
1. **Delegation Response**: Return NS records and any glue records
|
||||
2. **Glue Records**: Include A/AAAA records for nameservers within the same zone
|
||||
3. **Client Resolution**: Client queries the delegated nameservers directly
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Domain Management
|
||||
|
||||
- `POST /domain` - Register a new domain
|
||||
- `GET /domain/{name}.{tld}` - Get domain details
|
||||
- `DELETE /domain/{name}/{tld}` - Delete domain
|
||||
|
||||
### DNS Records
|
||||
|
||||
- `GET /domain/{name}.{tld}/records` - List all records for domain
|
||||
- `POST /domain/{name}.{tld}/records` - Create new DNS record
|
||||
- `DELETE /domain/{name}.{tld}/records/{id}` - Delete DNS record
|
||||
|
||||
### DNS Resolution
|
||||
|
||||
- `POST /resolve` - Legacy resolution for simple domains
|
||||
- `POST /resolve-full` - Advanced resolution with subdomain support
|
||||
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"domain": "api.blog.example.web",
|
||||
"record_type": "A" // optional filter
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "api.blog.example.web",
|
||||
"tld": "web",
|
||||
"records": [
|
||||
{
|
||||
"id": 123,
|
||||
"type": "A",
|
||||
"name": "api.blog",
|
||||
"value": "192.168.1.100",
|
||||
"ttl": 3600,
|
||||
"priority": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### DNS Resolution Errors
|
||||
|
||||
- `ERR_NAME_NOT_RESOLVED` - Domain not found or not approved
|
||||
- `ERR_CONNECTION_TIMED_OUT` - DNS server timeout
|
||||
- `ERR_CONNECTION_REFUSED` - Cannot connect to target server
|
||||
- `ERR_INVALID_URL` - Malformed domain name
|
||||
|
||||
### CNAME Specific Errors
|
||||
|
||||
- `CNAME chain too deep` - More than 5 CNAME redirections
|
||||
- `CNAME chain ended without A record` - Chain leads to non-resolvable target
|
||||
- `Failed to resolve CNAME target` - Intermediate domain resolution failure
|
||||
|
||||
### NS Delegation Errors
|
||||
|
||||
- `Domain is delegated to nameservers` - Cannot resolve directly, requires external resolution
|
||||
- `Only IPv6 (AAAA) records found` - IPv4 required for GURT protocol
|
||||
|
||||
## Tutorial: Setting Up NS Record Delegation
|
||||
|
||||
This tutorial walks you through setting up nameserver delegation for your domain, allowing you to delegate subdomains to external DNS providers or your own nameservers.
|
||||
|
||||
### Step 1: Planning Your Delegation
|
||||
|
||||
Before setting up NS records, decide:
|
||||
|
||||
- **Subdomain to delegate**: e.g., `blog.example.web`
|
||||
- **Target nameservers**: e.g., `ns1.blogprovider.web`, `ns2.blogprovider.web`
|
||||
- **Whether you need glue records**: Required if nameservers are within your domain
|
||||
|
||||
### Step 2: Creating NS Records
|
||||
|
||||
#### Option A: Through Web Interface
|
||||
|
||||
1. **Access dns.web**
|
||||
- Navigate to `gurt://dns.web`
|
||||
- Select the domain you want to configure (e.g., `example.web`)
|
||||
|
||||
2. **Add NS Record**
|
||||
- Select "NS" from the record type dropdown
|
||||
- Enter the subdomain name: `blog` (for `blog.example.web`)
|
||||
- Enter the nameserver: `ns1.blogprovider.com`
|
||||
- Click "Add Record"
|
||||
|
||||
3. **Add Additional Nameservers**
|
||||
- Repeat for each nameserver (typically 2-4 nameservers)
|
||||
- Use the same subdomain name but different nameserver values
|
||||
|
||||
### Step 3: Setting Up Glue Records (optional)
|
||||
|
||||
Glue records are required when your nameserver hostnames are within the domain you're delegating.
|
||||
|
||||
**Example**: If you want to delegate `blog.example.web` to `ns1.blog.example.web`:
|
||||
|
||||
1. **Create the NS Record**
|
||||
`NS`, "blog" -> `ns1.blog.example.web`
|
||||
|
||||
2. **Create the Glue Record**
|
||||
`A`, "ns1.blog" -> `203.0.113.10`
|
||||
|
||||
Without the glue record, there would be a circular dependency: to find the IP of `ns1.blog.example.web`, you'd need to query the nameservers for `blog.example.web`, but those nameservers are hosted at `ns1.blog.example.web`.
|
||||
|
||||
### Step 4: Configuring the Target Gurted DNS
|
||||
|
||||
If you're delegating to another Gurted DNS instance, the target server needs to be configured to handle your subdomain.
|
||||
|
||||
#### Target Server Setup
|
||||
|
||||
1. **Register the delegated domain** on the target Gurted DNS server
|
||||
- Navigate to the target DNS server (e.g., `gurt://blogprovider.web`)
|
||||
- Register `blog.example.web` as a new domain
|
||||
- Get it approved by the target server administrators
|
||||
|
||||
2. **Add records for the subdomain**
|
||||
- Add A records: `blog.example.web → 192.0.2.100`
|
||||
- Add subdomains: `www.blog.example.web → 192.0.2.100`
|
||||
- Add API endpoints: `api.blog.example.web → 192.0.2.101`
|
||||
|
||||
### Step 5: Testing Your Delegation
|
||||
|
||||
#### Through Flumi Browser
|
||||
1. **Test the delegated domain**
|
||||
- Navigate to `gurt://blog.example.web` in Flumi
|
||||
- Should resolve through the delegated nameservers
|
||||
|
||||
2. **Test subdomains**
|
||||
- Try `gurt://www.blog.example.web`
|
||||
- Try `gurt://api.blog.example.web`
|
||||
|
||||
3. **Check delegation in DNS interface**
|
||||
- Go to `gurt://dns.web`
|
||||
- Look up `blog.example.web`
|
||||
- Should show NS records pointing to your target nameservers
|
||||
|
||||
### Real-World Examples
|
||||
|
||||
#### Example 1: Blog Delegation
|
||||
```
|
||||
Domain: example.web (managed on dns.web)
|
||||
Delegation: blog.example.web → blogprovider.web
|
||||
|
||||
Setup:
|
||||
1. On dns.web: Add NS record "blog" → "dns.blogprovider.web"
|
||||
2. On blogprovider.web: Register and manage "blog.example.web"
|
||||
3. Test: gurt://blog.example.web should resolve through blogprovider.web
|
||||
```
|
||||
|
||||
#### Example 2: API Service Delegation
|
||||
```
|
||||
Domain: company.web (managed on dns.web)
|
||||
Delegation: api.company.web → apiservice.web
|
||||
|
||||
Setup:
|
||||
1. On dns.web: Add NS record "api" → "ns.apiservice.web"
|
||||
2. On apiservice.web: Register "api.company.web" and add A records
|
||||
3. Test: gurt://api.company.web/v1/users resolves to API server
|
||||
```
|
||||
|
||||
@@ -35,7 +35,11 @@ const config: Config = {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
|
||||
themes: ['@docusaurus/theme-mermaid'],
|
||||
markdown: {
|
||||
mermaid: true,
|
||||
},
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
|
||||
1196
docs/package-lock.json
generated
1196
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.8.1",
|
||||
"@docusaurus/preset-classic": "3.8.1",
|
||||
"@docusaurus/theme-mermaid": "^3.8.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[ext_resource type="Script" uid="uid://bmu8q4rm1wopd" path="res://Scripts/Tags/select.gd" id="1_select"]
|
||||
[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_xq313"]
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer"]
|
||||
[node name="VBoxContainer2" type="VBoxContainer"]
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
script = ExtResource("1_select")
|
||||
|
||||
@@ -60,7 +60,18 @@ corner_radius_bottom_left = 50
|
||||
bg_color = Color(0.6, 0.6, 0.6, 0)
|
||||
draw_center = false
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_u50mg"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ee4r6"]
|
||||
bg_color = Color(0.168627, 0.168627, 0.168627, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.247059, 0.466667, 0.807843, 1)
|
||||
corner_radius_top_left = 15
|
||||
corner_radius_top_right = 15
|
||||
corner_radius_bottom_right = 15
|
||||
corner_radius_bottom_left = 15
|
||||
expand_margin_left = 40.0
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cbgmd"]
|
||||
bg_color = Color(0.168627, 0.168627, 0.168627, 1)
|
||||
@@ -71,7 +82,7 @@ corner_radius_bottom_left = 15
|
||||
expand_margin_left = 40.0
|
||||
|
||||
[sub_resource type="Theme" id="Theme_jjvhh"]
|
||||
LineEdit/styles/focus = SubResource("StyleBoxEmpty_u50mg")
|
||||
LineEdit/styles/focus = SubResource("StyleBoxFlat_ee4r6")
|
||||
LineEdit/styles/normal = SubResource("StyleBoxFlat_cbgmd")
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_white"]
|
||||
@@ -192,10 +203,10 @@ caret_blink = true
|
||||
|
||||
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/HBoxContainer/LineEdit"]
|
||||
layout_mode = 2
|
||||
offset_left = -32.0
|
||||
offset_right = -2.0
|
||||
offset_bottom = 53.0
|
||||
scale = Vector2(0.85, 0.85)
|
||||
offset_left = -27.855
|
||||
offset_right = 2.145
|
||||
offset_bottom = 69.0
|
||||
scale = Vector2(0.65, 0.65)
|
||||
texture = ExtResource("3_8gbba")
|
||||
stretch_mode = 5
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ class_name GurtProtocol
|
||||
|
||||
const DNS_API_URL = "gurt://localhost:8877"
|
||||
|
||||
# DNS resolution cache: domain.tld -> IP address
|
||||
static var _dns_cache: Dictionary = {}
|
||||
|
||||
static func is_gurt_domain(url: String) -> bool:
|
||||
if url.begins_with("gurt://"):
|
||||
return true
|
||||
@@ -15,27 +18,63 @@ static func is_gurt_domain(url: String) -> bool:
|
||||
|
||||
static func parse_gurt_domain(url: String) -> Dictionary:
|
||||
var domain_part = url
|
||||
var path = "/"
|
||||
|
||||
if url.begins_with("gurt://"):
|
||||
domain_part = url.substr(7)
|
||||
|
||||
# Extract path from domain_part (e.g., "test.dawg/script.lua" -> "test.dawg" + "/script.lua")
|
||||
var path_start = domain_part.find("/")
|
||||
if path_start != -1:
|
||||
path = domain_part.substr(path_start)
|
||||
domain_part = domain_part.substr(0, path_start)
|
||||
|
||||
# Check if domain is cached (resolved before)
|
||||
var domain_key = domain_part
|
||||
if _dns_cache.has(domain_key):
|
||||
return {
|
||||
"direct_address": _dns_cache[domain_key],
|
||||
"display_url": domain_part + path,
|
||||
"is_direct": true,
|
||||
"path": path,
|
||||
"full_domain": domain_part
|
||||
}
|
||||
|
||||
if domain_part.contains(":") or domain_part.begins_with("127.0.0.1") or domain_part.begins_with("localhost") or is_ip_address(domain_part):
|
||||
return {
|
||||
"direct_address": domain_part,
|
||||
"display_url": domain_part,
|
||||
"is_direct": true
|
||||
"display_url": domain_part + path,
|
||||
"is_direct": true,
|
||||
"path": path,
|
||||
"full_domain": domain_part
|
||||
}
|
||||
|
||||
var parts = domain_part.split(".")
|
||||
if parts.size() != 2:
|
||||
if parts.size() < 2:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"name": parts[0],
|
||||
"tld": parts[1],
|
||||
"display_url": domain_part,
|
||||
"is_direct": false
|
||||
}
|
||||
# Support subdomains (e.g., api.blog.example.com)
|
||||
if parts.size() == 2:
|
||||
return {
|
||||
"name": parts[0],
|
||||
"tld": parts[1],
|
||||
"display_url": domain_part + path,
|
||||
"is_direct": false,
|
||||
"path": path,
|
||||
"full_domain": domain_part,
|
||||
"is_subdomain": false
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"name": parts[parts.size() - 2], # The domain name part
|
||||
"tld": parts[parts.size() - 1], # The TLD part
|
||||
"display_url": domain_part + path,
|
||||
"is_direct": false,
|
||||
"path": path,
|
||||
"full_domain": domain_part,
|
||||
"is_subdomain": true,
|
||||
"subdomain_parts": parts.slice(0, parts.size() - 2)
|
||||
}
|
||||
|
||||
static func is_ip_address(address: String) -> bool:
|
||||
var parts = address.split(".")
|
||||
@@ -51,40 +90,103 @@ static func is_ip_address(address: String) -> bool:
|
||||
|
||||
return true
|
||||
|
||||
static func fetch_domain_info(name: String, tld: String) -> Dictionary:
|
||||
var path = "/domain/" + name + "/" + tld
|
||||
var dns_address = "localhost:8877"
|
||||
static func fetch_domain_info(name: String, tld: String) -> Dictionary:
|
||||
var request_data = JSON.stringify({"name": name, "tld": tld})
|
||||
var result = await fetch_dns_post_working("localhost:8877", "/resolve", request_data)
|
||||
|
||||
print("DNS API URL: gurt://" + dns_address + path)
|
||||
if result.has("error"):
|
||||
return {"error": result.error}
|
||||
|
||||
var response = await fetch_content_via_gurt_direct(dns_address, path)
|
||||
|
||||
if response.has("error"):
|
||||
if "No response from GURT server" in response.error or "Failed to create GURT client" in response.error:
|
||||
return {"error": "DNS server is not responding"}
|
||||
else:
|
||||
return {"error": "Failed to make DNS request"}
|
||||
|
||||
if not response.has("content"):
|
||||
return {"error": "DNS server is not responding"}
|
||||
|
||||
var content = response.content
|
||||
if content.is_empty():
|
||||
return {"error": "DNS server is not responding"}
|
||||
if not result.has("content"):
|
||||
return {"error": "No content in DNS response"}
|
||||
|
||||
var content_str = result.content.get_string_from_utf8()
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(content.get_string_from_utf8())
|
||||
var parse_result = json.parse(content_str)
|
||||
|
||||
if parse_result != OK:
|
||||
return {"error": "Invalid JSON response from DNS server"}
|
||||
return {"error": "Invalid JSON in DNS response"}
|
||||
|
||||
var data = json.data
|
||||
return json.data
|
||||
|
||||
static func fetch_full_domain_info(full_domain: String, record_type: String = "") -> Dictionary:
|
||||
var request_data = {"domain": full_domain}
|
||||
if not record_type.is_empty():
|
||||
request_data["record_type"] = record_type
|
||||
|
||||
# Check if the response indicates an error (like 404)
|
||||
if data is Dictionary and data.has("error"):
|
||||
return {"error": "Domain not found or not approved"}
|
||||
var json_data = JSON.stringify(request_data)
|
||||
var result = await fetch_dns_post_working("localhost:8877", "/resolve-full", json_data)
|
||||
|
||||
return data
|
||||
if result.has("error"):
|
||||
return {"error": result.error}
|
||||
|
||||
if not result.has("content"):
|
||||
return {"error": "No content in DNS response"}
|
||||
|
||||
var content_str = result.content.get_string_from_utf8()
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(content_str)
|
||||
|
||||
if parse_result != OK:
|
||||
return {"error": "Invalid JSON in DNS response"}
|
||||
|
||||
return json.data
|
||||
|
||||
static func fetch_dns_post_working(server: String, path: String, json_data: String) -> Dictionary:
|
||||
var shared_result = {"finished": false}
|
||||
var thread = Thread.new()
|
||||
var mutex = Mutex.new()
|
||||
|
||||
var thread_func = func():
|
||||
var local_result = {}
|
||||
var client = GurtProtocolClient.new()
|
||||
|
||||
if not client.create_client(10):
|
||||
local_result = {"error": "Failed to create client"}
|
||||
else:
|
||||
var url = "gurt://" + server + path
|
||||
|
||||
# Prepare request options
|
||||
var options = {
|
||||
"method": "POST",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"body": json_data
|
||||
}
|
||||
|
||||
var response = client.request(url, options)
|
||||
|
||||
client.disconnect()
|
||||
|
||||
if not response:
|
||||
local_result = {"error": "No response from server"}
|
||||
elif not response.is_success:
|
||||
local_result = {"error": "Server error: " + str(response.status_code) + " " + str(response.status_message)}
|
||||
else:
|
||||
local_result = {"content": response.body}
|
||||
|
||||
mutex.lock()
|
||||
shared_result.clear()
|
||||
for key in local_result:
|
||||
shared_result[key] = local_result[key]
|
||||
shared_result["finished"] = true
|
||||
mutex.unlock()
|
||||
|
||||
thread.start(thread_func)
|
||||
|
||||
# Non-blocking wait
|
||||
while not shared_result.get("finished", false):
|
||||
await Engine.get_main_loop().process_frame
|
||||
|
||||
thread.wait_to_finish()
|
||||
|
||||
mutex.lock()
|
||||
var final_result = {}
|
||||
for key in shared_result:
|
||||
if key != "finished":
|
||||
final_result[key] = shared_result[key]
|
||||
mutex.unlock()
|
||||
|
||||
return final_result
|
||||
|
||||
static func fetch_content_via_gurt(ip: String, path: String = "/") -> Dictionary:
|
||||
var client = GurtProtocolClient.new()
|
||||
@@ -150,14 +252,10 @@ static func fetch_content_via_gurt_direct(address: String, path: String = "/") -
|
||||
|
||||
thread.start(thread_func)
|
||||
|
||||
var finished = false
|
||||
while not finished:
|
||||
# Non-blocking wait using signals instead of polling
|
||||
while not shared_result.get("finished", false):
|
||||
await Engine.get_main_loop().process_frame
|
||||
OS.delay_msec(10)
|
||||
|
||||
mutex.lock()
|
||||
finished = shared_result.get("finished", false)
|
||||
mutex.unlock()
|
||||
# Yield control back to the main thread without blocking delays
|
||||
|
||||
thread.wait_to_finish()
|
||||
|
||||
@@ -176,34 +274,125 @@ static func handle_gurt_domain(url: String) -> Dictionary:
|
||||
return {"error": "Invalid domain format. Use: domain.tld or IP:port", "html": create_error_page("Invalid domain format. Use: domain.tld or IP:port")}
|
||||
|
||||
var target_address: String
|
||||
var path = "/"
|
||||
var path = parsed.get("path", "/")
|
||||
|
||||
if parsed.get("is_direct", false):
|
||||
target_address = parsed.direct_address
|
||||
else:
|
||||
var domain_info = await fetch_domain_info(parsed.name, parsed.tld)
|
||||
var domain_info: Dictionary
|
||||
|
||||
# Use the new full domain resolution for subdomains
|
||||
if parsed.get("is_subdomain", false):
|
||||
domain_info = await fetch_full_domain_info(parsed.full_domain)
|
||||
else:
|
||||
domain_info = await fetch_domain_info(parsed.name, parsed.tld)
|
||||
|
||||
if domain_info.has("error"):
|
||||
return {"error": domain_info.error, "html": create_error_page(domain_info.error)}
|
||||
target_address = domain_info.ip
|
||||
|
||||
# Process DNS records to find target address
|
||||
var target_result = await resolve_target_address(domain_info, parsed.full_domain)
|
||||
if target_result.has("error"):
|
||||
return {"error": target_result.error, "html": create_error_page(target_result.error)}
|
||||
|
||||
target_address = target_result.address
|
||||
|
||||
# Cache the resolved address
|
||||
var domain_key = parsed.full_domain
|
||||
_dns_cache[domain_key] = target_address
|
||||
|
||||
var content_result = await fetch_content_via_gurt_direct(target_address, path)
|
||||
if content_result.has("error"):
|
||||
var error_msg = "Failed to fetch content from " + target_address + " via GURT protocol - " + content_result.error
|
||||
var error_msg = "Failed to fetch content from " + target_address + path + " via GURT protocol - " + content_result.error
|
||||
if content_result.has("content") and not content_result.content.is_empty():
|
||||
return {"html": content_result.content, "display_url": parsed.display_url}
|
||||
return {"error": error_msg, "html": create_error_page(error_msg)}
|
||||
|
||||
if not content_result.has("content"):
|
||||
var error_msg = "No content received from " + target_address
|
||||
var error_msg = "No content received from " + target_address + path
|
||||
return {"error": error_msg, "html": create_error_page(error_msg)}
|
||||
|
||||
var html_content = content_result.content
|
||||
if html_content.is_empty():
|
||||
var error_msg = "Empty content received from " + target_address
|
||||
var error_msg = "Empty content received from " + target_address + path
|
||||
return {"error": error_msg, "html": create_error_page(error_msg)}
|
||||
|
||||
return {"html": html_content, "display_url": parsed.display_url}
|
||||
|
||||
static func resolve_target_address(domain_info: Dictionary, original_domain: String) -> Dictionary:
|
||||
if not domain_info.has("records") or domain_info.records == null:
|
||||
return {"error": "No DNS records found for domain"}
|
||||
|
||||
var records = domain_info.records
|
||||
var max_cname_depth = 5 # Prevent infinite CNAME loops
|
||||
var cname_depth = 0
|
||||
|
||||
# First pass: Look for direct A/AAAA records
|
||||
var a_records = []
|
||||
var aaaa_records = []
|
||||
var cname_records = []
|
||||
var ns_records = []
|
||||
|
||||
for record in records:
|
||||
if not record.has("type") or not record.has("value"):
|
||||
continue
|
||||
|
||||
match record.type:
|
||||
"A":
|
||||
a_records.append(record.value)
|
||||
"AAAA":
|
||||
aaaa_records.append(record.value)
|
||||
"CNAME":
|
||||
cname_records.append(record.value)
|
||||
"NS":
|
||||
ns_records.append(record.value)
|
||||
|
||||
# If we have direct A records, use the first one
|
||||
if not a_records.is_empty():
|
||||
return {"address": a_records[0]}
|
||||
|
||||
# If we have IPv6 AAAA records and no A records, we need to handle this
|
||||
if not aaaa_records.is_empty() and a_records.is_empty():
|
||||
return {"error": "Only IPv6 (AAAA) records found, but IPv4 required for GURT protocol"}
|
||||
|
||||
# Follow CNAME chain
|
||||
if not cname_records.is_empty():
|
||||
var current_cname = cname_records[0]
|
||||
|
||||
while cname_depth < max_cname_depth:
|
||||
cname_depth += 1
|
||||
|
||||
# Try to resolve the CNAME target
|
||||
var cname_info = await fetch_full_domain_info(current_cname, "A")
|
||||
if cname_info.has("error"):
|
||||
return {"error": "Failed to resolve CNAME target: " + current_cname + " - " + cname_info.error}
|
||||
|
||||
if not cname_info.has("records") or cname_info.records == null:
|
||||
return {"error": "No records found for CNAME target: " + current_cname}
|
||||
|
||||
# Look for A records in the CNAME target
|
||||
var found_next_cname = false
|
||||
for record in cname_info.records:
|
||||
if record.has("type") and record.type == "A" and record.has("value"):
|
||||
return {"address": record.value}
|
||||
elif record.has("type") and record.type == "CNAME" and record.has("value"):
|
||||
# Another CNAME, continue the chain
|
||||
current_cname = record.value
|
||||
found_next_cname = true
|
||||
break
|
||||
|
||||
if not found_next_cname:
|
||||
# No more CNAMEs found, but also no A record
|
||||
return {"error": "CNAME chain ended without A record for: " + current_cname}
|
||||
|
||||
return {"error": "CNAME chain too deep (max " + str(max_cname_depth) + " levels)"}
|
||||
|
||||
# If we have NS records, this indicates delegation
|
||||
if not ns_records.is_empty():
|
||||
return {"error": "Domain is delegated to nameservers: " + str(ns_records) + ". Cannot resolve directly."}
|
||||
|
||||
return {"error": "No A record found for domain"}
|
||||
|
||||
static func get_error_type(error_message: String) -> Dictionary:
|
||||
if "DNS server is not responding" in error_message or "Domain not found" in error_message:
|
||||
return {"code": "ERR_NAME_NOT_RESOLVED", "title": "This site can't be reached", "icon": "🌐"}
|
||||
|
||||
@@ -57,7 +57,7 @@ func _get_font_weight_multiplier() -> float:
|
||||
elif element_styles.has("font-extrabold"):
|
||||
return 1.10
|
||||
elif element_styles.has("font-bold"):
|
||||
return 1.0
|
||||
return 1.04
|
||||
elif element_styles.has("font-semibold"):
|
||||
return 1.06
|
||||
elif element_styles.has("font-medium"):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -293,76 +293,58 @@ impl GurtServer {
|
||||
}
|
||||
|
||||
async fn handle_connection(&self, mut stream: TcpStream, addr: SocketAddr) -> Result<()> {
|
||||
let connection_result = timeout(self.connection_timeout, async {
|
||||
self.handle_initial_handshake(&mut stream, addr).await?;
|
||||
|
||||
if let Some(tls_acceptor) = &self.tls_acceptor {
|
||||
info!("Upgrading connection to TLS for {}", addr);
|
||||
let tls_stream = tls_acceptor.accept(stream).await
|
||||
.map_err(|e| GurtError::crypto(format!("TLS upgrade failed: {}", e)))?;
|
||||
|
||||
info!("TLS upgrade completed for {}", addr);
|
||||
|
||||
self.handle_tls_connection(tls_stream, addr).await
|
||||
} else {
|
||||
warn!("No TLS configuration available, but handshake completed - this violates GURT protocol");
|
||||
Err(GurtError::protocol("TLS is required after handshake but no TLS configuration available"))
|
||||
}
|
||||
}).await;
|
||||
// Remove timeout wrapper that causes connection aborts
|
||||
self.handle_initial_handshake(&mut stream, addr).await?;
|
||||
|
||||
match connection_result {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
warn!("Connection timeout for {}", addr);
|
||||
Err(GurtError::timeout("Connection timeout"))
|
||||
}
|
||||
if let Some(tls_acceptor) = &self.tls_acceptor {
|
||||
info!("Upgrading connection to TLS for {}", addr);
|
||||
let tls_stream = tls_acceptor.accept(stream).await
|
||||
.map_err(|e| GurtError::crypto(format!("TLS upgrade failed: {}", e)))?;
|
||||
|
||||
info!("TLS upgrade completed for {}", addr);
|
||||
|
||||
self.handle_tls_connection(tls_stream, addr).await
|
||||
} else {
|
||||
warn!("No TLS configuration available, but handshake completed - this violates GURT protocol");
|
||||
Err(GurtError::protocol("TLS is required after handshake but no TLS configuration available"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_initial_handshake(&self, stream: &mut TcpStream, addr: SocketAddr) -> Result<()> {
|
||||
let handshake_result = timeout(self.handshake_timeout, async {
|
||||
let mut buffer = Vec::new();
|
||||
let mut temp_buffer = [0u8; 8192];
|
||||
|
||||
loop {
|
||||
let bytes_read = stream.read(&mut temp_buffer).await?;
|
||||
if bytes_read == 0 {
|
||||
return Err(GurtError::connection("Connection closed during handshake"));
|
||||
}
|
||||
|
||||
buffer.extend_from_slice(&temp_buffer[..bytes_read]);
|
||||
|
||||
let body_separator = BODY_SEPARATOR.as_bytes();
|
||||
if buffer.windows(body_separator.len()).any(|w| w == body_separator) {
|
||||
break;
|
||||
}
|
||||
|
||||
if buffer.len() > MAX_MESSAGE_SIZE {
|
||||
return Err(GurtError::protocol("Handshake message too large"));
|
||||
}
|
||||
}
|
||||
|
||||
let message = GurtMessage::parse_bytes(&buffer)?;
|
||||
|
||||
match message {
|
||||
GurtMessage::Request(request) => {
|
||||
if request.method == GurtMethod::HANDSHAKE {
|
||||
self.send_handshake_response(stream, addr, &request).await
|
||||
} else {
|
||||
Err(GurtError::protocol("First message must be HANDSHAKE"))
|
||||
}
|
||||
}
|
||||
GurtMessage::Response(_) => {
|
||||
Err(GurtError::protocol("Server received response during handshake"))
|
||||
}
|
||||
}
|
||||
}).await;
|
||||
// Remove timeout wrapper that causes connection aborts
|
||||
let mut buffer = Vec::new();
|
||||
let mut temp_buffer = [0u8; 8192];
|
||||
|
||||
match handshake_result {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
warn!("Handshake timeout for {}", addr);
|
||||
Err(GurtError::timeout("Handshake timeout"))
|
||||
loop {
|
||||
let bytes_read = stream.read(&mut temp_buffer).await?;
|
||||
if bytes_read == 0 {
|
||||
return Err(GurtError::connection("Connection closed during handshake"));
|
||||
}
|
||||
|
||||
buffer.extend_from_slice(&temp_buffer[..bytes_read]);
|
||||
|
||||
let body_separator = BODY_SEPARATOR.as_bytes();
|
||||
if buffer.windows(body_separator.len()).any(|w| w == body_separator) {
|
||||
break;
|
||||
}
|
||||
|
||||
if buffer.len() > MAX_MESSAGE_SIZE {
|
||||
return Err(GurtError::protocol("Handshake message too large"));
|
||||
}
|
||||
}
|
||||
|
||||
let message = GurtMessage::parse_bytes(&buffer)?;
|
||||
|
||||
match message {
|
||||
GurtMessage::Request(request) => {
|
||||
if request.method == GurtMethod::HANDSHAKE {
|
||||
self.send_handshake_response(stream, addr, &request).await
|
||||
} else {
|
||||
Err(GurtError::protocol("First message must be HANDSHAKE"))
|
||||
}
|
||||
}
|
||||
GurtMessage::Response(_) => {
|
||||
Err(GurtError::protocol("Server received response during handshake"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,26 +375,17 @@ impl GurtServer {
|
||||
(buffer.starts_with(b"{") && buffer.ends_with(b"}"));
|
||||
|
||||
if has_complete_message {
|
||||
let process_result = timeout(self.request_timeout,
|
||||
self.process_tls_message(&mut tls_stream, addr, &buffer)
|
||||
).await;
|
||||
|
||||
match process_result {
|
||||
Ok(Ok(())) => {
|
||||
// Remove timeout wrapper that causes connection aborts
|
||||
match self.process_tls_message(&mut tls_stream, addr, &buffer).await {
|
||||
Ok(()) => {
|
||||
debug!("Processed message from {} successfully", addr);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
Err(e) => {
|
||||
error!("Encrypted message processing error from {}: {}", addr, e);
|
||||
let error_response = GurtResponse::internal_server_error()
|
||||
.with_string_body("Internal server error");
|
||||
let _ = tls_stream.write_all(&error_response.to_bytes()).await;
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Request timeout for {}", addr);
|
||||
let timeout_response = GurtResponse::new(GurtStatusCode::Timeout)
|
||||
.with_string_body("Request timeout");
|
||||
let _ = tls_stream.write_all(&timeout_response.to_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
|
||||
Reference in New Issue
Block a user