diff --git a/.gitignore b/.gitignore
index 3e2ae94..bb782b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
*target*
-*.pem
\ No newline at end of file
+*.pem
+gurty.toml
+certs
\ No newline at end of file
diff --git a/dns/Cargo.lock b/dns/Cargo.lock
index 2685d98..82947c7 100644
--- a/dns/Cargo.lock
+++ b/dns/Cargo.lock
@@ -797,6 +797,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@@ -1545,12 +1560,50 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags 2.9.2",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "parking_lot"
version = "0.12.3"
@@ -3082,6 +3135,9 @@ name = "uuid"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
+dependencies = [
+ "getrandom",
+]
[[package]]
name = "uwl"
@@ -3250,6 +3306,7 @@ name = "webx_dns"
version = "0.0.1"
dependencies = [
"anyhow",
+ "base64 0.22.1",
"bcrypt",
"chrono",
"clap",
@@ -3260,6 +3317,7 @@ dependencies = [
"jsonwebtoken",
"log",
"macros-rs",
+ "openssl",
"pretty_env_logger",
"prettytable",
"rand",
@@ -3267,9 +3325,11 @@ dependencies = [
"serde",
"serde_json",
"serenity",
+ "sha2",
"sqlx",
"tokio",
"toml",
+ "uuid",
]
[[package]]
diff --git a/dns/Cargo.toml b/dns/Cargo.toml
index ddd75d3..17c96dc 100644
--- a/dns/Cargo.toml
+++ b/dns/Cargo.toml
@@ -26,3 +26,7 @@ clap = { version = "4.5.4", features = ["derive"] }
rand = { version = "0.8.5", features = ["small_rng"] }
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0"
+sha2 = "0.10"
+base64 = "0.22"
+uuid = { version = "1.0", features = ["v4"] }
+openssl = "0.10"
diff --git a/dns/frontend/domain.html b/dns/frontend/domain.html
index 5a422c7..967d21f 100644
--- a/dns/frontend/domain.html
+++ b/dns/frontend/domain.html
@@ -120,7 +120,7 @@
Value:
diff --git a/dns/frontend/domain.lua b/dns/frontend/domain.lua
index 3700a76..986cc9a 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
@@ -125,7 +131,7 @@ renderRecords = function(appendOnly)
local typeCell = gurt.create('div', { text = record.type, style = 'font-bold' })
local nameCell = gurt.create('div', { text = record.name or '@' })
local valueCell = gurt.create('div', { text = record.value, style = 'font-mono text-sm break-all' })
- local ttlCell = gurt.create('div', { text = record.ttl or '3600' })
+ local ttlCell = gurt.create('div', { text = record.ttl or 'none' })
local actionsCell = gurt.create('div')
local deleteBtn = gurt.create('button', {
@@ -221,11 +227,10 @@ end
local function addRecord(type, name, value, ttl)
hideError('record-error')
- print('Adding DNS record: ' .. type .. ' ' .. name .. ' ' .. value)
-
+
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', {
method = 'POST',
- headers = {
+ headers = {
['Content-Type'] = 'application/json',
Authorization = 'Bearer ' .. authToken
},
@@ -237,30 +242,36 @@ local function addRecord(type, name, value, ttl)
})
})
- 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)
- -- Render only the new record
- renderRecords(true)
+ if response then
+ if response:ok() then
+ gurt.select('#record-name').value = ''
+ gurt.select('#record-value').value = ''
+ gurt.select('#record-ttl').value = ''
+
+ local newRecord = response:json()
+ if newRecord and newRecord.id then
+ table.insert(records, newRecord)
+
+ local wasEmpty = (#records == 1)
+
+ if wasEmpty then
+ renderRecords(false)
+ else
+ renderRecords(true)
+ end
+ else
+ 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)
+ showError('record-error', 'No response from server - connection failed')
+ print('Failed to add DNS record: No response')
end
+
end
local function logout()
@@ -273,15 +284,48 @@ 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'}
+ 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' 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
local recordName = gurt.select('#record-name').value
local recordValue = gurt.select('#record-value').value
- local recordTTL = tonumber(gurt.select('#record-ttl').value) or 3600
+ local recordTTL = tonumber(gurt.select('#record-ttl').value) or ''
if not recordValue or recordValue == '' then
showError('record-error', 'Record value is required')
@@ -297,4 +341,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..d97961d 100644
--- a/dns/frontend/index.html
+++ b/dns/frontend/index.html
@@ -7,35 +7,33 @@
@@ -49,7 +47,7 @@
Log In
-
Don't have an account? Register here
+
Don't have an account? Register here
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...
-
+ Note: After registration is approved, you can add DNS records (A, AAAA, CNAME, TXT) to configure where your domain points.
Submit for Approval
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/frontend/signup.html b/dns/frontend/signup.html
new file mode 100644
index 0000000..e86ade0
--- /dev/null
+++ b/dns/frontend/signup.html
@@ -0,0 +1,68 @@
+
+ Sign Up
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sign Up
+
+
+
New users get 3 free domain registrations to get started!
+
+
+
+
Already have an account? Login here
+
+
+
+
diff --git a/dns/frontend/signup.lua b/dns/frontend/signup.lua
new file mode 100644
index 0000000..544c168
--- /dev/null
+++ b/dns/frontend/signup.lua
@@ -0,0 +1,96 @@
+if gurt.crumbs.get("auth_token") then
+ gurt.location.goto("/dashboard.html")
+end
+
+local submitBtn = gurt.select('#submit')
+local username_input = gurt.select('#username')
+local password_input = gurt.select('#password')
+local confirm_password_input = gurt.select('#confirm-password')
+local log_output = gurt.select('#log-output')
+
+function addLog(message)
+ gurt.log(message)
+ log_output.text = log_output.text .. message .. '\n'
+end
+
+function clearLog()
+ log_output.text = ''
+end
+
+function validateForm(username, password, confirmPassword)
+ if not username or username == '' then
+ addLog('Error: Username is required')
+ return false
+ end
+
+ if not password or password == '' then
+ addLog('Error: Password is required')
+ return false
+ end
+
+ if password ~= confirmPassword then
+ addLog('Error: Passwords do not match')
+ return false
+ end
+
+ if string.len(password) < 6 then
+ addLog('Error: Password must be at least 6 characters long')
+ return false
+ end
+
+ return true
+end
+
+submitBtn:on('submit', function(event)
+ local username = event.data.username
+ local password = event.data.password
+ local confirmPassword = event.data['confirm-password']
+
+ clearLog()
+
+ if not validateForm(username, password, confirmPassword) then
+ return
+ end
+
+ local request_body = JSON.stringify({
+ username = username,
+ password = password
+ })
+
+ local url = 'gurt://127.0.0.1:8877/auth/register'
+ local headers = {
+ ['Content-Type'] = 'application/json'
+ }
+
+ addLog('Creating account for username: ' .. username)
+
+ local response = fetch(url, {
+ method = 'POST',
+ headers = headers,
+ body = request_body
+ })
+
+ addLog('Response Status: ' .. response.status .. ' ' .. response.statusText)
+
+ if response:ok() then
+ addLog('Account created successfully!')
+ local jsonData = response:json()
+ if jsonData then
+ addLog('Welcome, ' .. jsonData.user.username .. '!')
+ addLog('You have ' .. jsonData.user.registrations_remaining .. ' domain registrations available')
+
+ gurt.crumbs.set({
+ name = "auth_token",
+ value = jsonData.token,
+ lifespan = 604800
+ })
+
+ addLog('Redirecting to dashboard...')
+ gurt.location.goto("/dashboard.html")
+ end
+ else
+ addLog('Registration failed with status: ' .. response.status)
+ local error_data = response:text()
+ addLog('Error: ' .. error_data)
+ end
+end)
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/migrations/005_add_certificate_challenges.sql b/dns/migrations/005_add_certificate_challenges.sql
new file mode 100644
index 0000000..eef7d1e
--- /dev/null
+++ b/dns/migrations/005_add_certificate_challenges.sql
@@ -0,0 +1,33 @@
+-- Add certificate challenges table for CA functionality
+CREATE TABLE IF NOT EXISTS certificate_challenges (
+ id SERIAL PRIMARY KEY,
+ token VARCHAR(255) UNIQUE NOT NULL,
+ domain VARCHAR(255) NOT NULL,
+ challenge_type VARCHAR(20) NOT NULL CHECK (challenge_type IN ('dns')),
+ verification_data VARCHAR(500) NOT NULL,
+ status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'valid', 'invalid', 'expired')),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ expires_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS idx_certificate_challenges_token ON certificate_challenges(token);
+CREATE INDEX IF NOT EXISTS idx_certificate_challenges_domain ON certificate_challenges(domain);
+CREATE INDEX IF NOT EXISTS idx_certificate_challenges_expires_at ON certificate_challenges(expires_at);
+
+-- Add table to store issued certificates
+CREATE TABLE IF NOT EXISTS issued_certificates (
+ id SERIAL PRIMARY KEY,
+ domain VARCHAR(255) NOT NULL,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ certificate_pem TEXT NOT NULL,
+ private_key_pem TEXT NOT NULL,
+ issued_at TIMESTAMPTZ DEFAULT NOW(),
+ expires_at TIMESTAMPTZ NOT NULL,
+ revoked_at TIMESTAMPTZ,
+ serial_number VARCHAR(255) UNIQUE NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS idx_issued_certificates_domain ON issued_certificates(domain);
+CREATE INDEX IF NOT EXISTS idx_issued_certificates_user_id ON issued_certificates(user_id);
+CREATE INDEX IF NOT EXISTS idx_issued_certificates_serial ON issued_certificates(serial_number);
+CREATE INDEX IF NOT EXISTS idx_issued_certificates_expires_at ON issued_certificates(expires_at);
\ No newline at end of file
diff --git a/dns/migrations/007_cleanup_invalid_records.sql b/dns/migrations/007_cleanup_invalid_records.sql
new file mode 100644
index 0000000..65c8d58
--- /dev/null
+++ b/dns/migrations/007_cleanup_invalid_records.sql
@@ -0,0 +1,7 @@
+-- Remove invalid record types before applying constraint
+DELETE FROM dns_records WHERE record_type NOT IN ('A', 'AAAA', 'CNAME', 'TXT');
+
+-- Now apply the constraint
+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/008_add_ca_storage.sql b/dns/migrations/008_add_ca_storage.sql
new file mode 100644
index 0000000..266f3ef
--- /dev/null
+++ b/dns/migrations/008_add_ca_storage.sql
@@ -0,0 +1,8 @@
+-- Add table to store CA certificate and key
+CREATE TABLE IF NOT EXISTS ca_certificates (
+ id SERIAL PRIMARY KEY,
+ ca_cert_pem TEXT NOT NULL,
+ ca_key_pem TEXT NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ is_active BOOLEAN DEFAULT TRUE
+);
\ No newline at end of file
diff --git a/dns/migrations/009_add_csr_to_challenges.sql b/dns/migrations/009_add_csr_to_challenges.sql
new file mode 100644
index 0000000..75d2996
--- /dev/null
+++ b/dns/migrations/009_add_csr_to_challenges.sql
@@ -0,0 +1,2 @@
+-- Add CSR field to certificate challenges
+ALTER TABLE certificate_challenges ADD COLUMN IF NOT EXISTS csr_pem TEXT;
\ No newline at end of file
diff --git a/dns/src/crypto.rs b/dns/src/crypto.rs
new file mode 100644
index 0000000..6e0b4b6
--- /dev/null
+++ b/dns/src/crypto.rs
@@ -0,0 +1,172 @@
+use anyhow::Result;
+use openssl::pkey::PKey;
+use openssl::rsa::Rsa;
+use openssl::x509::X509Req;
+use openssl::x509::X509Name;
+use openssl::hash::MessageDigest;
+use std::process::Command;
+
+pub fn generate_ca_cert() -> Result<(String, String)> {
+ let rsa = Rsa::generate(4096)?;
+ let ca_key = PKey::from_rsa(rsa)?;
+
+ let mut name_builder = X509Name::builder()?;
+ name_builder.append_entry_by_text("C", "US")?;
+ name_builder.append_entry_by_text("O", "Gurted Network")?;
+ name_builder.append_entry_by_text("CN", "Gurted Root CA")?;
+ let ca_name = name_builder.build();
+
+ let mut cert_builder = openssl::x509::X509::builder()?;
+ cert_builder.set_version(2)?;
+ cert_builder.set_subject_name(&ca_name)?;
+ cert_builder.set_issuer_name(&ca_name)?;
+ cert_builder.set_pubkey(&ca_key)?;
+
+ // validity period (10 years)
+ let not_before = openssl::asn1::Asn1Time::days_from_now(0)?;
+ let not_after = openssl::asn1::Asn1Time::days_from_now(3650)?;
+ cert_builder.set_not_before(¬_before)?;
+ cert_builder.set_not_after(¬_after)?;
+
+ let serial = openssl::bn::BigNum::from_u32(1)?.to_asn1_integer()?;
+ cert_builder.set_serial_number(&serial)?;
+
+ let basic_constraints = openssl::x509::extension::BasicConstraints::new()
+ .critical()
+ .ca()
+ .build()?;
+ cert_builder.append_extension(basic_constraints)?;
+
+ let key_usage = openssl::x509::extension::KeyUsage::new()
+ .critical()
+ .key_cert_sign()
+ .crl_sign()
+ .build()?;
+ cert_builder.append_extension(key_usage)?;
+
+ cert_builder.sign(&ca_key, MessageDigest::sha256())?;
+ let ca_cert = cert_builder.build();
+
+ let ca_key_pem = ca_key.private_key_to_pem_pkcs8()?;
+ let ca_cert_pem = ca_cert.to_pem()?;
+
+ Ok((
+ String::from_utf8(ca_key_pem)?,
+ String::from_utf8(ca_cert_pem)?
+ ))
+}
+
+pub fn sign_csr_with_ca(
+ csr_pem: &str,
+ ca_cert_pem: &str,
+ ca_key_pem: &str,
+ domain: &str
+) -> Result {
+ let ca_cert = openssl::x509::X509::from_pem(ca_cert_pem.as_bytes())?;
+ let ca_key = PKey::private_key_from_pem(ca_key_pem.as_bytes())?;
+
+ let csr = X509Req::from_pem(csr_pem.as_bytes())?;
+
+ let mut cert_builder = openssl::x509::X509::builder()?;
+ cert_builder.set_version(2)?;
+ cert_builder.set_subject_name(csr.subject_name())?;
+ cert_builder.set_issuer_name(ca_cert.subject_name())?;
+ cert_builder.set_pubkey(csr.public_key()?.as_ref())?;
+
+ // validity period (90 days)
+ let not_before = openssl::asn1::Asn1Time::days_from_now(0)?;
+ let not_after = openssl::asn1::Asn1Time::days_from_now(90)?;
+ cert_builder.set_not_before(¬_before)?;
+ cert_builder.set_not_after(¬_after)?;
+
+ let mut serial = openssl::bn::BigNum::new()?;
+ serial.rand(128, openssl::bn::MsbOption::MAYBE_ZERO, false)?;
+ let asn1_serial = serial.to_asn1_integer()?;
+ cert_builder.set_serial_number(&asn1_serial)?;
+
+ let context = cert_builder.x509v3_context(Some(&ca_cert), None);
+
+ let mut san_builder = openssl::x509::extension::SubjectAlternativeName::new();
+ san_builder
+ .dns(domain)
+ .dns("localhost")
+ .ip("127.0.0.1");
+
+ if let Ok(public_ip) = get_public_ip() {
+ san_builder.ip(&public_ip);
+ }
+
+ let subject_alt_name = san_builder.build(&context)?;
+ cert_builder.append_extension(subject_alt_name)?;
+
+ let key_usage = openssl::x509::extension::KeyUsage::new()
+ .critical()
+ .digital_signature()
+ .key_encipherment()
+ .build()?;
+ cert_builder.append_extension(key_usage)?;
+
+ let ext_key_usage = openssl::x509::extension::ExtendedKeyUsage::new()
+ .server_auth()
+ .client_auth()
+ .build()?;
+ cert_builder.append_extension(ext_key_usage)?;
+
+ cert_builder.sign(&ca_key, MessageDigest::sha256())?;
+ let cert = cert_builder.build();
+
+ let cert_pem = cert.to_pem()?;
+ Ok(String::from_utf8(cert_pem)?)
+}
+
+fn get_public_ip() -> Result> {
+ // Method 1: Check if we can get it from environment or interface
+ if let Ok(output) = Command::new("curl")
+ .args(&["-s", "--max-time", "5", "https://api.ipify.org"])
+ .output()
+ {
+ if output.status.success() {
+ let ip = String::from_utf8(output.stdout)?.trim().to_string();
+ if is_valid_ip(&ip) {
+ return Ok(ip);
+ }
+ }
+ }
+
+ // Method 2: Try ifconfig.me
+ if let Ok(output) = Command::new("curl")
+ .args(&["-s", "--max-time", "5", "https://ifconfig.me/ip"])
+ .output()
+ {
+ if output.status.success() {
+ let ip = String::from_utf8(output.stdout)?.trim().to_string();
+ if is_valid_ip(&ip) {
+ return Ok(ip);
+ }
+ }
+ }
+
+ // Method 3: Try to get from network interfaces
+ if let Ok(output) = Command::new("hostname")
+ .args(&["-I"])
+ .output()
+ {
+ if output.status.success() {
+ let ips = String::from_utf8(output.stdout)?;
+ for ip in ips.split_whitespace() {
+ if is_valid_ip(ip) && !ip.starts_with("127.") && !ip.starts_with("192.168.") && !ip.starts_with("10.") {
+ return Ok(ip.to_string());
+ }
+ }
+ }
+ }
+
+ Err("Could not determine public IP".into())
+}
+
+fn is_valid_ip(ip: &str) -> bool {
+ ip.split('.')
+ .count() == 4
+ && ip.split('.')
+ .all(|part| part.parse::().is_ok())
+}
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..dd25f59 100644
--- a/dns/src/gurt_server.rs
+++ b/dns/src/gurt_server.rs
@@ -2,26 +2,25 @@ mod auth_routes;
mod helpers;
mod models;
mod routes;
+mod ca;
use crate::{auth::jwt_middleware_gurt, config::Config, discord_bot};
use colored::Colorize;
use macros_rs::fmt::{crashln, string};
-use std::{net::IpAddr, str::FromStr, sync::Arc, collections::HashMap};
+use std::{sync::Arc, collections::HashMap};
use gurt::prelude::*;
use gurt::{GurtStatusCode, Route};
#[derive(Clone)]
pub(crate) struct AppState {
- trusted: IpAddr,
config: Config,
db: sqlx::PgPool,
jwt_secret: String,
}
impl AppState {
- pub fn new(trusted: IpAddr, config: Config, db: sqlx::PgPool, jwt_secret: String) -> Self {
+ pub fn new(config: Config, db: sqlx::PgPool, jwt_secret: String) -> Self {
Self {
- trusted,
config,
db,
jwt_secret,
@@ -95,6 +94,12 @@ enum HandlerType {
DeleteDomain,
GetUserDomains,
CreateDomainRecord,
+ ResolveDomain,
+ ResolveFullDomain,
+ VerifyDomainOwnership,
+ RequestCertificate,
+ GetCertificate,
+ GetCaCertificate,
}
impl GurtHandler for AppHandler {
@@ -163,6 +168,12 @@ 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,
+ HandlerType::VerifyDomainOwnership => routes::verify_domain_ownership(&ctx, app_state).await,
+ HandlerType::RequestCertificate => routes::request_certificate(&ctx, app_state).await,
+ HandlerType::GetCertificate => routes::get_certificate(&ctx, app_state).await,
+ HandlerType::GetCaCertificate => routes::get_ca_certificate(&ctx, app_state).await,
};
let duration = start_time.elapsed();
@@ -185,11 +196,6 @@ impl GurtHandler for AppHandler {
pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
let config = Config::new().set_path(&cli.config).read();
- let trusted_ip = match IpAddr::from_str(&config.server.address) {
- Ok(addr) => addr,
- Err(err) => crashln!("Cannot parse address.\n{}", string!(err).white()),
- };
-
let db = match config.connect_to_db().await {
Ok(pool) => pool,
Err(err) => crashln!("Failed to connect to PostgreSQL database.\n{}", string!(err).white()),
@@ -203,7 +209,7 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
}
let jwt_secret = config.auth.jwt_secret.clone();
- let app_state = AppState::new(trusted_ip, config.clone(), db, jwt_secret);
+ let app_state = AppState::new(config.clone(), db, jwt_secret);
let rate_limit_state = RateLimitState::new();
// Create GURT server
@@ -231,7 +237,13 @@ 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 })
+ .route(Route::get("/verify-ownership/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::VerifyDomainOwnership })
+ .route(Route::post("/ca/request-certificate"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RequestCertificate })
+ .route(Route::get("/ca/certificate/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetCertificate })
+ .route(Route::get("/ca/root"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetCaCertificate });
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/ca.rs b/dns/src/gurt_server/ca.rs
new file mode 100644
index 0000000..2ceca5b
--- /dev/null
+++ b/dns/src/gurt_server/ca.rs
@@ -0,0 +1,45 @@
+use crate::crypto;
+use anyhow::Result;
+use sqlx::PgPool;
+
+pub struct CaCertificate {
+ pub ca_cert_pem: String,
+ pub ca_key_pem: String,
+}
+
+pub async fn get_or_create_ca(db: &PgPool) -> Result {
+ if let Some(ca_cert) = get_active_ca(db).await? {
+ return Ok(ca_cert);
+ }
+
+ log::info!("Generating new CA certificate...");
+ let (ca_key_pem, ca_cert_pem) = crypto::generate_ca_cert()?;
+
+ sqlx::query(
+ "INSERT INTO ca_certificates (ca_cert_pem, ca_key_pem, is_active) VALUES ($1, $2, TRUE)"
+ )
+ .bind(&ca_cert_pem)
+ .bind(&ca_key_pem)
+ .execute(db)
+ .await?;
+
+ log::info!("CA certificate generated and stored");
+
+ Ok(CaCertificate {
+ ca_cert_pem,
+ ca_key_pem,
+ })
+}
+
+async fn get_active_ca(db: &PgPool) -> Result> {
+ let result: Option<(String, String)> = sqlx::query_as(
+ "SELECT ca_cert_pem, ca_key_pem FROM ca_certificates WHERE is_active = TRUE ORDER BY created_at DESC LIMIT 1"
+ )
+ .fetch_optional(db)
+ .await?;
+
+ Ok(result.map(|(ca_cert_pem, ca_key_pem)| CaCertificate {
+ ca_cert_pem,
+ ca_key_pem,
+ }))
+}
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::().is_err() {
- return Err(GurtError::invalid_message("Invalid IP address"));
- }
-
- Ok(())
-}
pub fn deserialize_lowercase<'de, D>(deserializer: D) -> std::result::Result
where
diff --git a/dns/src/gurt_server/models.rs b/dns/src/gurt_server/models.rs
index 09d89a9..022952f 100644
--- a/dns/src/gurt_server/models.rs
+++ b/dns/src/gurt_server/models.rs
@@ -6,11 +6,12 @@ use sqlx::{FromRow, types::chrono::{DateTime, Utc}};
pub struct Domain {
#[serde(skip_deserializing)]
pub(crate) id: Option,
- 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,
#[serde(skip_deserializing)]
pub(crate) user_id: Option,
#[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>,
}
@@ -82,13 +82,21 @@ pub(crate) struct ResponseDnsRecord {
pub(crate) record_type: String,
pub(crate) name: String,
pub(crate) value: String,
- pub(crate) ttl: i32,
+ pub(crate) ttl: Option,
pub(crate) priority: Option,
}
#[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,
}
#[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,
}
diff --git a/dns/src/gurt_server/routes.rs b/dns/src/gurt_server/routes.rs
index ef0b053..5b8031d 100644
--- a/dns/src/gurt_server/routes.rs
+++ b/dns/src/gurt_server/routes.rs
@@ -1,7 +1,14 @@
-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 base64::{engine::general_purpose, Engine as _};
use gurt::prelude::*;
-use std::{env, collections::HashMap};
+use rand::{rngs::OsRng, Rng};
+use sha2::{Digest, Sha256};
+use std::time::{SystemTime, UNIX_EPOCH};
+use std::{collections::HashMap, env};
+
+const VALID_DNS_RECORD_TYPES: &[&str] = &["A", "AAAA", "CNAME", "TXT"];
fn parse_query_string(query: &str) -> HashMap {
let mut params = HashMap::new();
@@ -18,61 +25,104 @@ pub(crate) async fn index(_app_state: AppState) -> Result {
"GurtDNS v{}!\n\nThe available endpoints are:\n\n - [GET] /domains\n - [GET] /domain/{{name}}/{{tld}}\n - [POST] /domain\n - [PUT] /domain/{{key}}\n - [DELETE] /domain/{{key}}\n - [GET] /tlds\n\nRatelimits are as follows: 5 requests per 10 minutes on `[POST] /domain`.\n\nCode link: https://github.com/outpoot/gurted",
env!("CARGO_PKG_VERSION")
);
-
+
Ok(GurtResponse::ok().with_string_body(body))
}
pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -> Result {
- validate_ip(&domain)?;
-
- if !app.config.tld_list().contains(&domain.tld.as_str())
- || !domain.name.chars().all(|c| c.is_alphabetic() || c == '-')
+ if !app.config.tld_list().contains(&domain.tld.as_str())
+ || !domain.name.chars().all(|c| c.is_alphabetic() || c == '-')
|| domain.name.len() > 24
|| domain.name.is_empty()
|| domain.name.starts_with('-')
- || domain.name.ends_with('-') {
- return Err(GurtError::invalid_message("Invalid name, non-existent TLD, or name too long (24 chars)."));
+ || domain.name.ends_with('-')
+ {
+ return Err(GurtError::invalid_message(
+ "Invalid name, non-existent TLD, or name too long (24 chars).",
+ ));
}
- if app.config.offen_words().iter().any(|word| domain.name.contains(word)) {
- return Err(GurtError::invalid_message("The given domain name is offensive."));
+ if app
+ .config
+ .offen_words()
+ .iter()
+ .any(|word| domain.name.contains(word))
+ {
+ return Err(GurtError::invalid_message(
+ "The given domain name is offensive.",
+ ));
}
- let existing_count: i64 = sqlx::query_scalar(
- "SELECT COUNT(*) FROM domains WHERE name = $1 AND tld = $2"
- )
- .bind(&domain.name)
- .bind(&domain.tld)
- .fetch_one(&app.db)
- .await
- .map_err(|_| GurtError::invalid_message("Database error"))?;
+ let existing_count: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM domains WHERE name = $1 AND tld = $2")
+ .bind(&domain.name)
+ .bind(&domain.tld)
+ .fetch_one(&app.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
if existing_count > 0 {
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)
- .execute(&app.db)
- .await
- .map_err(|_| GurtError::invalid_message("Failed to update user registrations"))?;
+ sqlx::query(
+ "UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1",
+ )
+ .bind(user_id)
+ .execute(&app.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Failed to update user registrations"))?;
+
+ if !app.config.discord.bot_token.is_empty() && app.config.discord.channel_id != 0 {
+ 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)
}
-pub(crate) async fn create_domain(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result {
+pub(crate) async fn create_domain(
+ ctx: &ServerContext,
+ app_state: AppState,
+ claims: Claims,
+) -> Result {
// Check if user has registrations remaining
let user: (i32,) = sqlx::query_as("SELECT registrations_remaining FROM users WHERE id = $1")
.bind(claims.user_id)
@@ -91,29 +141,31 @@ pub(crate) async fn create_domain(ctx: &ServerContext, app_state: AppState, clai
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
match create_logic(domain.clone(), claims.user_id, &app_state).await {
- Ok(created_domain) => {
- Ok(GurtResponse::ok().with_json_body(&created_domain)?)
- }
- Err(e) => {
- Ok(GurtResponse::bad_request().with_json_body(&Error {
- msg: "Failed to create domain",
- error: e.to_string(),
- })?)
- }
+ Ok(created_domain) => Ok(GurtResponse::ok().with_json_body(&created_domain)?),
+ Err(e) => Ok(GurtResponse::bad_request().with_json_body(&Error {
+ msg: "Failed to create domain",
+ error: e.to_string(),
+ })?),
}
}
-pub(crate) async fn get_domain(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result {
+pub(crate) async fn get_domain(
+ ctx: &ServerContext,
+ app_state: AppState,
+ claims: Claims,
+) -> Result {
let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 3 {
- return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}"));
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid path format. Expected /domain/{domainName}"));
}
let domain_name = path_parts[2];
let domain_parts: Vec<&str> = domain_name.split('.').collect();
if domain_parts.len() < 2 {
- return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid domain format. Expected name.tld"));
}
let name = domain_parts[0];
@@ -138,7 +190,7 @@ pub(crate) async fn get_domain(ctx: &ServerContext, app_state: AppState, claims:
};
Ok(GurtResponse::ok().with_json_body(&response_domain)?)
}
- None => Ok(GurtResponse::not_found().with_string_body("Domain not found"))
+ None => Ok(GurtResponse::not_found().with_string_body("Domain not found")),
}
}
@@ -152,12 +204,14 @@ pub(crate) async fn get_domains(ctx: &ServerContext, app_state: AppState) -> Res
HashMap::new()
};
- let page = query_params.get("page")
+ let page = query_params
+ .get("page")
.and_then(|p| p.parse::().ok())
.unwrap_or(1)
.max(1); // Ensure page is at least 1
- let page_size = query_params.get("limit")
+ let page_size = query_params
+ .get("limit")
.and_then(|l| l.parse::().ok())
.unwrap_or(100)
.clamp(1, 1000); // Limit between 1 and 1000
@@ -173,14 +227,14 @@ pub(crate) async fn get_domains(ctx: &ServerContext, app_state: AppState) -> Res
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
- let response_domains: Vec = domains.into_iter().map(|domain| {
- ResponseDomain {
+ let response_domains: Vec = domains
+ .into_iter()
+ .map(|domain| ResponseDomain {
name: domain.name,
tld: domain.tld,
- ip: domain.ip,
records: None,
- }
- }).collect();
+ })
+ .collect();
let response = PaginationResponse {
domains: response_domains,
@@ -201,12 +255,15 @@ pub(crate) async fn check_domain(ctx: &ServerContext, app_state: AppState) -> Re
let query_string = &path[query_start + 1..];
parse_query_string(query_string)
} else {
- return Ok(GurtResponse::bad_request().with_string_body("Missing query parameters. Expected ?name=&tld="));
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Missing query parameters. Expected ?name=&tld="));
};
- let name = query_params.get("name")
+ let name = query_params
+ .get("name")
.ok_or_else(|| GurtError::invalid_message("Missing 'name' parameter"))?;
- let tld = query_params.get("tld")
+ let tld = query_params
+ .get("tld")
.ok_or_else(|| GurtError::invalid_message("Missing 'tld' parameter"))?;
let domain: Option = sqlx::query_as::<_, Domain>(
@@ -226,62 +283,24 @@ 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 {
- 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 = 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 {
+ 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 {
+pub(crate) async fn delete_domain(
+ ctx: &ServerContext,
+ app_state: AppState,
+ claims: Claims,
+) -> Result {
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}"));
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid path format. Expected /domain/{name}/{tld}"));
}
let name = path_parts[2];
@@ -313,7 +332,11 @@ pub(crate) async fn delete_domain(ctx: &ServerContext, app_state: AppState, clai
Ok(GurtResponse::ok().with_string_body("Domain deleted successfully"))
}
-pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result {
+pub(crate) async fn get_user_domains(
+ ctx: &ServerContext,
+ app_state: AppState,
+ claims: Claims,
+) -> Result {
// Parse pagination from query parameters
let path = ctx.path();
let query_params = if let Some(query_start) = path.find('?') {
@@ -323,12 +346,14 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
HashMap::new()
};
- let page = query_params.get("page")
+ let page = query_params
+ .get("page")
.and_then(|p| p.parse::().ok())
.unwrap_or(1)
.max(1);
- let page_size = query_params.get("limit")
+ let page_size = query_params
+ .get("limit")
.and_then(|l| l.parse::().ok())
.unwrap_or(100)
.clamp(1, 1000);
@@ -345,15 +370,15 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
- let response_domains: Vec = domains.into_iter().map(|domain| {
- UserDomain {
+ let response_domains: Vec = domains
+ .into_iter()
+ .map(|domain| UserDomain {
name: domain.name,
tld: domain.tld,
- ip: domain.ip,
status: domain.status.unwrap_or_else(|| "pending".to_string()),
denial_reason: domain.denial_reason,
- }
- }).collect();
+ })
+ .collect();
let response = UserDomainResponse {
domains: response_domains,
@@ -364,17 +389,23 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
Ok(GurtResponse::ok().with_json_body(&response)?)
}
-pub(crate) async fn get_domain_records(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result {
+pub(crate) async fn get_domain_records(
+ ctx: &ServerContext,
+ app_state: AppState,
+ claims: Claims,
+) -> Result {
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/{domainName}/records"));
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid path format. Expected /domain/{domainName}/records"));
}
let domain_name = path_parts[2];
let domain_parts: Vec<&str> = domain_name.split('.').collect();
if domain_parts.len() < 2 {
- return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid domain format. Expected name.tld"));
}
let name = domain_parts[0];
@@ -392,7 +423,11 @@ pub(crate) async fn get_domain_records(ctx: &ServerContext, app_state: AppState,
let domain = match domain {
Some(d) => d,
- None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied"))
+ None => {
+ return Ok(
+ GurtResponse::not_found().with_string_body("Domain not found or access denied")
+ )
+ }
};
let records: Vec = sqlx::query_as::<_, DnsRecord>(
@@ -403,31 +438,38 @@ pub(crate) async fn get_domain_records(ctx: &ServerContext, app_state: AppState,
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
- let response_records: Vec = records.into_iter().map(|record| {
- ResponseDnsRecord {
+ let response_records: Vec = 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),
+ ttl: record.ttl,
priority: record.priority,
- }
- }).collect();
+ })
+ .collect();
Ok(GurtResponse::ok().with_json_body(&response_records)?)
}
-pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result {
+pub(crate) async fn create_domain_record(
+ ctx: &ServerContext,
+ app_state: AppState,
+ claims: Claims,
+) -> Result {
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/{domainName}/records"));
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid path format. Expected /domain/{domainName}/records"));
}
let domain_name = path_parts[2];
let domain_parts: Vec<&str> = domain_name.split('.').collect();
if domain_parts.len() < 2 {
- return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid domain format. Expected name.tld"));
}
let name = domain_parts[0];
@@ -445,32 +487,63 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
let domain = match domain {
Some(d) => d,
- None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied"))
+ None => {
+ return Ok(
+ GurtResponse::not_found().with_string_body("Domain not found or access denied")
+ )
+ }
};
let record_data: CreateDnsRecord = {
let body_bytes = ctx.body();
let body_str = std::str::from_utf8(body_bytes).unwrap_or("");
log::info!("Received JSON body: {}", body_str);
-
- serde_json::from_slice(body_bytes)
- .map_err(|e| {
- log::error!("JSON parsing error: {} for body: {}", e, body_str);
- GurtError::invalid_message("Invalid JSON")
- })?
+
+ serde_json::from_slice(body_bytes).map_err(|e| {
+ log::error!("JSON parsing error: {} for body: {}", e, body_str);
+ GurtError::invalid_message("Invalid JSON")
+ })?
};
if record_data.record_type.is_empty() {
return Ok(GurtResponse::bad_request().with_string_body("Record type is required"));
}
-
- let valid_types = ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV"];
- if !valid_types.contains(&record_data.record_type.as_str()) {
- return Ok(GurtResponse::bad_request().with_string_body("Invalid record type"));
+
+ if !VALID_DNS_RECORD_TYPES.contains(&record_data.record_type.as_str()) {
+ return Ok(GurtResponse::bad_request().with_string_body(
+ "Invalid record type. Only A, AAAA, CNAME, and TXT records are supported.",
+ ));
}
let record_name = record_data.name.unwrap_or_else(|| "@".to_string());
- let ttl = record_data.ttl.unwrap_or(3600);
+ let ttl = record_data.ttl.filter(|t| *t > 0);
+
+ match record_data.record_type.as_str() {
+ "A" => {
+ if !record_data.value.parse::().is_ok() {
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid IPv4 address for A record"));
+ }
+ }
+ "AAAA" => {
+ if !record_data.value.parse::().is_ok() {
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid IPv6 address for AAAA record"));
+ }
+ }
+ "CNAME" => {
+ if record_data.value.is_empty() || !record_data.value.contains('.') {
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("CNAME records must contain a valid domain name"));
+ }
+ }
+ "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"
@@ -493,28 +566,36 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
record_type: record_data.record_type,
name: record_name,
value: record_data.value,
- ttl,
+ ttl: Some(ttl.unwrap_or(3600)),
priority: record_data.priority,
};
Ok(GurtResponse::ok().with_json_body(&response_record)?)
}
-pub(crate) async fn delete_domain_record(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result {
+pub(crate) async fn delete_domain_record(
+ ctx: &ServerContext,
+ app_state: AppState,
+ claims: Claims,
+) -> Result {
let path_parts: Vec<&str> = ctx.path().split('/').collect();
if path_parts.len() < 5 {
- return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}/records/{recordId}"));
+ return Ok(GurtResponse::bad_request().with_string_body(
+ "Invalid path format. Expected /domain/{domainName}/records/{recordId}",
+ ));
}
let domain_name = path_parts[2];
let record_id_str = path_parts[4];
- let record_id: i32 = record_id_str.parse()
+ let record_id: i32 = record_id_str
+ .parse()
.map_err(|_| GurtError::invalid_message("Invalid record ID"))?;
let domain_parts: Vec<&str> = domain_name.split('.').collect();
if domain_parts.len() < 2 {
- return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Invalid domain format. Expected name.tld"));
}
let name = domain_parts[0];
@@ -532,7 +613,11 @@ pub(crate) async fn delete_domain_record(ctx: &ServerContext, app_state: AppStat
let domain = match domain {
Some(d) => d,
- None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied"))
+ None => {
+ return Ok(
+ GurtResponse::not_found().with_string_body("Domain not found or access denied")
+ )
+ }
};
let rows_affected = sqlx::query("DELETE FROM dns_records WHERE id = $1 AND domain_id = $2")
@@ -550,8 +635,490 @@ 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 {
+ 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 {
+ // 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> {
+ 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 = 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 = 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 = records
+ .into_iter()
+ .map(|record| ResponseDnsRecord {
+ id: record.id.unwrap(),
+ record_type: record.record_type,
+ name: record.name,
+ value: record.value,
+ ttl: record.ttl,
+ 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> {
+ 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 = 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 = 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 = 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 = 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,
+ 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 {
+ #[derive(serde::Deserialize)]
+ struct FullDomainRequest {
+ domain: String,
+ record_type: Option,
+ }
+
+ 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(),
+ })?),
+ }
+}
+
+// Certificate Authority endpoints
+pub(crate) async fn verify_domain_ownership(
+ ctx: &ServerContext,
+ app_state: AppState,
+) -> Result {
+ let path_parts: Vec<&str> = ctx.path().split('/').collect();
+ if path_parts.len() < 3 {
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid path format"));
+ }
+
+ let domain = path_parts[2];
+
+ let domain_parts: Vec<&str> = domain.split('.').collect();
+ if domain_parts.len() < 2 {
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format"));
+ }
+
+ let name = domain_parts[0];
+ let tld = domain_parts[1];
+
+ let domain_record: Option = 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(name)
+ .bind(tld)
+ .fetch_optional(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ let exists = domain_record.is_some();
+
+ Ok(GurtResponse::ok().with_json_body(&serde_json::json!({
+ "domain": domain,
+ "exists": exists
+ }))?)
+}
+
+pub(crate) async fn request_certificate(
+ ctx: &ServerContext,
+ app_state: AppState,
+) -> Result {
+ #[derive(serde::Deserialize)]
+ struct CertRequest {
+ domain: String,
+ csr: String,
+ }
+
+ let cert_request: CertRequest = serde_json::from_slice(ctx.body())
+ .map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
+
+ let domain_parts: Vec<&str> = cert_request.domain.split('.').collect();
+ if domain_parts.len() < 2 {
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format"));
+ }
+
+ let name = domain_parts[0];
+ let tld = domain_parts[1];
+
+ let domain_record: Option = 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(name)
+ .bind(tld)
+ .fetch_optional(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ if domain_record.is_none() {
+ return Ok(GurtResponse::bad_request()
+ .with_string_body("Domain does not exist or is not approved"));
+ }
+
+ let token = uuid::Uuid::new_v4().to_string();
+ let verification_data = generate_challenge_data(&cert_request.domain, &token)?;
+
+ sqlx::query(
+ "INSERT INTO certificate_challenges (token, domain, challenge_type, verification_data, csr_pem, expires_at) VALUES ($1, $2, $3, $4, $5, $6)"
+ )
+ .bind(&token)
+ .bind(&cert_request.domain)
+ .bind("dns") // Only DNS challenges
+ .bind(&verification_data)
+ .bind(&cert_request.csr)
+ .bind(chrono::Utc::now() + chrono::Duration::hours(1))
+ .execute(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Failed to store challenge"))?;
+
+ let challenge = serde_json::json!({
+ "token": token,
+ "challenge_type": "dns",
+ "domain": cert_request.domain,
+ "verification_data": verification_data
+ });
+
+ Ok(GurtResponse::ok().with_json_body(&challenge)?)
+}
+
+pub(crate) async fn get_certificate(
+ ctx: &ServerContext,
+ app_state: AppState,
+) -> Result {
+ 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"));
+ }
+
+ let token = path_parts[3];
+
+ let challenge: Option<(String, String, String, Option, chrono::DateTime)> = sqlx::query_as(
+ "SELECT domain, challenge_type, verification_data, csr_pem, expires_at FROM certificate_challenges WHERE token = $1"
+ )
+ .bind(token)
+ .fetch_optional(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ let (domain, _challenge_type, verification_data, csr_pem, expires_at) = match challenge {
+ Some(c) => c,
+ None => return Ok(GurtResponse::not_found().with_string_body("Challenge not found")),
+ };
+
+ let csr_pem = match csr_pem {
+ Some(csr) => csr,
+ None => {
+ return Ok(
+ GurtResponse::bad_request().with_string_body("CSR not found for this challenge")
+ )
+ }
+ };
+
+ if chrono::Utc::now() > expires_at {
+ return Ok(GurtResponse::bad_request().with_string_body("Challenge expired"));
+ }
+
+ let challenge_domain = format!("_gurtca-challenge.{}", domain);
+ let domain_parts: Vec<&str> = challenge_domain.split('.').collect();
+ if domain_parts.len() < 3 {
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format"));
+ }
+
+ let record_name = "_gurtca-challenge";
+ let base_domain_name = domain_parts[domain_parts.len() - 2];
+ let tld = domain_parts[domain_parts.len() - 1];
+
+ let domain_record: Option = 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(base_domain_name)
+ .bind(tld)
+ .fetch_optional(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ let domain_record = match domain_record {
+ Some(d) => d,
+ None => {
+ return Ok(
+ GurtResponse::bad_request().with_string_body("Domain not found or not approved")
+ )
+ }
+ };
+
+ let txt_records: Vec = 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 = 'TXT' AND name = $2 AND value = $3"
+ )
+ .bind(domain_record.id.unwrap())
+ .bind(record_name)
+ .bind(&verification_data)
+ .fetch_all(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ if txt_records.is_empty() {
+ return Ok(GurtResponse::new(gurt::GurtStatusCode::Accepted)
+ .with_string_body("Challenge not completed yet"));
+ }
+
+ let ca_cert = super::ca::get_or_create_ca(&app_state.db)
+ .await
+ .map_err(|e| {
+ log::error!("Failed to get CA certificate: {}", e);
+ GurtError::invalid_message("CA certificate error")
+ })?;
+
+ let cert_pem = crate::crypto::sign_csr_with_ca(
+ &csr_pem,
+ &ca_cert.ca_cert_pem,
+ &ca_cert.ca_key_pem,
+ &domain,
+ )
+ .map_err(|e| {
+ log::error!("Failed to sign certificate: {}", e);
+ GurtError::invalid_message("Certificate signing failed")
+ })?;
+
+ let certificate = serde_json::json!({
+ "cert_pem": cert_pem,
+ "chain_pem": ca_cert.ca_cert_pem,
+ "expires_at": (chrono::Utc::now() + chrono::Duration::days(90)).to_rfc3339()
+ });
+
+ // Delete the challenge as it's completed
+ sqlx::query("DELETE FROM certificate_challenges WHERE token = $1")
+ .bind(token)
+ .execute(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Failed to cleanup challenge"))?;
+
+ Ok(GurtResponse::ok().with_json_body(&certificate)?)
+}
+
+pub(crate) async fn get_ca_certificate(
+ _ctx: &ServerContext,
+ app_state: AppState,
+) -> Result {
+ let ca_cert = super::ca::get_or_create_ca(&app_state.db)
+ .await
+ .map_err(|e| {
+ log::error!("Failed to get CA certificate: {}", e);
+ GurtError::invalid_message("CA certificate error")
+ })?;
+
+ Ok(GurtResponse::ok()
+ .with_header("Content-Type", "application/x-pem-file")
+ .with_header(
+ "Content-Disposition",
+ "attachment; filename=\"gurted-ca.crt\"",
+ )
+ .with_string_body(ca_cert.ca_cert_pem))
+}
+
+fn generate_challenge_data(domain: &str, token: &str) -> Result {
+ let timestamp = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map_err(|_| GurtError::invalid_message("System time error"))?
+ .as_nanos();
+
+ let mut rng = OsRng;
+ let random_bytes: [u8; 32] = rng.gen();
+ let secure_entropy = general_purpose::STANDARD.encode(random_bytes);
+
+ let uuid_entropy = uuid::Uuid::new_v4().to_string();
+
+ let data = format!(
+ "{}:{}:{}:{}:{}",
+ domain, token, timestamp, secure_entropy, uuid_entropy
+ );
+ let mut hasher = Sha256::new();
+ hasher.update(data.as_bytes());
+ let hash = hasher.finalize();
+
+ Ok(general_purpose::STANDARD.encode(hash))
+}
+
#[derive(serde::Serialize)]
struct Error {
msg: &'static str,
error: String,
-}
\ No newline at end of file
+}
diff --git a/dns/src/main.rs b/dns/src/main.rs
index cd8e687..b22f33a 100644
--- a/dns/src/main.rs
+++ b/dns/src/main.rs
@@ -1,8 +1,8 @@
mod config;
mod gurt_server;
-mod secret;
mod auth;
mod discord_bot;
+mod crypto;
use clap::{Parser, Subcommand};
use clap_verbosity_flag::{LogLevel, Verbosity};
diff --git a/dns/src/secret.rs b/dns/src/secret.rs
deleted file mode 100644
index ce3e4b7..0000000
--- a/dns/src/secret.rs
+++ /dev/null
@@ -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 = (0..step).map(|_| rng.gen::()).collect::>();
-
- 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
-}
diff --git a/docs/docs/dns-system.md b/docs/docs/dns-system.md
index 932d9eb..722ed0d 100644
--- a/docs/docs/dns-system.md
+++ b/docs/docs/dns-system.md
@@ -2,12 +2,136 @@
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)
+- Subdomain support with delegation
+- CNAME chain resolution
+- Nameserver delegation with glue records
-TODO: complete
\ No newline at end of file
+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"`
+
+## 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. 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]
+```
+
+## 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
diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts
index b09c51c..01eab1a 100644
--- a/docs/docusaurus.config.ts
+++ b/docs/docusaurus.config.ts
@@ -35,7 +35,11 @@ const config: Config = {
defaultLocale: 'en',
locales: ['en'],
},
-
+
+ themes: ['@docusaurus/theme-mermaid'],
+ markdown: {
+ mermaid: true,
+ },
presets: [
[
'classic',
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 555a221..f738966 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -10,6 +10,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",
@@ -265,6 +266,26 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
+ "dependencies": {
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@antfu/utils": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz",
+ "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -1856,6 +1877,45 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@braintree/sanitize-url": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz",
+ "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="
+ },
+ "node_modules/@chevrotain/cst-dts-gen": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
+ "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
+ "dependencies": {
+ "@chevrotain/gast": "11.0.3",
+ "@chevrotain/types": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/gast": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
+ "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
+ "dependencies": {
+ "@chevrotain/types": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/regexp-to-ast": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
+ "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="
+ },
+ "node_modules/@chevrotain/types": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
+ "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="
+ },
+ "node_modules/@chevrotain/utils": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
+ "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="
+ },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -3520,6 +3580,27 @@
"react-dom": "^18.0.0 || ^19.0.0"
}
},
+ "node_modules/@docusaurus/theme-mermaid": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.8.1.tgz",
+ "integrity": "sha512-IWYqjyTPjkNnHsFFu9+4YkeXS7PD1xI3Bn2shOhBq+f95mgDfWInkpfBN4aYvx4fTT67Am6cPtohRdwh4Tidtg==",
+ "dependencies": {
+ "@docusaurus/core": "3.8.1",
+ "@docusaurus/module-type-aliases": "3.8.1",
+ "@docusaurus/theme-common": "3.8.1",
+ "@docusaurus/types": "3.8.1",
+ "@docusaurus/utils-validation": "3.8.1",
+ "mermaid": ">=11.6.0",
+ "tslib": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@docusaurus/theme-search-algolia": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.1.tgz",
@@ -3675,6 +3756,26 @@
"@hapi/hoek": "^9.0.0"
}
},
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
+ },
+ "node_modules/@iconify/utils": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz",
+ "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.0.0",
+ "@antfu/utils": "^8.1.0",
+ "@iconify/types": "^2.0.0",
+ "debug": "^4.4.0",
+ "globals": "^15.14.0",
+ "kolorist": "^1.8.0",
+ "local-pkg": "^1.0.0",
+ "mlly": "^1.7.4"
+ }
+ },
"node_modules/@jest/schemas": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
@@ -3798,6 +3899,14 @@
"react": ">=16"
}
},
+ "node_modules/@mermaid-js/parser": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz",
+ "integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==",
+ "dependencies": {
+ "langium": "3.3.1"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4212,6 +4321,228 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -4284,6 +4615,11 @@
"@types/send": "*"
}
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
+ },
"node_modules/@types/gtag.js": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz",
@@ -4490,6 +4826,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "optional": true
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -5482,6 +5824,30 @@
"url": "https://github.com/sponsors/fb55"
}
},
+ "node_modules/chevrotain": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
+ "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
+ "dependencies": {
+ "@chevrotain/cst-dts-gen": "11.0.3",
+ "@chevrotain/gast": "11.0.3",
+ "@chevrotain/regexp-to-ast": "11.0.3",
+ "@chevrotain/types": "11.0.3",
+ "@chevrotain/utils": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/chevrotain-allstar": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
+ "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
+ "dependencies": {
+ "lodash-es": "^4.17.21"
+ },
+ "peerDependencies": {
+ "chevrotain": "^11.0.0"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -5745,6 +6111,11 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
+ "node_modules/confbox": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
+ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="
+ },
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@@ -5938,6 +6309,14 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
+ "node_modules/cose-base": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+ "dependencies": {
+ "layout-base": "^1.0.0"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
@@ -6397,6 +6776,487 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
+ "node_modules/cytoscape": {
+ "version": "3.33.1",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
+ "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/cytoscape-cose-bilkent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+ "dependencies": {
+ "cose-base": "^1.0.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
+ "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
+ "dependencies": {
+ "cose-base": "^2.2.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/cose-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
+ "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
+ "dependencies": {
+ "layout-base": "^2.0.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/layout-base": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
+ "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-sankey": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+ "dependencies": {
+ "d3-array": "1 - 2",
+ "d3-shape": "^1.2.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+ },
+ "node_modules/d3-sankey/node_modules/d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "dependencies": {
+ "d3-path": "1"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dagre-d3-es": {
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz",
+ "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==",
+ "dependencies": {
+ "d3": "^7.9.0",
+ "lodash-es": "^4.17.21"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
+ },
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -6530,6 +7390,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delaunator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -6656,6 +7524,14 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
+ "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -7198,6 +8074,11 @@
"node": ">= 0.6"
}
},
+ "node_modules/exsolve": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
+ "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -7682,6 +8563,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/globals": {
+ "version": "15.15.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
+ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
@@ -7800,6 +8692,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/hachure-fill": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
+ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="
+ },
"node_modules/handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -8450,6 +9347,14 @@
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -8857,6 +9762,29 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/katex": {
+ "version": "0.16.22",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
+ "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -8865,6 +9793,11 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/khroma": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+ },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -8881,6 +9814,26 @@
"node": ">=6"
}
},
+ "node_modules/kolorist": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
+ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="
+ },
+ "node_modules/langium": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
+ "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
+ "dependencies": {
+ "chevrotain": "~11.0.3",
+ "chevrotain-allstar": "~0.3.0",
+ "vscode-languageserver": "~9.0.1",
+ "vscode-languageserver-textdocument": "~1.0.11",
+ "vscode-uri": "~3.0.8"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/latest-version": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz",
@@ -8904,6 +9857,11 @@
"shell-quote": "^1.8.3"
}
},
+ "node_modules/layout-base": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -8949,6 +9907,22 @@
"node": ">=8.9.0"
}
},
+ "node_modules/local-pkg": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
+ "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
+ "dependencies": {
+ "mlly": "^1.7.4",
+ "pkg-types": "^2.3.0",
+ "quansync": "^0.2.11"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/locate-path": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
@@ -8968,6 +9942,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -9050,6 +10029,17 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/marked": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-16.2.0.tgz",
+ "integrity": "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg==",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -9482,6 +10472,45 @@
"node": ">= 8"
}
},
+ "node_modules/mermaid": {
+ "version": "11.10.0",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.10.0.tgz",
+ "integrity": "sha512-oQsFzPBy9xlpnGxUqLbVY8pvknLlsNIJ0NWwi8SUJjhbP1IT0E0o1lfhU4iYV3ubpy+xkzkaOyDUQMn06vQElQ==",
+ "dependencies": {
+ "@braintree/sanitize-url": "^7.0.4",
+ "@iconify/utils": "^2.1.33",
+ "@mermaid-js/parser": "^0.6.2",
+ "@types/d3": "^7.4.3",
+ "cytoscape": "^3.29.3",
+ "cytoscape-cose-bilkent": "^4.1.0",
+ "cytoscape-fcose": "^2.2.0",
+ "d3": "^7.9.0",
+ "d3-sankey": "^0.12.3",
+ "dagre-d3-es": "7.0.11",
+ "dayjs": "^1.11.13",
+ "dompurify": "^3.2.5",
+ "katex": "^0.16.22",
+ "khroma": "^2.1.0",
+ "lodash-es": "^4.17.21",
+ "marked": "^16.0.0",
+ "roughjs": "^4.6.6",
+ "stylis": "^4.3.6",
+ "ts-dedent": "^2.2.0",
+ "uuid": "^11.1.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -11284,6 +12313,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/mlly": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
+ "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "pathe": "^2.0.1",
+ "pkg-types": "^1.3.0",
+ "ufo": "^1.5.4"
+ }
+ },
+ "node_modules/mlly/node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="
+ },
+ "node_modules/mlly/node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -11722,6 +12777,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/package-manager-detector": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz",
+ "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="
+ },
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -11838,6 +12898,11 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/path-data-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
+ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="
+ },
"node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
@@ -11888,6 +12953,11 @@
"node": ">=8"
}
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -11918,6 +12988,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/pkg-types": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
+ "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+ "dependencies": {
+ "confbox": "^0.2.2",
+ "exsolve": "^1.0.7",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/points-on-curve": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="
+ },
+ "node_modules/points-on-path": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
+ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
+ "dependencies": {
+ "path-data-parser": "0.1.0",
+ "points-on-curve": "0.2.0"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -13430,6 +14524,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/quansync": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+ "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ]
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -14189,6 +15298,22 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
+ },
+ "node_modules/roughjs": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
+ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
+ "dependencies": {
+ "hachure-fill": "^0.5.2",
+ "path-data-parser": "^0.1.0",
+ "points-on-curve": "^0.2.0",
+ "points-on-path": "^0.2.1"
+ }
+ },
"node_modules/rtlcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz",
@@ -14228,6 +15353,11 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -14978,6 +16108,11 @@
"postcss": "^8.4.31"
}
},
+ "node_modules/stylis": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
+ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -15142,6 +16277,11 @@
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
+ "node_modules/tinyexec": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
+ "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="
+ },
"node_modules/tinypool": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
@@ -15195,6 +16335,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/ts-dedent": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
+ "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
+ "engines": {
+ "node": ">=6.10"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -15263,6 +16411,11 @@
"node": ">=14.17"
}
},
+ "node_modules/ufo": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
+ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="
+ },
"node_modules/undici-types": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
@@ -15718,6 +16871,49 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/vscode-jsonrpc": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/vscode-languageserver": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
+ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
+ "dependencies": {
+ "vscode-languageserver-protocol": "3.17.5"
+ },
+ "bin": {
+ "installServerIntoExtension": "bin/installServerIntoExtension"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
+ "dependencies": {
+ "vscode-jsonrpc": "8.2.0",
+ "vscode-languageserver-types": "3.17.5"
+ }
+ },
+ "node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="
+ },
+ "node_modules/vscode-languageserver-types": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
+ "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="
+ },
"node_modules/watchpack": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
diff --git a/docs/package.json b/docs/package.json
index 27c6942..6010ff3 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -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",
diff --git a/flumi/.claude/settings.local.json b/flumi/.claude/settings.local.json
deleted file mode 100644
index e4c3d43..0000000
--- a/flumi/.claude/settings.local.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "permissions": {
- "allow": [
- "WebSearch",
- "WebFetch(domain:github.com)"
- ],
- "deny": [],
- "ask": []
- }
-}
\ No newline at end of file
diff --git a/flumi/Assets/gurted-ca.crt b/flumi/Assets/gurted-ca.crt
new file mode 100644
index 0000000..dd6bdb6
--- /dev/null
+++ b/flumi/Assets/gurted-ca.crt
@@ -0,0 +1,30 @@
+-----BEGIN CERTIFICATE-----
+MIIFHDCCAwSgAwIBAgIBATANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJVUzEX
+MBUGA1UECgwOR3VydGVkIE5ldHdvcmsxFzAVBgNVBAMMDkd1cnRlZCBSb290IENB
+MB4XDTI1MDgyMTE1MjgyM1oXDTM1MDgxOTE1MjgyM1owPzELMAkGA1UEBhMCVVMx
+FzAVBgNVBAoMDkd1cnRlZCBOZXR3b3JrMRcwFQYDVQQDDA5HdXJ0ZWQgUm9vdCBD
+QTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANYNLAnXNo8x9qzJbAwT
+dDVC40XSfEVIBPWX4yBEKMSKefcUQy2ZBqzBSVVeig7q7OEPm29sL0XbSgxPN8nH
+Pkg8ZfKhsDIuHGLeZbt1NAvc4mlMUHY5ebTMUaopldNJlKAKOJ+Xh6XHF3Tl4d3D
+HnkdQv4s0wdfbI8Dem8G+JoqMu5Cn1BJcoB6vmmwH6/Fkq7qEdVe3WfKWflBQ7qk
+rmj3hrKjKG62EsQKF+4JVPWY7RVG8rJukABakRndCKCM9te+XTIeollL/WvIcY4p
+Ctf+6/p7FcnWQrDdcGwFmWpVj/SHGzgCi0PfTsI8V3vpCyBzIc2rZJvpLH8ndfUI
+fNzYCAiRA4HUoXbyTvpMxJ3io4q9VZKuJ5mbe50NlJ/oiX2wFvosm5OMHUAk4tNJ
+64jQLHTVrI/O+TKbLebKH9xEUCFOJpQX4rz4nzyRRdzM3C4qDZ4UTz3hAMeBus79
+jJtZj26T2O7zYweihWhPFkatvick66aDhD5jeQLnPp/w4mY4iuZMf3tb2L+Py/BR
+k8LHg9xTFL79lwpelwbLSVOdLXXQXSRDx6eF0qG4dDALAlbEBYCrK8wjQqvH3/Fg
+EJbG9RTgywi6UgAy+jVdYFtW5+2No1HTyqELzq0OeOInzJf1xVM8IAP1KFkQF3V2
+ofIc4Uz4fF2mOpzJeeOkBKU1AgMBAAGjIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYD
+VR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQCkIajpq/McB5w8S2CdC4IF
+x63BQ2Zje8PAd0LjbtHulH4RoZQzW8+hHJgb13KfOg0MLZ7iEy0gSS/D2eF4uJpd
+NKgT8ZuG1v+e/OWsxRoonNpopz2dHt8qstRiqRlKZ1/45pXNwAM+ztWuRR2AIHB1
+bSStCShLArdB80/42OXK7Uq1CzNN8ikw4JKKdU+JP4TrCLIBNlDYq5hcFCjb+6f2
+fmJ5+VjZVx3yXV281Q1K2enMo0ACzPiD+1hgnms144hhbBqyP7rrQcnN/Z0Vq65U
+nFNQT5yU6KYuyPbajYxtpr7jKwJDsPJMa0pOW4H93IN0+jdqIk5vr8zE7PHztOIp
+KB+gMyTbeWx6hVmf6eRDVd56uibS5s+QrESQ6FWjO2Ns73qg9/vhW81JtTQ5NRF9
+YSKy3YHIKN3+bmUPOVp6rhb+xU2QaI7CQxjXlDt3Y3+evFe2oGyG/N439z09+az5
+A1J4f5mWP4+n/t8k75Z6PuVpOAUsiklJIcTOpRnYRlW+U+md94MsYD60ITWSgiad
+A7Uu3uoyS+wN8W1yNmPaVci2L19rgKc9ZMXCPFj6x6QiiR6fG7/7M8WGOR6Lx1n0
+9DcYTpcbYAdSufUSUtd9isjR1jzTHeIYQ9rRfdlQaOw3lnIVG0H9wVSBcAzMeSnd
+tUnu0gVTdnuUfjO1Te86fA==
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/flumi/Scenes/Tags/select.tscn b/flumi/Scenes/Tags/select.tscn
index 4fe7887..f2060f4 100644
--- a/flumi/Scenes/Tags/select.tscn
+++ b/flumi/Scenes/Tags/select.tscn
@@ -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")
diff --git a/flumi/Scenes/main.tscn b/flumi/Scenes/main.tscn
index 23bd2bf..9b8ed90 100644
--- a/flumi/Scenes/main.tscn
+++ b/flumi/Scenes/main.tscn
@@ -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
diff --git a/flumi/Scripts/CertificateManager.gd b/flumi/Scripts/CertificateManager.gd
new file mode 100644
index 0000000..4787a00
--- /dev/null
+++ b/flumi/Scripts/CertificateManager.gd
@@ -0,0 +1,49 @@
+extends RefCounted
+class_name CertificateManager
+
+static var trusted_ca_certificates: Array[String] = []
+static var ca_cache: Dictionary = {}
+
+static func fetch_cert_via_http(url: String) -> String:
+ var http_request = HTTPRequest.new()
+
+ var main_scene = Engine.get_main_loop().current_scene
+ if not main_scene:
+ return ""
+
+ main_scene.add_child(http_request)
+
+ var error = http_request.request(url)
+ if error != OK:
+ http_request.queue_free()
+ return ""
+
+ var response = await http_request.request_completed
+ http_request.queue_free()
+
+ var result = response[0]
+ var response_code = response[1]
+ var body = response[3]
+
+ if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
+ return ""
+
+ return body.get_string_from_utf8()
+
+static func initialize():
+ load_builtin_ca()
+ print("📋 Certificate Manager initialized with ", trusted_ca_certificates.size(), " trusted CAs")
+
+static func load_builtin_ca():
+ var ca_file = FileAccess.open("res://Assets/gurted-ca.crt", FileAccess.READ)
+ if ca_file:
+ var ca_cert_pem = ca_file.get_as_text()
+ ca_file.close()
+
+ if not ca_cert_pem.is_empty():
+ trusted_ca_certificates.append(ca_cert_pem)
+ print("✅ Loaded built-in GURT CA certificate")
+ else:
+ print("⚠️ Built-in CA certificate not yet configured")
+ else:
+ print("❌ Could not load built-in CA certificate")
diff --git a/flumi/Scripts/CertificateManager.gd.uid b/flumi/Scripts/CertificateManager.gd.uid
new file mode 100644
index 0000000..48c2b0d
--- /dev/null
+++ b/flumi/Scripts/CertificateManager.gd.uid
@@ -0,0 +1 @@
+uid://bhnsb8ttn6f7n
diff --git a/flumi/Scripts/GurtProtocol.gd b/flumi/Scripts/GurtProtocol.gd
index 4bf3399..4672678 100644
--- a/flumi/Scripts/GurtProtocol.gd
+++ b/flumi/Scripts/GurtProtocol.gd
@@ -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,44 +90,113 @@ 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()
+
+ for ca_cert in CertificateManager.trusted_ca_certificates:
+ client.add_ca_certificate(ca_cert)
+
+ 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()
+ for ca_cert in CertificateManager.trusted_ca_certificates:
+ client.add_ca_certificate(ca_cert)
+
if not client.create_client(30):
return {"error": "Failed to create GURT client"}
@@ -117,6 +225,9 @@ static func fetch_content_via_gurt_direct(address: String, path: String = "/") -
var local_result = {}
var client = GurtProtocolClient.new()
+ for ca_cert in CertificateManager.trusted_ca_certificates:
+ client.add_ca_certificate(ca_cert)
+
if not client.create_client(10):
local_result = {"error": "Failed to create GURT client"}
else:
@@ -150,14 +261,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 +283,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": "🌐"}
diff --git a/flumi/Scripts/Tags/p.gd b/flumi/Scripts/Tags/p.gd
index 2916507..fca015b 100644
--- a/flumi/Scripts/Tags/p.gd
+++ b/flumi/Scripts/Tags/p.gd
@@ -45,31 +45,31 @@ func _auto_resize_to_content():
await get_tree().process_frame
var content_height = get_content_height()
- var explicit_height = custom_minimum_size.y if custom_minimum_size.y > 0 else null
- var final_height = explicit_height if explicit_height != null else max(content_height, min_height)
+ var explicit_height = custom_minimum_size.y if custom_minimum_size.y > 0 else 0.0
+ var final_height = explicit_height if explicit_height > 0 else max(content_height, min_height)
custom_minimum_size = Vector2(desired_width, final_height)
queue_redraw()
func _get_font_weight_multiplier() -> float:
- if element_styles.has("font-black"):
- return 1.12
- elif element_styles.has("font-extrabold"):
- return 1.10
- elif element_styles.has("font-bold"):
- return 1.0
- elif element_styles.has("font-semibold"):
- return 1.06
- elif element_styles.has("font-medium"):
- return 1.03
- elif element_styles.has("font-light"):
- return 0.98
- elif element_styles.has("font-extralight") or element_styles.has("font-thin"):
- return 0.95
+ #if element_styles.has("font-black"):
+ # return 1.12
+ #elif element_styles.has("font-extrabold"):
+ # return 1.10
+ #elif element_styles.has("font-bold"):
+ # return 1.04
+ #elif element_styles.has("font-semibold"):
+ # return 1.06
+ #elif element_styles.has("font-medium"):
+ # return 1.03
+ #elif element_styles.has("font-light"):
+ # return 0.98
+ #elif element_styles.has("font-extralight") or element_styles.has("font-thin"):
+ # return 0.95
- var text_content = get_parsed_text()
+ #var text_content = get_parsed_text()
- if text_content.contains("[b]"):
- return 1.08
+ #if text_content.contains("[b]"):
+ # return 1.08
return 1.0
diff --git a/flumi/Scripts/Utils/Lua/Network.gd b/flumi/Scripts/Utils/Lua/Network.gd
index c72a95d..44ae670 100644
--- a/flumi/Scripts/Utils/Lua/Network.gd
+++ b/flumi/Scripts/Utils/Lua/Network.gd
@@ -1,29 +1,31 @@
class_name LuaNetworkUtils
extends RefCounted
+
+static var gurt_client: GurtProtocolClient = null
+static var current_domain: String = ""
+static var client_last_used: int = 0
+static var client_timeout_ms: int = 30000 # 30 seconds timeout for idle connections
+
static func setup_network_api(vm: LuauVM):
vm.lua_pushcallable(_lua_fetch_handler, "fetch")
vm.lua_setglobal("fetch")
static func resolve_fetch_url(url: String) -> String:
- # If URL is already absolute, return as-is
if url.begins_with("http://") or url.begins_with("https://") or url.begins_with("gurt://"):
return url
- # Get current domain from main scene
var main_node = Engine.get_main_loop().current_scene
- var current_domain = ""
+
if main_node and main_node.has_method("get_current_url"):
current_domain = main_node.get_current_url()
- # If no current domain, default to gurt:// protocol for relative URLs
if current_domain.is_empty():
if url.begins_with("/"):
return "gurt://" + url.substr(1)
else:
return "gurt://" + url
- # Use URLUtils to resolve relative URL against current domain
return URLUtils.resolve_url(current_domain, url)
static func _lua_fetch_handler(vm: LuauVM) -> int:
@@ -272,7 +274,36 @@ static func make_http_request(url: String, method: String, headers: PackedString
http_client.close()
return response_data
-static var _gurt_client: GurtProtocolClient = null
+static func cleanup_idle_client():
+ if gurt_client != null and Time.get_ticks_msec() - client_last_used > client_timeout_ms:
+ gurt_client.disconnect()
+ gurt_client = null
+ current_domain = ""
+
+static func get_or_create_gurt_client(domain: String) -> GurtProtocolClient:
+ cleanup_idle_client()
+
+ if gurt_client != null and current_domain == domain:
+ client_last_used = Time.get_ticks_msec()
+ return gurt_client
+
+ if gurt_client != null:
+ gurt_client.disconnect()
+ gurt_client = null
+
+ gurt_client = GurtProtocolClient.new()
+
+ for ca_cert in CertificateManager.trusted_ca_certificates:
+ gurt_client.add_ca_certificate(ca_cert)
+
+ if not gurt_client.create_client(10):
+ gurt_client = null
+ current_domain = ""
+ return null
+
+ current_domain = domain
+ client_last_used = Time.get_ticks_msec()
+ return gurt_client
static func make_gurt_request(url: String, method: String, headers: PackedStringArray, body: String) -> Dictionary:
var response_data = {
@@ -282,15 +313,17 @@ static func make_gurt_request(url: String, method: String, headers: PackedString
"body": ""
}
- # Reuse existing client or create new one
- if _gurt_client == null:
- _gurt_client = GurtProtocolClient.new()
- if not _gurt_client.create_client(10):
- response_data.status = 0
- response_data.status_text = "Connection Failed"
- return response_data
+ var domain_part = url.replace("gurt://", "")
+ if domain_part.contains("/"):
+ domain_part = domain_part.split("/")[0]
+ if domain_part.contains(":"):
+ domain_part = domain_part.split(":")[0]
- var client = _gurt_client
+ var client = get_or_create_gurt_client(domain_part)
+ if client == null:
+ response_data.status = 0
+ response_data.status_text = "Connection Failed"
+ return response_data
# Convert headers array to dictionary
var headers_dict = {}
@@ -312,9 +345,6 @@ static func make_gurt_request(url: String, method: String, headers: PackedString
var response = client.request(url, options)
- # Keep connection alive for reuse instead of disconnecting after every request
- # client.disconnect()
-
if not response:
response_data.status = 0
response_data.status_text = "No Response"
@@ -331,3 +361,9 @@ static func make_gurt_request(url: String, method: String, headers: PackedString
response_data.body = str(body_content)
return response_data
+
+static func cleanup_connections():
+ if gurt_client != null:
+ gurt_client.disconnect()
+ gurt_client = null
+ current_domain = ""
diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd
index 16b6f6a..118a406 100644
--- a/flumi/Scripts/main.gd
+++ b/flumi/Scripts/main.gd
@@ -53,6 +53,8 @@ func _ready():
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
DisplayServer.window_set_min_size(MIN_SIZE)
+ CertificateManager.initialize()
+
call_deferred("render")
var current_domain = "" # Store current domain for display
diff --git a/flumi/addons/gdluau/bin/windows/~gdluau.windows.template_debug.x86_64.dll b/flumi/addons/gdluau/bin/windows/~gdluau.windows.template_debug.x86_64.dll
deleted file mode 100644
index 7e832a1..0000000
Binary files a/flumi/addons/gdluau/bin/windows/~gdluau.windows.template_debug.x86_64.dll and /dev/null differ
diff --git a/flumi/addons/godot-flexbox/bin/windows/~godot-flexbox.windows.template_release.double.x86_64.dll b/flumi/addons/godot-flexbox/bin/windows/~godot-flexbox.windows.template_release.double.x86_64.dll
deleted file mode 100644
index dc6a041..0000000
Binary files a/flumi/addons/godot-flexbox/bin/windows/~godot-flexbox.windows.template_release.double.x86_64.dll and /dev/null differ
diff --git a/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll
index a5f623e..dad5170 100644
Binary files a/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll and b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll differ
diff --git a/flumi/addons/gurt-protocol/bin/windows/~gurt_godot.dll b/flumi/addons/gurt-protocol/bin/windows/~gurt_godot.dll
deleted file mode 100644
index a5f623e..0000000
Binary files a/flumi/addons/gurt-protocol/bin/windows/~gurt_godot.dll and /dev/null differ
diff --git a/protocol/cli/README.md b/protocol/cli/README.md
index 60d23b3..642484a 100644
--- a/protocol/cli/README.md
+++ b/protocol/cli/README.md
@@ -31,35 +31,34 @@ Gurty uses a TOML configuration file to manage server settings. The `gurty.templ
## Setup for Production
-For production deployments, you'll need to generate your own certificates since traditional Certificate Authorities don't support custom protocols:
+For production deployments, you can use the Gurted Certificate Authority to get proper TLS certificates:
-1. **Generate production certificates with OpenSSL:**
+1. **Install the Gurted CA CLI:**
+
+ 🔗 https://gurted.com/download
+
+2. **Request a certificate for your domain:**
```bash
- # Generate private key
- openssl genpkey -algorithm RSA -out gurt-server.key -pkcs8 -v
-
- # Generate certificate signing request
- openssl req -new -key gurt-server.key -out gurt-server.csr
-
- # Generate self-signed certificate (valid for 365 days)
- openssl x509 -req -days 365 -in gurt-server.csr -signkey gurt-server.key -out gurt-server.crt
-
- # Or generate both key and certificate in one step
- openssl req -x509 -newkey rsa:4096 -keyout gurt-server.key -out gurt-server.crt -days 365 -nodes
+ gurtca request yourdomain.web --output ./certs
```
-2. **Copy the configuration template and customize:**
+3. **Follow the DNS challenge instructions:**
+ When prompted, add the TXT record to your domain:
+ - Go to gurt://localhost:8877 (or your DNS server)
+ - Login and navigate to your domain
+ - Add a TXT record with:
+ - Name: `_gurtca-challenge`
+ - Value: (provided by the CLI tool)
+ - Press Enter to continue verification
+
+4. **Copy the configuration template and customize:**
```bash
cp gurty.template.toml gurty.toml
```
-3. **Deploy with production certificates and configuration:**
+5. **Deploy with CA-issued certificates:**
```bash
- gurty serve --config gurty.toml
- ```
- Or specify certificates explicitly:
- ```bash
- gurty serve --cert gurt-server.crt --key gurt-server.key --config gurty.toml
+ gurty serve --cert ./certs/yourdomain.web.crt --key ./certs/yourdomain.web.key --config gurty.toml
```
## Development Environment Setup
diff --git a/protocol/gdextension/src/lib.rs b/protocol/gdextension/src/lib.rs
index fce966c..742e06a 100644
--- a/protocol/gdextension/src/lib.rs
+++ b/protocol/gdextension/src/lib.rs
@@ -17,6 +17,7 @@ struct GurtProtocolClient {
client: Arc>>,
runtime: Arc>>,
+ ca_certificates: Arc>>,
}
#[derive(GodotClass)]
@@ -94,6 +95,15 @@ struct GurtProtocolServer {
#[godot_api]
impl GurtProtocolClient {
+ fn init(base: Base) -> Self {
+ Self {
+ base,
+ client: Arc::new(RefCell::new(None)),
+ runtime: Arc::new(RefCell::new(None)),
+ ca_certificates: Arc::new(RefCell::new(Vec::new())),
+ }
+ }
+
#[signal]
fn request_completed(response: Gd);
@@ -110,6 +120,9 @@ impl GurtProtocolClient {
let mut config = GurtClientConfig::default();
config.request_timeout = tokio::time::Duration::from_secs(timeout_seconds as u64);
+ // Add custom CA certificates
+ config.custom_ca_certificates = self.ca_certificates.borrow().clone();
+
let client = GurtClient::with_config(config);
*self.runtime.borrow_mut() = Some(runtime);
@@ -228,6 +241,21 @@ impl GurtProtocolClient {
gurt::DEFAULT_PORT as i32
}
+ #[func]
+ fn add_ca_certificate(&self, cert_pem: GString) {
+ self.ca_certificates.borrow_mut().push(cert_pem.to_string());
+ }
+
+ #[func]
+ fn clear_ca_certificates(&self) {
+ self.ca_certificates.borrow_mut().clear();
+ }
+
+ #[func]
+ fn get_ca_certificate_count(&self) -> i32 {
+ self.ca_certificates.borrow().len() as i32
+ }
+
fn convert_response(&self, response: GurtResponse) -> Gd {
let mut gd_response = GurtGDResponse::new_gd();
diff --git a/protocol/gurtca/Cargo.lock b/protocol/gurtca/Cargo.lock
new file mode 100644
index 0000000..59a2714
--- /dev/null
+++ b/protocol/gurtca/Cargo.lock
@@ -0,0 +1,1744 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
+dependencies = [
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff"
+dependencies = [
+ "bindgen",
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bindgen"
+version = "0.69.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cc"
+version = "1.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
+dependencies = [
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+
+[[package]]
+name = "cmake"
+version = "0.1.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "errno"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "gurt"
+version = "0.1.0"
+dependencies = [
+ "base64",
+ "chrono",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tokio-rustls",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "gurtca"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "base64",
+ "chrono",
+ "clap",
+ "gurt",
+ "openssl",
+ "serde",
+ "serde_json",
+ "tokio",
+ "uuid",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jobserver"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+dependencies = [
+ "getrandom 0.3.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.175"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+
+[[package]]
+name = "libloading"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.53.3",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
+dependencies = [
+ "aws-lc-rs",
+ "log",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.143"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.47.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "io-uring",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "slab",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec961601b32b6f5d14ae8dabd35ff2ff2e2c6cb4c0e6641845ff105abe96d958"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.3",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/protocol/gurtca/Cargo.toml b/protocol/gurtca/Cargo.toml
new file mode 100644
index 0000000..aa04a24
--- /dev/null
+++ b/protocol/gurtca/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "gurtca"
+version = "0.1.0"
+edition = "2021"
+
+[[bin]]
+name = "gurtca"
+path = "src/main.rs"
+
+[dependencies]
+clap = { version = "4.0", features = ["derive"] }
+tokio = { version = "1.0", features = ["full"] }
+gurt = { path = "../library" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+openssl = "0.10"
+base64 = "0.22"
+anyhow = "1.0"
+chrono = { version = "0.4", features = ["serde"] }
+uuid = { version = "1.0", features = ["v4"] }
\ No newline at end of file
diff --git a/protocol/gurtca/src/ca.rs b/protocol/gurtca/src/ca.rs
new file mode 100644
index 0000000..e69de29
diff --git a/protocol/gurtca/src/challenges.rs b/protocol/gurtca/src/challenges.rs
new file mode 100644
index 0000000..4fd47db
--- /dev/null
+++ b/protocol/gurtca/src/challenges.rs
@@ -0,0 +1,49 @@
+use anyhow::Result;
+use crate::client::{Challenge, GurtCAClient};
+
+pub async fn complete_dns_challenge(challenge: &Challenge, client: &GurtCAClient) -> Result<()> {
+ println!("Please add this TXT record to your domain:");
+ println!(" 1. Go to gurt://dns.web (or your DNS server)");
+ println!(" 2. Login and navigate to your domain: {}", challenge.domain);
+ println!(" 3. Add TXT record:");
+ println!(" Name: _gurtca-challenge");
+ println!(" Value: {}", challenge.verification_data);
+ println!(" 4. Press Enter when ready...");
+
+ let mut input = String::new();
+ std::io::stdin().read_line(&mut input)?;
+
+ println!("🔍 Verifying DNS record...");
+
+ if verify_dns_txt_record(&challenge.domain, &challenge.verification_data, client).await? {
+ println!("✅ DNS challenge completed successfully!");
+ Ok(())
+ } else {
+ anyhow::bail!("❌ DNS verification failed. Make sure the TXT record is correctly set.");
+ }
+}
+
+async fn verify_dns_txt_record(domain: &str, expected_value: &str, client: &GurtCAClient) -> Result {
+ let request = serde_json::json!({
+ "domain": format!("_gurtca-challenge.{}", domain),
+ "record_type": "TXT"
+ });
+
+ let response = client
+ .post_json("gurt://localhost:8877/resolve-full", &request)
+ .await?;
+
+ if response.is_success() {
+ let dns_response: serde_json::Value = serde_json::from_slice(&response.body)?;
+
+ if let Some(records) = dns_response["records"].as_array() {
+ for record in records {
+ if record["type"] == "TXT" && record["value"] == expected_value {
+ return Ok(true);
+ }
+ }
+ }
+ }
+
+ Ok(false)
+}
\ No newline at end of file
diff --git a/protocol/gurtca/src/client.rs b/protocol/gurtca/src/client.rs
new file mode 100644
index 0000000..d2cc4ea
--- /dev/null
+++ b/protocol/gurtca/src/client.rs
@@ -0,0 +1,147 @@
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use gurt::prelude::*;
+
+pub struct GurtCAClient {
+ ca_url: String,
+ gurt_client: GurtClient,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct CertificateRequest {
+ pub domain: String,
+ pub csr: String,
+ pub challenge_type: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Challenge {
+ pub token: String,
+ pub challenge_type: String,
+ pub domain: String,
+ pub verification_data: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Certificate {
+ pub cert_pem: String,
+ pub chain_pem: String,
+ pub expires_at: chrono::DateTime,
+}
+
+impl GurtCAClient {
+ pub fn new(ca_url: String) -> Result {
+ let gurt_client = GurtClient::new();
+
+ Ok(Self {
+ ca_url,
+ gurt_client,
+ })
+ }
+
+ pub async fn new_with_ca_discovery(ca_url: String) -> Result {
+ println!("🔍 Attempting to connect with system CA trust store...");
+
+ let test_client = Self::new(ca_url.clone())?;
+
+ match test_client.test_connection().await {
+ Ok(_) => {
+ println!("✅ Connection successful with system CA trust store");
+ return Ok(test_client);
+ }
+ Err(e) => {
+ if e.to_string().contains("UnknownIssuer") {
+ anyhow::bail!("Custom CA certificate required - server not trusted by system")
+ } else {
+ return Err(e);
+ }
+ }
+ }
+ }
+
+ async fn test_connection(&self) -> Result<()> {
+ // Try a simple request to test if connection works
+ let _response = self.gurt_client
+ .get(&format!("{}/ca/root", self.ca_url))
+ .await?;
+ Ok(())
+ }
+
+ pub async fn fetch_ca_certificate(&self) -> Result {
+ let response = self.gurt_client
+ .get(&format!("{}/ca/root", self.ca_url))
+ .await?;
+
+ if response.is_success() {
+ let ca_cert = response.text()?;
+ // Basic validation that this looks like a PEM certificate
+ if ca_cert.contains("BEGIN CERTIFICATE") && ca_cert.contains("END CERTIFICATE") {
+ Ok(ca_cert)
+ } else {
+ anyhow::bail!("Invalid CA certificate format received")
+ }
+ } else {
+ anyhow::bail!("Failed to fetch CA certificate: HTTP {}", response.status_code)
+ }
+ }
+
+ pub async fn verify_domain_exists(&self, domain: &str) -> Result {
+ let response = self.gurt_client
+ .get(&format!("{}/verify-ownership/{}", self.ca_url, domain))
+ .await?;
+
+ if response.is_success() {
+ let result: serde_json::Value = serde_json::from_slice(&response.body)?;
+ Ok(result["exists"].as_bool().unwrap_or(false))
+ } else {
+ Ok(false)
+ }
+ }
+
+ pub async fn request_certificate(&self, domain: &str, csr: &str) -> Result {
+ let request = CertificateRequest {
+ domain: domain.to_string(),
+ csr: csr.to_string(),
+ challenge_type: "dns".to_string(),
+ };
+
+ let response = self.gurt_client
+ .post_json(&format!("{}/ca/request-certificate", self.ca_url), &request)
+ .await?;
+
+ if response.is_success() {
+ let challenge: Challenge = serde_json::from_slice(&response.body)?;
+ Ok(challenge)
+ } else {
+ let error_text = response.text()?;
+ anyhow::bail!("Certificate request failed: {}", error_text)
+ }
+ }
+
+ pub async fn poll_certificate(&self, challenge_token: &str) -> Result {
+ for _ in 0..60 {
+ let response = self.gurt_client
+ .get(&format!("{}/ca/certificate/{}", self.ca_url, challenge_token))
+ .await?;
+
+ if response.is_success() {
+ let body_text = response.text()?;
+ if body_text.trim().is_empty() {
+ // Empty response, certificate not ready yet
+ tokio::time::sleep(std::time::Duration::from_secs(5)).await;
+ continue;
+ }
+ let cert: Certificate = serde_json::from_str(&body_text)?;
+ return Ok(cert);
+ } else if response.status_code == 202 {
+ tokio::time::sleep(std::time::Duration::from_secs(5)).await;
+ continue;
+ } else {
+ let error_text = response.text()?;
+ anyhow::bail!("Certificate polling failed: {}", error_text);
+ }
+ }
+
+ anyhow::bail!("Certificate issuance timed out")
+ }
+}
\ No newline at end of file
diff --git a/protocol/gurtca/src/crypto.rs b/protocol/gurtca/src/crypto.rs
new file mode 100644
index 0000000..db401fc
--- /dev/null
+++ b/protocol/gurtca/src/crypto.rs
@@ -0,0 +1,32 @@
+use anyhow::Result;
+use openssl::pkey::PKey;
+use openssl::rsa::Rsa;
+use openssl::x509::X509Req;
+use openssl::x509::X509Name;
+use openssl::hash::MessageDigest;
+
+pub fn generate_key_and_csr(domain: &str) -> Result<(String, String)> {
+ let rsa = Rsa::generate(2048)?;
+ let private_key = PKey::from_rsa(rsa)?;
+
+ let mut name_builder = X509Name::builder()?;
+ name_builder.append_entry_by_text("C", "US")?;
+ name_builder.append_entry_by_text("O", "Gurted Network")?;
+ name_builder.append_entry_by_text("CN", domain)?;
+ let name = name_builder.build();
+
+ let mut req_builder = X509Req::builder()?;
+ req_builder.set_subject_name(&name)?;
+ req_builder.set_pubkey(&private_key)?;
+ req_builder.sign(&private_key, MessageDigest::sha256())?;
+
+ let csr = req_builder.build();
+
+ let private_key_pem = private_key.private_key_to_pem_pkcs8()?;
+ let csr_pem = csr.to_pem()?;
+
+ Ok((
+ String::from_utf8(private_key_pem)?,
+ String::from_utf8(csr_pem)?
+ ))
+}
\ No newline at end of file
diff --git a/protocol/gurtca/src/main.rs b/protocol/gurtca/src/main.rs
new file mode 100644
index 0000000..538de32
--- /dev/null
+++ b/protocol/gurtca/src/main.rs
@@ -0,0 +1,111 @@
+use clap::{Parser, Subcommand};
+use anyhow::Result;
+
+mod challenges;
+mod crypto;
+mod client;
+
+#[derive(Parser)]
+#[command(name = "gurtca")]
+#[command(about = "Gurted Certificate Authority CLI - Get TLS certificates for your domains")]
+struct Cli {
+ #[command(subcommand)]
+ command: Commands,
+
+ #[arg(long, default_value = "gurt://localhost:8877")]
+ ca_url: String,
+}
+
+#[derive(Subcommand)]
+enum Commands {
+ Request {
+ domain: String,
+
+ #[arg(long, default_value = "./certs")]
+ output: String,
+ },
+ GetCa {
+ #[arg(long, default_value = "./ca.crt")]
+ output: String,
+ },
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ let cli = Cli::parse();
+
+ let client = client::GurtCAClient::new_with_ca_discovery(cli.ca_url).await?;
+
+ match cli.command {
+ Commands::Request { domain, output } => {
+ println!("🔐 Requesting certificate for: {}", domain);
+ request_certificate(&client, &domain, &output).await?;
+ },
+ Commands::GetCa { output } => {
+ println!("📋 Fetching CA certificate from server...");
+ get_ca_certificate(&client, &output).await?;
+ },
+ }
+
+ Ok(())
+}
+
+async fn request_certificate(
+ client: &client::GurtCAClient,
+ domain: &str,
+ output_dir: &str
+) -> Result<()> {
+ println!("🔍 Verifying domain exists...");
+ if !client.verify_domain_exists(domain).await? {
+ anyhow::bail!("❌ Domain does not exist or is not approved: {}", domain);
+ }
+
+ println!("🔑 Generating key pair...");
+ let (private_key, csr) = crypto::generate_key_and_csr(domain)?;
+
+ println!("📝 Submitting certificate request...");
+ let challenge = client.request_certificate(domain, &csr).await?;
+
+ println!("🧩 Completing DNS challenge...");
+ challenges::complete_dns_challenge(&challenge, client).await?;
+
+ println!("⏳ Waiting for certificate issuance...");
+ let certificate = client.poll_certificate(&challenge.token).await?;
+
+ println!("💾 Saving certificate files...");
+ std::fs::create_dir_all(output_dir)?;
+
+ std::fs::write(
+ format!("{}/{}.crt", output_dir, domain),
+ certificate.cert_pem
+ )?;
+
+ std::fs::write(
+ format!("{}/{}.key", output_dir, domain),
+ private_key
+ )?;
+
+ println!("✅ Certificate successfully issued for: {}", domain);
+ println!("📁 Files saved to: {}", output_dir);
+ println!(" - Certificate: {}/{}.crt", output_dir, domain);
+ println!(" - Private Key: {}/{}.key", output_dir, domain);
+
+ Ok(())
+}
+
+async fn get_ca_certificate(
+ client: &client::GurtCAClient,
+ output_path: &str
+) -> Result<()> {
+ let ca_cert = client.fetch_ca_certificate().await?;
+
+ std::fs::write(output_path, &ca_cert)?;
+
+ println!("✅ CA certificate saved to: {}", output_path);
+ println!("💡 To trust this CA system-wide:");
+ println!(" Windows: Import {} into 'Trusted Root Certification Authorities'", output_path);
+ println!(" macOS: sudo security add-trusted-cert -d -r trustRoot -k /System/Library/Keychains/SystemRootCertificates.keychain {}", output_path);
+ println!(" Linux: Copy {} to /usr/local/share/ca-certificates/ and run sudo update-ca-certificates", output_path);
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/protocol/library/src/client.rs b/protocol/library/src/client.rs
index 2d5269b..09e7ed1 100644
--- a/protocol/library/src/client.rs
+++ b/protocol/library/src/client.rs
@@ -23,6 +23,7 @@ pub struct GurtClientConfig {
pub max_redirects: usize,
pub enable_connection_pooling: bool,
pub max_connections_per_host: usize,
+ pub custom_ca_certificates: Vec,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -46,6 +47,7 @@ impl Default for GurtClientConfig {
max_redirects: 5,
enable_connection_pooling: true,
max_connections_per_host: 4,
+ custom_ca_certificates: Vec::new(),
}
}
}
@@ -53,21 +55,18 @@ impl Default for GurtClientConfig {
#[derive(Debug)]
enum Connection {
Plain(TcpStream),
- Tls(tokio_rustls::client::TlsStream),
}
impl Connection {
async fn read(&mut self, buf: &mut [u8]) -> Result {
match self {
Connection::Plain(stream) => stream.read(buf).await.map_err(|e| GurtError::connection(e.to_string())),
- Connection::Tls(stream) => stream.read(buf).await.map_err(|e| GurtError::connection(e.to_string())),
}
}
async fn write_all(&mut self, buf: &[u8]) -> Result<()> {
match self {
Connection::Plain(stream) => stream.write_all(buf).await.map_err(|e| GurtError::connection(e.to_string())),
- Connection::Tls(stream) => stream.write_all(buf).await.map_err(|e| GurtError::connection(e.to_string())),
}
}
}
@@ -81,10 +80,6 @@ impl PooledConnection {
fn new(stream: TcpStream) -> Self {
Self { connection: Connection::Plain(stream) }
}
-
- fn with_tls(stream: tokio_rustls::client::TlsStream) -> Self {
- Self { connection: Connection::Tls(stream) }
- }
}
pub struct GurtClient {
@@ -257,7 +252,6 @@ impl GurtClient {
let tcp_stream = match plain_conn.connection {
Connection::Plain(stream) => stream,
- _ => return Err(GurtError::protocol("Expected plain connection for handshake")),
};
self.upgrade_to_tls(tcp_stream, host).await
@@ -275,8 +269,27 @@ impl GurtClient {
added += 1;
}
}
+
+ for ca_cert_pem in &self.config.custom_ca_certificates {
+ let mut pem_bytes = ca_cert_pem.as_bytes();
+ let cert_iter = rustls_pemfile::certs(&mut pem_bytes);
+ for cert_result in cert_iter {
+ match cert_result {
+ Ok(cert) => {
+ if root_store.add(cert).is_ok() {
+ added += 1;
+ debug!("Added custom CA certificate");
+ }
+ }
+ Err(e) => {
+ debug!("Failed to parse CA certificate: {}", e);
+ }
+ }
+ }
+ }
+
if added == 0 {
- return Err(GurtError::crypto("No valid system certificates found".to_string()));
+ return Err(GurtError::crypto("No valid certificates found (system or custom)".to_string()));
}
let mut client_config = TlsClientConfig::builder()
diff --git a/protocol/library/src/server.rs b/protocol/library/src/server.rs
index faabcbf..42f940a 100644
--- a/protocol/library/src/server.rs
+++ b/protocol/library/src/server.rs
@@ -7,7 +7,7 @@ use crate::{
};
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
-use tokio::time::{timeout, Duration};
+use tokio::time::Duration;
use tokio_rustls::{TlsAcceptor, server::TlsStream};
use rustls::pki_types::CertificateDer;
use std::collections::HashMap;
@@ -293,76 +293,56 @@ 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;
+ 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;
+ 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 +373,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();