CA
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
*target*
|
*target*
|
||||||
*.pem
|
*.pem
|
||||||
*gurty.toml
|
gurty.toml
|
||||||
|
certs
|
||||||
60
dns/Cargo.lock
generated
60
dns/Cargo.lock
generated
@@ -797,6 +797,21 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
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]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -1545,12 +1560,50 @@ version = "1.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
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]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
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]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -3082,6 +3135,9 @@ name = "uuid"
|
|||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uwl"
|
name = "uwl"
|
||||||
@@ -3250,6 +3306,7 @@ name = "webx_dns"
|
|||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64 0.22.1",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -3260,6 +3317,7 @@ dependencies = [
|
|||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"log",
|
"log",
|
||||||
"macros-rs",
|
"macros-rs",
|
||||||
|
"openssl",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"prettytable",
|
"prettytable",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -3267,9 +3325,11 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serenity",
|
"serenity",
|
||||||
|
"sha2",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -26,3 +26,7 @@ clap = { version = "4.5.4", features = ["derive"] }
|
|||||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||||
serde = { version = "1.0.203", features = ["derive"] }
|
serde = { version = "1.0.203", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
sha2 = "0.10"
|
||||||
|
base64 = "0.22"
|
||||||
|
uuid = { version = "1.0", features = ["v4"] }
|
||||||
|
openssl = "0.10"
|
||||||
|
|||||||
@@ -116,7 +116,6 @@
|
|||||||
<option value="AAAA">AAAA</option>
|
<option value="AAAA">AAAA</option>
|
||||||
<option value="CNAME">CNAME</option>
|
<option value="CNAME">CNAME</option>
|
||||||
<option value="TXT">TXT</option>
|
<option value="TXT">TXT</option>
|
||||||
<option value="NS">NS</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="form-group">
|
<div style="form-group">
|
||||||
@@ -131,7 +130,6 @@
|
|||||||
<span id="help-AAAA" style="hidden">Enter an IPv6 address (e.g., 2001:db8::1)</span>
|
<span id="help-AAAA" style="hidden">Enter an IPv6 address (e.g., 2001:db8::1)</span>
|
||||||
<span id="help-CNAME" style="hidden">Enter a domain name (e.g., example.com)</span>
|
<span id="help-CNAME" style="hidden">Enter a domain name (e.g., example.com)</span>
|
||||||
<span id="help-TXT" style="hidden">Enter any text content</span>
|
<span id="help-TXT" style="hidden">Enter any text content</span>
|
||||||
<span id="help-NS" style="hidden">Enter a nameserver domain (e.g., ns1.example.com)</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="form-group">
|
<div style="form-group">
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ renderRecords = function(appendOnly)
|
|||||||
local typeCell = gurt.create('div', { text = record.type, style = 'font-bold' })
|
local typeCell = gurt.create('div', { text = record.type, style = 'font-bold' })
|
||||||
local nameCell = gurt.create('div', { text = record.name or '@' })
|
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 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 actionsCell = gurt.create('div')
|
||||||
local deleteBtn = gurt.create('button', {
|
local deleteBtn = gurt.create('button', {
|
||||||
@@ -227,12 +227,6 @@ end
|
|||||||
|
|
||||||
local function addRecord(type, name, value, ttl)
|
local function addRecord(type, name, value, ttl)
|
||||||
hideError('record-error')
|
hideError('record-error')
|
||||||
print('Adding DNS record: ' .. type .. ' ' .. name .. ' ' .. value)
|
|
||||||
print('Network request details:')
|
|
||||||
print(' URL: gurt://localhost:8877/domain/' .. domainName .. '/records')
|
|
||||||
print(' Method: POST')
|
|
||||||
print(' Auth token: ' .. (authToken and 'present' or 'missing'))
|
|
||||||
print(' Domain name: ' .. (domainName or 'nil'))
|
|
||||||
|
|
||||||
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', {
|
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', {
|
||||||
method = 'POST',
|
method = 'POST',
|
||||||
@@ -248,38 +242,24 @@ local function addRecord(type, name, value, ttl)
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
print('Response received: ' .. tostring(response))
|
|
||||||
|
|
||||||
if response then
|
if response then
|
||||||
print('Response status: ' .. tostring(response.status))
|
|
||||||
print('Response ok: ' .. tostring(response:ok()))
|
|
||||||
|
|
||||||
if response:ok() then
|
if response:ok() then
|
||||||
print('DNS record added successfully')
|
|
||||||
|
|
||||||
-- Clear form
|
|
||||||
gurt.select('#record-name').value = ''
|
gurt.select('#record-name').value = ''
|
||||||
gurt.select('#record-value').value = ''
|
gurt.select('#record-value').value = ''
|
||||||
gurt.select('#record-ttl').value = '3600'
|
gurt.select('#record-ttl').value = ''
|
||||||
|
|
||||||
-- Add the new record to existing records array
|
|
||||||
local newRecord = response:json()
|
local newRecord = response:json()
|
||||||
if newRecord and newRecord.id then
|
if newRecord and newRecord.id then
|
||||||
-- Server returned the created record, add it to our local array
|
|
||||||
table.insert(records, newRecord)
|
table.insert(records, newRecord)
|
||||||
|
|
||||||
-- Check if we had no records before (showing empty message)
|
local wasEmpty = (#records == 1)
|
||||||
local wasEmpty = (#records == 1) -- If this is the first record
|
|
||||||
|
|
||||||
if wasEmpty then
|
if wasEmpty then
|
||||||
-- Full re-render to replace empty message with proper table
|
|
||||||
renderRecords(false)
|
renderRecords(false)
|
||||||
else
|
else
|
||||||
-- Just append the new record to existing table
|
|
||||||
renderRecords(true)
|
renderRecords(true)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- Server didn't return record details, reload to get the actual data
|
|
||||||
loadRecords()
|
loadRecords()
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -288,7 +268,6 @@ local function addRecord(type, name, value, ttl)
|
|||||||
print('Failed to add DNS record: ' .. error)
|
print('Failed to add DNS record: ' .. error)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
print('No response received from server')
|
|
||||||
showError('record-error', 'No response from server - connection failed')
|
showError('record-error', 'No response from server - connection failed')
|
||||||
print('Failed to add DNS record: No response')
|
print('Failed to add DNS record: No response')
|
||||||
end
|
end
|
||||||
@@ -310,7 +289,7 @@ local function updateHelpText()
|
|||||||
local recordType = gurt.select('#record-type').value
|
local recordType = gurt.select('#record-type').value
|
||||||
|
|
||||||
-- Hide all help texts
|
-- Hide all help texts
|
||||||
local helpTypes = {'A', 'AAAA', 'CNAME', 'TXT', 'NS'}
|
local helpTypes = {'A', 'AAAA', 'CNAME', 'TXT'}
|
||||||
for _, helpType in ipairs(helpTypes) do
|
for _, helpType in ipairs(helpTypes) do
|
||||||
local helpElement = gurt.select('#help-' .. helpType)
|
local helpElement = gurt.select('#help-' .. helpType)
|
||||||
if helpElement then
|
if helpElement then
|
||||||
@@ -330,7 +309,7 @@ local function updateHelpText()
|
|||||||
valueInput.placeholder = '192.168.1.1'
|
valueInput.placeholder = '192.168.1.1'
|
||||||
elseif recordType == 'AAAA' then
|
elseif recordType == 'AAAA' then
|
||||||
valueInput.placeholder = '2001:db8::1'
|
valueInput.placeholder = '2001:db8::1'
|
||||||
elseif recordType == 'CNAME' or recordType == 'NS' then
|
elseif recordType == 'CNAME' then
|
||||||
valueInput.placeholder = 'example.com'
|
valueInput.placeholder = 'example.com'
|
||||||
elseif recordType == 'TXT' then
|
elseif recordType == 'TXT' then
|
||||||
valueInput.placeholder = 'Any text content'
|
valueInput.placeholder = 'Any text content'
|
||||||
@@ -346,7 +325,7 @@ gurt.select('#add-record-btn'):on('click', function()
|
|||||||
local recordType = gurt.select('#record-type').value
|
local recordType = gurt.select('#record-type').value
|
||||||
local recordName = gurt.select('#record-name').value
|
local recordName = gurt.select('#record-name').value
|
||||||
local recordValue = gurt.select('#record-value').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
|
if not recordValue or recordValue == '' then
|
||||||
showError('record-error', 'Record value is required')
|
showError('record-error', 'Record value is required')
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<input id="password" type="password" placeholder="Password" required="true" />
|
<input id="password" type="password" placeholder="Password" required="true" />
|
||||||
<button type="submit" id="submit">Log In</button>
|
<button type="submit" id="submit">Log In</button>
|
||||||
</form>
|
</form>
|
||||||
<p style="text-center mt-4 text-[#999999] text-base">Don't have an account? <a href="#">Register here</a></p>
|
<p style="text-center mt-4 text-[#999999] text-base">Don't have an account? <a href="/signup.html">Register here</a></p>
|
||||||
|
|
||||||
<p id="log-output" style="min-h-24"></p>
|
<p id="log-output" style="min-h-24"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
68
dns/frontend/signup.html
Normal file
68
dns/frontend/signup.html
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<head></head>
|
||||||
|
<title>Sign Up</title>
|
||||||
|
<icon src="https://cdn-icons-png.flaticon.com/512/295/295128.png">
|
||||||
|
<meta name="theme-color" content="#1b1b1b">
|
||||||
|
<meta name="description" content="Create a new account">
|
||||||
|
|
||||||
|
<font name="roboto" src="https://fonts.gstatic.com/s/roboto/v48/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
bg-[#171616] font-sans text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-card {
|
||||||
|
bg-[#262626] p-8 rounded-lg shadow-lg max-w-md mx-auto my-auto h-full
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-3xl font-bold text-center mb-6
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white mb-4 placeholder:text-[#999999] outline-none active:border-red-500
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
bg-[#dc2626] text-white font-medium p-3 rounded-lg w-full cursor-pointer transition-colors hover:bg-[#b91c1c] active:bg-[#991b1b]
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-[#ef4444] hover:text-[#dc2626] cursor-pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
#log-output {
|
||||||
|
text-[#fca5a5] p-4 rounded-md mt-4 font-mono max-h-40
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
bg-[#1f2937] p-4 rounded-md mb-4 border border-[#374151]
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
text-[#d1d5db] text-sm
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="signup.lua" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div style="signup-card">
|
||||||
|
<h1>Sign Up</h1>
|
||||||
|
|
||||||
|
<div style="info-box">
|
||||||
|
<p style="info-text">New users get 3 free domain registrations to get started!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="signup-form" style="mx-auto">
|
||||||
|
<input id="username" type="text" placeholder="Username" required="true" />
|
||||||
|
<input id="password" type="password" placeholder="Password" required="true" />
|
||||||
|
<input id="confirm-password" type="password" placeholder="Confirm Password" required="true" />
|
||||||
|
<button type="submit" id="submit">Create Account</button>
|
||||||
|
</form>
|
||||||
|
<p style="text-center mt-4 text-[#999999] text-base">Already have an account? <a href="index.html">Login here</a></p>
|
||||||
|
|
||||||
|
<p id="log-output" style="min-h-24"></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
96
dns/frontend/signup.lua
Normal file
96
dns/frontend/signup.lua
Normal file
@@ -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)
|
||||||
33
dns/migrations/005_add_certificate_challenges.sql
Normal file
33
dns/migrations/005_add_certificate_challenges.sql
Normal file
@@ -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);
|
||||||
7
dns/migrations/007_cleanup_invalid_records.sql
Normal file
7
dns/migrations/007_cleanup_invalid_records.sql
Normal file
@@ -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'));
|
||||||
8
dns/migrations/008_add_ca_storage.sql
Normal file
8
dns/migrations/008_add_ca_storage.sql
Normal file
@@ -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
|
||||||
|
);
|
||||||
2
dns/migrations/009_add_csr_to_challenges.sql
Normal file
2
dns/migrations/009_add_csr_to_challenges.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add CSR field to certificate challenges
|
||||||
|
ALTER TABLE certificate_challenges ADD COLUMN IF NOT EXISTS csr_pem TEXT;
|
||||||
114
dns/src/crypto.rs
Normal file
114
dns/src/crypto.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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_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 context = cert_builder.x509v3_context(None, None);
|
||||||
|
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<String> {
|
||||||
|
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 subject_alt_name = openssl::x509::extension::SubjectAlternativeName::new()
|
||||||
|
.dns(domain)
|
||||||
|
.dns("localhost")
|
||||||
|
.ip("127.0.0.1")
|
||||||
|
.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)?)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ mod auth_routes;
|
|||||||
mod helpers;
|
mod helpers;
|
||||||
mod models;
|
mod models;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
mod ca;
|
||||||
|
|
||||||
use crate::{auth::jwt_middleware_gurt, config::Config, discord_bot};
|
use crate::{auth::jwt_middleware_gurt, config::Config, discord_bot};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
@@ -97,6 +98,10 @@ enum HandlerType {
|
|||||||
CreateDomainRecord,
|
CreateDomainRecord,
|
||||||
ResolveDomain,
|
ResolveDomain,
|
||||||
ResolveFullDomain,
|
ResolveFullDomain,
|
||||||
|
VerifyDomainOwnership,
|
||||||
|
RequestCertificate,
|
||||||
|
GetCertificate,
|
||||||
|
GetCaCertificate,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GurtHandler for AppHandler {
|
impl GurtHandler for AppHandler {
|
||||||
@@ -167,6 +172,10 @@ impl GurtHandler for AppHandler {
|
|||||||
},
|
},
|
||||||
HandlerType::ResolveDomain => routes::resolve_domain(&ctx, app_state).await,
|
HandlerType::ResolveDomain => routes::resolve_domain(&ctx, app_state).await,
|
||||||
HandlerType::ResolveFullDomain => routes::resolve_full_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();
|
let duration = start_time.elapsed();
|
||||||
@@ -237,7 +246,11 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
|
|||||||
.route(Route::put("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::UpdateDomain })
|
.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"), 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::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());
|
log::info!("GURT server listening on {}", config.get_address());
|
||||||
server.listen(&config.get_address()).await.map_err(|e| {
|
server.listen(&config.get_address()).await.map_err(|e| {
|
||||||
|
|||||||
45
dns/src/gurt_server/ca.rs
Normal file
45
dns/src/gurt_server/ca.rs
Normal file
@@ -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<CaCertificate> {
|
||||||
|
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<Option<CaCertificate>> {
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ pub(crate) struct ResponseDnsRecord {
|
|||||||
pub(crate) record_type: String,
|
pub(crate) record_type: String,
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
pub(crate) value: String,
|
pub(crate) value: String,
|
||||||
pub(crate) ttl: i32,
|
pub(crate) ttl: Option<i32>,
|
||||||
pub(crate) priority: Option<i32>,
|
pub(crate) priority: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -390,7 +390,7 @@ pub(crate) async fn get_domain_records(ctx: &ServerContext, app_state: AppState,
|
|||||||
record_type: record.record_type,
|
record_type: record.record_type,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
value: record.value,
|
value: record.value,
|
||||||
ttl: record.ttl.unwrap_or(3600),
|
ttl: record.ttl,
|
||||||
priority: record.priority,
|
priority: record.priority,
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
@@ -445,13 +445,13 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
|
|||||||
return Ok(GurtResponse::bad_request().with_string_body("Record type is required"));
|
return Ok(GurtResponse::bad_request().with_string_body("Record type is required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let valid_types = ["A", "AAAA", "CNAME", "TXT", "NS"];
|
let valid_types = ["A", "AAAA", "CNAME", "TXT"];
|
||||||
if !valid_types.contains(&record_data.record_type.as_str()) {
|
if !valid_types.contains(&record_data.record_type.as_str()) {
|
||||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type. Only A, AAAA, CNAME, TXT, and NS records are supported."));
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type. Only A, AAAA, CNAME, and TXT records are supported."));
|
||||||
}
|
}
|
||||||
|
|
||||||
let record_name = record_data.name.unwrap_or_else(|| "@".to_string());
|
let record_name = record_data.name.unwrap_or_else(|| "@".to_string());
|
||||||
let ttl = record_data.ttl.unwrap_or(3600);
|
let ttl = record_data.ttl.filter(|t| *t > 0);
|
||||||
|
|
||||||
match record_data.record_type.as_str() {
|
match record_data.record_type.as_str() {
|
||||||
"A" => {
|
"A" => {
|
||||||
@@ -464,9 +464,9 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
|
|||||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid IPv6 address for AAAA record"));
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid IPv6 address for AAAA record"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CNAME" | "NS" => {
|
"CNAME" => {
|
||||||
if record_data.value.is_empty() || !record_data.value.contains('.') {
|
if record_data.value.is_empty() || !record_data.value.contains('.') {
|
||||||
return Ok(GurtResponse::bad_request().with_string_body("CNAME and NS records must contain a valid domain name"));
|
return Ok(GurtResponse::bad_request().with_string_body("CNAME records must contain a valid domain name"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"TXT" => {
|
"TXT" => {
|
||||||
@@ -498,7 +498,7 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
|
|||||||
record_type: record_data.record_type,
|
record_type: record_data.record_type,
|
||||||
name: record_name,
|
name: record_name,
|
||||||
value: record_data.value,
|
value: record_data.value,
|
||||||
ttl,
|
ttl: Some(ttl.unwrap_or(3600)),
|
||||||
priority: record_data.priority,
|
priority: record_data.priority,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -637,7 +637,7 @@ async fn try_exact_match(query_name: &str, tld: &str, app_state: &AppState) -> R
|
|||||||
record_type: record.record_type,
|
record_type: record.record_type,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
value: record.value,
|
value: record.value,
|
||||||
ttl: record.ttl.unwrap_or(3600),
|
ttl: record.ttl,
|
||||||
priority: record.priority,
|
priority: record.priority,
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
@@ -718,7 +718,7 @@ async fn try_delegation_match(query_name: &str, tld: &str, app_state: &AppState)
|
|||||||
record_type: record.record_type,
|
record_type: record.record_type,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
value: record.value,
|
value: record.value,
|
||||||
ttl: record.ttl.unwrap_or(3600),
|
ttl: record.ttl,
|
||||||
priority: record.priority,
|
priority: record.priority,
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
@@ -761,6 +761,221 @@ pub(crate) async fn resolve_full_domain(ctx: &ServerContext, app_state: AppState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Certificate Authority endpoints
|
||||||
|
pub(crate) async fn verify_domain_ownership(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> {
|
||||||
|
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<Domain> = sqlx::query_as::<_, Domain>(
|
||||||
|
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
|
||||||
|
)
|
||||||
|
.bind(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<GurtResponse> {
|
||||||
|
#[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<Domain> = sqlx::query_as::<_, Domain>(
|
||||||
|
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
|
||||||
|
)
|
||||||
|
.bind(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<GurtResponse> {
|
||||||
|
let path_parts: Vec<&str> = ctx.path().split('/').collect();
|
||||||
|
if path_parts.len() < 4 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = path_parts[3];
|
||||||
|
|
||||||
|
let challenge: Option<(String, String, String, Option<String>, chrono::DateTime<chrono::Utc>)> = 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<Domain> = sqlx::query_as::<_, Domain>(
|
||||||
|
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
|
||||||
|
)
|
||||||
|
.bind(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<DnsRecord> = sqlx::query_as::<_, DnsRecord>(
|
||||||
|
"SELECT id, domain_id, record_type, name, value, ttl, priority, created_at FROM dns_records WHERE domain_id = $1 AND record_type = '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<GurtResponse> {
|
||||||
|
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<String> {
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
|
||||||
|
let data = format!("{}:{}", domain, token);
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(data.as_bytes());
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
|
||||||
|
Ok(base64::encode(hash))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct Error {
|
struct Error {
|
||||||
msg: &'static str,
|
msg: &'static str,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod config;
|
|||||||
mod gurt_server;
|
mod gurt_server;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod discord_bot;
|
mod discord_bot;
|
||||||
|
mod crypto;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use clap_verbosity_flag::{LogLevel, Verbosity};
|
use clap_verbosity_flag::{LogLevel, Verbosity};
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch(domain:github.com)"
|
|
||||||
],
|
|
||||||
"deny": [],
|
|
||||||
"ask": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
flumi/Assets/gurted-ca.crt
Normal file
30
flumi/Assets/gurted-ca.crt
Normal file
@@ -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-----
|
||||||
49
flumi/Scripts/CertificateManager.gd
Normal file
49
flumi/Scripts/CertificateManager.gd
Normal file
@@ -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")
|
||||||
1
flumi/Scripts/CertificateManager.gd.uid
Normal file
1
flumi/Scripts/CertificateManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bhnsb8ttn6f7n
|
||||||
@@ -141,6 +141,9 @@ static func fetch_dns_post_working(server: String, path: String, json_data: Stri
|
|||||||
var local_result = {}
|
var local_result = {}
|
||||||
var client = GurtProtocolClient.new()
|
var client = GurtProtocolClient.new()
|
||||||
|
|
||||||
|
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||||
|
client.add_ca_certificate(ca_cert)
|
||||||
|
|
||||||
if not client.create_client(10):
|
if not client.create_client(10):
|
||||||
local_result = {"error": "Failed to create client"}
|
local_result = {"error": "Failed to create client"}
|
||||||
else:
|
else:
|
||||||
@@ -191,6 +194,9 @@ static func fetch_dns_post_working(server: String, path: String, json_data: Stri
|
|||||||
static func fetch_content_via_gurt(ip: String, path: String = "/") -> Dictionary:
|
static func fetch_content_via_gurt(ip: String, path: String = "/") -> Dictionary:
|
||||||
var client = GurtProtocolClient.new()
|
var client = GurtProtocolClient.new()
|
||||||
|
|
||||||
|
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||||
|
client.add_ca_certificate(ca_cert)
|
||||||
|
|
||||||
if not client.create_client(30):
|
if not client.create_client(30):
|
||||||
return {"error": "Failed to create GURT client"}
|
return {"error": "Failed to create GURT client"}
|
||||||
|
|
||||||
@@ -219,6 +225,9 @@ static func fetch_content_via_gurt_direct(address: String, path: String = "/") -
|
|||||||
var local_result = {}
|
var local_result = {}
|
||||||
var client = GurtProtocolClient.new()
|
var client = GurtProtocolClient.new()
|
||||||
|
|
||||||
|
for ca_cert in CertificateManager.trusted_ca_certificates:
|
||||||
|
client.add_ca_certificate(ca_cert)
|
||||||
|
|
||||||
if not client.create_client(10):
|
if not client.create_client(10):
|
||||||
local_result = {"error": "Failed to create GURT client"}
|
local_result = {"error": "Failed to create GURT client"}
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ static func _lua_fetch_handler(vm: LuauVM) -> int:
|
|||||||
if not has_user_agent:
|
if not has_user_agent:
|
||||||
headers_array.append("User-Agent: " + UserAgent.get_user_agent())
|
headers_array.append("User-Agent: " + UserAgent.get_user_agent())
|
||||||
|
|
||||||
var response_data = make_http_request(url, method, headers_array, body)
|
var response_data = await make_http_request(url, method, headers_array, body)
|
||||||
|
|
||||||
# Create response object with actual data
|
# Create response object with actual data
|
||||||
vm.lua_newtable()
|
vm.lua_newtable()
|
||||||
@@ -127,7 +127,7 @@ static func _response_ok_handler(vm: LuauVM) -> int:
|
|||||||
|
|
||||||
static func make_http_request(url: String, method: String, headers: PackedStringArray, body: String) -> Dictionary:
|
static func make_http_request(url: String, method: String, headers: PackedStringArray, body: String) -> Dictionary:
|
||||||
if url.begins_with("gurt://"):
|
if url.begins_with("gurt://"):
|
||||||
return make_gurt_request(url, method, headers, body)
|
return await make_gurt_request(url, method, headers, body)
|
||||||
var http_client = HTTPClient.new()
|
var http_client = HTTPClient.new()
|
||||||
var response_data = {
|
var response_data = {
|
||||||
"status": 0,
|
"status": 0,
|
||||||
@@ -282,9 +282,20 @@ static func make_gurt_request(url: String, method: String, headers: PackedString
|
|||||||
"body": ""
|
"body": ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reuse existing client or create new one
|
var domain_part = url.replace("gurt://", "")
|
||||||
if _gurt_client == null:
|
if domain_part.contains("/"):
|
||||||
|
domain_part = domain_part.split("/")[0]
|
||||||
|
if domain_part.contains(":"):
|
||||||
|
domain_part = domain_part.split(":")[0]
|
||||||
|
|
||||||
|
if _gurt_client != null:
|
||||||
|
_gurt_client.disconnect()
|
||||||
|
|
||||||
_gurt_client = GurtProtocolClient.new()
|
_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):
|
if not _gurt_client.create_client(10):
|
||||||
response_data.status = 0
|
response_data.status = 0
|
||||||
response_data.status_text = "Connection Failed"
|
response_data.status_text = "Connection Failed"
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ func _ready():
|
|||||||
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
|
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
|
||||||
DisplayServer.window_set_min_size(MIN_SIZE)
|
DisplayServer.window_set_min_size(MIN_SIZE)
|
||||||
|
|
||||||
|
CertificateManager.initialize()
|
||||||
|
|
||||||
call_deferred("render")
|
call_deferred("render")
|
||||||
|
|
||||||
var current_domain = "" # Store current domain for display
|
var current_domain = "" # Store current domain for display
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -31,35 +31,34 @@ Gurty uses a TOML configuration file to manage server settings. The `gurty.templ
|
|||||||
|
|
||||||
## Setup for Production
|
## 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
|
```bash
|
||||||
# Generate private key
|
gurtca request yourdomain.web --output ./certs
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```bash
|
||||||
cp gurty.template.toml gurty.toml
|
cp gurty.template.toml gurty.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Deploy with production certificates and configuration:**
|
5. **Deploy with CA-issued certificates:**
|
||||||
```bash
|
```bash
|
||||||
gurty serve --config gurty.toml
|
gurty serve --cert ./certs/yourdomain.web.crt --key ./certs/yourdomain.web.key --config gurty.toml
|
||||||
```
|
|
||||||
Or specify certificates explicitly:
|
|
||||||
```bash
|
|
||||||
gurty serve --cert gurt-server.crt --key gurt-server.key --config gurty.toml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Environment Setup
|
## Development Environment Setup
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ struct GurtProtocolClient {
|
|||||||
|
|
||||||
client: Arc<RefCell<Option<GurtClient>>>,
|
client: Arc<RefCell<Option<GurtClient>>>,
|
||||||
runtime: Arc<RefCell<Option<Runtime>>>,
|
runtime: Arc<RefCell<Option<Runtime>>>,
|
||||||
|
ca_certificates: Arc<RefCell<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(GodotClass)]
|
#[derive(GodotClass)]
|
||||||
@@ -94,6 +95,15 @@ struct GurtProtocolServer {
|
|||||||
|
|
||||||
#[godot_api]
|
#[godot_api]
|
||||||
impl GurtProtocolClient {
|
impl GurtProtocolClient {
|
||||||
|
fn init(base: Base<RefCounted>) -> Self {
|
||||||
|
Self {
|
||||||
|
base,
|
||||||
|
client: Arc::new(RefCell::new(None)),
|
||||||
|
runtime: Arc::new(RefCell::new(None)),
|
||||||
|
ca_certificates: Arc::new(RefCell::new(Vec::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[signal]
|
#[signal]
|
||||||
fn request_completed(response: Gd<GurtGDResponse>);
|
fn request_completed(response: Gd<GurtGDResponse>);
|
||||||
|
|
||||||
@@ -110,6 +120,9 @@ impl GurtProtocolClient {
|
|||||||
let mut config = GurtClientConfig::default();
|
let mut config = GurtClientConfig::default();
|
||||||
config.request_timeout = tokio::time::Duration::from_secs(timeout_seconds as u64);
|
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);
|
let client = GurtClient::with_config(config);
|
||||||
|
|
||||||
*self.runtime.borrow_mut() = Some(runtime);
|
*self.runtime.borrow_mut() = Some(runtime);
|
||||||
@@ -228,6 +241,21 @@ impl GurtProtocolClient {
|
|||||||
gurt::DEFAULT_PORT as i32
|
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<GurtGDResponse> {
|
fn convert_response(&self, response: GurtResponse) -> Gd<GurtGDResponse> {
|
||||||
let mut gd_response = GurtGDResponse::new_gd();
|
let mut gd_response = GurtGDResponse::new_gd();
|
||||||
|
|
||||||
|
|||||||
1744
protocol/gurtca/Cargo.lock
generated
Normal file
1744
protocol/gurtca/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
protocol/gurtca/Cargo.toml
Normal file
20
protocol/gurtca/Cargo.toml
Normal file
@@ -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"] }
|
||||||
0
protocol/gurtca/src/ca.rs
Normal file
0
protocol/gurtca/src/ca.rs
Normal file
52
protocol/gurtca/src/challenges.rs
Normal file
52
protocol/gurtca/src/challenges.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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).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) -> Result<bool> {
|
||||||
|
use gurt::prelude::*;
|
||||||
|
let client = GurtClient::new();
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
167
protocol/gurtca/src/client.rs
Normal file
167
protocol/gurtca/src/client.rs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
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<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GurtCAClient {
|
||||||
|
pub fn new(ca_url: String) -> Result<Self> {
|
||||||
|
let gurt_client = GurtClient::new();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
ca_url,
|
||||||
|
gurt_client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_insecure(ca_url: String) -> Result<Self> {
|
||||||
|
println!("⚠️ WARNING: Using insecure mode - TLS certificates will not be verified!");
|
||||||
|
println!("⚠️ This should only be used for bootstrapping or testing purposes.");
|
||||||
|
|
||||||
|
// For now, just use default client - we'd need to add insecure support to GURT library
|
||||||
|
let gurt_client = GurtClient::new();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
ca_url,
|
||||||
|
gurt_client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_with_ca_discovery(ca_url: String) -> Result<Self> {
|
||||||
|
println!("🔍 Attempting to connect with system CA trust store...");
|
||||||
|
|
||||||
|
// Try default connection first - might work if server uses publicly trusted cert
|
||||||
|
let test_client = Self::new(ca_url.clone())?;
|
||||||
|
|
||||||
|
// Test connection to see if it works
|
||||||
|
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") {
|
||||||
|
println!("❌ Server uses custom CA certificate not in system trust store");
|
||||||
|
println!("💡 Solutions:");
|
||||||
|
println!(" 1. Ask server admin to provide CA certificate");
|
||||||
|
println!(" 2. Use --insecure flag for testing (not recommended)");
|
||||||
|
println!(" 3. Install server's CA certificate in system trust store");
|
||||||
|
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<String> {
|
||||||
|
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<bool> {
|
||||||
|
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<Challenge> {
|
||||||
|
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<Certificate> {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
32
protocol/gurtca/src/crypto.rs
Normal file
32
protocol/gurtca/src/crypto.rs
Normal file
@@ -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)?
|
||||||
|
))
|
||||||
|
}
|
||||||
118
protocol/gurtca/src/main.rs
Normal file
118
protocol/gurtca/src/main.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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,
|
||||||
|
|
||||||
|
#[arg(long, help = "Skip TLS certificate verification (insecure, for bootstrapping only)")]
|
||||||
|
insecure: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = if cli.insecure {
|
||||||
|
client::GurtCAClient::new_insecure(cli.ca_url)?
|
||||||
|
} else {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ pub struct GurtClientConfig {
|
|||||||
pub max_redirects: usize,
|
pub max_redirects: usize,
|
||||||
pub enable_connection_pooling: bool,
|
pub enable_connection_pooling: bool,
|
||||||
pub max_connections_per_host: usize,
|
pub max_connections_per_host: usize,
|
||||||
|
pub custom_ca_certificates: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
@@ -46,6 +47,7 @@ impl Default for GurtClientConfig {
|
|||||||
max_redirects: 5,
|
max_redirects: 5,
|
||||||
enable_connection_pooling: true,
|
enable_connection_pooling: true,
|
||||||
max_connections_per_host: 4,
|
max_connections_per_host: 4,
|
||||||
|
custom_ca_certificates: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,8 +277,27 @@ impl GurtClient {
|
|||||||
added += 1;
|
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 {
|
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()
|
let mut client_config = TlsClientConfig::builder()
|
||||||
|
|||||||
Reference in New Issue
Block a user