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,180 @@
1
+ const {log, error} = Candy.core('Log', false).init('SSL')
2
+
3
+ const acme = require('acme-client')
4
+ const fs = require('fs')
5
+ const os = require('os')
6
+ const selfsigned = require('selfsigned')
7
+
8
+ class SSL {
9
+ #checking = false
10
+ #checked = {}
11
+
12
+ async check() {
13
+ if (this.#checking || !Candy.core('Config').config.websites) return
14
+ this.#checking = true
15
+ this.#self()
16
+ for (const domain of Object.keys(Candy.core('Config').config.websites)) {
17
+ if (Candy.core('Config').config.websites[domain].cert === false) continue
18
+ if (
19
+ !Candy.core('Config').config.websites[domain].cert?.ssl ||
20
+ Date.now() + 1000 * 60 * 60 * 24 * 30 > Candy.core('Config').config.websites[domain].cert.ssl.expiry
21
+ )
22
+ await this.#ssl(domain)
23
+ }
24
+ this.#checking = false
25
+ }
26
+
27
+ renew(domain) {
28
+ if (domain.match(/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/))
29
+ return Candy.server('Api').result(false, __('SSL renewal is not available for IP addresses.'))
30
+ if (!Candy.core('Config').config.websites[domain]) {
31
+ for (const key of Object.keys(Candy.core('Config').config.websites)) {
32
+ for (const subdomain of Candy.core('Config').config.websites[key].subdomain)
33
+ if (subdomain + '.' + key == domain) {
34
+ domain = key
35
+ break
36
+ }
37
+ }
38
+ if (!Candy.core('Config').config.websites[domain]) return Candy.server('Api').result(false, __('Domain %s not found.', domain))
39
+ }
40
+ this.#ssl(domain)
41
+ return Candy.server('Api').result(true, __('SSL certificate for domain %s renewed successfully.', domain))
42
+ }
43
+
44
+ #self() {
45
+ let ssl = Candy.core('Config').config.ssl ?? {}
46
+ if (ssl && ssl.expiry > Date.now() && ssl.key && ssl.cert && fs.existsSync(ssl.key) && fs.existsSync(ssl.cert)) return
47
+ log('Generating self-signed SSL certificate...')
48
+ const attrs = [{name: 'commonName', value: 'CandyPack'}]
49
+ const pems = selfsigned.generate(attrs, {days: 365, keySize: 2048})
50
+ if (!fs.existsSync(os.homedir() + '/.candypack/cert/ssl')) fs.mkdirSync(os.homedir() + '/.candypack/cert/ssl', {recursive: true})
51
+ let key_file = os.homedir() + '/.candypack/cert/ssl/candypack.key'
52
+ let crt_file = os.homedir() + '/.candypack/cert/ssl/candypack.crt'
53
+ fs.writeFileSync(key_file, pems.private)
54
+ fs.writeFileSync(crt_file, pems.cert)
55
+ ssl.key = key_file
56
+ ssl.cert = crt_file
57
+ ssl.expiry = Date.now() + 86400000
58
+ Candy.core('Config').config.ssl = ssl
59
+ }
60
+
61
+ async #ssl(domain) {
62
+ if (this.#checked[domain]?.interval > Date.now()) return
63
+
64
+ try {
65
+ const accountPrivateKey = await acme.forge.createPrivateKey()
66
+
67
+ // Create ACME client with proper error handling configuration
68
+ const client = new acme.Client({
69
+ directoryUrl: acme.directory.letsencrypt.production,
70
+ accountKey: accountPrivateKey
71
+ })
72
+
73
+ let subdomains = [domain]
74
+ for (const subdomain of Candy.core('Config').config.websites[domain].subdomain ?? []) {
75
+ subdomains.push(subdomain + '.' + domain)
76
+ }
77
+
78
+ const [key, csr] = await acme.forge.createCsr({
79
+ commonName: domain,
80
+ altNames: subdomains
81
+ })
82
+
83
+ log('Requesting SSL certificate for domain %s...', domain)
84
+
85
+ const cert = await client.auto({
86
+ csr,
87
+ termsOfServiceAgreed: true,
88
+ challengePriority: ['dns-01'],
89
+ challengeCreateFn: async (authz, challenge, keyAuthorization) => {
90
+ if (challenge.type === 'dns-01') {
91
+ log('Creating DNS challenge for %s', authz.identifier.value)
92
+ Candy.server('DNS').record({
93
+ name: '_acme-challenge.' + authz.identifier.value,
94
+ type: 'TXT',
95
+ value: keyAuthorization,
96
+ ttl: 100,
97
+ unique: true
98
+ })
99
+ }
100
+ },
101
+ challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
102
+ if (challenge.type === 'dns-01') {
103
+ log('Removing DNS challenge for %s', authz.identifier.value)
104
+ Candy.server('DNS').delete({
105
+ name: '_acme-challenge.' + authz.identifier.value,
106
+ type: 'TXT',
107
+ value: keyAuthorization
108
+ })
109
+ }
110
+ }
111
+ })
112
+
113
+ if (!cert) {
114
+ error('SSL certificate generation failed for domain %s: No certificate returned', domain)
115
+ return
116
+ }
117
+
118
+ // Save certificate files
119
+ this.#saveCertificate(domain, key, cert)
120
+ } catch (err) {
121
+ this.#handleSSLError(domain, err)
122
+ }
123
+ }
124
+
125
+ #handleSSLError(domain, err) {
126
+ if (!this.#checked[domain]) this.#checked[domain] = {error: 0}
127
+ if (this.#checked[domain].error < 5) {
128
+ this.#checked[domain].error = this.#checked[domain].error + 1
129
+ }
130
+ this.#checked[domain].interval = this.#checked[domain].error * 1000 * 60 * 5 + Date.now()
131
+
132
+ // More specific error handling
133
+ if (err.message && err.message.includes('validateStatus')) {
134
+ error(
135
+ 'SSL certificate request failed due to HTTP validation error for domain %s. This may be due to network issues or ACME server problems.',
136
+ domain
137
+ )
138
+ } else if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED') {
139
+ error('SSL certificate request failed due to network connectivity issues for domain %s: %s', domain, err.message)
140
+ } else {
141
+ error('SSL certificate request failed for domain %s: %s', domain, err.message)
142
+ }
143
+ }
144
+
145
+ #saveCertificate(domain, key, cert) {
146
+ try {
147
+ delete this.#checked[domain]
148
+
149
+ if (!fs.existsSync(os.homedir() + '/.candypack/cert/ssl')) {
150
+ fs.mkdirSync(os.homedir() + '/.candypack/cert/ssl', {recursive: true})
151
+ }
152
+
153
+ fs.writeFileSync(os.homedir() + '/.candypack/cert/ssl/' + domain + '.key', key)
154
+ fs.writeFileSync(os.homedir() + '/.candypack/cert/ssl/' + domain + '.crt', cert)
155
+
156
+ let websites = Candy.core('Config').config.websites ?? {}
157
+ let website = websites[domain]
158
+ if (!website) return
159
+
160
+ if (!website.cert) website.cert = {}
161
+ website.cert.ssl = {
162
+ key: os.homedir() + '/.candypack/cert/ssl/' + domain + '.key',
163
+ cert: os.homedir() + '/.candypack/cert/ssl/' + domain + '.crt',
164
+ expiry: Date.now() + 1000 * 60 * 60 * 24 * 30 * 3
165
+ }
166
+
167
+ websites[domain] = website
168
+ Candy.core('Config').config.websites = websites
169
+
170
+ Candy.server('Web').clearSSLCache(domain)
171
+ Candy.server('Mail').clearSSLCache(domain)
172
+
173
+ log('SSL certificate successfully generated and saved for domain %s', domain)
174
+ } catch (err) {
175
+ error('Failed to save SSL certificate for domain %s: %s', domain, err.message)
176
+ }
177
+ }
178
+ }
179
+
180
+ module.exports = new SSL()
@@ -0,0 +1,27 @@
1
+ class Server {
2
+ constructor() {
3
+ Candy.core('Config').config.server.pid = process.pid
4
+ Candy.core('Config').config.server.started = Date.now()
5
+ Candy.server('Service')
6
+ Candy.server('DNS')
7
+ Candy.server('Web')
8
+ Candy.server('Mail')
9
+ Candy.server('Api')
10
+ Candy.server('Hub')
11
+ setTimeout(function () {
12
+ setInterval(function () {
13
+ Candy.server('Service').check()
14
+ Candy.server('SSL').check()
15
+ Candy.server('Web').check()
16
+ Candy.server('Mail').check()
17
+ }, 1000)
18
+ }, 1000)
19
+ }
20
+
21
+ stop() {
22
+ Candy.server('Service').stopAll()
23
+ Candy.server('Web').stopAll()
24
+ }
25
+ }
26
+
27
+ module.exports = new Server()
@@ -0,0 +1,248 @@
1
+ const {log, error} = Candy.core('Log', false).init('Service')
2
+
3
+ const childProcess = require('child_process')
4
+ const fs = require('fs')
5
+ const os = require('os')
6
+ const path = require('path')
7
+
8
+ class Service {
9
+ #services = []
10
+ #watcher = {}
11
+ #loaded = false
12
+ #logs = {}
13
+ #errs = {}
14
+ #error_counts = {}
15
+ #active = {}
16
+
17
+ #get(id) {
18
+ if (!this.#loaded && this.#services.length == 0) {
19
+ this.#services = Candy.core('Config').config.services ?? []
20
+ this.#loaded = true
21
+ }
22
+ for (const service of this.#services) {
23
+ if (service.id == id || service.name == id || service.file == id) return service
24
+ }
25
+ return false
26
+ }
27
+
28
+ #add(file) {
29
+ let name = path.basename(file)
30
+ if (name.substr(-3) == '.js') name = name.substr(0, name.length - 3)
31
+ let service = {
32
+ id: this.#services.length,
33
+ name: path.basename(file),
34
+ file: file,
35
+ active: true
36
+ }
37
+ this.#services.push(service)
38
+ this.#services[service.id] = service
39
+ Candy.core('Config').config.services = this.#services
40
+ return true
41
+ }
42
+
43
+ #set(id, key, value) {
44
+ let service = this.#get(id)
45
+ let index = this.#services.indexOf(service)
46
+ if (service) {
47
+ if (typeof key == 'object') {
48
+ for (const k in key) service[k] = key[k]
49
+ } else {
50
+ service[key] = value
51
+ }
52
+ } else {
53
+ return false
54
+ }
55
+ this.#services[index] = service
56
+ Candy.core('Config').config.services = this.#services
57
+ }
58
+
59
+ async check() {
60
+ this.#services = Candy.core('Config').config.services ?? []
61
+ for (const service of this.#services) {
62
+ if (service.active) {
63
+ if (!service.pid) {
64
+ this.#run(service.id)
65
+ } else {
66
+ if (!this.#watcher[service.pid]) {
67
+ log('Service %s (PID %s) is not running. Restarting...', service.name, service.pid)
68
+ Candy.core('Process').stop(service.pid)
69
+ this.#run(service.id)
70
+ this.#set(service.id, 'pid', null)
71
+ }
72
+ }
73
+ }
74
+ if (this.#logs[service.id])
75
+ fs.writeFile(os.homedir() + '/.candypack/logs/' + service.name + '.log', this.#logs[service.id], 'utf8', function (err) {
76
+ if (err) error(err)
77
+ })
78
+ if (this.#errs[service.id])
79
+ fs.writeFile(os.homedir() + '/.candypack/logs/' + service.name + '.err.log', this.#errs[service.id], 'utf8', function (err) {
80
+ if (err) error(err)
81
+ })
82
+ }
83
+ }
84
+
85
+ async delete(id) {
86
+ return new Promise(resolve => {
87
+ let service = this.#get(id)
88
+ if (service) {
89
+ this.stop(service.id)
90
+ this.#services = this.#services.filter(s => s.id != service.id)
91
+ Candy.core('Config').config.services = this.#services
92
+ return resolve(Candy.server('Api').result(true, __('Service %s deleted successfully.', service.name)))
93
+ } else {
94
+ return resolve(Candy.server('Api').result(false, __('Service %s not found.', id)))
95
+ }
96
+ })
97
+ }
98
+
99
+ async #run(id) {
100
+ if (this.#active[id]) return
101
+ this.#active[id] = true
102
+ let service = this.#get(id)
103
+ if (!service) return
104
+ log('Service %s is not running. Starting...', service.name)
105
+ if (this.#error_counts[id] > 10) {
106
+ this.#active[id] = false
107
+ log('Service %s has exceeded the maximum error limit. Please check the logs for more details.', service.name)
108
+ return
109
+ }
110
+ if ((service.status == 'errored' || service.status == 'stopped') && Date.now() - service.updated < this.#error_counts[id] * 1000) {
111
+ this.#active[id] = false
112
+ log('Service %s is in a cooldown period.', service.name)
113
+ return
114
+ }
115
+ log('Starting service %s...', service.name)
116
+ this.#set(id, 'updated', Date.now())
117
+ var child = childProcess.spawn('node', [service.file], {
118
+ cwd: path.dirname(service.file)
119
+ })
120
+ let pid = child.pid
121
+ child.stdout.on('data', data => {
122
+ if (!this.#logs[id]) this.#logs[id] = ''
123
+ this.#logs[id] +=
124
+ '[LOG][' +
125
+ Date.now() +
126
+ '] ' +
127
+ data
128
+ .toString()
129
+ .trim()
130
+ .split('\n')
131
+ .join('\n[LOG][' + Date.now() + '] ') +
132
+ '\n'
133
+ if (this.#logs[id].length > 1000000) this.#logs[id] = this.#logs[id].substr(this.#logs[id].length - 1000000)
134
+ })
135
+ child.stderr.on('data', data => {
136
+ if (!this.#errs[id]) this.#errs[id] = ''
137
+ this.#logs[id] +=
138
+ '[ERR][' +
139
+ Date.now() +
140
+ '] ' +
141
+ data
142
+ .toString()
143
+ .trim()
144
+ .split('\n')
145
+ .join('\n[ERR][' + Date.now() + '] ') +
146
+ '\n'
147
+ this.#errs[id] += data.toString()
148
+ if (this.#errs[id].length > 1000000) this.#errs[id] = this.#errs[id].substr(this.#errs[id].length - 1000000)
149
+ this.#set(id, {
150
+ status: 'errored',
151
+ updated: Date.now()
152
+ })
153
+ })
154
+ child.on('exit', () => {
155
+ if (this.#get(service.id).status == 'running') {
156
+ this.#set(id, {
157
+ pid: null,
158
+ started: null,
159
+ status: 'stopped',
160
+ updated: Date.now()
161
+ })
162
+ }
163
+ this.#watcher[pid] = false
164
+ this.#error_counts[id] = this.#error_counts[id] ?? 0
165
+ this.#error_counts[id]++
166
+ this.#active[id] = false
167
+ })
168
+ this.#set(id, {
169
+ active: true,
170
+ pid: pid,
171
+ started: Date.now(),
172
+ status: 'running'
173
+ })
174
+ this.#watcher[pid] = true
175
+ }
176
+
177
+ async init() {
178
+ log('Initializing services...')
179
+ this.#services = Candy.core('Config').config.services ?? []
180
+ for (const service of this.#services) {
181
+ fs.readFile(os.homedir() + '/.candypack/logs/' + service.name + '.log', 'utf8', (err, data) => {
182
+ if (!err) this.#logs[service.id] = data.toString()
183
+ })
184
+ }
185
+ this.#loaded = true
186
+ }
187
+
188
+ async start(file) {
189
+ return new Promise(resolve => {
190
+ if (file && file.length > 0) {
191
+ file = path.resolve(file)
192
+ if (fs.existsSync(file)) {
193
+ if (!this.#get(file)) {
194
+ this.#add(file)
195
+ return resolve(Candy.server('Api').result(true, __('Service %s added successfully.', file)))
196
+ } else {
197
+ return resolve(Candy.server('Api').result(false, __('Service %s already exists.', file)))
198
+ }
199
+ } else {
200
+ return resolve(Candy.server('Api').result(false, __('Service file %s not found.', file)))
201
+ }
202
+ } else {
203
+ return resolve(Candy.server('Api').result(false, __('Service file not specified.')))
204
+ }
205
+ })
206
+ }
207
+
208
+ stop(id) {
209
+ let service = this.#get(id)
210
+ if (service) {
211
+ if (service.pid) {
212
+ Candy.core('Process').stop(service.pid)
213
+ this.#set(id, 'pid', null)
214
+ this.#set(id, 'started', null)
215
+ this.#set(id, 'active', false)
216
+ } else {
217
+ log(__('Service %s is not running.', id))
218
+ }
219
+ } else {
220
+ log(__('Service %s not found.', id))
221
+ }
222
+ }
223
+
224
+ async status() {
225
+ let services = Candy.core('Config').config.services ?? []
226
+ for (const service of services) {
227
+ if (service.status == 'running') {
228
+ var uptime = Date.now() - service.started
229
+ let seconds = Math.floor(uptime / 1000)
230
+ let minutes = Math.floor(seconds / 60)
231
+ let hours = Math.floor(minutes / 60)
232
+ let days = Math.floor(hours / 24)
233
+ seconds %= 60
234
+ minutes %= 60
235
+ hours %= 24
236
+ let uptimeString = ''
237
+ if (days) uptimeString += days + 'd '
238
+ if (hours) uptimeString += hours + 'h '
239
+ if (minutes) uptimeString += minutes + 'm '
240
+ if (seconds) uptimeString += seconds + 's'
241
+ service.uptime = uptimeString
242
+ }
243
+ }
244
+ return services
245
+ }
246
+ }
247
+
248
+ module.exports = new Service()
@@ -0,0 +1,64 @@
1
+ class Subdomain {
2
+ async create(subdomain) {
3
+ let domain = subdomain.split('.')
4
+ subdomain = subdomain.trim().split('.')
5
+ if (subdomain.length < 3) return Candy.server('Api').result(false, await __('Invalid subdomain name.'))
6
+ if (Candy.core('Config').config.websites[domain.join('.')])
7
+ return Candy.server('Api').result(false, await __('Domain %s already exists.', domain.join('.')))
8
+ while (domain.length > 2) {
9
+ domain.shift()
10
+ if (Candy.core('Config').config.websites[domain.join('.')]) {
11
+ domain = domain.join('.')
12
+ break
13
+ }
14
+ }
15
+ if (typeof domain == 'object') return Candy.server('Api').result(false, await __('Domain %s not found.', domain.join('.')))
16
+ subdomain = subdomain.join('.').substr(0, subdomain.join('.').length - domain.length - 1)
17
+ let fulldomain = [subdomain, domain].join('.')
18
+ if (Candy.core('Config').config.websites[domain].subdomain.includes(subdomain))
19
+ return Candy.server('Api').result(false, await __('Subdomain %s already exists.', fulldomain))
20
+ Candy.server('DNS').record({name: fulldomain, type: 'A'}, {name: 'www.' + fulldomain, type: 'CNAME'}, {name: fulldomain, type: 'MX'})
21
+ let websites = Candy.core('Config').config.websites
22
+ websites[domain].subdomain.push(subdomain)
23
+ websites[domain].subdomain.push('www.' + subdomain)
24
+ websites[domain].subdomain.sort()
25
+ Candy.core('Config').config.websites = websites
26
+ Candy.server('SSL').renew(domain)
27
+ return Candy.server('Api').result(true, await __('Subdomain %s1 created successfully for domain %s2.', fulldomain, domain))
28
+ }
29
+
30
+ async delete(subdomain) {
31
+ let domain = subdomain.split('.')
32
+ subdomain = subdomain.trim().split('.')
33
+ if (subdomain.length < 3) return Candy.server('Api').result(false, await __('Invalid subdomain name.'))
34
+ if (Candy.core('Config').config.websites[domain.join('.')])
35
+ return Candy.server('Api').result(false, await __('%s is a domain.', domain.join('.')))
36
+ while (domain.length > 2) {
37
+ domain.shift()
38
+ if (Candy.core('Config').config.websites[domain.join('.')]) {
39
+ domain = domain.join('.')
40
+ break
41
+ }
42
+ }
43
+ if (typeof domain == 'object') return Candy.server('Api').result(false, await __('Domain %s not found.', domain.join('.')))
44
+ subdomain = subdomain.join('.').substr(0, subdomain.join('.').length - domain.length - 1)
45
+ let fulldomain = [subdomain, domain].join('.')
46
+ if (!Candy.core('Config').config.websites[domain].subdomain.includes(subdomain))
47
+ return Candy.server('Api').result(false, await __('Subdomain %s not found.', fulldomain))
48
+ Candy.server('DNS').delete({name: fulldomain, type: 'A'}, {name: 'www.' + fulldomain, type: 'CNAME'}, {name: fulldomain, type: 'MX'})
49
+ let websites = Candy.core('Config').config.websites
50
+ websites[domain].subdomain = websites[domain].subdomain.filter(s => s != subdomain && s != 'www.' + subdomain)
51
+ Candy.core('Config').config.websites = websites
52
+ return Candy.server('Api').result(true, await __('Subdomain %s1 deleted successfully from domain %s2.', fulldomain, domain))
53
+ }
54
+
55
+ async list(domain) {
56
+ if (!Candy.core('Config').config.websites[domain]) return Candy.server('Api').result(false, await __('Domain %s not found.', domain))
57
+ let subdomains = Candy.core('Config').config.websites[domain].subdomain.map(subdomain => {
58
+ return subdomain + '.' + domain
59
+ })
60
+ return Candy.server('Api').result(true, (await __('Subdomains of %s:', domain)) + '\n ' + subdomains.join('\n '))
61
+ }
62
+ }
63
+
64
+ module.exports = new Subdomain()
@@ -0,0 +1,170 @@
1
+ const {log} = Candy.core('Log', false).init('Firewall')
2
+
3
+ /**
4
+ * Firewall class to handle IP blocking and rate limiting.
5
+ */
6
+ class Firewall {
7
+ #requestCounts = new Map() // IP -> { count, timestamp }
8
+ #config = {}
9
+ #cleanupInterval = null
10
+
11
+ constructor() {
12
+ this.load()
13
+ // Run cleanup every minute
14
+ this.#cleanupInterval = setInterval(() => this.cleanup(), 60000)
15
+ if (this.#cleanupInterval.unref) this.#cleanupInterval.unref()
16
+ }
17
+
18
+ /**
19
+ * Load configuration from the global Config module.
20
+ */
21
+ load() {
22
+ // Load configuration from Candy.core('Config').config
23
+ const config = Candy.core('Config').config.firewall || {}
24
+
25
+ this.#config = {
26
+ enabled: config.enabled !== false,
27
+ rateLimit: {
28
+ enabled: config.rateLimit?.enabled !== false,
29
+ windowMs: config.rateLimit?.windowMs ?? 60000, // 1 minute
30
+ max: config.rateLimit?.max ?? 300 // limit each IP to 300 requests per windowMs
31
+ },
32
+ blacklist: new Set(config.blacklist || []),
33
+ whitelist: new Set(config.whitelist || [])
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check if a request should be allowed.
39
+ * @param {Object} req - The HTTP request object.
40
+ * @returns {Object} An object containing {allowed: boolean, reason?: string}.
41
+ */
42
+ check(req) {
43
+ if (!this.#config.enabled) return {allowed: true}
44
+
45
+ // Extract IP address safely, handling x-forwarded-for which can be a comma-separated list
46
+ let ip = req.socket?.remoteAddress || req.headers['x-forwarded-for']?.split(',')[0]?.trim()
47
+
48
+ // Normalize IPv6-mapped IPv4 addresses
49
+ if (ip && ip.startsWith('::ffff:')) {
50
+ ip = ip.substring(7)
51
+ }
52
+
53
+ // Note: Native IPv6 addresses are not fully normalized (e.g. :: vs 0:0...).
54
+ // Blacklist/Whitelist entries for IPv6 should match the format provided by the socket (usually compressed).
55
+
56
+ if (!ip) return {allowed: true}
57
+
58
+ // 1. Check whitelist (bypass everything)
59
+ if (this.#config.whitelist.has(ip)) return {allowed: true}
60
+
61
+ // 2. Check blacklist
62
+ if (this.#config.blacklist.has(ip)) {
63
+ log(`Blocked request from blacklisted IP: ${ip}`)
64
+ return {allowed: false, reason: 'blacklist'}
65
+ }
66
+
67
+ // 3. Rate limiting
68
+ if (this.#config.rateLimit.enabled) {
69
+ // Memory protection: if map gets too big, clear it to prevent memory leak
70
+ if (this.#requestCounts.size > 20000) {
71
+ this.#requestCounts.clear()
72
+ log('Firewall request counts cleared due to memory limit')
73
+ }
74
+
75
+ const now = Date.now()
76
+ let record = this.#requestCounts.get(ip)
77
+
78
+ if (!record) {
79
+ record = {count: 0, timestamp: now}
80
+ this.#requestCounts.set(ip, record)
81
+ }
82
+
83
+ // Check window
84
+ if (now - record.timestamp > this.#config.rateLimit.windowMs) {
85
+ // Reset window
86
+ record.count = 1
87
+ record.timestamp = now
88
+ } else {
89
+ record.count++
90
+ }
91
+
92
+ if (record.count > this.#config.rateLimit.max) {
93
+ if (record.count === this.#config.rateLimit.max + 1) {
94
+ log(`Rate limit exceeded for IP: ${ip}`)
95
+ }
96
+ return {allowed: false, reason: 'rate_limit'}
97
+ }
98
+ }
99
+
100
+ return {allowed: true}
101
+ }
102
+
103
+ /**
104
+ * Cleanup stale rate limit records.
105
+ */
106
+ cleanup() {
107
+ if (!this.#config.rateLimit?.windowMs) return
108
+
109
+ const now = Date.now()
110
+ const windowMs = this.#config.rateLimit.windowMs
111
+
112
+ for (const [ip, record] of this.#requestCounts) {
113
+ if (now - record.timestamp > windowMs) {
114
+ this.#requestCounts.delete(ip)
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Add an IP to the blacklist.
121
+ * @param {string} ip - The IP address to block.
122
+ */
123
+ addBlock(ip) {
124
+ if (this.#config.whitelist.has(ip)) this.#config.whitelist.delete(ip)
125
+ this.#config.blacklist.add(ip)
126
+ this.#save()
127
+ }
128
+
129
+ /**
130
+ * Remove an IP from the blacklist.
131
+ * @param {string} ip - The IP address to unblock.
132
+ */
133
+ removeBlock(ip) {
134
+ this.#config.blacklist.delete(ip)
135
+ this.#save()
136
+ }
137
+
138
+ /**
139
+ * Add an IP to the whitelist.
140
+ * @param {string} ip - The IP address to whitelist.
141
+ */
142
+ addWhitelist(ip) {
143
+ if (this.#config.blacklist.has(ip)) this.#config.blacklist.delete(ip)
144
+ this.#config.whitelist.add(ip)
145
+ this.#save()
146
+ }
147
+
148
+ /**
149
+ * Remove an IP from the whitelist.
150
+ * @param {string} ip - The IP address to remove from whitelist.
151
+ */
152
+ removeWhitelist(ip) {
153
+ this.#config.whitelist.delete(ip)
154
+ this.#save()
155
+ }
156
+
157
+ #save() {
158
+ // Update the global config
159
+ if (!Candy.core('Config').config.firewall) Candy.core('Config').config.firewall = {}
160
+
161
+ Candy.core('Config').config.firewall.blacklist = Array.from(this.#config.blacklist)
162
+ Candy.core('Config').config.firewall.whitelist = Array.from(this.#config.whitelist)
163
+ // Config module handles saving automatically when properties change if using Proxy,
164
+ // but here we are modifying the object structure.
165
+ // Assuming Config module watches for changes or we need to trigger save.
166
+ // Looking at Config.js, it uses Proxy to detect changes.
167
+ }
168
+ }
169
+
170
+ module.exports = Firewall