@devioarts/capacitor-mdns 0.1.0 → 0.2.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/CHANGELOG.md +110 -0
- package/DevioartsCapacitorMdns.podspec +3 -3
- package/Package.swift +1 -1
- package/README.md +221 -37
- package/android/build.gradle +3 -9
- package/android/src/main/java/com/devioarts/capacitor/mdns/AndroidNsdBackend.kt +145 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/MdnsOptions.kt +32 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/MdnsValidation.kt +26 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/NsdBackend.kt +59 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNS.kt +467 -243
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +80 -57
- package/dist/docs.json +250 -14
- package/dist/esm/definitions.d.ts +64 -17
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js +22 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +6 -1
- package/dist/esm/web.js +23 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +45 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +45 -2
- package/dist/plugin.js.map +1 -1
- package/docs/electron.md +29 -3
- package/docs/platform-setup.md +6 -0
- package/docs/playground.md +33 -2
- package/docs/runtime-behavior.md +68 -2
- package/electron/dist/plugin-settings.js +10 -3
- package/electron/dist/plugin-settings.js.map +1 -1
- package/electron/dist/plugin.cjs.js +519 -89
- package/electron/dist/plugin.cjs.js.map +1 -1
- package/electron/mdns-bridge.cjs +25 -2
- package/electron/mdns.ts +514 -81
- package/electron/rollup.config.mjs +1 -1
- package/electron/src/index.ts +1 -0
- package/electron/src/plugin-settings.ts +10 -3
- package/electron/types-public/index.d.ts +25 -3
- package/ios/Sources/mDNSPlugin/mDNS.swift +407 -117
- package/ios/Sources/mDNSPlugin/mDNSPlugin.swift +77 -24
- package/ios/Tests/mDNSPluginTests/BridgeTests.swift +46 -0
- package/ios/Tests/mDNSPluginTests/MDNSTests.swift +490 -0
- package/package.json +11 -4
- package/ios/Tests/mDNSPluginTests/mDNSPluginTests.swift +0 -9
|
@@ -2,29 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
var Bonjour = require('bonjour-service');
|
|
4
4
|
var electron = require('electron');
|
|
5
|
+
var node_path = require('node:path');
|
|
6
|
+
var node_url = require('node:url');
|
|
7
|
+
var node_events = require('node:events');
|
|
8
|
+
var node_net = require('node:net');
|
|
5
9
|
|
|
10
|
+
const DEFAULT_DISCOVER_TIMEOUT_MS = 3000;
|
|
11
|
+
/** `setTimeout` treats delays above 2^31-1 as 1ms, so clamp. */
|
|
12
|
+
const MAX_TIMER_MS = 2147483647;
|
|
6
13
|
/**
|
|
7
14
|
* Electron main-process implementation of the mDNS/Bonjour functionality.
|
|
8
15
|
*
|
|
9
16
|
* Uses the `bonjour-service` package to:
|
|
10
17
|
* - publish (advertise) a single service;
|
|
11
18
|
* - discover services of a given type and normalize results to the shared schema;
|
|
12
|
-
* - wire the API over Electron's ipcMain (
|
|
19
|
+
* - wire the API over Electron's ipcMain through init().
|
|
13
20
|
*
|
|
14
21
|
* Notes:
|
|
15
22
|
* - Instance name matching is normalized and prefix-safe, mirroring iOS/Android behavior.
|
|
16
23
|
* - TXT records are forwarded as strings when present.
|
|
17
24
|
*/
|
|
18
|
-
class mDNS {
|
|
19
|
-
constructor() {
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
class mDNS extends node_events.EventEmitter {
|
|
26
|
+
constructor(bonjour, options = {}) {
|
|
27
|
+
var _a, _b, _c;
|
|
28
|
+
super();
|
|
22
29
|
this.broadcastQueue = Promise.resolve();
|
|
23
30
|
this.destroyed = false;
|
|
31
|
+
/** Settle callbacks of in-flight discoveries, so destroy() can end them instead of leaving them waiting. */
|
|
32
|
+
this.activeDiscoveries = new Set();
|
|
33
|
+
this.stoppedBrowsers = new Set();
|
|
34
|
+
this.deactivateBrowser = new Map();
|
|
24
35
|
//<editor-fold desc="Init/Destroy">
|
|
25
36
|
this.ipcRegistered = false;
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
this.onNetworkError = (err) => {
|
|
38
|
+
var _a, _b, _c;
|
|
39
|
+
this.networkError = this.toErr(err);
|
|
40
|
+
(_a = this.pendingPublishError) === null || _a === void 0 ? void 0 : _a.call(this, err);
|
|
41
|
+
const type = (_c = (_b = this.watch) === null || _b === void 0 ? void 0 : _b.types[0]) !== null && _c !== void 0 ? _c : '';
|
|
42
|
+
const watching = !!this.watch;
|
|
43
|
+
void this.stopWatching();
|
|
44
|
+
for (const abort of [...this.activeDiscoveries])
|
|
45
|
+
abort(this.toErr(err));
|
|
46
|
+
if (watching)
|
|
47
|
+
this.notify('watchError', { type, errorMessage: this.toErr(err), watching: false });
|
|
48
|
+
else
|
|
49
|
+
console.warn('[mDNS]', err);
|
|
50
|
+
};
|
|
51
|
+
this.trustedOrigins = new Set((_a = options.trustedOrigins) !== null && _a !== void 0 ? _a : []);
|
|
52
|
+
if (bonjour)
|
|
53
|
+
this.attachBonjour(bonjour);
|
|
54
|
+
this.publishTimeoutMs = (_b = options.publishTimeoutMs) !== null && _b !== void 0 ? _b : 5000;
|
|
55
|
+
this.stopTimeoutMs = (_c = options.stopTimeoutMs) !== null && _c !== void 0 ? _c : 3000;
|
|
56
|
+
}
|
|
57
|
+
attachBonjour(bonjour) {
|
|
58
|
+
var _a, _b;
|
|
59
|
+
this.bonjourInstance = bonjour;
|
|
60
|
+
// shortcut: Bonjour 1.4 needs its internal transport for errors/TTL; recheck these tests on dependency upgrades.
|
|
61
|
+
this.transport = (_a = bonjour.server) === null || _a === void 0 ? void 0 : _a.mdns;
|
|
62
|
+
(_b = this.transport) === null || _b === void 0 ? void 0 : _b.on('error', this.onNetworkError);
|
|
63
|
+
return bonjour;
|
|
64
|
+
}
|
|
65
|
+
get bonjour() {
|
|
66
|
+
var _a;
|
|
67
|
+
return ((_a = this.bonjourInstance) !== null && _a !== void 0 ? _a : this.attachBonjour(new Bonjour({}, (err) => console.warn('[mDNS] query response failed:', err))));
|
|
68
|
+
}
|
|
69
|
+
trustedUrl(raw) {
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(raw);
|
|
72
|
+
// Capacitor Electron's default dev renderer; packaged apps require an explicit origin.
|
|
73
|
+
if ((electron.app === null || electron.app === void 0 ? void 0 : electron.app.isPackaged) === false && `${url.protocol}//${url.host}` === 'http://localhost:5173')
|
|
74
|
+
return true;
|
|
75
|
+
if (url.protocol === 'capacitor-electron:' &&
|
|
76
|
+
url.hostname === 'localhost' &&
|
|
77
|
+
!url.username &&
|
|
78
|
+
!url.password &&
|
|
79
|
+
!url.port)
|
|
80
|
+
return true;
|
|
81
|
+
if (url.protocol === 'file:') {
|
|
82
|
+
const path = node_path.relative(electron.app.getAppPath(), node_url.fileURLToPath(url));
|
|
83
|
+
return path !== '..' && !path.startsWith(`..${node_path.sep}`) && !node_path.isAbsolute(path);
|
|
84
|
+
}
|
|
85
|
+
return this.trustedOrigins.has(`${url.protocol}//${url.host}`);
|
|
86
|
+
}
|
|
87
|
+
catch (_a) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
trustedSender(event) {
|
|
92
|
+
return (!!event.senderFrame && event.senderFrame === event.sender.mainFrame && this.trustedUrl(event.senderFrame.url));
|
|
28
93
|
}
|
|
29
94
|
registerIpc() {
|
|
30
95
|
if (this.ipcRegistered)
|
|
@@ -34,14 +99,25 @@ class mDNS {
|
|
|
34
99
|
electron.ipcMain.removeHandler('mdns:stopBroadcast');
|
|
35
100
|
electron.ipcMain.removeHandler('mdns:discover');
|
|
36
101
|
electron.ipcMain.removeHandler('mdns:getPluginPlatform');
|
|
102
|
+
electron.ipcMain.removeHandler('mdns:watchServices');
|
|
103
|
+
electron.ipcMain.removeHandler('mdns:stopWatching');
|
|
37
104
|
}
|
|
38
105
|
catch (_a) {
|
|
39
106
|
/* ignore */
|
|
40
107
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
108
|
+
const handle = (channel, action) => {
|
|
109
|
+
electron.ipcMain.handle(channel, (event, options) => {
|
|
110
|
+
if (!this.trustedSender(event))
|
|
111
|
+
throw new Error('Untrusted mDNS IPC sender');
|
|
112
|
+
return action(options);
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
handle('mdns:startBroadcast', (o) => this.startBroadcast(o));
|
|
116
|
+
handle('mdns:stopBroadcast', () => this.stopBroadcast());
|
|
117
|
+
handle('mdns:discover', (o) => this.discover(o));
|
|
118
|
+
handle('mdns:getPluginPlatform', () => this.getPluginPlatform());
|
|
119
|
+
handle('mdns:watchServices', (o) => this.watchServices(o));
|
|
120
|
+
handle('mdns:stopWatching', () => this.stopWatching());
|
|
45
121
|
this.ipcRegistered = true;
|
|
46
122
|
}
|
|
47
123
|
unregisterIpc() {
|
|
@@ -52,23 +128,33 @@ class mDNS {
|
|
|
52
128
|
electron.ipcMain.removeHandler('mdns:stopBroadcast');
|
|
53
129
|
electron.ipcMain.removeHandler('mdns:discover');
|
|
54
130
|
electron.ipcMain.removeHandler('mdns:getPluginPlatform');
|
|
131
|
+
electron.ipcMain.removeHandler('mdns:watchServices');
|
|
132
|
+
electron.ipcMain.removeHandler('mdns:stopWatching');
|
|
55
133
|
}
|
|
56
134
|
catch (_a) {
|
|
57
135
|
/* ignore */
|
|
58
136
|
}
|
|
59
137
|
this.ipcRegistered = false;
|
|
60
138
|
}
|
|
61
|
-
/**
|
|
139
|
+
/** Register manual IPC handlers. Idempotent. */
|
|
62
140
|
init() {
|
|
63
141
|
this.registerIpc();
|
|
64
142
|
}
|
|
65
143
|
async destroy() {
|
|
144
|
+
var _a;
|
|
66
145
|
this.destroyed = true;
|
|
146
|
+
(_a = this.pendingPublishError) === null || _a === void 0 ? void 0 : _a.call(this, new Error('mDNS instance destroyed during publish'));
|
|
147
|
+
await this.stopWatching();
|
|
67
148
|
this.unregisterIpc();
|
|
149
|
+
for (const abort of [...this.activeDiscoveries])
|
|
150
|
+
abort('mDNS instance destroyed during discovery');
|
|
68
151
|
await this.withBroadcastLock(async () => {
|
|
152
|
+
var _a, _b;
|
|
69
153
|
await this.stopBroadcastUnlocked();
|
|
70
154
|
try {
|
|
71
|
-
this.
|
|
155
|
+
(_a = this.bonjourInstance) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
156
|
+
(_b = this.transport) === null || _b === void 0 ? void 0 : _b.removeListener('error', this.onNetworkError);
|
|
157
|
+
this.advertiser = undefined;
|
|
72
158
|
}
|
|
73
159
|
catch (err) {
|
|
74
160
|
console.warn('[mDNS] bonjour.destroy error:', err);
|
|
@@ -94,7 +180,7 @@ class mDNS {
|
|
|
94
180
|
return run;
|
|
95
181
|
}
|
|
96
182
|
normalize(name) {
|
|
97
|
-
return name.replace(/ \(\d+\)$/, '');
|
|
183
|
+
return name.replace(/ \(\d+\)$/, '').replace(/\./g, '-');
|
|
98
184
|
}
|
|
99
185
|
matchesTarget(candidate, target) {
|
|
100
186
|
if (!target)
|
|
@@ -102,47 +188,79 @@ class mDNS {
|
|
|
102
188
|
const c = this.normalize(candidate), t = this.normalize(target);
|
|
103
189
|
return c === t || c.startsWith(t);
|
|
104
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Parse a `_name._tcp.` style type. Absent/blank input means the default `_http._tcp.`;
|
|
193
|
+
* anything malformed is rejected (null) rather than silently advertised as something else.
|
|
194
|
+
*/
|
|
105
195
|
parseType(typeWithDot) {
|
|
196
|
+
if (typeWithDot != null && typeof typeWithDot !== 'string')
|
|
197
|
+
return null;
|
|
106
198
|
const raw = typeof typeWithDot === 'string' && typeWithDot.trim() ? typeWithDot.trim() : '_http._tcp.';
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
199
|
+
const m = /^_([a-z0-9](?:[a-z0-9-]{0,13}[a-z0-9])?)\._(tcp|udp)\.?$/i.exec(raw);
|
|
200
|
+
return m && /[a-z]/i.test(m[1]) && !m[1].includes('--')
|
|
201
|
+
? { type: m[1].toLowerCase(), protocol: m[2].toLowerCase() }
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
validateName(name) {
|
|
205
|
+
return name != null &&
|
|
206
|
+
(typeof name !== 'string' || Buffer.byteLength(name.trim(), 'utf8') > 63 || /[\x00-\x1f\x7f]/.test(name))
|
|
207
|
+
? 'Invalid instance name (maximum 63 UTF-8 bytes, no control characters)'
|
|
208
|
+
: null;
|
|
209
|
+
}
|
|
210
|
+
validateTxt(txt) {
|
|
211
|
+
if (txt == null)
|
|
212
|
+
return null;
|
|
213
|
+
if (!this.isRecord(txt))
|
|
214
|
+
return 'TXT must be an object of strings';
|
|
215
|
+
const keys = new Set();
|
|
216
|
+
let totalBytes = 0;
|
|
217
|
+
for (const [key, value] of Object.entries(txt)) {
|
|
218
|
+
if (!/^[\x20-\x7e]+$/.test(key) || key.includes('=') || keys.has(key.toLowerCase()))
|
|
219
|
+
return 'Invalid or duplicate TXT key';
|
|
220
|
+
if (typeof value !== 'string' || Buffer.byteLength(`${key}=${value}`, 'utf8') > 255)
|
|
221
|
+
return 'TXT entries must be strings of at most 255 UTF-8 bytes including key=';
|
|
222
|
+
totalBytes += Buffer.byteLength(`${key}=${value}`, 'utf8') + 1;
|
|
223
|
+
if (totalBytes > 1300)
|
|
224
|
+
return 'TXT record must fit in 1300 bytes including length prefixes';
|
|
225
|
+
keys.add(key.toLowerCase());
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
invalidTypeMessage(typeWithDot) {
|
|
230
|
+
return `Invalid service type: ${String(typeWithDot)} (expected e.g. "_http._tcp.")`;
|
|
231
|
+
}
|
|
232
|
+
normalizeTimeout(value) {
|
|
233
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
234
|
+
return DEFAULT_DISCOVER_TIMEOUT_MS;
|
|
235
|
+
return Math.min(Math.max(0, Math.trunc(value)), MAX_TIMER_MS);
|
|
112
236
|
}
|
|
113
237
|
toFullType(type, protocol) {
|
|
114
238
|
return `_${type}._${protocol}.`;
|
|
115
239
|
}
|
|
116
|
-
hasCbStop(x) {
|
|
117
|
-
return !!x && typeof x.stop === 'function';
|
|
118
|
-
}
|
|
119
240
|
async safeStopService(svc) {
|
|
120
|
-
if (!svc
|
|
241
|
+
if (!svc)
|
|
121
242
|
return;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
243
|
+
await new Promise((res, reject) => {
|
|
244
|
+
let settled = false;
|
|
245
|
+
const done = (error) => {
|
|
246
|
+
if (settled)
|
|
247
|
+
return;
|
|
248
|
+
settled = true;
|
|
249
|
+
if (timeout)
|
|
250
|
+
clearTimeout(timeout);
|
|
251
|
+
if (error)
|
|
252
|
+
reject(error);
|
|
253
|
+
else
|
|
131
254
|
res();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
console.warn('[mDNS] service.stop error:', err);
|
|
145
|
-
}
|
|
255
|
+
};
|
|
256
|
+
const timeout = setTimeout(() => done(new Error(`Timed out waiting for service stop after ${this.stopTimeoutMs}ms`)), this.stopTimeoutMs);
|
|
257
|
+
try {
|
|
258
|
+
svc.stop(done);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
done(e);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
146
264
|
}
|
|
147
265
|
safeStopBrowser(b) {
|
|
148
266
|
if (!b)
|
|
@@ -168,16 +286,31 @@ class mDNS {
|
|
|
168
286
|
*/
|
|
169
287
|
async startBroadcast(options) {
|
|
170
288
|
return this.withBroadcastLock(async () => {
|
|
289
|
+
var _a, _b;
|
|
171
290
|
const safeOptions = (options !== null && options !== void 0 ? options : {});
|
|
172
|
-
const portError = this.validatePort(safeOptions.port);
|
|
291
|
+
const portError = (_b = (_a = this.validatePort(safeOptions.port)) !== null && _a !== void 0 ? _a : this.validateName(safeOptions.name)) !== null && _b !== void 0 ? _b : this.validateTxt(safeOptions.txt);
|
|
173
292
|
if (portError)
|
|
174
|
-
return { publishing:
|
|
293
|
+
return { publishing: !!this.advertiser, name: '', error: true, errorMessage: portError };
|
|
175
294
|
if (this.destroyed)
|
|
176
295
|
return { publishing: false, name: '', error: true, errorMessage: 'mDNS instance is destroyed' };
|
|
177
|
-
|
|
178
|
-
|
|
296
|
+
if (this.networkError)
|
|
297
|
+
return { publishing: !!this.advertiser, name: '', error: true, errorMessage: this.networkError };
|
|
298
|
+
const parsed = this.parseType(safeOptions.type);
|
|
299
|
+
if (!parsed) {
|
|
300
|
+
return {
|
|
301
|
+
publishing: !!this.advertiser,
|
|
302
|
+
name: '',
|
|
303
|
+
error: true,
|
|
304
|
+
errorMessage: this.invalidTypeMessage(safeOptions.type),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const { type, protocol } = parsed;
|
|
308
|
+
const stopped = await this.stopBroadcastUnlocked();
|
|
309
|
+
if (stopped.error)
|
|
310
|
+
return Object.assign(Object.assign({}, stopped), { name: '' });
|
|
179
311
|
return new Promise((resolve) => {
|
|
180
312
|
let settled = false;
|
|
313
|
+
let failing = false;
|
|
181
314
|
let timeout = null;
|
|
182
315
|
const safeResolve = (r) => {
|
|
183
316
|
if (!settled) {
|
|
@@ -186,6 +319,7 @@ class mDNS {
|
|
|
186
319
|
clearTimeout(timeout);
|
|
187
320
|
timeout = null;
|
|
188
321
|
}
|
|
322
|
+
this.pendingPublishError = undefined;
|
|
189
323
|
resolve(r);
|
|
190
324
|
}
|
|
191
325
|
};
|
|
@@ -209,32 +343,68 @@ class mDNS {
|
|
|
209
343
|
}
|
|
210
344
|
};
|
|
211
345
|
const onUp = () => {
|
|
346
|
+
if (failing || settled)
|
|
347
|
+
return;
|
|
212
348
|
cleanupListeners();
|
|
213
349
|
safeResolve({ publishing: true, name: svc.name || '', error: false, errorMessage: null });
|
|
214
350
|
};
|
|
215
|
-
const onError = (err) => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const onTimeout = () => {
|
|
351
|
+
const onError = async (err) => {
|
|
352
|
+
if (failing || settled)
|
|
353
|
+
return;
|
|
354
|
+
failing = true;
|
|
355
|
+
if (timeout)
|
|
356
|
+
clearTimeout(timeout);
|
|
357
|
+
timeout = null;
|
|
223
358
|
cleanupListeners();
|
|
224
|
-
|
|
225
|
-
if (this.advertiser === svc)
|
|
226
|
-
this.advertiser = undefined;
|
|
359
|
+
const stopped = await this.stopBroadcastUnlocked();
|
|
227
360
|
safeResolve({
|
|
228
|
-
publishing:
|
|
361
|
+
publishing: stopped.publishing,
|
|
229
362
|
name: '',
|
|
230
363
|
error: true,
|
|
231
|
-
errorMessage:
|
|
364
|
+
errorMessage: [this.toErr(err), stopped.errorMessage].filter(Boolean).join('; '),
|
|
232
365
|
});
|
|
233
366
|
};
|
|
367
|
+
const onTimeout = () => {
|
|
368
|
+
void onError(new Error(svc.activated === false
|
|
369
|
+
? 'Service name is already in use on the network'
|
|
370
|
+
: `Timed out waiting for service publish after ${this.publishTimeoutMs}ms`));
|
|
371
|
+
};
|
|
372
|
+
// shortcut: Bonjour 1.4 sends goodbye for conflicting probes; remove this hook when its probe teardown is fixed.
|
|
373
|
+
const serviceRecords = svc.records;
|
|
374
|
+
let announcementStarted = svc.published === true;
|
|
375
|
+
svc.records = () => {
|
|
376
|
+
if (svc.destroyed)
|
|
377
|
+
return [];
|
|
378
|
+
// Bonjour sets published only after the send callback, but records are already registered before it.
|
|
379
|
+
announcementStarted = true;
|
|
380
|
+
return serviceRecords.call(svc);
|
|
381
|
+
};
|
|
382
|
+
const stop = svc.stop.bind(svc);
|
|
383
|
+
svc.stop = (callback) => {
|
|
384
|
+
const announced = announcementStarted || svc.published === true;
|
|
385
|
+
const conflict = !callback && svc.activated && !announced;
|
|
386
|
+
const records = svc.records;
|
|
387
|
+
if (!announced)
|
|
388
|
+
svc.records = () => [];
|
|
389
|
+
try {
|
|
390
|
+
stop(callback);
|
|
391
|
+
svc.destroyed = true;
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
svc.records = records;
|
|
395
|
+
}
|
|
396
|
+
if (conflict)
|
|
397
|
+
void onError(new Error('Service name is already in use on the network'));
|
|
398
|
+
};
|
|
234
399
|
svc.once('up', onUp);
|
|
235
400
|
svc.once('error', onError);
|
|
236
401
|
timeout = setTimeout(onTimeout, this.publishTimeoutMs);
|
|
237
402
|
this.advertiser = svc;
|
|
403
|
+
this.pendingPublishError = (error) => {
|
|
404
|
+
void onError(error);
|
|
405
|
+
};
|
|
406
|
+
if (this.networkError)
|
|
407
|
+
this.pendingPublishError(new Error(this.networkError));
|
|
238
408
|
}
|
|
239
409
|
catch (e) {
|
|
240
410
|
safeResolve({ publishing: false, name: '', error: true, errorMessage: this.toErr(e) });
|
|
@@ -258,7 +428,7 @@ class mDNS {
|
|
|
258
428
|
return { publishing: false, error: false, errorMessage: null };
|
|
259
429
|
}
|
|
260
430
|
catch (e) {
|
|
261
|
-
return { publishing:
|
|
431
|
+
return { publishing: !!this.advertiser, error: true, errorMessage: this.toErr(e) };
|
|
262
432
|
}
|
|
263
433
|
}
|
|
264
434
|
/**
|
|
@@ -268,24 +438,51 @@ class mDNS {
|
|
|
268
438
|
* @param options See MdnsDiscoverOptions for type/name/timeout.
|
|
269
439
|
*/
|
|
270
440
|
async discover(options = {}) {
|
|
441
|
+
var _a;
|
|
271
442
|
const safeOptions = (options !== null && options !== void 0 ? options : {});
|
|
272
443
|
if (this.destroyed) {
|
|
273
444
|
return { error: true, errorMessage: 'mDNS instance is destroyed', servicesFound: 0, services: [] };
|
|
274
445
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
446
|
+
if (this.networkError)
|
|
447
|
+
return { error: true, errorMessage: this.networkError, servicesFound: 0, services: [] };
|
|
448
|
+
const parsed = this.parseType(safeOptions.type);
|
|
449
|
+
if (!parsed) {
|
|
450
|
+
return {
|
|
451
|
+
error: true,
|
|
452
|
+
errorMessage: this.invalidTypeMessage(safeOptions.type),
|
|
453
|
+
servicesFound: 0,
|
|
454
|
+
services: [],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const inputError = (_a = this.validateName(safeOptions.name)) !== null && _a !== void 0 ? _a : (safeOptions.timeout != null && (typeof safeOptions.timeout !== 'number' || !Number.isFinite(safeOptions.timeout))
|
|
458
|
+
? 'Invalid timeout'
|
|
459
|
+
: null);
|
|
460
|
+
if (inputError)
|
|
461
|
+
return { error: true, errorMessage: inputError, servicesFound: 0, services: [] };
|
|
462
|
+
for (const abort of [...this.activeDiscoveries])
|
|
463
|
+
abort('Discovery replaced by a new request');
|
|
464
|
+
const { type, protocol } = parsed;
|
|
465
|
+
const targetId = typeof safeOptions.name === 'string' && safeOptions.name.trim() ? safeOptions.name.trim() : null;
|
|
466
|
+
const timeoutMs = this.normalizeTimeout(safeOptions.timeout);
|
|
278
467
|
let browser = null;
|
|
279
468
|
const services = [];
|
|
280
469
|
let hadError = false;
|
|
281
470
|
let errMsg = null;
|
|
282
471
|
return new Promise((resolve) => {
|
|
472
|
+
var _a;
|
|
283
473
|
let timer = null;
|
|
284
474
|
let settled = false;
|
|
285
|
-
const finish = () => {
|
|
475
|
+
const finish = (abortMessage) => {
|
|
476
|
+
var _a;
|
|
286
477
|
if (settled)
|
|
287
478
|
return;
|
|
288
479
|
settled = true;
|
|
480
|
+
this.activeDiscoveries.delete(finish);
|
|
481
|
+
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.removeListener('response', onResponse);
|
|
482
|
+
if (abortMessage) {
|
|
483
|
+
hadError = true;
|
|
484
|
+
errMsg = abortMessage;
|
|
485
|
+
}
|
|
289
486
|
if (browser) {
|
|
290
487
|
this.safeStopBrowser(browser);
|
|
291
488
|
browser = null;
|
|
@@ -297,36 +494,66 @@ class mDNS {
|
|
|
297
494
|
resolve({
|
|
298
495
|
error: hadError,
|
|
299
496
|
errorMessage: errMsg,
|
|
300
|
-
services,
|
|
301
|
-
servicesFound: services.length,
|
|
497
|
+
services: hadError ? [] : services.filter((s) => s.hosts.length > 0),
|
|
498
|
+
servicesFound: hadError ? 0 : services.filter((s) => s.hosts.length > 0).length,
|
|
302
499
|
});
|
|
303
500
|
};
|
|
501
|
+
const up = (s) => {
|
|
502
|
+
if (settled)
|
|
503
|
+
return;
|
|
504
|
+
const item = this.serviceResult(s, this.toFullType(type, protocol));
|
|
505
|
+
if (!this.matchesTarget(item.name, targetId))
|
|
506
|
+
return;
|
|
507
|
+
const previous = services.find((x) => x.name === item.name && x.port === item.port);
|
|
508
|
+
if (previous)
|
|
509
|
+
Object.assign(previous, item, { hosts: this.orderHosts([...previous.hosts, ...item.hosts]) });
|
|
510
|
+
else
|
|
511
|
+
services.push(item);
|
|
512
|
+
if (targetId && item.hosts.length)
|
|
513
|
+
finish();
|
|
514
|
+
else if (!item.hosts.length && item.hostname) {
|
|
515
|
+
this.queryAddresses(item.hostname);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const onResponse = (packet) => {
|
|
519
|
+
var _a, _b, _c;
|
|
520
|
+
if (settled)
|
|
521
|
+
return;
|
|
522
|
+
const records = [...((_a = packet.answers) !== null && _a !== void 0 ? _a : []), ...((_b = packet.additionals) !== null && _b !== void 0 ? _b : [])];
|
|
523
|
+
for (const item of services) {
|
|
524
|
+
for (const record of records) {
|
|
525
|
+
if ((record.type === 'A' || record.type === 'AAAA') &&
|
|
526
|
+
record.ttl &&
|
|
527
|
+
typeof record.data === 'string' &&
|
|
528
|
+
record.name.toLowerCase() === ((_c = item.hostname) === null || _c === void 0 ? void 0 : _c.toLowerCase()) &&
|
|
529
|
+
!item.hosts.includes(record.data))
|
|
530
|
+
item.hosts = this.orderHosts([...item.hosts, record.data]);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (targetId && services.some((item) => item.hosts.length))
|
|
534
|
+
finish();
|
|
535
|
+
};
|
|
536
|
+
this.activeDiscoveries.add(finish);
|
|
304
537
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const key = `${item.name}:${item.port}`;
|
|
318
|
-
if (!services.some((x) => `${x.name}:${x.port}` === key))
|
|
319
|
-
services.push(item);
|
|
320
|
-
if (targetId && this.matchesTarget(item.name, targetId))
|
|
321
|
-
finish();
|
|
322
|
-
});
|
|
538
|
+
const bonjour = this.bonjour;
|
|
539
|
+
if (settled)
|
|
540
|
+
return;
|
|
541
|
+
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.on('response', onResponse);
|
|
542
|
+
browser = bonjour.find({ type, protocol }, up);
|
|
543
|
+
if (settled) {
|
|
544
|
+
this.safeStopBrowser(browser);
|
|
545
|
+
browser = null;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
browser.on('srv-update', up);
|
|
549
|
+
browser.on('txt-update', up);
|
|
323
550
|
// Record error and let the timeout conclude; results will still be returned.
|
|
324
551
|
const onBrowserError = (err) => {
|
|
325
552
|
hadError = true;
|
|
326
553
|
errMsg = this.toErr(err);
|
|
327
554
|
};
|
|
328
555
|
browser.on('error', onBrowserError);
|
|
329
|
-
timer = setTimeout(finish
|
|
556
|
+
timer = setTimeout(() => finish(), timeoutMs);
|
|
330
557
|
}
|
|
331
558
|
catch (err) {
|
|
332
559
|
hadError = true;
|
|
@@ -338,6 +565,209 @@ class mDNS {
|
|
|
338
565
|
normalizeTxt(txt) {
|
|
339
566
|
return Object.fromEntries(Object.entries(txt).map(([key, value]) => [key, Buffer.isBuffer(value) ? value.toString('utf8') : String(value)]));
|
|
340
567
|
}
|
|
568
|
+
queryAddresses(hostname) {
|
|
569
|
+
var _a, _b, _c, _d;
|
|
570
|
+
try {
|
|
571
|
+
(_b = (_a = this.transport) === null || _a === void 0 ? void 0 : _a.query) === null || _b === void 0 ? void 0 : _b.call(_a, hostname, 'A');
|
|
572
|
+
(_d = (_c = this.transport) === null || _c === void 0 ? void 0 : _c.query) === null || _d === void 0 ? void 0 : _d.call(_c, hostname, 'AAAA');
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
this.onNetworkError(new Error(this.toErr(err)));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
orderHosts(hosts) {
|
|
579
|
+
return [...new Set(hosts)].sort((a, b) => Number(node_net.isIPv4(b)) - Number(node_net.isIPv4(a)));
|
|
580
|
+
}
|
|
581
|
+
serviceResult(s, type) {
|
|
582
|
+
var _a, _b;
|
|
583
|
+
const suffix = `.${type}local`;
|
|
584
|
+
const name = ((_a = s.fqdn) === null || _a === void 0 ? void 0 : _a.toLowerCase().endsWith(suffix.toLowerCase())) ? s.fqdn.slice(0, -suffix.length) : s.name || '';
|
|
585
|
+
return Object.assign(Object.assign({ name,
|
|
586
|
+
type, domain: 'local.', port: (_b = s.port) !== null && _b !== void 0 ? _b : 0, hosts: this.orderHosts(Array.isArray(s.addresses) ? s.addresses : []) }, (s.host ? { hostname: s.host } : {})), (s.txt && Object.keys(s.txt).length ? { txt: this.normalizeTxt(s.txt) } : {}));
|
|
587
|
+
}
|
|
588
|
+
notify(event, data) {
|
|
589
|
+
var _a;
|
|
590
|
+
this.emit(event, data);
|
|
591
|
+
for (const win of (_a = electron.BrowserWindow === null || electron.BrowserWindow === void 0 ? void 0 : electron.BrowserWindow.getAllWindows()) !== null && _a !== void 0 ? _a : []) {
|
|
592
|
+
if (!win.isDestroyed() && !win.webContents.isDestroyed() && this.trustedUrl(win.webContents.getURL())) {
|
|
593
|
+
win.webContents.send(`event-mDNS-${event}`, data);
|
|
594
|
+
win.webContents.send(`mdns:event:${event}`, data);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/** Thanks to Idrimi for the original continuous discovery feature (PR #7). */
|
|
599
|
+
async watchServices(options) {
|
|
600
|
+
var _a;
|
|
601
|
+
const failed = (message) => ({ error: true, errorMessage: message, watchersStarted: 0 });
|
|
602
|
+
if (this.destroyed)
|
|
603
|
+
return failed('mDNS instance is destroyed');
|
|
604
|
+
if (this.networkError)
|
|
605
|
+
return failed(this.networkError);
|
|
606
|
+
if (!Array.isArray(options === null || options === void 0 ? void 0 : options.types) || !options.types.length)
|
|
607
|
+
return failed("Missing or empty 'types' array");
|
|
608
|
+
const types = new Map();
|
|
609
|
+
for (const raw of options.types) {
|
|
610
|
+
const parsed = typeof raw === 'string' && raw.trim() ? this.parseType(raw) : null;
|
|
611
|
+
if (!parsed)
|
|
612
|
+
return failed(this.invalidTypeMessage(raw));
|
|
613
|
+
types.set(this.toFullType(parsed.type, parsed.protocol), parsed);
|
|
614
|
+
}
|
|
615
|
+
const cleanup = this.stopWatch();
|
|
616
|
+
if (cleanup)
|
|
617
|
+
return failed(cleanup);
|
|
618
|
+
const bonjour = this.bonjour;
|
|
619
|
+
if (this.networkError)
|
|
620
|
+
return failed(this.networkError);
|
|
621
|
+
const watch = { types: [...types.keys()], browsers: [], services: new Map() };
|
|
622
|
+
this.watch = watch;
|
|
623
|
+
const addBrowser = (type, parsed) => {
|
|
624
|
+
let active = true;
|
|
625
|
+
const up = (s) => {
|
|
626
|
+
var _a;
|
|
627
|
+
if (this.watch !== watch || !active)
|
|
628
|
+
return;
|
|
629
|
+
const service = this.serviceResult(s, type);
|
|
630
|
+
const key = `${type}|${service.name}`;
|
|
631
|
+
const previous = watch.services.get(key);
|
|
632
|
+
if (!service.hosts.length && (previous === null || previous === void 0 ? void 0 : previous.service.hostname) === service.hostname)
|
|
633
|
+
service.hosts = [...((_a = previous === null || previous === void 0 ? void 0 : previous.service.hosts) !== null && _a !== void 0 ? _a : [])];
|
|
634
|
+
const isNew = !(previous === null || previous === void 0 ? void 0 : previous.announced) && service.hosts.length > 0;
|
|
635
|
+
watch.services.set(key, {
|
|
636
|
+
service,
|
|
637
|
+
fqdn: s.fqdn || `${service.name}.${type}local`,
|
|
638
|
+
expires: s.ttl ? Date.now() + s.ttl * 1000 : Infinity,
|
|
639
|
+
announced: !!(previous === null || previous === void 0 ? void 0 : previous.announced) || isNew,
|
|
640
|
+
});
|
|
641
|
+
if (!service.hosts.length && service.hostname)
|
|
642
|
+
this.queryAddresses(service.hostname);
|
|
643
|
+
if (isNew)
|
|
644
|
+
this.notify('serviceFound', { service: structuredClone(service) });
|
|
645
|
+
};
|
|
646
|
+
const browser = bonjour.find(parsed, up);
|
|
647
|
+
browser.on('down', (s) => {
|
|
648
|
+
if (this.watch !== watch || !active)
|
|
649
|
+
return;
|
|
650
|
+
const key = `${type}|${this.serviceResult(s, type).name}`;
|
|
651
|
+
const previous = watch.services.get(key);
|
|
652
|
+
watch.services.delete(key);
|
|
653
|
+
if (previous === null || previous === void 0 ? void 0 : previous.announced)
|
|
654
|
+
this.notify('serviceLost', { service: structuredClone(previous.service) });
|
|
655
|
+
});
|
|
656
|
+
browser.on('srv-update', up);
|
|
657
|
+
browser.on('txt-update', up);
|
|
658
|
+
this.deactivateBrowser.set(browser, () => {
|
|
659
|
+
active = false;
|
|
660
|
+
});
|
|
661
|
+
if (this.watch !== watch) {
|
|
662
|
+
this.stopWatchBrowser(browser);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
watch.browsers.push(browser);
|
|
666
|
+
};
|
|
667
|
+
try {
|
|
668
|
+
for (const [type, parsed] of types)
|
|
669
|
+
addBrowser(type, parsed);
|
|
670
|
+
if (this.watch !== watch)
|
|
671
|
+
return failed('Watch stopped during startup');
|
|
672
|
+
watch.onResponse = (packet) => {
|
|
673
|
+
var _a, _b;
|
|
674
|
+
if (this.watch !== watch)
|
|
675
|
+
return;
|
|
676
|
+
const records = [...((_a = packet.answers) !== null && _a !== void 0 ? _a : []), ...((_b = packet.additionals) !== null && _b !== void 0 ? _b : [])];
|
|
677
|
+
for (const item of watch.services.values()) {
|
|
678
|
+
const ptr = records.find((r) => r.type === 'PTR' && String(r.data).toLowerCase() === item.fqdn.toLowerCase());
|
|
679
|
+
if (ptr)
|
|
680
|
+
item.expires = Date.now() + ptr.ttl * 1000;
|
|
681
|
+
const addresses = records.filter((r) => { var _a; return (r.type === 'A' || r.type === 'AAAA') && r.name.toLowerCase() === ((_a = item.service.hostname) === null || _a === void 0 ? void 0 : _a.toLowerCase()); });
|
|
682
|
+
for (const address of addresses) {
|
|
683
|
+
if (typeof address.data !== 'string')
|
|
684
|
+
continue;
|
|
685
|
+
const hosts = item.service.hosts.filter((host) => host !== address.data);
|
|
686
|
+
item.service.hosts = this.orderHosts(address.ttl ? [...hosts, address.data] : hosts);
|
|
687
|
+
}
|
|
688
|
+
if (!item.announced && item.service.hosts.length) {
|
|
689
|
+
item.announced = true;
|
|
690
|
+
this.notify('serviceFound', { service: structuredClone(item.service) });
|
|
691
|
+
if (this.watch !== watch)
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.on('response', watch.onResponse);
|
|
697
|
+
let ticks = 0;
|
|
698
|
+
watch.timer = setInterval(() => {
|
|
699
|
+
var _a;
|
|
700
|
+
if (this.watch !== watch)
|
|
701
|
+
return;
|
|
702
|
+
try {
|
|
703
|
+
const expired = [...watch.services.entries()].filter(([, item]) => item.expires <= Date.now());
|
|
704
|
+
for (const [key, item] of expired) {
|
|
705
|
+
watch.services.delete(key);
|
|
706
|
+
if (item.announced)
|
|
707
|
+
this.notify('serviceLost', { service: structuredClone(item.service) });
|
|
708
|
+
if (this.watch !== watch)
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// A fresh Browser forgets expired entries retained by bonjour-service's internal cache.
|
|
712
|
+
if (expired.length) {
|
|
713
|
+
for (const browser of watch.browsers.splice(0))
|
|
714
|
+
this.stopWatchBrowser(browser);
|
|
715
|
+
for (const [type, parsed] of types)
|
|
716
|
+
addBrowser(type, parsed);
|
|
717
|
+
}
|
|
718
|
+
else if (++ticks % 30 === 0) {
|
|
719
|
+
for (const browser of watch.browsers)
|
|
720
|
+
(_a = browser.update) === null || _a === void 0 ? void 0 : _a.call(browser);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
this.onNetworkError(err instanceof Error ? err : new Error(this.toErr(err)));
|
|
725
|
+
}
|
|
726
|
+
}, 1000);
|
|
727
|
+
watch.timer.unref();
|
|
728
|
+
return { error: false, errorMessage: null, watchersStarted: types.size };
|
|
729
|
+
}
|
|
730
|
+
catch (err) {
|
|
731
|
+
this.stopWatch();
|
|
732
|
+
return failed(this.toErr(err));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
stopWatchBrowser(browser) {
|
|
736
|
+
var _a;
|
|
737
|
+
(_a = this.deactivateBrowser.get(browser)) === null || _a === void 0 ? void 0 : _a();
|
|
738
|
+
this.deactivateBrowser.delete(browser);
|
|
739
|
+
try {
|
|
740
|
+
browser.stop();
|
|
741
|
+
this.stoppedBrowsers.delete(browser);
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
this.stoppedBrowsers.add(browser);
|
|
745
|
+
throw err;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
stopWatch() {
|
|
749
|
+
var _a, _b;
|
|
750
|
+
const watch = this.watch;
|
|
751
|
+
this.watch = undefined;
|
|
752
|
+
if (watch === null || watch === void 0 ? void 0 : watch.timer)
|
|
753
|
+
clearInterval(watch.timer);
|
|
754
|
+
if (watch === null || watch === void 0 ? void 0 : watch.onResponse)
|
|
755
|
+
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.removeListener('response', watch.onResponse);
|
|
756
|
+
const errors = [];
|
|
757
|
+
for (const browser of new Set([...((_b = watch === null || watch === void 0 ? void 0 : watch.browsers) !== null && _b !== void 0 ? _b : []), ...this.stoppedBrowsers])) {
|
|
758
|
+
try {
|
|
759
|
+
this.stopWatchBrowser(browser);
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
errors.push(this.toErr(err));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return errors.length ? errors.join('; ') : null;
|
|
766
|
+
}
|
|
767
|
+
async stopWatching() {
|
|
768
|
+
const errorMessage = this.stopWatch();
|
|
769
|
+
return { watching: false, error: !!errorMessage, errorMessage };
|
|
770
|
+
}
|
|
341
771
|
}
|
|
342
772
|
|
|
343
773
|
exports.mDNS = mDNS;
|