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,535 @@
|
|
|
1
|
+
const {log} = Candy.core('Log', false).init('Hub')
|
|
2
|
+
|
|
3
|
+
const axios = require('axios')
|
|
4
|
+
const nodeCrypto = require('crypto')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
|
|
8
|
+
class Hub {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.websocket = null
|
|
11
|
+
this.httpInterval = null
|
|
12
|
+
this.websocketReconnectAttempts = 0
|
|
13
|
+
this.maxReconnectAttempts = 5
|
|
14
|
+
this.lastNetworkStats = null
|
|
15
|
+
this.lastNetworkTime = null
|
|
16
|
+
|
|
17
|
+
this.startHttpPolling()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
startHttpPolling() {
|
|
21
|
+
if (this.httpInterval) {
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
log('Starting HTTP polling (60s interval)')
|
|
26
|
+
this.check()
|
|
27
|
+
this.httpInterval = setInterval(() => {
|
|
28
|
+
if (!this.websocket) {
|
|
29
|
+
this.check()
|
|
30
|
+
}
|
|
31
|
+
}, 10000)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
stopHttpPolling() {
|
|
35
|
+
if (this.httpInterval) {
|
|
36
|
+
log('Stopping HTTP polling')
|
|
37
|
+
clearInterval(this.httpInterval)
|
|
38
|
+
this.httpInterval = null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async check() {
|
|
43
|
+
const hub = Candy.core('Config').config.hub
|
|
44
|
+
if (!hub || !hub.token) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const status = this.getSystemStatus()
|
|
50
|
+
status.timestamp = Math.floor(Date.now() / 1000)
|
|
51
|
+
|
|
52
|
+
const response = await this.call('status', status)
|
|
53
|
+
|
|
54
|
+
if (!response.authenticated) {
|
|
55
|
+
log('Server not authenticated: %s', response.reason || 'unknown')
|
|
56
|
+
if (response.reason === 'token_invalid' || response.reason === 'signature_invalid') {
|
|
57
|
+
log('Authentication credentials invalid, clearing config')
|
|
58
|
+
delete Candy.core('Config').config.hub
|
|
59
|
+
}
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (response.websocket && !this.websocket) {
|
|
64
|
+
log('WebSocket requested by cloud')
|
|
65
|
+
this.connectWebSocket(response.websocketUrl, response.websocketToken)
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
log('Failed to report status: %s', error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
connectWebSocket(url, token) {
|
|
73
|
+
if (this.websocket) {
|
|
74
|
+
log('WebSocket already connected')
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const WebSocket = require('ws')
|
|
80
|
+
const wsUrl = `${url}?token=${token}`
|
|
81
|
+
|
|
82
|
+
log('Connecting to WebSocket: %s', url)
|
|
83
|
+
this.websocket = new WebSocket(wsUrl, {
|
|
84
|
+
rejectUnauthorized: true
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
this.websocket.on('open', () => {
|
|
88
|
+
log('WebSocket connected')
|
|
89
|
+
this.websocketReconnectAttempts = 0
|
|
90
|
+
this.stopHttpPolling()
|
|
91
|
+
this.sendWebSocketStatus()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
this.websocket.on('message', data => {
|
|
95
|
+
this.handleWebSocketMessage(data)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
this.websocket.on('close', () => {
|
|
99
|
+
log('WebSocket disconnected')
|
|
100
|
+
this.websocket = null
|
|
101
|
+
this.startHttpPolling()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
this.websocket.on('error', error => {
|
|
105
|
+
log('WebSocket error: %s', error.message)
|
|
106
|
+
})
|
|
107
|
+
} catch (error) {
|
|
108
|
+
log('Failed to connect WebSocket: %s', error.message)
|
|
109
|
+
this.websocket = null
|
|
110
|
+
this.startHttpPolling()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
disconnectWebSocket() {
|
|
115
|
+
if (this.websocket) {
|
|
116
|
+
log('Disconnecting WebSocket')
|
|
117
|
+
this.websocket.close()
|
|
118
|
+
this.websocket = null
|
|
119
|
+
this.startHttpPolling()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
sendWebSocketStatus() {
|
|
124
|
+
if (!this.websocket || this.websocket.readyState !== 1) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const status = this.getSystemStatus()
|
|
129
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
130
|
+
|
|
131
|
+
const message = {
|
|
132
|
+
type: 'status',
|
|
133
|
+
data: status,
|
|
134
|
+
timestamp: timestamp,
|
|
135
|
+
signature: this.signWebSocketMessage({type: 'status', data: status, timestamp})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.websocket.send(JSON.stringify(message))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
handleWebSocketMessage(data) {
|
|
142
|
+
try {
|
|
143
|
+
const message = JSON.parse(data.toString())
|
|
144
|
+
|
|
145
|
+
if (message.type === 'disconnect') {
|
|
146
|
+
log('Cloud requested disconnect: %s', message.reason || 'unknown')
|
|
147
|
+
this.disconnectWebSocket()
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (message.type === 'command') {
|
|
152
|
+
if (this.verifyWebSocketMessage(message)) {
|
|
153
|
+
this.processCommand(message.data)
|
|
154
|
+
} else {
|
|
155
|
+
log('WebSocket message verification failed')
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
log('Failed to handle WebSocket message: %s', error.message)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
signWebSocketMessage(message) {
|
|
164
|
+
const hub = Candy.core('Config').config.hub
|
|
165
|
+
if (!hub || !hub.secret) {
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const payload = JSON.stringify({type: message.type, data: message.data, timestamp: message.timestamp})
|
|
170
|
+
return nodeCrypto.createHmac('sha256', hub.secret).update(payload).digest('hex')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
verifyWebSocketMessage(message) {
|
|
174
|
+
const {type, data, timestamp, signature} = message
|
|
175
|
+
|
|
176
|
+
if (!signature || !timestamp) {
|
|
177
|
+
log('Missing signature or timestamp in WebSocket message')
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const now = Math.floor(Date.now() / 1000)
|
|
182
|
+
if (Math.abs(now - timestamp) > 300) {
|
|
183
|
+
log('WebSocket message timestamp too old or in future')
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const expectedSignature = this.signWebSocketMessage({type, data, timestamp})
|
|
188
|
+
if (signature !== expectedSignature) {
|
|
189
|
+
log('Invalid WebSocket message signature')
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
processCommand(command) {
|
|
197
|
+
log('Processing command: %s', command.action)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getSystemStatus() {
|
|
201
|
+
const totalMem = os.totalmem()
|
|
202
|
+
const freeMem = os.freemem()
|
|
203
|
+
const diskInfo = this.getDiskUsage()
|
|
204
|
+
const networkInfo = this.getNetworkUsage()
|
|
205
|
+
const servicesInfo = this.getServicesInfo()
|
|
206
|
+
|
|
207
|
+
const serverStarted = Candy.core('Config').config.server.started
|
|
208
|
+
const candypackUptime = serverStarted ? Math.floor((Date.now() - serverStarted) / 1000) : 0
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
cpu: this.getCpuUsage(),
|
|
212
|
+
memory: {
|
|
213
|
+
used: totalMem - freeMem,
|
|
214
|
+
total: totalMem
|
|
215
|
+
},
|
|
216
|
+
disk: diskInfo,
|
|
217
|
+
network: networkInfo,
|
|
218
|
+
services: servicesInfo,
|
|
219
|
+
uptime: candypackUptime,
|
|
220
|
+
hostname: os.hostname(),
|
|
221
|
+
platform: os.platform(),
|
|
222
|
+
arch: os.arch(),
|
|
223
|
+
node: process.version
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getServicesInfo() {
|
|
228
|
+
try {
|
|
229
|
+
const config = Candy.core('Config').config
|
|
230
|
+
|
|
231
|
+
const websites = config.websites ? Object.keys(config.websites).length : 0
|
|
232
|
+
const services = config.services ? config.services.length : 0
|
|
233
|
+
const mailAccounts = config.mail && config.mail.accounts ? Object.keys(config.mail.accounts).length : 0
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
websites: websites,
|
|
237
|
+
services: services,
|
|
238
|
+
mail: mailAccounts
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
log('Failed to get services info: %s', error.message)
|
|
242
|
+
return {
|
|
243
|
+
websites: 0,
|
|
244
|
+
services: 0,
|
|
245
|
+
mail: 0
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
getDiskUsage() {
|
|
251
|
+
try {
|
|
252
|
+
const {execSync} = require('child_process')
|
|
253
|
+
let command
|
|
254
|
+
|
|
255
|
+
if (os.platform() === 'win32') {
|
|
256
|
+
command = 'wmic logicaldisk get size,freespace,caption'
|
|
257
|
+
} else {
|
|
258
|
+
command = "df -k / | tail -1 | awk '{print $2,$3}'"
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const output = execSync(command, {encoding: 'utf8'})
|
|
262
|
+
|
|
263
|
+
if (os.platform() === 'win32') {
|
|
264
|
+
const lines = output.trim().split('\n')
|
|
265
|
+
if (lines.length > 1) {
|
|
266
|
+
const parts = lines[1].trim().split(/\s+/)
|
|
267
|
+
const free = parseInt(parts[1]) || 0
|
|
268
|
+
const total = parseInt(parts[2]) || 0
|
|
269
|
+
return {
|
|
270
|
+
used: total - free,
|
|
271
|
+
total: total
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
const parts = output.trim().split(/\s+/)
|
|
276
|
+
const total = parseInt(parts[0]) * 1024
|
|
277
|
+
const used = parseInt(parts[1]) * 1024
|
|
278
|
+
return {
|
|
279
|
+
used: used,
|
|
280
|
+
total: total
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
log('Failed to get disk usage: %s', error.message)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
used: 0,
|
|
289
|
+
total: 0
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getNetworkUsage() {
|
|
294
|
+
try {
|
|
295
|
+
const {execSync} = require('child_process')
|
|
296
|
+
let command
|
|
297
|
+
|
|
298
|
+
if (os.platform() === 'win32') {
|
|
299
|
+
command = 'netstat -e'
|
|
300
|
+
} else if (os.platform() === 'darwin') {
|
|
301
|
+
command = "netstat -ib | grep -e 'en0' | head -1 | awk '{print $7,$10}'"
|
|
302
|
+
} else {
|
|
303
|
+
command = "cat /proc/net/dev | grep -E 'eth0|ens|enp' | head -1 | awk '{print $2,$10}'"
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const output = execSync(command, {encoding: 'utf8', timeout: 5000})
|
|
307
|
+
let currentStats = {received: 0, sent: 0}
|
|
308
|
+
|
|
309
|
+
if (os.platform() === 'win32') {
|
|
310
|
+
const lines = output.split('\n')
|
|
311
|
+
for (const line of lines) {
|
|
312
|
+
if (line.includes('Bytes')) {
|
|
313
|
+
const parts = line.trim().split(/\s+/)
|
|
314
|
+
currentStats.received = parseInt(parts[1]) || 0
|
|
315
|
+
currentStats.sent = parseInt(parts[2]) || 0
|
|
316
|
+
break
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
const parts = output.trim().split(/\s+/)
|
|
321
|
+
currentStats.received = parseInt(parts[0]) || 0
|
|
322
|
+
currentStats.sent = parseInt(parts[1]) || 0
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const now = Date.now()
|
|
326
|
+
|
|
327
|
+
if (this.lastNetworkStats && this.lastNetworkTime) {
|
|
328
|
+
const timeDiff = (now - this.lastNetworkTime) / 1000
|
|
329
|
+
const receivedDiff = currentStats.received - this.lastNetworkStats.received
|
|
330
|
+
const sentDiff = currentStats.sent - this.lastNetworkStats.sent
|
|
331
|
+
|
|
332
|
+
const bandwidth = {
|
|
333
|
+
download: Math.max(0, Math.round(receivedDiff / timeDiff)),
|
|
334
|
+
upload: Math.max(0, Math.round(sentDiff / timeDiff))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.lastNetworkStats = currentStats
|
|
338
|
+
this.lastNetworkTime = now
|
|
339
|
+
|
|
340
|
+
return bandwidth
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
this.lastNetworkStats = currentStats
|
|
344
|
+
this.lastNetworkTime = now
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
download: 0,
|
|
348
|
+
upload: 0
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
log('Failed to get network usage: %s', error.message)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
download: 0,
|
|
356
|
+
upload: 0
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
getCpuUsage() {
|
|
361
|
+
const cpus = os.cpus()
|
|
362
|
+
let totalIdle = 0
|
|
363
|
+
let totalTick = 0
|
|
364
|
+
|
|
365
|
+
for (const cpu of cpus) {
|
|
366
|
+
for (const type in cpu.times) {
|
|
367
|
+
totalTick += cpu.times[type]
|
|
368
|
+
}
|
|
369
|
+
totalIdle += cpu.times.idle
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const idle = totalIdle / cpus.length
|
|
373
|
+
const total = totalTick / cpus.length
|
|
374
|
+
const usage = 100 - ~~((100 * idle) / total)
|
|
375
|
+
|
|
376
|
+
return usage
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
getLinuxDistro() {
|
|
380
|
+
log('Getting Linux distro info...')
|
|
381
|
+
if (os.platform() !== 'linux') {
|
|
382
|
+
log('Platform is not Linux: %s', os.platform())
|
|
383
|
+
return null
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
log('Reading /etc/os-release...')
|
|
388
|
+
const osRelease = fs.readFileSync('/etc/os-release', 'utf8')
|
|
389
|
+
const lines = osRelease.split('\n')
|
|
390
|
+
const distro = {}
|
|
391
|
+
|
|
392
|
+
for (const line of lines) {
|
|
393
|
+
const [key, value] = line.split('=')
|
|
394
|
+
if (key && value) {
|
|
395
|
+
distro[key] = value.replace(/"/g, '')
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result = {
|
|
400
|
+
name: distro.NAME || distro.ID || 'Unknown',
|
|
401
|
+
version: distro.VERSION_ID || distro.VERSION || 'Unknown',
|
|
402
|
+
id: distro.ID || 'unknown'
|
|
403
|
+
}
|
|
404
|
+
log('Distro detected: %s %s', result.name, result.version)
|
|
405
|
+
return result
|
|
406
|
+
} catch (err) {
|
|
407
|
+
log('Failed to read distro info: %s', err.message)
|
|
408
|
+
return null
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async auth(code) {
|
|
413
|
+
log('CandyPack authenticating...')
|
|
414
|
+
log('Auth code received: %s', code ? code.substring(0, 8) + '...' : 'none')
|
|
415
|
+
const packageJson = require('../../package.json')
|
|
416
|
+
const distro = this.getLinuxDistro()
|
|
417
|
+
|
|
418
|
+
let data = {
|
|
419
|
+
code: code,
|
|
420
|
+
os: os.platform(),
|
|
421
|
+
arch: os.arch(),
|
|
422
|
+
hostname: os.hostname(),
|
|
423
|
+
version: packageJson.version,
|
|
424
|
+
node: process.version
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
log('Auth data prepared: os=%s, arch=%s, hostname=%s, version=%s', data.os, data.arch, data.hostname, data.version)
|
|
428
|
+
|
|
429
|
+
if (distro) {
|
|
430
|
+
data.distro = distro
|
|
431
|
+
log('Distro info added to auth data')
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
log('Calling hub API for authentication...')
|
|
435
|
+
const response = await this.call('auth', data)
|
|
436
|
+
let token = response.token
|
|
437
|
+
let secret = response.secret
|
|
438
|
+
log('Token received: %s...', token ? token.substring(0, 8) : 'none')
|
|
439
|
+
Candy.core('Config').config.hub = {token: token, secret: secret}
|
|
440
|
+
log('CandyPack authenticated!')
|
|
441
|
+
return Candy.server('Api').result(true, __('Authentication successful'))
|
|
442
|
+
} catch (error) {
|
|
443
|
+
log('Authentication failed: %s', error ? error : 'Unknown error')
|
|
444
|
+
return Candy.server('Api').result(false, error || __('Authentication failed'))
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
signRequest(data) {
|
|
449
|
+
const hub = Candy.core('Config').config.hub
|
|
450
|
+
if (!hub || !hub.secret) {
|
|
451
|
+
return null
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const signature = nodeCrypto.createHmac('sha256', hub.secret).update(JSON.stringify(data)).digest('hex')
|
|
455
|
+
|
|
456
|
+
return signature
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
call(action, data) {
|
|
460
|
+
log('Hub API call: %s', action)
|
|
461
|
+
return new Promise((resolve, reject) => {
|
|
462
|
+
const url = 'https://hub.candypack.dev/' + action
|
|
463
|
+
log('POST request to: %s', url)
|
|
464
|
+
|
|
465
|
+
const headers = {}
|
|
466
|
+
const hub = Candy.core('Config').config.hub
|
|
467
|
+
if (hub && hub.token) {
|
|
468
|
+
headers['Authorization'] = `Bearer ${hub.token}`
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (action !== 'auth' && data.timestamp) {
|
|
472
|
+
const signature = this.signRequest(data)
|
|
473
|
+
if (signature) {
|
|
474
|
+
headers['X-Signature'] = signature
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
axios
|
|
479
|
+
.post(url, data, {
|
|
480
|
+
headers,
|
|
481
|
+
httpsAgent: new (require('https').Agent)({
|
|
482
|
+
rejectUnauthorized: true
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
.then(response => {
|
|
486
|
+
log('Raw response received for %s', action)
|
|
487
|
+
log('Response structure: %j', {
|
|
488
|
+
hasData: !!response.data,
|
|
489
|
+
hasResult: !!(response.data && response.data.result),
|
|
490
|
+
dataKeys: response.data ? Object.keys(response.data) : []
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
if (!response.data) {
|
|
494
|
+
log('Response has no data')
|
|
495
|
+
return reject('Invalid response: no data')
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!response.data.result) {
|
|
499
|
+
log('Response has no result field')
|
|
500
|
+
return reject('Invalid response: no result field')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!response.data.result.success) {
|
|
504
|
+
log('API returned error: %s', response.data.result.message)
|
|
505
|
+
|
|
506
|
+
if (response.data.result.authenticated === false) {
|
|
507
|
+
log('Authentication failed, returning result for handling')
|
|
508
|
+
return resolve(response.data.result)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return reject(response.data.result.message)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
log('API call successful: %s', action)
|
|
515
|
+
resolve(response.data.data)
|
|
516
|
+
})
|
|
517
|
+
.catch(error => {
|
|
518
|
+
log('API call failed: %s - %s', action, error.message)
|
|
519
|
+
if (error.response) {
|
|
520
|
+
log('Error response status: %s', error.response.status)
|
|
521
|
+
log('Error response data: %j', error.response.data)
|
|
522
|
+
reject(error.response.data)
|
|
523
|
+
} else if (error.request) {
|
|
524
|
+
log('No response received, request was made')
|
|
525
|
+
reject('No response from server')
|
|
526
|
+
} else {
|
|
527
|
+
log('Request setup error: %s', error.message)
|
|
528
|
+
reject(error.message)
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
module.exports = new Hub()
|