livedesk 0.1.443 → 0.1.445

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/hub/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -1,5 +1,6 @@
1
- import crypto from 'node:crypto';
2
- import dgram from 'node:dgram';
1
+ import crypto from 'node:crypto';
2
+ import { lookup } from 'node:dns/promises';
3
+ import dgram from 'node:dgram';
3
4
  import {
4
5
  UDP_MAX_DATAGRAM_BYTES,
5
6
  UDP_P2P_PROTOCOL,
@@ -26,10 +27,15 @@ function numberEnv(value, min, max, fallback) {
26
27
  return Number.isFinite(number) ? Math.max(min, Math.min(max, Math.floor(number))) : fallback;
27
28
  }
28
29
 
29
- function sameEndpoint(left, right) {
30
- return !!left && !!right && String(left.address) === String(right.address) && Number(left.port) === Number(right.port);
31
- }
32
-
30
+ function sameEndpoint(left, right) {
31
+ return !!left && !!right && String(left.address) === String(right.address) && Number(left.port) === Number(right.port);
32
+ }
33
+
34
+ function normalizedAddress(value) {
35
+ const address = safeText(value, 128).toLowerCase();
36
+ return address.startsWith('::ffff:') ? address.slice(7) : address;
37
+ }
38
+
33
39
  export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
34
40
  const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED, true);
35
41
  const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P, false);
@@ -37,7 +43,8 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
37
43
  const requestedPort = numberEnv(env.LIVEDESK_UDP_PORT, 0, 65535, 0);
38
44
  const advertiseHost = safeText(env.LIVEDESK_UDP_ADVERTISE_HOST || env.LIVEDESK_REMOTE_PUBLIC_HOST || '', 128);
39
45
  const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'www.gnsoftech.com', 128);
40
- const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
46
+ const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
47
+ const rendezvousRefreshMs = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_REFRESH_MS, 1000, 50_000, 25_000);
41
48
  const punchTimeoutMs = numberEnv(env.LIVEDESK_UDP_PUNCH_TIMEOUT_MS, 500, 10_000, 3000);
42
49
  const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000, 500);
43
50
  const maxPendingFrames = numberEnv(env.LIVEDESK_UDP_MAX_PENDING_FRAMES, 1, 16, 2);
@@ -46,9 +53,12 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
46
53
  const sessions = new Map();
47
54
  let frameHandler = () => {};
48
55
  let eventHandler = () => {};
49
- let started = false;
50
- let boundPort = requestedPort;
51
- let sequence = crypto.randomInt(1, 0x7fffffff);
56
+ let started = false;
57
+ let boundPort = requestedPort;
58
+ let sequence = crypto.randomInt(1, 0x7fffffff);
59
+ let rendezvousAddresses = new Set();
60
+ let rendezvousRefreshTimer = null;
61
+ let rendezvousRefreshPromise = null;
52
62
 
53
63
  function emit(type, event = {}) {
54
64
  eventHandler(type, event);
@@ -60,8 +70,8 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
60
70
  return sequence;
61
71
  }
62
72
 
