Sebebeb commited on
Commit
e654bdf
·
verified ·
1 Parent(s): 0ad0ef0

Delete server.js

Browse files
Files changed (1) hide show
  1. server.js +0 -374
server.js DELETED
@@ -1,374 +0,0 @@
1
- const http = require('http');
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { WebSocketServer, WebSocket } = require('ws');
5
- const { v4: uuidv4 } = require('uuid');
6
- const crypto = require('crypto');
7
-
8
- // ─── Auth ─────────────────────────────────────────────────────────────────────
9
- // Secret key from environment variable "Key"
10
- const SECRET = process.env.Key || '';
11
- if (!SECRET) console.warn('[Auth] WARNING: Environment variable "Key" is not set. Admin login will be disabled.');
12
-
13
- // HMAC-based session tokens: token = hmac(SECRET, nonce+timestamp)
14
- // Sessions are stored server-side; possession of token alone is not enough —
15
- // the server must have issued it.
16
- const activeSessions = new Set(); // Set<token>
17
-
18
- function createSession() {
19
- const nonce = crypto.randomBytes(32).toString('hex');
20
- const token = crypto.createHmac('sha256', SECRET).update(nonce).digest('hex');
21
- activeSessions.add(token);
22
- return token;
23
- }
24
-
25
- function isValidSession(token) {
26
- return typeof token === 'string' && activeSessions.has(token);
27
- }
28
-
29
- // ─── In-Memory State ──────────────────────────────────────────────────────────
30
- const sounds = {};
31
- const notifications = {};
32
- const devices = {};
33
-
34
- const deviceClients = {};
35
- const adminClients = new Set(); // only authenticated WS connections
36
-
37
- const MIME = {
38
- '.html': 'text/html',
39
- '.js': 'application/javascript',
40
- '.css': 'text/css',
41
- '.json': 'application/json',
42
- '.png': 'image/png',
43
- '.ico': 'image/x-icon',
44
- };
45
-
46
- // ─── HTTP File Server ─────────────────────────────────────────────────────────
47
- const httpServer = http.createServer((req, res) => {
48
- let urlPath = req.url.split('?')[0];
49
- if (urlPath === '/' || urlPath === '') urlPath = '/index.html';
50
- if (urlPath === '/admin' || urlPath === '/admin/') urlPath = '/admin/index.html';
51
-
52
- const filePath = path.join(__dirname, 'public', urlPath);
53
-
54
- // Prevent path traversal
55
- const publicDir = path.resolve(__dirname, 'public');
56
- const resolved = path.resolve(filePath);
57
- if (!resolved.startsWith(publicDir)) {
58
- res.writeHead(403); res.end('Forbidden'); return;
59
- }
60
-
61
- const ext = path.extname(filePath);
62
- const mime = MIME[ext] || 'application/octet-stream';
63
-
64
- fs.readFile(filePath, (err, data) => {
65
- if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not found'); return; }
66
- res.writeHead(200, { 'Content-Type': mime });
67
- res.end(data);
68
- });
69
- });
70
-
71
- // ─── WebSocket Server ─────────────────────────────────────────────────────────
72
- const wss = new WebSocketServer({ server: httpServer });
73
-
74
- wss.on('connection', (ws, req) => {
75
- const urlPath = req.url || '/';
76
- if (urlPath.startsWith('/admin-ws')) {
77
- handleAdminConnection(ws);
78
- } else {
79
- handleDeviceConnection(ws);
80
- }
81
- });
82
-
83
- // ─── Helpers ──────────────────────────────────────────────────────────────────
84
- function send(ws, msg) {
85
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
86
- }
87
-
88
- function broadcastAdmin(msg) {
89
- for (const ws of adminClients) send(ws, msg);
90
- }
91
-
92
- function broadcastDevice(uuid, msg) {
93
- const ws = deviceClients[uuid];
94
- if (ws) send(ws, msg);
95
- }
96
-
97
- function broadcastAllDevices(msg) {
98
- for (const uuid of Object.keys(deviceClients)) broadcastDevice(uuid, msg);
99
- }
100
-
101
- function getFullState() {
102
- return {
103
- sounds: Object.values(sounds),
104
- notifications: Object.values(notifications),
105
- devices: Object.values(devices).map(devicePublic),
106
- };
107
- }
108
-
109
- function devicePublic(d) {
110
- return {
111
- uuid: d.uuid,
112
- name: d.name,
113
- notifications: d.notifications,
114
- lastConnection: d.lastConnection,
115
- schedule: d.schedule || [],
116
- pendingChanges: d.pendingChanges || [],
117
- online: !!deviceClients[d.uuid],
118
- cachedSounds: d.cachedSounds || [],
119
- };
120
- }
121
-
122
- // ─── Admin Connection Handler ─────────────────────────────────────────────────
123
- function handleAdminConnection(ws) {
124
- let authenticated = false;
125
-
126
- // Give them 10 seconds to authenticate, then close
127
- const authTimeout = setTimeout(() => {
128
- if (!authenticated) {
129
- send(ws, { type: 'auth_timeout' });
130
- ws.close();
131
- }
132
- }, 10000);
133
-
134
- ws.on('message', (raw) => {
135
- let msg;
136
- try { msg = JSON.parse(raw); } catch { return; }
137
-
138
- // ── Authentication handshake ────────���─────────────────────────────────────
139
- if (!authenticated) {
140
- if (msg.type === 'auth_resume' && isValidSession(msg.token)) {
141
- // Client is resuming a valid session
142
- authenticated = true;
143
- clearTimeout(authTimeout);
144
- adminClients.add(ws);
145
- send(ws, { type: 'auth_ok', token: msg.token });
146
- send(ws, { type: 'full_state', ...getFullState() });
147
- console.log('[Admin] session resumed');
148
- return;
149
- }
150
-
151
- if (msg.type === 'auth_login') {
152
- if (!SECRET) {
153
- send(ws, { type: 'auth_error', reason: 'Server has no Key configured.' });
154
- return;
155
- }
156
- // Constant-time comparison to prevent timing attacks
157
- const provided = Buffer.from(String(msg.password || ''));
158
- const expected = Buffer.from(SECRET);
159
- const match = provided.length === expected.length &&
160
- crypto.timingSafeEqual(provided, expected);
161
- if (match) {
162
- authenticated = true;
163
- clearTimeout(authTimeout);
164
- const token = createSession();
165
- adminClients.add(ws);
166
- send(ws, { type: 'auth_ok', token });
167
- send(ws, { type: 'full_state', ...getFullState() });
168
- console.log('[Admin] authenticated');
169
- } else {
170
- send(ws, { type: 'auth_error', reason: 'Invalid password.' });
171
- console.warn('[Admin] failed login attempt');
172
- }
173
- return;
174
- }
175
-
176
- // Any other message before auth: reject silently
177
- send(ws, { type: 'auth_required' });
178
- return;
179
- }
180
-
181
- // ── Authenticated messages ─────────────────────────────────────────────────
182
- handleAdminMessage(ws, msg);
183
- });
184
-
185
- ws.on('close', () => {
186
- adminClients.delete(ws);
187
- clearTimeout(authTimeout);
188
- console.log(`[Admin] disconnected, total=${adminClients.size}`);
189
- });
190
-
191
- ws.on('error', (e) => console.error('[Admin] WS error', e.message));
192
-
193
- // Prompt client to authenticate
194
- send(ws, { type: 'auth_required' });
195
- }
196
-
197
- function handleAdminMessage(ws, msg) {
198
- switch (msg.type) {
199
-
200
- case 'create_sound': {
201
- const id = uuidv4();
202
- const sound = { id, name: msg.name, data: msg.data };
203
- sounds[id] = sound;
204
- broadcastAdmin({ type: 'sound_added', sound });
205
- break;
206
- }
207
-
208
- case 'create_notification': {
209
- const id = uuidv4();
210
- const notif = {
211
- id, name: msg.name, heading: msg.heading, body: msg.body,
212
- hyperlink: msg.hyperlink || '', displayed: false, soundId: msg.soundId || null,
213
- };
214
- notifications[id] = notif;
215
- for (const uuid of Object.keys(devices)) {
216
- if (!devices[uuid].notifications.find(n => n.id === id))
217
- devices[uuid].notifications.push({ ...notif });
218
- }
219
- broadcastAdmin({ type: 'notification_added', notification: notif });
220
- broadcastAdmin({ type: 'devices_updated', devices: Object.values(devices).map(devicePublic) });
221
- broadcastAllDevices({ type: 'notification_added', notification: notif });
222
- break;
223
- }
224
-
225
- case 'schedule_notification': {
226
- const device = devices[msg.uuid];
227
- if (!device) return;
228
- device.schedule = device.schedule || [];
229
- const existing = device.schedule.find(s => s.notificationId === msg.notificationId);
230
- if (existing) { existing.scheduledAt = msg.scheduledAt; }
231
- else { device.schedule.push({ notificationId: msg.notificationId, scheduledAt: msg.scheduledAt }); }
232
- const scheduleMsg = { type: 'schedule_update', schedule: device.schedule };
233
- if (deviceClients[msg.uuid]) { broadcastDevice(msg.uuid, scheduleMsg); }
234
- else { (device.pendingChanges = device.pendingChanges || []).push(scheduleMsg); }
235
- broadcastAdmin({ type: 'devices_updated', devices: Object.values(devices).map(devicePublic) });
236
- break;
237
- }
238
-
239
- case 'play_now': {
240
- const playMsg = { type: 'play_now', notificationId: msg.notificationId };
241
- if (deviceClients[msg.uuid]) { broadcastDevice(msg.uuid, playMsg); }
242
- else {
243
- const device = devices[msg.uuid];
244
- if (device) (device.pendingChanges = device.pendingChanges || []).push(playMsg);
245
- }
246
- break;
247
- }
248
-
249
- case 'update_device_name': {
250
- const device = devices[msg.uuid];
251
- if (!device) return;
252
- device.name = msg.name;
253
- const nameMsg = { type: 'name_update', name: msg.name };
254
- if (deviceClients[msg.uuid]) { broadcastDevice(msg.uuid, nameMsg); }
255
- else { (device.pendingChanges = device.pendingChanges || []).push(nameMsg); }
256
- broadcastAdmin({ type: 'devices_updated', devices: Object.values(devices).map(devicePublic) });
257
- break;
258
- }
259
-
260
- case 'remove_schedule': {
261
- const device = devices[msg.uuid];
262
- if (!device) return;
263
- device.schedule = (device.schedule || []).filter(s => s.notificationId !== msg.notificationId);
264
- const rmMsg = { type: 'schedule_update', schedule: device.schedule };
265
- if (deviceClients[msg.uuid]) { broadcastDevice(msg.uuid, rmMsg); }
266
- else { (device.pendingChanges = device.pendingChanges || []).push(rmMsg); }
267
- broadcastAdmin({ type: 'devices_updated', devices: Object.values(devices).map(devicePublic) });
268
- break;
269
- }
270
-
271
- case 'request_sound': {
272
- const sound = sounds[msg.id];
273
- if (sound) send(ws, { type: 'sound_data', sound });
274
- break;
275
- }
276
-
277
- default:
278
- console.warn('[Admin] unknown message type:', msg.type);
279
- }
280
- }
281
-
282
- // ─── Device Connection Handler ────────────────────────────────────────────────
283
- function handleDeviceConnection(ws) {
284
- let deviceUUID = null;
285
-
286
- ws.on('message', (raw) => {
287
- let msg;
288
- try { msg = JSON.parse(raw); } catch { return; }
289
-
290
- if (msg.type === 'hello') {
291
- if (msg.uuid && devices[msg.uuid]) {
292
- deviceUUID = msg.uuid;
293
- } else {
294
- deviceUUID = msg.uuid || uuidv4();
295
- devices[deviceUUID] = {
296
- uuid: deviceUUID,
297
- name: `Device ${Object.keys(devices).length + 1}`,
298
- notifications: Object.values(notifications).map(n => ({ ...n })),
299
- lastConnection: null, schedule: [], pendingChanges: [],
300
- };
301
- }
302
-
303
- deviceClients[deviceUUID] = ws;
304
- const device = devices[deviceUUID];
305
- device.lastConnection = null;
306
-
307
- if (device.pendingChanges && device.pendingChanges.length > 0) {
308
- for (const change of device.pendingChanges) send(ws, change);
309
- device.pendingChanges = [];
310
- }
311
-
312
- send(ws, {
313
- type: 'device_init', uuid: deviceUUID,
314
- notifications: device.notifications, schedule: device.schedule, name: device.name,
315
- });
316
-
317
- broadcastAdmin({ type: 'devices_updated', devices: Object.values(devices).map(devicePublic) });
318
- console.log(`[Device] ${deviceUUID} connected (${device.name})`);
319
- return;
320
- }
321
-
322
- if (!deviceUUID) return;
323
- handleDeviceMessage(ws, deviceUUID, msg);
324
- });
325
-
326
- ws.on('close', () => {
327
- if (deviceUUID && devices[deviceUUID]) {
328
- devices[deviceUUID].lastConnection = Date.now();
329
- delete deviceClients[deviceUUID];
330
- broadcastAdmin({ type: 'devices_updated', devices: Object.values(devices).map(devicePublic) });
331
- console.log(`[Device] ${deviceUUID} disconnected`);
332
- }
333
- });
334
-
335
- ws.on('error', (e) => console.error('[Device] WS error', e.message));
336
- }
337
-
338
- function handleDeviceMessage(ws, uuid, msg) {
339
- const device = devices[uuid];
340
- if (!device) return;
341
-
342
- switch (msg.type) {
343
- case 'mark_displayed': {
344
- const notif = device.notifications.find(n => n.id === msg.notificationId);
345
- if (notif) notif.displayed = true;
346
- if (notifications[msg.notificationId]) notifications[msg.notificationId].displayed = true;
347
- broadcastAdmin({ type: 'devices_updated', devices: Object.values(devices).map(devicePublic) });
348
- break;
349
- }
350
- case 'request_sound': {
351
- const sound = sounds[msg.soundId];
352
- if (sound) send(ws, { type: 'sound_data', sound });
353
- break;
354
- }
355
- case 'sync_schedule': {
356
- device.schedule = msg.schedule || [];
357
- broadcastAdmin({ type: 'devices_updated', devices: Object.values(devices).map(devicePublic) });
358
- break;
359
- }
360
- case 'cached_sounds': {
361
- device.cachedSounds = msg.soundIds || [];
362
- broadcastAdmin({ type: 'devices_updated', devices: Object.values(devices).map(devicePublic) });
363
- break;
364
- }
365
- default:
366
- console.warn('[Device] unknown message type:', msg.type);
367
- }
368
- }
369
-
370
- // ─── Start ────────────────────────────────────────────────────────────────────
371
- const PORT = process.env.PORT || 7860;
372
- httpServer.listen(PORT, () => {
373
- console.log(`Server running on http://0.0.0.0:${PORT}`);
374
- });