odac 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +21 -0
- package/.github/workflows/auto-pr-description.yml +49 -0
- package/.github/workflows/release.yml +32 -0
- package/.github/workflows/test-coverage.yml +58 -0
- package/.husky/pre-commit +2 -0
- package/.kiro/steering/code-style.md +56 -0
- package/.kiro/steering/product.md +20 -0
- package/.kiro/steering/structure.md +77 -0
- package/.kiro/steering/tech.md +87 -0
- package/.prettierrc +10 -0
- package/.releaserc.js +134 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +181 -0
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +661 -0
- package/README.md +57 -0
- package/SECURITY.md +26 -0
- package/bin/candy +10 -0
- package/bin/candypack +10 -0
- package/cli/index.js +3 -0
- package/cli/src/Cli.js +348 -0
- package/cli/src/Connector.js +93 -0
- package/cli/src/Monitor.js +416 -0
- package/core/Candy.js +87 -0
- package/core/Commands.js +239 -0
- package/core/Config.js +1094 -0
- package/core/Lang.js +52 -0
- package/core/Log.js +43 -0
- package/core/Process.js +26 -0
- package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
- package/docs/backend/01-overview/03-development-server.md +79 -0
- package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
- package/docs/backend/03-config/00-configuration-overview.md +214 -0
- package/docs/backend/03-config/01-database-connection.md +60 -0
- package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
- package/docs/backend/03-config/03-request-timeout.md +11 -0
- package/docs/backend/03-config/04-environment-variables.md +227 -0
- package/docs/backend/03-config/05-early-hints.md +352 -0
- package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
- package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
- package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
- package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
- package/docs/backend/04-routing/05-advanced-routing.md +14 -0
- package/docs/backend/04-routing/06-error-pages.md +101 -0
- package/docs/backend/04-routing/07-cron-jobs.md +149 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
- package/docs/backend/05-controllers/03-controller-classes.md +93 -0
- package/docs/backend/05-forms/01-custom-forms.md +395 -0
- package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
- package/docs/backend/07-views/01-the-view-directory.md +73 -0
- package/docs/backend/07-views/02-rendering-a-view.md +179 -0
- package/docs/backend/07-views/03-template-syntax.md +181 -0
- package/docs/backend/07-views/03-variables.md +328 -0
- package/docs/backend/07-views/04-request-data.md +231 -0
- package/docs/backend/07-views/05-conditionals.md +290 -0
- package/docs/backend/07-views/06-loops.md +353 -0
- package/docs/backend/07-views/07-translations.md +358 -0
- package/docs/backend/07-views/08-backend-javascript.md +398 -0
- package/docs/backend/07-views/09-comments.md +297 -0
- package/docs/backend/08-database/01-database-connection.md +99 -0
- package/docs/backend/08-database/02-using-mysql.md +322 -0
- package/docs/backend/09-validation/01-the-validator-service.md +424 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
- package/docs/backend/10-authentication/03-register.md +134 -0
- package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
- package/docs/backend/10-authentication/05-session-management.md +159 -0
- package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
- package/docs/backend/11-mail/01-the-mail-service.md +42 -0
- package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
- package/docs/backend/13-utilities/01-candy-var.md +504 -0
- package/docs/frontend/01-overview/01-introduction.md +146 -0
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
- package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
- package/docs/frontend/03-forms/01-form-handling.md +420 -0
- package/docs/frontend/04-api-requests/01-get-post.md +443 -0
- package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
- package/docs/index.json +452 -0
- package/docs/server/01-installation/01-quick-install.md +19 -0
- package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
- package/docs/server/02-get-started/01-core-concepts.md +7 -0
- package/docs/server/02-get-started/02-basic-commands.md +57 -0
- package/docs/server/02-get-started/03-cli-reference.md +276 -0
- package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
- package/docs/server/03-service/01-start-a-new-service.md +57 -0
- package/docs/server/03-service/02-delete-a-service.md +48 -0
- package/docs/server/04-web/01-create-a-website.md +36 -0
- package/docs/server/04-web/02-list-websites.md +9 -0
- package/docs/server/04-web/03-delete-a-website.md +29 -0
- package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
- package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
- package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
- package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
- package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
- package/docs/server/07-mail/04-change-account-password.md +23 -0
- package/eslint.config.mjs +120 -0
- package/framework/index.js +4 -0
- package/framework/src/Auth.js +309 -0
- package/framework/src/Candy.js +81 -0
- package/framework/src/Config.js +79 -0
- package/framework/src/Env.js +60 -0
- package/framework/src/Lang.js +57 -0
- package/framework/src/Mail.js +83 -0
- package/framework/src/Mysql.js +575 -0
- package/framework/src/Request.js +301 -0
- package/framework/src/Route/Cron.js +128 -0
- package/framework/src/Route/Internal.js +439 -0
- package/framework/src/Route.js +455 -0
- package/framework/src/Server.js +15 -0
- package/framework/src/Stream.js +163 -0
- package/framework/src/Token.js +37 -0
- package/framework/src/Validator.js +271 -0
- package/framework/src/Var.js +211 -0
- package/framework/src/View/EarlyHints.js +190 -0
- package/framework/src/View/Form.js +600 -0
- package/framework/src/View.js +513 -0
- package/framework/web/candy.js +838 -0
- package/jest.config.js +22 -0
- package/locale/de-DE.json +80 -0
- package/locale/en-US.json +79 -0
- package/locale/es-ES.json +80 -0
- package/locale/fr-FR.json +80 -0
- package/locale/pt-BR.json +80 -0
- package/locale/ru-RU.json +80 -0
- package/locale/tr-TR.json +85 -0
- package/locale/zh-CN.json +80 -0
- package/package.json +86 -0
- package/server/index.js +5 -0
- package/server/src/Api.js +88 -0
- package/server/src/DNS.js +940 -0
- package/server/src/Hub.js +535 -0
- package/server/src/Mail.js +571 -0
- package/server/src/SSL.js +180 -0
- package/server/src/Server.js +27 -0
- package/server/src/Service.js +248 -0
- package/server/src/Subdomain.js +64 -0
- package/server/src/Web/Firewall.js +170 -0
- package/server/src/Web/Proxy.js +134 -0
- package/server/src/Web.js +451 -0
- package/server/src/mail/imap.js +1091 -0
- package/server/src/mail/server.js +32 -0
- package/server/src/mail/smtp.js +786 -0
- package/test/cli/Cli.test.js +36 -0
- package/test/core/Candy.test.js +234 -0
- package/test/core/Commands.test.js +538 -0
- package/test/core/Config.test.js +1435 -0
- package/test/core/Lang.test.js +250 -0
- package/test/core/Process.test.js +156 -0
- package/test/framework/Route.test.js +239 -0
- package/test/framework/View/EarlyHints.test.js +282 -0
- package/test/scripts/check-coverage.js +132 -0
- package/test/server/Api.test.js +647 -0
- package/test/server/Client.test.js +338 -0
- package/test/server/DNS.test.js +2050 -0
- package/test/server/DNS.test.js.bak +2084 -0
- package/test/server/Log.test.js +73 -0
- package/test/server/Mail.account.test_.js +460 -0
- package/test/server/Mail.init.test_.js +411 -0
- package/test/server/Mail.test_.js +1340 -0
- package/test/server/SSL.test_.js +1491 -0
- package/test/server/Server.test.js +765 -0
- package/test/server/Service.test_.js +1127 -0
- package/test/server/Subdomain.test.js +440 -0
- package/test/server/Web/Firewall.test.js +175 -0
- package/test/server/Web.test_.js +1562 -0
- package/test/server/__mocks__/acme-client.js +17 -0
- package/test/server/__mocks__/bcrypt.js +50 -0
- package/test/server/__mocks__/child_process.js +389 -0
- package/test/server/__mocks__/crypto.js +432 -0
- package/test/server/__mocks__/fs.js +450 -0
- package/test/server/__mocks__/globalCandy.js +227 -0
- package/test/server/__mocks__/http-proxy.js +105 -0
- package/test/server/__mocks__/http.js +575 -0
- package/test/server/__mocks__/https.js +272 -0
- package/test/server/__mocks__/index.js +249 -0
- package/test/server/__mocks__/mail/server.js +100 -0
- package/test/server/__mocks__/mail/smtp.js +31 -0
- package/test/server/__mocks__/mailparser.js +81 -0
- package/test/server/__mocks__/net.js +369 -0
- package/test/server/__mocks__/node-forge.js +328 -0
- package/test/server/__mocks__/os.js +320 -0
- package/test/server/__mocks__/path.js +291 -0
- package/test/server/__mocks__/selfsigned.js +8 -0
- package/test/server/__mocks__/server/src/mail/server.js +100 -0
- package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
- package/test/server/__mocks__/smtp-server.js +106 -0
- package/test/server/__mocks__/sqlite3.js +394 -0
- package/test/server/__mocks__/testFactories.js +299 -0
- package/test/server/__mocks__/testHelpers.js +363 -0
- package/test/server/__mocks__/tls.js +229 -0
- package/watchdog/index.js +3 -0
- package/watchdog/src/Watchdog.js +156 -0
- package/web/config.json +5 -0
- package/web/controller/page/about.js +27 -0
- package/web/controller/page/index.js +34 -0
- package/web/package.json +18 -0
- package/web/public/assets/css/style.css +1835 -0
- package/web/public/assets/js/app.js +96 -0
- package/web/route/www.js +19 -0
- package/web/skeleton/main.html +22 -0
- package/web/view/content/about.html +65 -0
- package/web/view/content/home.html +205 -0
- package/web/view/footer/main.html +11 -0
- package/web/view/head/main.html +5 -0
- package/web/view/header/main.html +14 -0
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
const {log, error} = Candy.core('Log', false).init('DNS')
|
|
2
|
+
|
|
3
|
+
const axios = require('axios')
|
|
4
|
+
const dns = require('native-dns')
|
|
5
|
+
const {execSync} = require('child_process')
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const os = require('os')
|
|
8
|
+
|
|
9
|
+
class DNS {
|
|
10
|
+
ip = '127.0.0.1'
|
|
11
|
+
#loaded = false
|
|
12
|
+
#tcp
|
|
13
|
+
#types = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'CAA']
|
|
14
|
+
#udp
|
|
15
|
+
#requestCount = new Map() // Rate limiting
|
|
16
|
+
#rateLimit = 100 // requests per minute per IP
|
|
17
|
+
#rateLimitWindow = 60000 // 1 minute
|
|
18
|
+
|
|
19
|
+
delete(...args) {
|
|
20
|
+
for (let obj of args) {
|
|
21
|
+
let domain = obj.name
|
|
22
|
+
while (!Candy.core('Config').config.websites[domain] && domain.includes('.')) domain = domain.split('.').slice(1).join('.')
|
|
23
|
+
if (!Candy.core('Config').config.websites[domain]) continue
|
|
24
|
+
if (!obj.type) continue
|
|
25
|
+
let type = obj.type.toUpperCase()
|
|
26
|
+
if (!this.#types.includes(type)) continue
|
|
27
|
+
if (!Candy.core('Config').config.websites[domain].DNS || !Candy.core('Config').config.websites[domain].DNS[type]) continue
|
|
28
|
+
Candy.core('Config').config.websites[domain].DNS[type] = Candy.core('Config').config.websites[domain].DNS[type].filter(
|
|
29
|
+
record => !(record.name === obj.name && (!obj.value || record.value === obj.value))
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
init() {
|
|
35
|
+
this.#udp = dns.createServer()
|
|
36
|
+
this.#tcp = dns.createTCPServer()
|
|
37
|
+
this.#getExternalIP()
|
|
38
|
+
this.#publish()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async #getExternalIP() {
|
|
42
|
+
// Multiple IP detection services as fallbacks
|
|
43
|
+
const ipServices = [
|
|
44
|
+
'https://curlmyip.org/',
|
|
45
|
+
'https://ipv4.icanhazip.com/',
|
|
46
|
+
'https://api.ipify.org/',
|
|
47
|
+
'https://checkip.amazonaws.com/',
|
|
48
|
+
'https://ipinfo.io/ip'
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
for (const service of ipServices) {
|
|
52
|
+
try {
|
|
53
|
+
log(`Attempting to get external IP from ${service}`)
|
|
54
|
+
const response = await axios.get(service, {
|
|
55
|
+
timeout: 5000,
|
|
56
|
+
headers: {
|
|
57
|
+
'User-Agent': 'CandyPack-DNS/1.0'
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const ip = response.data.trim()
|
|
62
|
+
if (ip && /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) {
|
|
63
|
+
log('External IP detected:', ip)
|
|
64
|
+
this.ip = ip
|
|
65
|
+
return
|
|
66
|
+
} else {
|
|
67
|
+
log(`Invalid IP format from ${service}:`, ip)
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log(`Failed to get IP from ${service}:`, err.message)
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If all services fail, try to get local network IP
|
|
76
|
+
try {
|
|
77
|
+
const networkInterfaces = require('os').networkInterfaces()
|
|
78
|
+
for (const interfaceName in networkInterfaces) {
|
|
79
|
+
const interfaces = networkInterfaces[interfaceName]
|
|
80
|
+
for (const iface of interfaces) {
|
|
81
|
+
// Skip loopback and non-IPv4 addresses
|
|
82
|
+
if (!iface.internal && iface.family === 'IPv4') {
|
|
83
|
+
log('Using local network IP as fallback:', iface.address)
|
|
84
|
+
this.ip = iface.address
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
log('Failed to get local network IP:', err.message)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
log('Could not determine external IP, using default 127.0.0.1')
|
|
94
|
+
error('DNS', 'All IP detection methods failed, DNS A records will use 127.0.0.1')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#publish() {
|
|
98
|
+
if (this.#loaded || !Object.keys(Candy.core('Config').config.websites ?? {}).length) return
|
|
99
|
+
this.#loaded = true
|
|
100
|
+
|
|
101
|
+
// Set up request handlers
|
|
102
|
+
this.#udp.on('request', (request, response) => {
|
|
103
|
+
try {
|
|
104
|
+
this.#request(request, response)
|
|
105
|
+
} catch (err) {
|
|
106
|
+
error('DNS UDP request handler error:', err.message)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
this.#tcp.on('request', (request, response) => {
|
|
110
|
+
try {
|
|
111
|
+
this.#request(request, response)
|
|
112
|
+
} catch (err) {
|
|
113
|
+
error('DNS TCP request handler error:', err.message)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Log system information before starting
|
|
118
|
+
this.#logSystemInfo()
|
|
119
|
+
|
|
120
|
+
this.#startDNSServers()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#logSystemInfo() {
|
|
124
|
+
try {
|
|
125
|
+
log('DNS Server initialization - System information:')
|
|
126
|
+
log('Platform:', os.platform())
|
|
127
|
+
log('Architecture:', os.arch())
|
|
128
|
+
|
|
129
|
+
// Check what's using port 53
|
|
130
|
+
try {
|
|
131
|
+
const port53Info = execSync(
|
|
132
|
+
'lsof -i :53 2>/dev/null || netstat -tulpn 2>/dev/null | grep :53 || ss -tulpn 2>/dev/null | grep :53 || echo "Port 53 appears to be free"',
|
|
133
|
+
{
|
|
134
|
+
encoding: 'utf8',
|
|
135
|
+
timeout: 5000
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
log('Port 53 status:', port53Info.trim() || 'No processes found on port 53')
|
|
139
|
+
} catch (err) {
|
|
140
|
+
log('Could not check port 53 status:', err.message)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check systemd-resolved status on Linux
|
|
144
|
+
if (os.platform() === 'linux') {
|
|
145
|
+
try {
|
|
146
|
+
const resolvedStatus = execSync('systemctl is-active systemd-resolved 2>/dev/null || echo "not-active"', {
|
|
147
|
+
encoding: 'utf8',
|
|
148
|
+
timeout: 3000
|
|
149
|
+
}).trim()
|
|
150
|
+
log('systemd-resolved status:', resolvedStatus)
|
|
151
|
+
|
|
152
|
+
if (resolvedStatus === 'active') {
|
|
153
|
+
try {
|
|
154
|
+
const resolvedConfig = execSync(
|
|
155
|
+
'systemd-resolve --status 2>/dev/null | head -20 || resolvectl status 2>/dev/null | head -20 || echo "Could not get resolver status"',
|
|
156
|
+
{
|
|
157
|
+
encoding: 'utf8',
|
|
158
|
+
timeout: 3000
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
log('Current DNS resolver configuration:', resolvedConfig.trim())
|
|
162
|
+
} catch {
|
|
163
|
+
log('Could not get DNS resolver configuration')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
log('Could not check systemd-resolved status:', err.message)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
log('Error logging system info:', err.message)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async #startDNSServers() {
|
|
176
|
+
// First, proactively check if port 53 is available
|
|
177
|
+
const portAvailable = await this.#checkPortAvailability(53)
|
|
178
|
+
if (!portAvailable) {
|
|
179
|
+
log('Port 53 is already in use, attempting to resolve conflict...')
|
|
180
|
+
const resolved = await this.#handleSystemdResolveConflict()
|
|
181
|
+
if (resolved) {
|
|
182
|
+
// Wait a bit and retry
|
|
183
|
+
setTimeout(() => this.#attemptDNSStart(53), 3000)
|
|
184
|
+
} else {
|
|
185
|
+
log('Could not resolve port 53 conflict, using alternative port...')
|
|
186
|
+
this.#useAlternativePort()
|
|
187
|
+
}
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Port seems available, try to start
|
|
192
|
+
this.#attemptDNSStart(53)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#attemptDNSStart(port) {
|
|
196
|
+
try {
|
|
197
|
+
// Set up error handlers before starting
|
|
198
|
+
this.#udp.on('error', async err => {
|
|
199
|
+
error('DNS UDP Server Error:', err.message)
|
|
200
|
+
if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
|
|
201
|
+
log(`Port ${port} conflict detected via error event, attempting resolution...`)
|
|
202
|
+
if (port === 53) {
|
|
203
|
+
const resolved = await this.#handleSystemdResolveConflict()
|
|
204
|
+
if (!resolved) {
|
|
205
|
+
this.#useAlternativePort()
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
this.#tcp.on('error', async err => {
|
|
212
|
+
error('DNS TCP Server Error:', err.message)
|
|
213
|
+
if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
|
|
214
|
+
log(`Port ${port} conflict detected via error event, attempting resolution...`)
|
|
215
|
+
if (port === 53) {
|
|
216
|
+
const resolved = await this.#handleSystemdResolveConflict()
|
|
217
|
+
if (!resolved) {
|
|
218
|
+
this.#useAlternativePort()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Try to start servers
|
|
225
|
+
this.#udp.serve(port)
|
|
226
|
+
this.#tcp.serve(port)
|
|
227
|
+
log(`DNS servers started on port ${port}`)
|
|
228
|
+
|
|
229
|
+
// Update system DNS configuration for internet access
|
|
230
|
+
if (port === 53) {
|
|
231
|
+
this.#setupSystemDNSForInternet()
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
error('Failed to start DNS servers:', err.message)
|
|
235
|
+
if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
|
|
236
|
+
log(`Port ${port} is in use (caught exception), attempting resolution...`)
|
|
237
|
+
if (port === 53) {
|
|
238
|
+
this.#handleSystemdResolveConflict().then(resolved => {
|
|
239
|
+
if (resolved) {
|
|
240
|
+
setTimeout(() => this.#attemptDNSStart(53), 3000)
|
|
241
|
+
} else {
|
|
242
|
+
this.#useAlternativePort()
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
} else {
|
|
246
|
+
this.#useAlternativePort()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async #checkPortAvailability(port) {
|
|
253
|
+
try {
|
|
254
|
+
// Check if anything is listening on the port
|
|
255
|
+
const portCheck = execSync(
|
|
256
|
+
`lsof -i :${port} 2>/dev/null || netstat -tulpn 2>/dev/null | grep :${port} || ss -tulpn 2>/dev/null | grep :${port} || true`,
|
|
257
|
+
{
|
|
258
|
+
encoding: 'utf8',
|
|
259
|
+
timeout: 5000
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if (portCheck.trim()) {
|
|
264
|
+
log(`Port ${port} is in use by:`, portCheck.trim())
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return true
|
|
269
|
+
} catch (err) {
|
|
270
|
+
log('Error checking port availability:', err.message)
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async #handleSystemdResolveConflict() {
|
|
276
|
+
try {
|
|
277
|
+
// Check if we're on Linux
|
|
278
|
+
if (os.platform() !== 'linux') {
|
|
279
|
+
log('Not on Linux, skipping systemd-resolve conflict resolution')
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// More comprehensive check for what's using port 53
|
|
284
|
+
let portInfo = ''
|
|
285
|
+
try {
|
|
286
|
+
portInfo = execSync(
|
|
287
|
+
'lsof -i :53 2>/dev/null || netstat -tulpn 2>/dev/null | grep :53 || ss -tulpn 2>/dev/null | grep :53 || true',
|
|
288
|
+
{
|
|
289
|
+
encoding: 'utf8',
|
|
290
|
+
timeout: 5000
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
} catch (err) {
|
|
294
|
+
log('Could not check port 53 usage:', err.message)
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!portInfo || (!portInfo.includes('systemd-resolve') && !portInfo.includes('resolved'))) {
|
|
299
|
+
log('systemd-resolve not detected on port 53, conflict may be with another service')
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
log('Detected systemd-resolve using port 53, attempting resolution...')
|
|
304
|
+
|
|
305
|
+
// Try the direct approach first - disable DNS stub listener
|
|
306
|
+
const stubDisabled = await this.#disableSystemdResolveStub()
|
|
307
|
+
if (stubDisabled) {
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// If that fails, try alternative approach
|
|
312
|
+
return this.#tryAlternativeApproach()
|
|
313
|
+
} catch (err) {
|
|
314
|
+
error('Error handling systemd-resolve conflict:', err.message)
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async #disableSystemdResolveStub() {
|
|
320
|
+
try {
|
|
321
|
+
log('Attempting to disable systemd-resolved DNS stub listener...')
|
|
322
|
+
|
|
323
|
+
// Check if systemd-resolved is active
|
|
324
|
+
const isActive = execSync('systemctl is-active systemd-resolved 2>/dev/null || echo inactive', {
|
|
325
|
+
encoding: 'utf8',
|
|
326
|
+
timeout: 5000
|
|
327
|
+
}).trim()
|
|
328
|
+
|
|
329
|
+
if (isActive !== 'active') {
|
|
330
|
+
log('systemd-resolved is not active')
|
|
331
|
+
return false
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Create or update resolved.conf to disable DNS stub
|
|
335
|
+
const resolvedConfDir = '/etc/systemd/resolved.conf.d'
|
|
336
|
+
const resolvedConfFile = `${resolvedConfDir}/candypack-dns.conf`
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
// Ensure directory exists
|
|
340
|
+
if (!fs.existsSync(resolvedConfDir)) {
|
|
341
|
+
execSync(`sudo mkdir -p ${resolvedConfDir}`, {timeout: 10000})
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Create configuration to disable DNS stub listener and use public DNS
|
|
345
|
+
const resolvedConfig = `[Resolve]
|
|
346
|
+
DNSStubListener=no
|
|
347
|
+
DNS=1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4
|
|
348
|
+
FallbackDNS=1.1.1.1 1.0.0.1
|
|
349
|
+
`
|
|
350
|
+
|
|
351
|
+
execSync(`echo '${resolvedConfig}' | sudo tee ${resolvedConfFile}`, {timeout: 10000})
|
|
352
|
+
log('Created systemd-resolved configuration to disable DNS stub listener')
|
|
353
|
+
|
|
354
|
+
// Restart systemd-resolved
|
|
355
|
+
execSync('sudo systemctl restart systemd-resolved', {timeout: 15000})
|
|
356
|
+
log('Restarted systemd-resolved service')
|
|
357
|
+
|
|
358
|
+
// Wait for service to restart and port to be freed
|
|
359
|
+
return new Promise(resolve => {
|
|
360
|
+
setTimeout(() => {
|
|
361
|
+
try {
|
|
362
|
+
// Check if port 53 is now free
|
|
363
|
+
const portCheck = execSync('lsof -i :53 2>/dev/null || true', {
|
|
364
|
+
encoding: 'utf8',
|
|
365
|
+
timeout: 3000
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
if (!portCheck.includes('systemd-resolve') && !portCheck.includes('resolved')) {
|
|
369
|
+
log('Port 53 is now available')
|
|
370
|
+
resolve(true)
|
|
371
|
+
} else {
|
|
372
|
+
log('Port 53 still in use, trying alternative approach')
|
|
373
|
+
resolve(this.#tryAlternativeApproach())
|
|
374
|
+
}
|
|
375
|
+
} catch (err) {
|
|
376
|
+
log('Error checking port availability:', err.message)
|
|
377
|
+
resolve(this.#tryAlternativeApproach())
|
|
378
|
+
}
|
|
379
|
+
}, 3000)
|
|
380
|
+
})
|
|
381
|
+
} catch (sudoErr) {
|
|
382
|
+
log('Could not configure systemd-resolved (no sudo access):', sudoErr.message)
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
log('Error disabling systemd-resolved stub:', err.message)
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#tryAlternativeApproach() {
|
|
392
|
+
try {
|
|
393
|
+
log('Trying alternative approach: temporarily stopping systemd-resolved...')
|
|
394
|
+
|
|
395
|
+
// Check if we can stop systemd-resolved
|
|
396
|
+
try {
|
|
397
|
+
execSync('sudo systemctl stop systemd-resolved', {timeout: 10000})
|
|
398
|
+
log('Temporarily stopped systemd-resolved')
|
|
399
|
+
|
|
400
|
+
// Set up cleanup handlers to restart systemd-resolved when process exits
|
|
401
|
+
this.#setupCleanupHandlers()
|
|
402
|
+
|
|
403
|
+
return true
|
|
404
|
+
} catch (stopErr) {
|
|
405
|
+
log('Could not stop systemd-resolved:', stopErr.message)
|
|
406
|
+
|
|
407
|
+
// Last resort: try to use a different port for our DNS server
|
|
408
|
+
return this.#useAlternativePort()
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
log('Alternative approach failed:', err.message)
|
|
412
|
+
return false
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#setupCleanupHandlers() {
|
|
417
|
+
const restartSystemdResolved = () => {
|
|
418
|
+
try {
|
|
419
|
+
execSync('sudo systemctl start systemd-resolved', {timeout: 10000})
|
|
420
|
+
log('Restarted systemd-resolved on cleanup')
|
|
421
|
+
} catch (err) {
|
|
422
|
+
error('Failed to restart systemd-resolved on cleanup:', err.message)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Handle various exit scenarios
|
|
427
|
+
process.on('exit', restartSystemdResolved)
|
|
428
|
+
process.on('SIGINT', () => {
|
|
429
|
+
restartSystemdResolved()
|
|
430
|
+
process.exit(0)
|
|
431
|
+
})
|
|
432
|
+
process.on('SIGTERM', () => {
|
|
433
|
+
restartSystemdResolved()
|
|
434
|
+
process.exit(0)
|
|
435
|
+
})
|
|
436
|
+
process.on('uncaughtException', err => {
|
|
437
|
+
error('Uncaught exception:', err.message)
|
|
438
|
+
restartSystemdResolved()
|
|
439
|
+
process.exit(1)
|
|
440
|
+
})
|
|
441
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
442
|
+
error('Unhandled rejection at:', promise, 'reason:', reason)
|
|
443
|
+
restartSystemdResolved()
|
|
444
|
+
process.exit(1)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
log('Set up cleanup handlers to restart systemd-resolved on exit')
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async #useAlternativePort() {
|
|
451
|
+
try {
|
|
452
|
+
log('Attempting to use alternative port for DNS server...')
|
|
453
|
+
|
|
454
|
+
// Try ports 5353, 1053, 8053 as alternatives
|
|
455
|
+
const alternativePorts = [5353, 1053, 8053]
|
|
456
|
+
|
|
457
|
+
for (const port of alternativePorts) {
|
|
458
|
+
const available = await this.#checkPortAvailability(port)
|
|
459
|
+
if (available) {
|
|
460
|
+
try {
|
|
461
|
+
// Create new server instances for alternative port
|
|
462
|
+
const udpAlt = dns.createServer()
|
|
463
|
+
const tcpAlt = dns.createTCPServer()
|
|
464
|
+
|
|
465
|
+
// Copy event handlers
|
|
466
|
+
udpAlt.on('request', (request, response) => {
|
|
467
|
+
try {
|
|
468
|
+
this.#request(request, response)
|
|
469
|
+
} catch (err) {
|
|
470
|
+
error('DNS UDP request handler error:', err.message)
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
tcpAlt.on('request', (request, response) => {
|
|
475
|
+
try {
|
|
476
|
+
this.#request(request, response)
|
|
477
|
+
} catch (err) {
|
|
478
|
+
error('DNS TCP request handler error:', err.message)
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
udpAlt.on('error', err => error('DNS UDP Server Error (alt port):', err.stack))
|
|
483
|
+
tcpAlt.on('error', err => error('DNS TCP Server Error (alt port):', err.stack))
|
|
484
|
+
|
|
485
|
+
// Start on alternative port
|
|
486
|
+
udpAlt.serve(port)
|
|
487
|
+
tcpAlt.serve(port)
|
|
488
|
+
|
|
489
|
+
// Replace original servers
|
|
490
|
+
this.#udp = udpAlt
|
|
491
|
+
this.#tcp = tcpAlt
|
|
492
|
+
|
|
493
|
+
log(`DNS servers started on alternative port ${port}`)
|
|
494
|
+
|
|
495
|
+
// Update system to use our alternative port
|
|
496
|
+
this.#updateSystemDNSConfig(port)
|
|
497
|
+
return true
|
|
498
|
+
} catch (portErr) {
|
|
499
|
+
log(`Failed to start on port ${port}:`, portErr.message)
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
log(`Port ${port} is also in use, trying next...`)
|
|
504
|
+
continue
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
error('All alternative ports are in use')
|
|
509
|
+
return false
|
|
510
|
+
} catch (err) {
|
|
511
|
+
error('Failed to use alternative port:', err.message)
|
|
512
|
+
return false
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
#setupSystemDNSForInternet() {
|
|
517
|
+
try {
|
|
518
|
+
// Configure system to use public DNS for internet access
|
|
519
|
+
const resolvConf = `# CandyPack DNS Configuration
|
|
520
|
+
# CandyPack handles local domains on port 53
|
|
521
|
+
# Public DNS servers handle all internet domains
|
|
522
|
+
|
|
523
|
+
nameserver 1.1.1.1
|
|
524
|
+
nameserver 1.0.0.1
|
|
525
|
+
nameserver 8.8.8.8
|
|
526
|
+
nameserver 8.8.4.4
|
|
527
|
+
|
|
528
|
+
# Cloudflare DNS (1.1.1.1) - Fast and privacy-focused
|
|
529
|
+
# Google DNS (8.8.8.8) - Reliable fallback
|
|
530
|
+
# Original configuration backed up to /etc/resolv.conf.candypack.backup
|
|
531
|
+
`
|
|
532
|
+
|
|
533
|
+
// Backup original resolv.conf
|
|
534
|
+
execSync('sudo cp /etc/resolv.conf /etc/resolv.conf.candypack.backup 2>/dev/null || true', {timeout: 5000})
|
|
535
|
+
|
|
536
|
+
// Update resolv.conf with public DNS servers
|
|
537
|
+
execSync(`echo '${resolvConf}' | sudo tee /etc/resolv.conf`, {timeout: 5000})
|
|
538
|
+
log('Configured system to use public DNS servers for internet access')
|
|
539
|
+
log('Cloudflare DNS (1.1.1.1) and Google DNS (8.8.8.8) will handle non-CandyPack domains')
|
|
540
|
+
|
|
541
|
+
// Set up restoration on exit
|
|
542
|
+
process.on('exit', () => {
|
|
543
|
+
try {
|
|
544
|
+
execSync('sudo mv /etc/resolv.conf.candypack.backup /etc/resolv.conf 2>/dev/null || true', {timeout: 5000})
|
|
545
|
+
} catch {
|
|
546
|
+
// Silent fail on exit
|
|
547
|
+
}
|
|
548
|
+
})
|
|
549
|
+
} catch (err) {
|
|
550
|
+
log('Warning: Could not configure system DNS for internet access:', err.message)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#updateSystemDNSConfig(port) {
|
|
555
|
+
try {
|
|
556
|
+
// Use reliable public DNS servers for internet access
|
|
557
|
+
// CandyPack DNS only handles local domains, everything else goes to public DNS
|
|
558
|
+
const resolvConf = `# CandyPack DNS Configuration
|
|
559
|
+
# Local domains handled by CandyPack DNS on port ${port}
|
|
560
|
+
# All other domains handled by reliable public DNS servers
|
|
561
|
+
|
|
562
|
+
nameserver 1.1.1.1
|
|
563
|
+
nameserver 1.0.0.1
|
|
564
|
+
nameserver 8.8.8.8
|
|
565
|
+
nameserver 8.8.4.4
|
|
566
|
+
|
|
567
|
+
# Cloudflare DNS (1.1.1.1) - Fast and privacy-focused
|
|
568
|
+
# Google DNS (8.8.8.8) - Reliable fallback
|
|
569
|
+
# Original configuration backed up to /etc/resolv.conf.candypack.backup
|
|
570
|
+
`
|
|
571
|
+
|
|
572
|
+
// Backup original resolv.conf
|
|
573
|
+
execSync('sudo cp /etc/resolv.conf /etc/resolv.conf.candypack.backup 2>/dev/null || true', {timeout: 5000})
|
|
574
|
+
|
|
575
|
+
// Update resolv.conf with public DNS servers
|
|
576
|
+
execSync(`echo '${resolvConf}' | sudo tee /etc/resolv.conf`, {timeout: 5000})
|
|
577
|
+
log('Updated /etc/resolv.conf to use reliable public DNS servers (1.1.1.1, 8.8.8.8)')
|
|
578
|
+
log('CandyPack domains will be handled locally, all other domains via public DNS')
|
|
579
|
+
|
|
580
|
+
// Set up restoration on exit
|
|
581
|
+
process.on('exit', () => {
|
|
582
|
+
try {
|
|
583
|
+
execSync('sudo mv /etc/resolv.conf.candypack.backup /etc/resolv.conf 2>/dev/null || true', {timeout: 5000})
|
|
584
|
+
} catch {
|
|
585
|
+
// Silent fail on exit
|
|
586
|
+
}
|
|
587
|
+
})
|
|
588
|
+
} catch (err) {
|
|
589
|
+
log('Warning: Could not update system DNS configuration:', err.message)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
#request(request, response) {
|
|
594
|
+
try {
|
|
595
|
+
// Basic rate limiting (skip for localhost)
|
|
596
|
+
const clientIP = request.address?.address || 'unknown'
|
|
597
|
+
const now = Date.now()
|
|
598
|
+
|
|
599
|
+
// Skip rate limiting for localhost/loopback addresses
|
|
600
|
+
if (clientIP !== '127.0.0.1' && clientIP !== '::1' && clientIP !== 'localhost') {
|
|
601
|
+
if (!this.#requestCount.has(clientIP)) {
|
|
602
|
+
this.#requestCount.set(clientIP, {count: 1, firstRequest: now})
|
|
603
|
+
} else {
|
|
604
|
+
const clientData = this.#requestCount.get(clientIP)
|
|
605
|
+
if (now - clientData.firstRequest > this.#rateLimitWindow) {
|
|
606
|
+
// Reset window
|
|
607
|
+
this.#requestCount.set(clientIP, {count: 1, firstRequest: now})
|
|
608
|
+
} else {
|
|
609
|
+
clientData.count++
|
|
610
|
+
if (clientData.count > this.#rateLimit) {
|
|
611
|
+
log(`Rate limit exceeded for ${clientIP}`)
|
|
612
|
+
return response.send()
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Validate request structure
|
|
619
|
+
if (!request || !response || !response.question || !response.question[0]) {
|
|
620
|
+
log(`Invalid DNS request structure from ${clientIP}`)
|
|
621
|
+
return response.send()
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const questionName = response.question[0].name.toLowerCase()
|
|
625
|
+
const questionType = response.question[0].type
|
|
626
|
+
response.question[0].name = questionName
|
|
627
|
+
|
|
628
|
+
let domain = questionName
|
|
629
|
+
while (!Candy.core('Config').config.websites[domain] && domain.includes('.')) {
|
|
630
|
+
domain = domain.split('.').slice(1).join('.')
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (!Candy.core('Config').config.websites[domain] || !Candy.core('Config').config.websites[domain].DNS) {
|
|
634
|
+
// For unknown domains, send proper NXDOMAIN response instead of empty response
|
|
635
|
+
response.header.rcode = dns.consts.NAME_TO_RCODE.NXDOMAIN
|
|
636
|
+
return response.send()
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const dnsRecords = Candy.core('Config').config.websites[domain].DNS
|
|
640
|
+
|
|
641
|
+
// Only process records relevant to the question type for better performance
|
|
642
|
+
switch (questionType) {
|
|
643
|
+
case dns.consts.NAME_TO_QTYPE.A:
|
|
644
|
+
this.#processARecords(dnsRecords.A, questionName, response)
|
|
645
|
+
break
|
|
646
|
+
case dns.consts.NAME_TO_QTYPE.AAAA:
|
|
647
|
+
this.#processAAAARecords(dnsRecords.AAAA, questionName, response)
|
|
648
|
+
break
|
|
649
|
+
case dns.consts.NAME_TO_QTYPE.CNAME:
|
|
650
|
+
this.#processCNAMERecords(dnsRecords.CNAME, questionName, response)
|
|
651
|
+
break
|
|
652
|
+
case dns.consts.NAME_TO_QTYPE.MX:
|
|
653
|
+
this.#processMXRecords(dnsRecords.MX, questionName, response)
|
|
654
|
+
break
|
|
655
|
+
case dns.consts.NAME_TO_QTYPE.TXT:
|
|
656
|
+
this.#processTXTRecords(dnsRecords.TXT, questionName, response)
|
|
657
|
+
break
|
|
658
|
+
case dns.consts.NAME_TO_QTYPE.NS:
|
|
659
|
+
this.#processNSRecords(dnsRecords.NS, questionName, response, domain)
|
|
660
|
+
break
|
|
661
|
+
case dns.consts.NAME_TO_QTYPE.SOA:
|
|
662
|
+
this.#processSOARecords(dnsRecords.SOA, questionName, response)
|
|
663
|
+
break
|
|
664
|
+
case dns.consts.NAME_TO_QTYPE.CAA:
|
|
665
|
+
this.#processCAARecords(dnsRecords.CAA, questionName, response)
|
|
666
|
+
// If no CAA records found, add default Let's Encrypt CAA records
|
|
667
|
+
if (!response.answer.length && dnsRecords.CAA?.length === 0) {
|
|
668
|
+
this.#addDefaultCAARecords(questionName, response)
|
|
669
|
+
}
|
|
670
|
+
break
|
|
671
|
+
default:
|
|
672
|
+
// For ANY queries or unknown types, process all relevant records
|
|
673
|
+
this.#processARecords(dnsRecords.A, questionName, response)
|
|
674
|
+
this.#processAAAARecords(dnsRecords.AAAA, questionName, response)
|
|
675
|
+
this.#processCNAMERecords(dnsRecords.CNAME, questionName, response)
|
|
676
|
+
this.#processMXRecords(dnsRecords.MX, questionName, response)
|
|
677
|
+
this.#processTXTRecords(dnsRecords.TXT, questionName, response)
|
|
678
|
+
this.#processNSRecords(dnsRecords.NS, questionName, response, domain)
|
|
679
|
+
this.#processSOARecords(dnsRecords.SOA, questionName, response)
|
|
680
|
+
this.#processCAARecords(dnsRecords.CAA, questionName, response)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
response.send()
|
|
684
|
+
} catch (err) {
|
|
685
|
+
error('DNS request processing error:', err.message)
|
|
686
|
+
// Log client info for debugging
|
|
687
|
+
const clientIP = request?.address?.address || 'unknown'
|
|
688
|
+
log(`Error processing DNS request from ${clientIP}`)
|
|
689
|
+
|
|
690
|
+
// Try to send an empty response if possible
|
|
691
|
+
try {
|
|
692
|
+
if (response && typeof response.send === 'function') {
|
|
693
|
+
response.send()
|
|
694
|
+
}
|
|
695
|
+
} catch (sendErr) {
|
|
696
|
+
error('Failed to send DNS error response:', sendErr.message)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
#processARecords(records, questionName, response) {
|
|
702
|
+
try {
|
|
703
|
+
for (const record of records ?? []) {
|
|
704
|
+
if (record.name !== questionName) continue
|
|
705
|
+
response.answer.push(
|
|
706
|
+
dns.A({
|
|
707
|
+
name: record.name,
|
|
708
|
+
address: record.value ?? this.ip,
|
|
709
|
+
ttl: record.ttl ?? 3600
|
|
710
|
+
})
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
} catch (err) {
|
|
714
|
+
error('Error processing A records:', err.message)
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
#processAAAARecords(records, questionName, response) {
|
|
719
|
+
try {
|
|
720
|
+
for (const record of records ?? []) {
|
|
721
|
+
if (record.name !== questionName) continue
|
|
722
|
+
response.answer.push(
|
|
723
|
+
dns.AAAA({
|
|
724
|
+
name: record.name,
|
|
725
|
+
address: record.value,
|
|
726
|
+
ttl: record.ttl ?? 3600
|
|
727
|
+
})
|
|
728
|
+
)
|
|
729
|
+
}
|
|
730
|
+
} catch (err) {
|
|
731
|
+
error('Error processing AAAA records:', err.message)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
#processCNAMERecords(records, questionName, response) {
|
|
736
|
+
try {
|
|
737
|
+
for (const record of records ?? []) {
|
|
738
|
+
if (record.name !== questionName) continue
|
|
739
|
+
response.answer.push(
|
|
740
|
+
dns.CNAME({
|
|
741
|
+
name: record.name,
|
|
742
|
+
data: record.value ?? questionName,
|
|
743
|
+
ttl: record.ttl ?? 3600
|
|
744
|
+
})
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
} catch (err) {
|
|
748
|
+
error('Error processing CNAME records:', err.message)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
#processMXRecords(records, questionName, response) {
|
|
753
|
+
try {
|
|
754
|
+
for (const record of records ?? []) {
|
|
755
|
+
if (record.name !== questionName) continue
|
|
756
|
+
response.answer.push(
|
|
757
|
+
dns.MX({
|
|
758
|
+
name: record.name,
|
|
759
|
+
exchange: record.value ?? questionName,
|
|
760
|
+
priority: record.priority ?? 10,
|
|
761
|
+
ttl: record.ttl ?? 3600
|
|
762
|
+
})
|
|
763
|
+
)
|
|
764
|
+
}
|
|
765
|
+
} catch (err) {
|
|
766
|
+
error('Error processing MX records:', err.message)
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
#processNSRecords(records, questionName, response, domain) {
|
|
771
|
+
try {
|
|
772
|
+
for (const record of records ?? []) {
|
|
773
|
+
if (record.name !== questionName) continue
|
|
774
|
+
response.header.aa = 1
|
|
775
|
+
response.authority.push(
|
|
776
|
+
dns.NS({
|
|
777
|
+
name: record.name,
|
|
778
|
+
data: record.value ?? domain,
|
|
779
|
+
ttl: record.ttl ?? 3600
|
|
780
|
+
})
|
|
781
|
+
)
|
|
782
|
+
}
|
|
783
|
+
} catch (err) {
|
|
784
|
+
error('Error processing NS records:', err.message)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
#processTXTRecords(records, questionName, response) {
|
|
789
|
+
try {
|
|
790
|
+
for (const record of records ?? []) {
|
|
791
|
+
if (!record || record.name !== questionName) continue
|
|
792
|
+
response.answer.push(
|
|
793
|
+
dns.TXT({
|
|
794
|
+
name: record.name,
|
|
795
|
+
data: [record.value],
|
|
796
|
+
ttl: record.ttl ?? 3600
|
|
797
|
+
})
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
} catch (err) {
|
|
801
|
+
error('Error processing TXT records:', err.message)
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
#processSOARecords(records, questionName, response) {
|
|
806
|
+
try {
|
|
807
|
+
for (const record of records ?? []) {
|
|
808
|
+
if (!record || !record.value) continue
|
|
809
|
+
const soaParts = record.value.split(' ')
|
|
810
|
+
if (soaParts.length < 7) continue
|
|
811
|
+
response.header.aa = 1
|
|
812
|
+
response.authority.push(
|
|
813
|
+
dns.SOA({
|
|
814
|
+
name: record.name,
|
|
815
|
+
primary: soaParts[0],
|
|
816
|
+
admin: soaParts[1],
|
|
817
|
+
serial: parseInt(soaParts[2]) || 1,
|
|
818
|
+
refresh: parseInt(soaParts[3]) || 3600,
|
|
819
|
+
retry: parseInt(soaParts[4]) || 600,
|
|
820
|
+
expiration: parseInt(soaParts[5]) || 604800,
|
|
821
|
+
minimum: parseInt(soaParts[6]) || 3600,
|
|
822
|
+
ttl: record.ttl ?? 3600
|
|
823
|
+
})
|
|
824
|
+
)
|
|
825
|
+
}
|
|
826
|
+
} catch (err) {
|
|
827
|
+
error('Error processing SOA records:', err.message)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
#processCAARecords(records, questionName, response) {
|
|
832
|
+
try {
|
|
833
|
+
for (const record of records ?? []) {
|
|
834
|
+
if (!record || record.name !== questionName) continue
|
|
835
|
+
|
|
836
|
+
// CAA record format: flags tag value
|
|
837
|
+
// Example: "0 issue letsencrypt.org"
|
|
838
|
+
const caaParts = record.value.split(' ')
|
|
839
|
+
if (caaParts.length < 3) continue
|
|
840
|
+
|
|
841
|
+
const flags = parseInt(caaParts[0]) || 0
|
|
842
|
+
const tag = caaParts[1]
|
|
843
|
+
const value = caaParts.slice(2).join(' ')
|
|
844
|
+
|
|
845
|
+
response.answer.push(
|
|
846
|
+
dns.CAA({
|
|
847
|
+
name: record.name,
|
|
848
|
+
flags: flags,
|
|
849
|
+
tag: tag,
|
|
850
|
+
value: value,
|
|
851
|
+
ttl: record.ttl ?? 3600
|
|
852
|
+
})
|
|
853
|
+
)
|
|
854
|
+
}
|
|
855
|
+
} catch (err) {
|
|
856
|
+
error('Error processing CAA records:', err.message)
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
#addDefaultCAARecords(questionName, response) {
|
|
861
|
+
try {
|
|
862
|
+
// Add default CAA records allowing Let's Encrypt
|
|
863
|
+
response.answer.push(
|
|
864
|
+
dns.CAA({
|
|
865
|
+
name: questionName,
|
|
866
|
+
flags: 0,
|
|
867
|
+
tag: 'issue',
|
|
868
|
+
value: 'letsencrypt.org',
|
|
869
|
+
ttl: 3600
|
|
870
|
+
})
|
|
871
|
+
)
|
|
872
|
+
response.answer.push(
|
|
873
|
+
dns.CAA({
|
|
874
|
+
name: questionName,
|
|
875
|
+
flags: 0,
|
|
876
|
+
tag: 'issuewild',
|
|
877
|
+
value: 'letsencrypt.org',
|
|
878
|
+
ttl: 3600
|
|
879
|
+
})
|
|
880
|
+
)
|
|
881
|
+
log("Added default CAA records for Let's Encrypt to response for:", questionName)
|
|
882
|
+
} catch (err) {
|
|
883
|
+
error('Error adding default CAA records:', err.message)
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
record(...args) {
|
|
888
|
+
let domains = []
|
|
889
|
+
for (let obj of args) {
|
|
890
|
+
let domain = obj.name
|
|
891
|
+
while (!Candy.core('Config').config.websites[domain] && domain.includes('.')) domain = domain.split('.').slice(1).join('.')
|
|
892
|
+
if (!Candy.core('Config').config.websites[domain]) continue
|
|
893
|
+
if (!obj.type) continue
|
|
894
|
+
let type = obj.type.toUpperCase()
|
|
895
|
+
delete obj.type
|
|
896
|
+
if (!this.#types.includes(type)) continue
|
|
897
|
+
if (!Candy.core('Config').config.websites[domain].DNS) Candy.core('Config').config.websites[domain].DNS = {}
|
|
898
|
+
if (!Candy.core('Config').config.websites[domain].DNS[type]) Candy.core('Config').config.websites[domain].DNS[type] = []
|
|
899
|
+
if (obj.unique !== false) {
|
|
900
|
+
Candy.core('Config').config.websites[domain].DNS[type] = Candy.core('Config').config.websites[domain].DNS[type].filter(
|
|
901
|
+
record => record.name !== obj.name
|
|
902
|
+
)
|
|
903
|
+
}
|
|
904
|
+
Candy.core('Config').config.websites[domain].DNS[type].push(obj)
|
|
905
|
+
domains.push(domain)
|
|
906
|
+
}
|
|
907
|
+
let date = new Date()
|
|
908
|
+
.toISOString()
|
|
909
|
+
.replace(/[^0-9]/g, '')
|
|
910
|
+
.slice(0, 10)
|
|
911
|
+
for (let domain of domains) {
|
|
912
|
+
// Add SOA record
|
|
913
|
+
Candy.core('Config').config.websites[domain].DNS.SOA = [
|
|
914
|
+
{
|
|
915
|
+
name: domain,
|
|
916
|
+
value: 'ns1.' + domain + ' hostmaster.' + domain + ' ' + date + ' 3600 600 604800 3600'
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
|
|
920
|
+
// Add default CAA records for Let's Encrypt SSL certificates
|
|
921
|
+
if (!Candy.core('Config').config.websites[domain].DNS.CAA) {
|
|
922
|
+
Candy.core('Config').config.websites[domain].DNS.CAA = [
|
|
923
|
+
{
|
|
924
|
+
name: domain,
|
|
925
|
+
value: '0 issue letsencrypt.org',
|
|
926
|
+
ttl: 3600
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
name: domain,
|
|
930
|
+
value: '0 issuewild letsencrypt.org',
|
|
931
|
+
ttl: 3600
|
|
932
|
+
}
|
|
933
|
+
]
|
|
934
|
+
log("Added default CAA records for Let's Encrypt to domain:", domain)
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
module.exports = new DNS()
|