63
- function sendPacket(session, type, header = {}, payload = Buffer.alloc(0), address = session.peerEndpoint || session.clientEndpoint) {
64
- if (!address || !session) return false;
73
+ function sendPacket(session, type, header = {}, payload = Buffer.alloc(0), address = session.clientEndpoint || session.peerEndpoint) {
74
+ if (!address || !session) return false;
65
75
  let packet;
66
76
  try {
67
77
  packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
@@ -73,34 +83,105 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
73
83
  return true;
74
84
  }
75
85
 
76
- function sendRendezvousRegister(session) {
77
- if (!rendezvousHost) return;
78
- const message = encodeRendezvousMessage({
79
- type: 'rendezvous.register',
86
+ function sendRendezvousRegister(session) {
87
+ if (!rendezvousHost || rendezvousAddresses.size === 0 || !started) return false;
88
+ const message = encodeRendezvousMessage({
89
+ type: 'rendezvous.register',
80
90
  protocol: UDP_P2P_PROTOCOL,
81
91
  roomId: session.sessionId,
82
- role: 'hub',
83
- token: session.rendezvousToken
84
- });
85
- socket.send(message, rendezvousPort, rendezvousHost);
86
- }
87
-
88
- function sessionForPacket(packet, rinfo) {
89
- if (!Buffer.isBuffer(packet) || packet.length < 7) return null;
90
- const sessionLength = packet[6];
91
- if (!sessionLength || packet.length < 27 + sessionLength) return null;
92
- const sessionId = packet.subarray(27, 27 + sessionLength).toString('utf8');
93
- const session = [...sessions.values()].find(item => item.sessionId === sessionId);
94
- if (!session) return null;
95
- if (!session.clientEndpoint) session.clientEndpoint = { address: rinfo.address, port: rinfo.port };
96
- if (!sameEndpoint(session.clientEndpoint, rinfo)) return null;
97
- return session;
98
- }
99
-
100
- function onRendezvousPeer(message, rinfo) {
101
- const session = [...sessions.values()].find(item => item.sessionId === safeText(message.roomId, 160));
102
- if (!session || !rendezvousHost || Number(rinfo.port) !== rendezvousPort) return;
103
- const address = safeText(message.host, 128);
92
+ role: 'hub',
93
+ token: session.rendezvousToken
94
+ });
95
+ let sent = false;
96
+ for (const address of rendezvousAddresses) {
97
+ try {
98
+ socket.send(message, rendezvousPort, address, error => {
99
+ if (error) logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
100
+ });
101
+ sent = true;
102
+ } catch (error) {
103
+ logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
104
+ }
105
+ }
106
+ return sent;
107
+ }
108
+
109
+ async function resolveRendezvousAddresses() {
110
+ if (!rendezvousHost) return false;
111
+ try {
112
+ const records = await lookup(rendezvousHost, { all: true, family: 4, verbatim: true });
113
+ const resolved = new Set(records.map(record => normalizedAddress(record.address)).filter(Boolean));
114
+ if (resolved.size === 0) throw new Error('no-ipv4-address');
115
+ rendezvousAddresses = resolved;
116
+ return true;
117
+ } catch (error) {
118
+ logWarn('udp', `UDP rendezvous host resolution failed: ${error?.message || error}`);
119
+ return false;
120
+ }
121
+ }
122
+
123
+ function refreshRendezvousRegistrations(reason = 'periodic') {
124
+ if (!started || !rendezvousHost) return Promise.resolve(false);
125
+ if (rendezvousRefreshPromise) return rendezvousRefreshPromise;
126
+ rendezvousRefreshPromise = (async () => {
127
+ await resolveRendezvousAddresses();
128
+ if (!started) return false;
129
+ let refreshed = false;
130
+ for (const session of sessions.values()) {
131
+ refreshed = sendRendezvousRegister(session) || refreshed;
132
+ }
133
+ if (trace && refreshed) emit('UdpRendezvousRegistrationRefreshed', { reason });
134
+ return refreshed;
135
+ })().finally(() => {
136
+ rendezvousRefreshPromise = null;
137
+ });
138
+ return rendezvousRefreshPromise;
139
+ }
140
+
141
+ function startRendezvousRefreshTimer() {
142
+ if (!rendezvousHost || rendezvousRefreshTimer) return;
143
+ rendezvousRefreshTimer = setInterval(() => {
144
+ void refreshRendezvousRegistrations('periodic');
145
+ }, rendezvousRefreshMs);
146
+ rendezvousRefreshTimer.unref?.();
147
+ }
148
+
149
+ function stopRendezvousRefreshTimer() {
150
+ if (rendezvousRefreshTimer) clearInterval(rendezvousRefreshTimer);
151
+ rendezvousRefreshTimer = null;
152
+ }
153
+
154
+ function isTrustedRendezvousSource(rinfo) {
155
+ return Number(rinfo?.port) === rendezvousPort
156
+ && rendezvousAddresses.has(normalizedAddress(rinfo?.address));
157
+ }
158
+
159
+ function sessionForPacket(packet) {
160
+ if (!Buffer.isBuffer(packet) || packet.length < 7) return null;
161
+ const sessionLength = packet[6];
162
+ if (!sessionLength || packet.length < 27 + sessionLength) return null;
163
+ const sessionId = packet.subarray(27, 27 + sessionLength).toString('utf8');
164
+ return [...sessions.values()].find(item => item.sessionId === sessionId) || null;
165
+ }
166
+
167
+ function bindAuthenticatedClientEndpoint(session, rinfo) {
168
+ const endpoint = { address: rinfo.address, port: rinfo.port };
169
+ if (sameEndpoint(session.clientEndpoint, endpoint)) return false;
170
+ session.clientEndpoint = endpoint;
171
+ session.lastPeerAt = Date.now();
172
+ sendRendezvousRegister(session);
173
+ void refreshRendezvousRegistrations('authenticated-endpoint-change');
174
+ emit('UdpAuthenticatedEndpointObserved', {
175
+ deviceId: session.deviceId,
176
+ endpoint: `${endpoint.address}:${endpoint.port}`
177
+ });
178
+ return true;
179
+ }
180
+
181
+ function onRendezvousPeer(message, rinfo) {
182
+ const session = [...sessions.values()].find(item => item.sessionId === safeText(message.roomId, 160));
183
+ if (!session || message.protocol !== UDP_P2P_PROTOCOL || !isTrustedRendezvousSource(rinfo)) return;
184
+ const address = safeText(message.host, 128);
104
185
  const port = Number(message.port);
105
186
  if (!address || !Number.isInteger(port) || port < 1 || port > 65535) return;
106
187
  session.peerEndpoint = { address, port };
@@ -112,24 +193,23 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
112
193
  socket.on('message', (payload, rinfo) => {
113
194
  const rendezvous = decodeRendezvousMessage(payload);
114
195
  if (rendezvous?.type === 'rendezvous.peer') {
115
- onRendezvousPeer(rendezvous, rinfo);
116
- return;
117
- }
118
- const session = sessionForPacket(payload, rinfo);
119
- if (!session) return;
120
- const packet = decodeUdpPacket(payload, session.key);
121
- if (!packet || packet.sessionId !== session.sessionId || !session.replay.accept(packet.sequence)) return;
122
- session.lastPacketAt = Date.now();
123
- if (packet.type === 'probe') {
124
- session.clientEndpoint = { address: rinfo.address, port: rinfo.port };
125
- sendPacket(session, 'probe-ack', { observedHost: rinfo.address, observedPort: rinfo.port });
196
+ onRendezvousPeer(rendezvous, rinfo);
197
+ return;
198
+ }
199
+ const session = sessionForPacket(payload);
200
+ if (!session) return;
201
+ const packet = decodeUdpPacket(payload, session.key);
202
+ if (!packet || packet.sessionId !== session.sessionId || !session.replay.accept(packet.sequence)) return;
203
+ bindAuthenticatedClientEndpoint(session, rinfo);
204
+ session.lastPacketAt = Date.now();
205
+ if (packet.type === 'probe') {
206
+ sendPacket(session, 'probe-ack', { observedHost: rinfo.address, observedPort: rinfo.port });
126
207
  emit('UdpProbeReceived', { deviceId: session.deviceId });
127
208
  return;
128
209
  }
129
- if (packet.type === 'ready') {
130
- session.ready = true;
131
- session.clientEndpoint = { address: rinfo.address, port: rinfo.port };
132
- sendPacket(session, 'pong', { state: 'ready' });
210
+ if (packet.type === 'ready') {
211
+ session.ready = true;
212
+ sendPacket(session, 'pong', { state: 'ready' });
133
213
  session.sendControl?.({
134
214
  type: 'udp.session.ready',
135
215
  protocol: UDP_P2P_PROTOCOL,
@@ -189,7 +269,9 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
189
269
  logWarn('udp', `UDP socket buffer tuning was unavailable: ${error?.message || error}`);
190
270
  }
191
271
  logEvent('udp', `UDP P2P socket listening on udp://${bindHost}:${boundPort}`);
192
- return getStatus();
272
+ void refreshRendezvousRegistrations('startup');
273
+ startRendezvousRefreshTimer();
274
+ return getStatus();
193
275
  }
194
276
 
195
277
  function registerDevice({ deviceId, sessionId: tcpSessionId, sendControl, capabilities } = {}) {
@@ -260,11 +342,12 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
260
342
  };
261
343
  }
262
344
 
263
- async function close() {
264
- sessions.clear();
265
- if (!started) return;
266
- await new Promise(resolve => socket.close(() => resolve()));
267
- started = false;
345
+ async function close() {
346
+ stopRendezvousRefreshTimer();
347
+ sessions.clear();
348
+ if (!started) return;
349
+ started = false;
350
+ await new Promise(resolve => socket.close(() => resolve()));
268
351
  }
269
352
 
270
353
  return {
@@ -286,8 +369,10 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
286
369
  bindHost,
287
370
  port: boundPort,
288
371
  advertiseHost,
289
- rendezvousHost,
290
- rendezvousPort,
372
+ rendezvousHost,
373
+ rendezvousPort,
374
+ rendezvousRefreshMs,
375
+ rendezvousResolvedAddressCount: rendezvousAddresses.size,
291
376
  maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
292
377
  sessionCount: sessions.size,
293
378
  readySessionCount: [...sessions.values()].filter(session => session.ready).length
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.443",
4
- "livedeskClientVersion": "0.1.198",
3
+ "version": "0.1.445",
4
+ "livedeskClientVersion": "0.1.200",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -50,10 +50,10 @@
50
50
  "ws": "^8.18.3"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@livedesk/fast-linux-x64": "0.1.405",
54
- "@livedesk/fast-osx-arm64": "0.1.405",
55
- "@livedesk/fast-osx-x64": "0.1.405",
56
- "@livedesk/fast-win-x64": "0.1.405"
53
+ "@livedesk/fast-linux-x64": "0.1.407",
54
+ "@livedesk/fast-osx-arm64": "0.1.407",
55
+ "@livedesk/fast-osx-x64": "0.1.407",
56
+ "@livedesk/fast-win-x64": "0.1.407"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"