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.
Files changed (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. 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()