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,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
|