@devioarts/capacitor-mdns 0.0.3 → 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 +272 -101
- package/android/build.gradle +3 -9
- package/android/src/main/AndroidManifest.xml +2 -0
- 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 +496 -175
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +85 -49
- package/dist/docs.json +313 -15
- package/dist/esm/definitions.d.ts +89 -16
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js +22 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +8 -1
- package/dist/esm/web.js +35 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +57 -4
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +57 -4
- package/dist/plugin.js.map +1 -1
- package/docs/electron.md +91 -0
- package/docs/platform-setup.md +58 -0
- package/docs/playground.md +50 -0
- package/docs/runtime-behavior.md +80 -0
- package/electron/dist/plugin-settings.js +22 -0
- package/electron/dist/plugin-settings.js.map +1 -0
- package/electron/dist/plugin.cjs.js +774 -0
- package/electron/dist/plugin.cjs.js.map +1 -0
- package/electron/global.d.ts +2 -0
- package/electron/mdns-bridge.cjs +33 -4
- package/electron/mdns.ts +628 -107
- package/electron/rollup.config.mjs +22 -0
- package/electron/src/index.ts +2 -0
- package/electron/src/plugin-settings.ts +19 -0
- package/electron/tsconfig.json +21 -0
- package/electron/types-public/index.d.ts +47 -0
- package/ios/Sources/mDNSPlugin/mDNS.swift +481 -124
- package/ios/Sources/mDNSPlugin/mDNSPlugin.swift +85 -23
- package/ios/Tests/mDNSPluginTests/BridgeTests.swift +46 -0
- package/ios/Tests/mDNSPluginTests/MDNSTests.swift +490 -0
- package/package.json +55 -20
- package/ios/Tests/mDNSPluginTests/mDNSPluginTests.swift +0 -15
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import Darwin
|
|
3
|
-
#if canImport(Network)
|
|
4
3
|
import Network
|
|
5
|
-
#endif
|
|
6
4
|
|
|
7
5
|
/// iOS mDNS manager.
|
|
8
6
|
/// - Publishes via `NetService`.
|
|
@@ -10,18 +8,34 @@ import Network
|
|
|
10
8
|
/// - Resolves each candidate with `NetService` to obtain `port`, `hosts`, and optional TXT.
|
|
11
9
|
/// - Produces a normalized array of dictionaries consumable by the Capacitor bridge.
|
|
12
10
|
public class MDNS: NSObject {
|
|
11
|
+
// Tunables and seams. Internal (not private) so unit tests can shorten timers and inject fake services.
|
|
12
|
+
internal var publishTimeoutMs = 5000
|
|
13
|
+
internal var resolveTimeoutSeconds: TimeInterval = 5.0
|
|
14
|
+
internal var makeBrowser: () -> NetServiceBrowser = { NetServiceBrowser() }
|
|
15
|
+
internal var makeService: (_ domain: String, _ type: String, _ name: String, _ port: Int32?) -> NetService =
|
|
16
|
+
MDNS.defaultMakeService
|
|
17
|
+
|
|
18
|
+
private static func defaultMakeService(_ domain: String, _ type: String, _ name: String, _ port: Int32?) -> NetService {
|
|
19
|
+
if let port = port {
|
|
20
|
+
return NetService(domain: domain, type: type, name: name, port: port)
|
|
21
|
+
}
|
|
22
|
+
return NetService(domain: domain, type: type, name: name)
|
|
23
|
+
}
|
|
13
24
|
|
|
14
25
|
// MARK: - Advertising
|
|
15
26
|
|
|
27
|
+
public weak var watchDelegate: MDNSWatchDelegate?
|
|
28
|
+
private var watchSession: WatchSession?
|
|
29
|
+
|
|
16
30
|
private var publisher: NetService?
|
|
17
31
|
private var publishCompletion: ((Result<String, Error>) -> Void)?
|
|
32
|
+
private var publishTimeout: DispatchWorkItem?
|
|
33
|
+
private var publishSession: UInt64 = 0
|
|
18
34
|
|
|
19
35
|
// MARK: - Discovery state
|
|
20
36
|
|
|
21
37
|
private var nsBrowser: NetServiceBrowser?
|
|
22
|
-
|
|
23
|
-
private var nwBrowser: Any? // keep as `Any` to allow building against older SDKs
|
|
24
|
-
#endif
|
|
38
|
+
private var nwBrowser: NWBrowser?
|
|
25
39
|
|
|
26
40
|
/// Map the resolving `NetService` → service box (accumulates details while resolving).
|
|
27
41
|
private var resolveMap: [NetService: ServiceBox] = [:]
|
|
@@ -33,10 +47,20 @@ public class MDNS: NSObject {
|
|
|
33
47
|
|
|
34
48
|
/// Completion invoked once with either success(array) or failure(error).
|
|
35
49
|
private var discoverCompletion: ((Result<[[String: Any]], Error>) -> Void)?
|
|
50
|
+
private var discoveryError: Error?
|
|
51
|
+
private var discoverySession: UInt64 = 0
|
|
36
52
|
|
|
37
|
-
///
|
|
53
|
+
/// Discovery hard timeout.
|
|
38
54
|
private var hardTimeout: DispatchWorkItem?
|
|
39
|
-
|
|
55
|
+
|
|
56
|
+
deinit {
|
|
57
|
+
watchSession?.stop()
|
|
58
|
+
publishTimeout?.cancel()
|
|
59
|
+
publisher?.stop()
|
|
60
|
+
publisher?.delegate = nil
|
|
61
|
+
cancelTimers()
|
|
62
|
+
stopBrowsers()
|
|
63
|
+
}
|
|
40
64
|
|
|
41
65
|
// MARK: - Public API (thread-safe)
|
|
42
66
|
|
|
@@ -48,18 +72,34 @@ public class MDNS: NSObject {
|
|
|
48
72
|
/// - txt: Optional TXT dictionary (string → string).
|
|
49
73
|
/// - completion: Called with the final (possibly uniquified) `name`, or an error.
|
|
50
74
|
public func broadcast(
|
|
51
|
-
type: String,
|
|
52
|
-
name: String,
|
|
75
|
+
type rawType: String,
|
|
76
|
+
name rawName: String,
|
|
53
77
|
port: Int,
|
|
54
78
|
txt: [String: String]?,
|
|
55
79
|
completion: @escaping (Result<String, Error>) -> Void
|
|
56
80
|
) {
|
|
81
|
+
let type: String
|
|
82
|
+
let name: String
|
|
83
|
+
do {
|
|
84
|
+
guard (1...65535).contains(port) else { throw Self.error("Missing/invalid port") }
|
|
85
|
+
type = try Self.validatedType(rawType)
|
|
86
|
+
let normalizedName = try Self.validatedName(rawName)
|
|
87
|
+
name = normalizedName.isEmpty ? "DevIOArtsMDNS" : normalizedName
|
|
88
|
+
try Self.validateTXT(txt)
|
|
89
|
+
} catch { completion(.failure(error)); return }
|
|
90
|
+
|
|
57
91
|
runOnMain { [weak self] in
|
|
58
|
-
guard let self = self else {
|
|
59
|
-
|
|
92
|
+
guard let self = self else {
|
|
93
|
+
completion(.failure(Self.error("mDNS manager was released before publishing could start")))
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
self.publishSession &+= 1
|
|
98
|
+
let session = self.publishSession
|
|
99
|
+
self.stopPublisherIfNeeded(reason: "Replaced by a new broadcast request")
|
|
60
100
|
self.publishCompletion = completion
|
|
61
101
|
|
|
62
|
-
let svc =
|
|
102
|
+
let svc = self.makeService("local.", type, name, Int32(port))
|
|
63
103
|
svc.includesPeerToPeer = true
|
|
64
104
|
|
|
65
105
|
if let txt = txt {
|
|
@@ -71,6 +111,7 @@ public class MDNS: NSObject {
|
|
|
71
111
|
|
|
72
112
|
svc.delegate = self
|
|
73
113
|
self.publisher = svc
|
|
114
|
+
self.schedulePublishTimeout(session: session)
|
|
74
115
|
// Use default publish options; connection listening not required here.
|
|
75
116
|
svc.publish()
|
|
76
117
|
}
|
|
@@ -80,7 +121,7 @@ public class MDNS: NSObject {
|
|
|
80
121
|
/// Exposed as `throws` by the bridge, but this implementation does not throw.
|
|
81
122
|
public func stopBroadcast() throws {
|
|
82
123
|
runOnMain { [weak self] in
|
|
83
|
-
self?.stopPublisherIfNeeded()
|
|
124
|
+
self?.stopPublisherIfNeeded(reason: "Publish stopped before completion")
|
|
84
125
|
}
|
|
85
126
|
}
|
|
86
127
|
|
|
@@ -92,36 +133,52 @@ public class MDNS: NSObject {
|
|
|
92
133
|
/// - useNW: Prefer `NWBrowser` when available (default `true`), fallback to `NetServiceBrowser` otherwise.
|
|
93
134
|
/// - completion: Success with normalized service dictionaries, or failure with error.
|
|
94
135
|
public func discover(
|
|
95
|
-
type: String,
|
|
96
|
-
name: String?,
|
|
136
|
+
type rawType: String,
|
|
137
|
+
name rawName: String?,
|
|
97
138
|
timeoutMs: Int,
|
|
98
139
|
useNW: Bool = true,
|
|
99
140
|
completion: @escaping (Result<[[String: Any]], Error>) -> Void
|
|
100
141
|
) {
|
|
142
|
+
let type: String
|
|
143
|
+
let name: String?
|
|
144
|
+
do {
|
|
145
|
+
type = try Self.validatedType(rawType)
|
|
146
|
+
let trimmed = try Self.validatedName(rawName ?? "")
|
|
147
|
+
name = trimmed.isEmpty ? nil : trimmed
|
|
148
|
+
} catch { completion(.failure(error)); return }
|
|
101
149
|
runOnMain { [weak self] in
|
|
102
|
-
guard let self = self else {
|
|
150
|
+
guard let self = self else {
|
|
151
|
+
completion(.failure(Self.error("mDNS manager was released before discovery could start")))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if self.discoverCompletion != nil {
|
|
156
|
+
self.finishDiscovery(error: Self.error("Discovery replaced by a new request"))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
self.discoverySession &+= 1
|
|
160
|
+
let session = self.discoverySession
|
|
103
161
|
// Reset session state.
|
|
104
162
|
self.cancelTimers()
|
|
105
163
|
self.stopBrowsers()
|
|
106
164
|
self.resolveMap.removeAll()
|
|
107
165
|
self.discovered.removeAll()
|
|
108
166
|
self.targetName = name
|
|
167
|
+
self.discoveryError = nil
|
|
109
168
|
self.discoverCompletion = completion
|
|
110
169
|
|
|
111
170
|
// Schedule hard timeout (always resolves/finishes).
|
|
112
|
-
let ht = DispatchWorkItem { [weak self] in self?.finishDiscovery() }
|
|
171
|
+
let ht = DispatchWorkItem { [weak self] in self?.finishDiscovery(session: session) }
|
|
113
172
|
self.hardTimeout = ht
|
|
114
173
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(max(0, timeoutMs)), execute: ht)
|
|
115
174
|
|
|
116
175
|
// Start browsing: NWBrowser preferred when available and requested.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
self.startNWBrowse(typeWithDot: type)
|
|
176
|
+
if useNW {
|
|
177
|
+
self.startNWBrowse(typeWithDot: type, session: session)
|
|
120
178
|
return
|
|
121
179
|
}
|
|
122
|
-
#endif
|
|
123
180
|
// Fallback: NetServiceBrowser
|
|
124
|
-
let b =
|
|
181
|
+
let b = self.makeBrowser()
|
|
125
182
|
b.includesPeerToPeer = true
|
|
126
183
|
b.delegate = self
|
|
127
184
|
self.nsBrowser = b
|
|
@@ -132,9 +189,7 @@ public class MDNS: NSObject {
|
|
|
132
189
|
|
|
133
190
|
// MARK: - Internal: NWBrowser discovery
|
|
134
191
|
|
|
135
|
-
|
|
136
|
-
@available(iOS 12.0, *)
|
|
137
|
-
private func startNWBrowse(typeWithDot: String) {
|
|
192
|
+
private func startNWBrowse(typeWithDot: String, session: UInt64) {
|
|
138
193
|
// NWBrowser expects type without trailing dot.
|
|
139
194
|
let typeNoDot = typeWithDot.hasSuffix(".") ? String(typeWithDot.dropLast()) : typeWithDot
|
|
140
195
|
|
|
@@ -144,15 +199,22 @@ public class MDNS: NSObject {
|
|
|
144
199
|
let browser = NWBrowser(for: .bonjour(type: typeNoDot, domain: nil), using: params)
|
|
145
200
|
self.nwBrowser = browser
|
|
146
201
|
|
|
147
|
-
browser.stateUpdateHandler = { state in
|
|
148
|
-
// Intentionally quiet; errors are reflected via timeout or partial results.
|
|
149
|
-
// print("[mDNS][NW] state: \(state)")
|
|
202
|
+
browser.stateUpdateHandler = { [weak self] state in
|
|
150
203
|
switch state {
|
|
151
204
|
case .failed(let err):
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
205
|
+
DispatchQueue.main.async { [weak self] in
|
|
206
|
+
self?.finishDiscovery(session: session, error: err)
|
|
207
|
+
}
|
|
208
|
+
case .waiting(let err):
|
|
209
|
+
// Denied Local Network permission parks the browser in `.waiting` forever; surface it.
|
|
210
|
+
if Self.isLocalNetworkDenied(err) {
|
|
211
|
+
DispatchQueue.main.async { [weak self] in
|
|
212
|
+
self?.finishDiscovery(
|
|
213
|
+
session: session,
|
|
214
|
+
error: Self.error("Local network access denied. Enable it in Settings > Privacy > Local Network.")
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
156
218
|
default:
|
|
157
219
|
break
|
|
158
220
|
}
|
|
@@ -160,38 +222,22 @@ public class MDNS: NSObject {
|
|
|
160
222
|
|
|
161
223
|
browser.browseResultsChangedHandler = { [weak self] results, _ in
|
|
162
224
|
guard let self = self else { return }
|
|
225
|
+
guard session == self.discoverySession, self.discoverCompletion != nil else { return }
|
|
163
226
|
for r in results {
|
|
164
227
|
guard case let NWEndpoint.service(name: n, type: t, domain: d, interface: _) = r.endpoint else { continue }
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// Use NetService for resolve (port/hosts/TXT) to keep output parity with fallback.
|
|
168
|
-
let key = self.keyFor(name: n, type: t, domain: d)
|
|
169
|
-
if self.discovered.contains(where: { $0.identityKey == key }) { continue }
|
|
170
|
-
|
|
171
|
-
let resolver = NetService(domain: d.isEmpty ? "local." : d,
|
|
172
|
-
type: t.hasSuffix(".") ? t : t + ".",
|
|
173
|
-
name: n)
|
|
174
|
-
resolver.includesPeerToPeer = true
|
|
175
|
-
resolver.delegate = self
|
|
176
|
-
|
|
177
|
-
let box = ServiceBox(name: n, type: resolver.type, domain: resolver.domain)
|
|
178
|
-
self.discovered.append(box)
|
|
179
|
-
self.resolveMap[resolver] = box
|
|
180
|
-
resolver.resolve(withTimeout: 5.0)
|
|
181
|
-
|
|
182
|
-
// Reschedule short settle window on each new find.
|
|
183
|
-
self.scheduleSettleDebounce()
|
|
228
|
+
self.handleServiceFound(name: n, type: t, domain: d)
|
|
184
229
|
}
|
|
185
230
|
}
|
|
186
231
|
|
|
187
232
|
browser.start(queue: .main)
|
|
188
233
|
}
|
|
189
|
-
#endif
|
|
190
234
|
|
|
191
235
|
// MARK: - Internal: finalize & helpers
|
|
192
236
|
|
|
193
237
|
/// Build the final output and complete the discovery promise.
|
|
194
|
-
private func finishDiscovery() {
|
|
238
|
+
private func finishDiscovery(session: UInt64? = nil, error: Error? = nil) {
|
|
239
|
+
if let session = session, session != discoverySession { return }
|
|
240
|
+
|
|
195
241
|
cancelTimers()
|
|
196
242
|
stopBrowsers()
|
|
197
243
|
|
|
@@ -199,55 +245,40 @@ public class MDNS: NSObject {
|
|
|
199
245
|
var seen = Set<String>()
|
|
200
246
|
var out: [[String: Any]] = []
|
|
201
247
|
|
|
202
|
-
for box in discovered where box.resolved {
|
|
248
|
+
for box in discovered where box.resolved && !box.hosts.isEmpty {
|
|
203
249
|
let key = "\(box.name):\(box.port):\(box.hosts.first ?? "")"
|
|
204
250
|
if seen.contains(key) { continue }
|
|
205
251
|
seen.insert(key)
|
|
206
252
|
|
|
207
|
-
|
|
208
|
-
"name": box.name,
|
|
209
|
-
"type": box.type,
|
|
210
|
-
"domain": box.domain,
|
|
211
|
-
"port": box.port
|
|
212
|
-
]
|
|
213
|
-
if !box.hosts.isEmpty { entry["hosts"] = box.hosts }
|
|
214
|
-
if !box.txt.isEmpty { entry["txt"] = box.txt }
|
|
215
|
-
out.append(entry)
|
|
253
|
+
out.append(box.result)
|
|
216
254
|
}
|
|
217
255
|
|
|
218
256
|
// Clear state before invoking the callback.
|
|
219
257
|
resolveMap.removeAll()
|
|
220
258
|
discovered.removeAll()
|
|
221
259
|
targetName = nil
|
|
260
|
+
let finalError = error ?? discoveryError
|
|
261
|
+
discoveryError = nil
|
|
222
262
|
|
|
223
263
|
let cb = discoverCompletion
|
|
224
264
|
discoverCompletion = nil
|
|
225
|
-
|
|
265
|
+
if let finalError = finalError {
|
|
266
|
+
cb?(.failure(finalError))
|
|
267
|
+
} else {
|
|
268
|
+
cb?(.success(out))
|
|
269
|
+
}
|
|
226
270
|
}
|
|
227
271
|
|
|
228
|
-
/// Cancel
|
|
272
|
+
/// Cancel the discovery timeout.
|
|
229
273
|
private func cancelTimers() {
|
|
230
274
|
hardTimeout?.cancel(); hardTimeout = nil
|
|
231
|
-
settleDebounce?.cancel(); settleDebounce = nil
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/// Schedule a short debounce to finish discovery when the system becomes idle.
|
|
235
|
-
private func scheduleSettleDebounce(_ ms: Int = 350) {
|
|
236
|
-
settleDebounce?.cancel()
|
|
237
|
-
let wi = DispatchWorkItem { [weak self] in self?.finishDiscovery() }
|
|
238
|
-
settleDebounce = wi
|
|
239
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(ms), execute: wi)
|
|
240
275
|
}
|
|
241
276
|
|
|
242
277
|
/// Stop any active browsers.
|
|
243
278
|
private func stopBrowsers() {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
nwBrowser = nil
|
|
248
|
-
}
|
|
249
|
-
#endif
|
|
250
|
-
// Stop all in-flight resolvers as well (best-effort).
|
|
279
|
+
nwBrowser?.cancel()
|
|
280
|
+
nwBrowser = nil
|
|
281
|
+
// Stop all in-flight resolvers as well (best-effort).
|
|
251
282
|
for (svc, _) in resolveMap {
|
|
252
283
|
svc.stop()
|
|
253
284
|
svc.delegate = nil
|
|
@@ -259,17 +290,32 @@ public class MDNS: NSObject {
|
|
|
259
290
|
}
|
|
260
291
|
|
|
261
292
|
/// Stop the current publisher, if any.
|
|
262
|
-
private func stopPublisherIfNeeded() {
|
|
293
|
+
private func stopPublisherIfNeeded(reason: String) {
|
|
294
|
+
publishTimeout?.cancel()
|
|
295
|
+
publishTimeout = nil
|
|
263
296
|
publisher?.stop()
|
|
264
297
|
publisher?.delegate = nil
|
|
265
298
|
publisher = nil
|
|
266
|
-
|
|
299
|
+
publishCompletion?(.failure(Self.error(reason)))
|
|
267
300
|
publishCompletion = nil
|
|
268
301
|
}
|
|
269
302
|
|
|
303
|
+
/// Enforce that NetService publish cannot leave a JS promise pending forever.
|
|
304
|
+
private func schedulePublishTimeout(session: UInt64) {
|
|
305
|
+
publishTimeout?.cancel()
|
|
306
|
+
let wi = DispatchWorkItem { [weak self] in
|
|
307
|
+
guard let self = self, session == self.publishSession, self.publishCompletion != nil else { return }
|
|
308
|
+
self.stopPublisherIfNeeded(reason: "Timed out waiting for service publish after \(self.publishTimeoutMs)ms")
|
|
309
|
+
}
|
|
310
|
+
publishTimeout = wi
|
|
311
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(publishTimeoutMs), execute: wi)
|
|
312
|
+
}
|
|
313
|
+
|
|
270
314
|
/// Normalize a service name by removing the system-appended `" (n)"` suffix.
|
|
271
315
|
private func normalize(_ s: String) -> String {
|
|
272
|
-
return s
|
|
316
|
+
return s
|
|
317
|
+
.replacingOccurrences(of: #" \(\d+\)$"#, with: "", options: .regularExpression)
|
|
318
|
+
.replacingOccurrences(of: ".", with: "-")
|
|
273
319
|
}
|
|
274
320
|
|
|
275
321
|
/// Does `candidate` match `targetName` exactly or by prefix (both normalized)? If no target, accept all.
|
|
@@ -288,6 +334,93 @@ public class MDNS: NSObject {
|
|
|
288
334
|
private func runOnMain(_ block: @escaping () -> Void) {
|
|
289
335
|
if Thread.isMainThread { block() } else { DispatchQueue.main.async(execute: block) }
|
|
290
336
|
}
|
|
337
|
+
|
|
338
|
+
private static func error(_ message: String) -> NSError {
|
|
339
|
+
return NSError(domain: "mDNS", code: -1, userInfo: [NSLocalizedDescriptionKey: message])
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
extension MDNS {
|
|
344
|
+
private static func validatedWatchTypes(_ types: [String]) throws -> [String] {
|
|
345
|
+
guard !types.isEmpty else { throw error("Missing or empty 'types' array") }
|
|
346
|
+
var unique: [String] = []
|
|
347
|
+
for raw in types {
|
|
348
|
+
guard !raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw error("Invalid service type") }
|
|
349
|
+
let type = try validatedType(raw)
|
|
350
|
+
if !unique.contains(type) { unique.append(type) }
|
|
351
|
+
}
|
|
352
|
+
return unique
|
|
353
|
+
}
|
|
354
|
+
public var isPublishing: Bool {
|
|
355
|
+
if Thread.isMainThread { return publisher != nil }
|
|
356
|
+
return DispatchQueue.main.sync { publisher != nil }
|
|
357
|
+
}
|
|
358
|
+
internal static func validatedType(_ raw: String) throws -> String {
|
|
359
|
+
var type = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
360
|
+
if type.isEmpty { type = "_http._tcp." }
|
|
361
|
+
if type.hasSuffix(".") { type.removeLast() }
|
|
362
|
+
let service = type.components(separatedBy: ".").first.map { String($0.dropFirst()) } ?? ""
|
|
363
|
+
guard type.range(of: "^_[a-z0-9](?:[a-z0-9-]{0,13}[a-z0-9])?\\._(tcp|udp)$", options: .regularExpression) != nil,
|
|
364
|
+
service.range(of: "[a-z]", options: .regularExpression) != nil, !service.contains("--") else {
|
|
365
|
+
throw error("Invalid service type: \(raw)")
|
|
366
|
+
}
|
|
367
|
+
return type + "."
|
|
368
|
+
}
|
|
369
|
+
internal static func validatedName(_ raw: String) throws -> String {
|
|
370
|
+
let name = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
371
|
+
guard name.utf8.count <= 63, !raw.unicodeScalars.contains(where: { $0.value < 32 || $0.value == 127 }) else {
|
|
372
|
+
throw error("Invalid instance name (maximum 63 UTF-8 bytes, no control characters)")
|
|
373
|
+
}
|
|
374
|
+
return name
|
|
375
|
+
}
|
|
376
|
+
internal static func validateTXT(_ txt: [String: String]?) throws {
|
|
377
|
+
var keys = Set<String>()
|
|
378
|
+
var totalBytes = 0
|
|
379
|
+
for (key, value) in txt ?? [:] {
|
|
380
|
+
guard !key.isEmpty,
|
|
381
|
+
key.unicodeScalars.allSatisfy({ (32...126).contains($0.value) && $0 != "=" }),
|
|
382
|
+
keys.insert(key.lowercased()).inserted else {
|
|
383
|
+
throw error("Invalid or duplicate TXT key")
|
|
384
|
+
}
|
|
385
|
+
let bytes = "\(key)=\(value)".utf8.count
|
|
386
|
+
totalBytes += bytes + 1
|
|
387
|
+
guard totalBytes <= 1300 else { throw error("TXT record must fit in 1300 bytes including length prefixes") }
|
|
388
|
+
guard bytes <= 255 else { throw error("TXT entries must be at most 255 UTF-8 bytes including key=") }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// `NWBrowser` parks in `.waiting(.dns(PolicyDenied))` when the Local Network permission is denied.
|
|
393
|
+
static func isLocalNetworkDenied(_ error: NWError) -> Bool {
|
|
394
|
+
if case .dns(let code) = error { return code == -65570 } // kDNSServiceErr_PolicyDenied
|
|
395
|
+
return false
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// MARK: - Internal: candidate handling (shared by NWBrowser and NetServiceBrowser)
|
|
399
|
+
|
|
400
|
+
/// Register one browse result and start resolving it.
|
|
401
|
+
///
|
|
402
|
+
/// The identity key is built from the *normalized* type/domain, the same values `ServiceBox`
|
|
403
|
+
/// stores. `NWBrowser` re-delivers its whole result set on every change, so a key built from the
|
|
404
|
+
/// raw values would never match and every change would spawn a new resolver per known service.
|
|
405
|
+
internal func handleServiceFound(name: String, type: String, domain: String) {
|
|
406
|
+
guard discoverCompletion != nil, matchesTarget(name) else { return }
|
|
407
|
+
|
|
408
|
+
let normType = (type.hasSuffix(".") ? type : type + ".").lowercased()
|
|
409
|
+
let normDomain = domain.isEmpty ? "local." : (domain.hasSuffix(".") ? domain : domain + ".")
|
|
410
|
+
let key = keyFor(name: name, type: normType, domain: normDomain)
|
|
411
|
+
if discovered.contains(where: { $0.identityKey == key }) { return }
|
|
412
|
+
|
|
413
|
+
// Dedicated resolver (never reuse the browser's service object).
|
|
414
|
+
let resolver = makeService(normDomain, normType, name, nil)
|
|
415
|
+
resolver.includesPeerToPeer = true
|
|
416
|
+
resolver.delegate = self
|
|
417
|
+
|
|
418
|
+
let box = ServiceBox(name: name, type: normType, domain: normDomain)
|
|
419
|
+
discovered.append(box)
|
|
420
|
+
resolveMap[resolver] = box
|
|
421
|
+
resolver.resolve(withTimeout: resolveTimeoutSeconds)
|
|
422
|
+
|
|
423
|
+
}
|
|
291
424
|
}
|
|
292
425
|
|
|
293
426
|
// MARK: - NetServiceDelegate (publish + resolve)
|
|
@@ -296,12 +429,19 @@ extension MDNS: NetServiceDelegate {
|
|
|
296
429
|
|
|
297
430
|
// Publish callbacks
|
|
298
431
|
public func netServiceDidPublish(_ sender: NetService) {
|
|
432
|
+
guard sender === publisher else { return }
|
|
433
|
+
publishTimeout?.cancel()
|
|
434
|
+
publishTimeout = nil
|
|
299
435
|
let cb = publishCompletion
|
|
300
436
|
publishCompletion = nil
|
|
301
437
|
cb?(.success(sender.name))
|
|
302
438
|
}
|
|
303
439
|
|
|
304
440
|
public func netService(_ sender: NetService, didNotPublish errorDict: [String: NSNumber]) {
|
|
441
|
+
guard sender === publisher else { return }
|
|
442
|
+
publishTimeout?.cancel()
|
|
443
|
+
publishTimeout = nil
|
|
444
|
+
publisher = nil
|
|
305
445
|
let cb = publishCompletion
|
|
306
446
|
publishCompletion = nil
|
|
307
447
|
let code = (errorDict[NetService.errorCode] as NSNumber?)?.intValue ?? -1
|
|
@@ -315,30 +455,26 @@ extension MDNS: NetServiceDelegate {
|
|
|
315
455
|
|
|
316
456
|
// Resolve callbacks
|
|
317
457
|
public func netServiceDidResolveAddress(_ sender: NetService) {
|
|
458
|
+
if watchResolved(sender) { return }
|
|
318
459
|
guard let box = resolveMap[sender] else { return }
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
box.resolved = true
|
|
460
|
+
readResolved(sender, into: box)
|
|
461
|
+
|
|
462
|
+
// This resolver is done.
|
|
463
|
+
resolveMap.removeValue(forKey: sender)
|
|
464
|
+
sender.stop()
|
|
465
|
+
sender.delegate = nil
|
|
328
466
|
|
|
329
467
|
// Early finish if a specific target is requested and this service matches it.
|
|
330
468
|
if let t = targetName, !t.isEmpty, matchesTarget(sender.name) {
|
|
331
|
-
finishDiscovery()
|
|
332
|
-
} else {
|
|
333
|
-
// Otherwise, allow a brief settle window to collect late peers.
|
|
334
|
-
scheduleSettleDebounce()
|
|
469
|
+
finishDiscovery(session: discoverySession)
|
|
335
470
|
}
|
|
336
471
|
}
|
|
337
472
|
|
|
338
473
|
public func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
|
|
474
|
+
if watchResolveFailed(sender) { return }
|
|
339
475
|
// Drop the unresolved service and try to continue with others.
|
|
340
|
-
resolveMap.removeValue(forKey: sender)
|
|
341
|
-
|
|
476
|
+
guard resolveMap.removeValue(forKey: sender) != nil else { return }
|
|
477
|
+
sender.delegate = nil
|
|
342
478
|
}
|
|
343
479
|
}
|
|
344
480
|
|
|
@@ -347,32 +483,243 @@ extension MDNS: NetServiceDelegate {
|
|
|
347
483
|
extension MDNS: NetServiceBrowserDelegate {
|
|
348
484
|
|
|
349
485
|
public func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
|
|
350
|
-
|
|
351
|
-
|
|
486
|
+
if watchSession?.browsers.values.contains(where: { $0 === browser }) == true {
|
|
487
|
+
handleWatchFound(name: service.name, type: service.type, domain: service.domain)
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
guard browser === nsBrowser else { return }
|
|
491
|
+
handleServiceFound(name: service.name, type: service.type, domain: service.domain)
|
|
492
|
+
}
|
|
352
493
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
494
|
+
public func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
|
|
495
|
+
guard watchSession?.browsers.values.contains(where: { $0 === browser }) == true else { return }
|
|
496
|
+
handleWatchLost(name: service.name, type: service.type, domain: service.domain)
|
|
497
|
+
}
|
|
357
498
|
|
|
358
|
-
|
|
359
|
-
let
|
|
360
|
-
|
|
361
|
-
|
|
499
|
+
public func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String: NSNumber]) {
|
|
500
|
+
if let type = watchSession?.browsers.first(where: { $0.value === browser })?.key {
|
|
501
|
+
failWatch(type: type, error: Self.error("Failed to search for services: \(errorDict)"))
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
guard browser === nsBrowser else { return }
|
|
505
|
+
let code = (errorDict[NetService.errorCode] as NSNumber?)?.intValue ?? -1
|
|
506
|
+
let err = NSError(
|
|
507
|
+
domain: "mDNS.discovery",
|
|
508
|
+
code: code,
|
|
509
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to search for services", "info": errorDict]
|
|
510
|
+
)
|
|
511
|
+
finishDiscovery(session: discoverySession, error: err)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// MARK: - Continuous discovery
|
|
362
516
|
|
|
363
|
-
|
|
517
|
+
public protocol MDNSWatchDelegate: AnyObject {
|
|
518
|
+
func mdns(_ mdns: MDNS, didFindService service: [String: Any])
|
|
519
|
+
func mdns(_ mdns: MDNS, didLoseService service: [String: Any])
|
|
520
|
+
func mdns(_ mdns: MDNS, watchError type: String, message: String, watching: Bool)
|
|
521
|
+
}
|
|
364
522
|
|
|
365
|
-
|
|
366
|
-
|
|
523
|
+
internal struct BrowseService {
|
|
524
|
+
let name: String
|
|
525
|
+
let type: String
|
|
526
|
+
let domain: String
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private final class WatchSession {
|
|
530
|
+
var browsers: [String: NetServiceBrowser] = [:]
|
|
531
|
+
var networkBrowsers: [NWBrowser] = []
|
|
532
|
+
var known: [String: ServiceBox] = [:]
|
|
533
|
+
var resolvers: [NetService: ServiceBox] = [:]
|
|
534
|
+
var retries: [String: DispatchWorkItem] = [:]
|
|
535
|
+
var attempts: [String: Int] = [:]
|
|
536
|
+
|
|
537
|
+
func stop() {
|
|
538
|
+
for browser in networkBrowsers {
|
|
539
|
+
browser.stateUpdateHandler = nil
|
|
540
|
+
browser.browseResultsChangedHandler = nil
|
|
541
|
+
browser.cancel()
|
|
542
|
+
}
|
|
543
|
+
networkBrowsers.removeAll()
|
|
544
|
+
for browser in browsers.values { browser.delegate = nil; browser.stop() }
|
|
545
|
+
browsers.removeAll()
|
|
546
|
+
for resolver in resolvers.keys { resolver.delegate = nil; resolver.stop() }
|
|
547
|
+
resolvers.removeAll()
|
|
548
|
+
retries.values.forEach { $0.cancel() }
|
|
549
|
+
retries.removeAll()
|
|
550
|
+
known.removeAll()
|
|
551
|
+
attempts.removeAll()
|
|
367
552
|
}
|
|
553
|
+
}
|
|
368
554
|
|
|
369
|
-
|
|
370
|
-
|
|
555
|
+
extension MDNS {
|
|
556
|
+
/// Thanks to Idrimi for the original continuous discovery feature (PR #7).
|
|
557
|
+
public func watchServices(types: [String], useNW: Bool = true, completion: @escaping (Result<Int, Error>) -> Void) {
|
|
558
|
+
runOnMain { [weak self] in
|
|
559
|
+
guard let self = self else { completion(.failure(Self.error("mDNS manager released"))); return }
|
|
560
|
+
let unique: [String]
|
|
561
|
+
do { unique = try Self.validatedWatchTypes(types) } catch { completion(.failure(error)); return }
|
|
562
|
+
self.stopWatch()
|
|
563
|
+
let watch = WatchSession()
|
|
564
|
+
self.watchSession = watch
|
|
565
|
+
for type in unique {
|
|
566
|
+
if useNW {
|
|
567
|
+
let params = NWParameters()
|
|
568
|
+
params.includePeerToPeer = true
|
|
569
|
+
let browser = NWBrowser(for: .bonjour(type: String(type.dropLast()), domain: nil), using: params)
|
|
570
|
+
watch.networkBrowsers.append(browser)
|
|
571
|
+
browser.stateUpdateHandler = { [weak self, weak watch] state in
|
|
572
|
+
guard let self = self, let watch = watch, self.watchSession === watch else { return }
|
|
573
|
+
self.handleWatchState(state, type: type)
|
|
574
|
+
}
|
|
575
|
+
browser.browseResultsChangedHandler = { [weak self, weak watch] results, _ in
|
|
576
|
+
guard let self = self, let watch = watch, self.watchSession === watch else { return }
|
|
577
|
+
let services: [BrowseService] = results.compactMap { result in
|
|
578
|
+
guard case let .service(name, type, domain, _) = result.endpoint else { return nil }
|
|
579
|
+
return BrowseService(name: name, type: type, domain: domain)
|
|
580
|
+
}
|
|
581
|
+
self.handleWatchResults(services, type: type)
|
|
582
|
+
}
|
|
583
|
+
browser.start(queue: .main)
|
|
584
|
+
} else {
|
|
585
|
+
let browser = self.makeBrowser()
|
|
586
|
+
browser.includesPeerToPeer = true
|
|
587
|
+
browser.delegate = self
|
|
588
|
+
watch.browsers[type] = browser
|
|
589
|
+
browser.searchForServices(ofType: type, inDomain: "local.")
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
completion(.success(unique.count))
|
|
593
|
+
}
|
|
371
594
|
}
|
|
372
595
|
|
|
373
|
-
public func
|
|
374
|
-
|
|
375
|
-
|
|
596
|
+
public func stopWatching(completion: @escaping () -> Void = {}) {
|
|
597
|
+
runOnMain { [weak self] in self?.stopWatch(); completion() }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private func stopWatch() {
|
|
601
|
+
let old = watchSession
|
|
602
|
+
watchSession = nil
|
|
603
|
+
old?.stop()
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
internal func failWatch(type: String, error: Error) {
|
|
607
|
+
guard watchSession != nil else { return }
|
|
608
|
+
stopWatch()
|
|
609
|
+
watchDelegate?.mdns(self, watchError: type, message: error.localizedDescription, watching: false)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
internal func handleWatchState(_ state: NWBrowser.State, type: String) {
|
|
613
|
+
switch state {
|
|
614
|
+
case .failed(let error): failWatch(type: type, error: error)
|
|
615
|
+
case .waiting(let error) where Self.isLocalNetworkDenied(error):
|
|
616
|
+
failWatch(type: type, error: Self.error("Local network access denied. Enable it in Settings > Privacy > Local Network."))
|
|
617
|
+
default: break
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
internal func handleWatchResults(_ services: [BrowseService], type: String) {
|
|
622
|
+
guard let watch = watchSession else { return }
|
|
623
|
+
var present = Set<String>()
|
|
624
|
+
for service in services {
|
|
625
|
+
let normalized = normalizedBox(name: service.name, type: service.type, domain: service.domain)
|
|
626
|
+
present.insert(normalized.identityKey)
|
|
627
|
+
handleWatchFound(name: service.name, type: service.type, domain: service.domain)
|
|
628
|
+
}
|
|
629
|
+
for box in Array(watch.known.values) where box.type == type && !present.contains(box.identityKey) {
|
|
630
|
+
guard watchSession === watch else { return }
|
|
631
|
+
handleWatchLost(name: box.name, type: box.type, domain: box.domain)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private func normalizedBox(name: String, type: String, domain: String) -> ServiceBox {
|
|
636
|
+
let type = (type.hasSuffix(".") ? type : type + ".").lowercased()
|
|
637
|
+
let domain = domain.isEmpty ? "local." : (domain.hasSuffix(".") ? domain : domain + ".")
|
|
638
|
+
return ServiceBox(name: name, type: type, domain: domain)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
internal func handleWatchFound(name: String, type: String, domain: String) {
|
|
642
|
+
guard let watch = watchSession else { return }
|
|
643
|
+
let box = normalizedBox(name: name, type: type, domain: domain)
|
|
644
|
+
guard watch.known[box.identityKey] == nil else { return }
|
|
645
|
+
watch.known[box.identityKey] = box
|
|
646
|
+
resolveWatched(box, watch: watch)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private func resolveWatched(_ box: ServiceBox, watch: WatchSession) {
|
|
650
|
+
guard watchSession === watch, watch.known[box.identityKey] === box else { return }
|
|
651
|
+
watch.retries.removeValue(forKey: box.identityKey)
|
|
652
|
+
watch.attempts[box.identityKey, default: 0] += 1
|
|
653
|
+
let service = makeService(box.domain, box.type, box.name, nil)
|
|
654
|
+
service.includesPeerToPeer = true
|
|
655
|
+
service.delegate = self
|
|
656
|
+
watch.resolvers[service] = box
|
|
657
|
+
let timeout = DispatchWorkItem { [weak self, weak watch, weak service] in
|
|
658
|
+
guard let self = self, let watch = watch, let service = service, self.watchSession === watch else { return }
|
|
659
|
+
_ = self.watchResolveFailed(service)
|
|
660
|
+
}
|
|
661
|
+
watch.retries[box.identityKey] = timeout
|
|
662
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + resolveTimeoutSeconds, execute: timeout)
|
|
663
|
+
service.resolve(withTimeout: resolveTimeoutSeconds)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
internal func handleWatchLost(name: String, type: String, domain: String) {
|
|
667
|
+
guard let watch = watchSession else { return }
|
|
668
|
+
let key = normalizedBox(name: name, type: type, domain: domain).identityKey
|
|
669
|
+
guard let box = watch.known.removeValue(forKey: key) else { return }
|
|
670
|
+
watch.retries.removeValue(forKey: key)?.cancel()
|
|
671
|
+
watch.attempts.removeValue(forKey: key)
|
|
672
|
+
for (resolver, pending) in Array(watch.resolvers) where pending === box {
|
|
673
|
+
watch.resolvers.removeValue(forKey: resolver)
|
|
674
|
+
resolver.delegate = nil
|
|
675
|
+
resolver.stop()
|
|
676
|
+
}
|
|
677
|
+
if box.resolved { watchDelegate?.mdns(self, didLoseService: box.result) }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private func watchResolved(_ sender: NetService) -> Bool {
|
|
681
|
+
guard let watch = watchSession, let box = watch.resolvers[sender] else { return false }
|
|
682
|
+
readResolved(sender, into: box)
|
|
683
|
+
guard !box.hosts.isEmpty else { return true }
|
|
684
|
+
watch.resolvers.removeValue(forKey: sender)
|
|
685
|
+
watch.retries.removeValue(forKey: box.identityKey)?.cancel()
|
|
686
|
+
readResolved(sender, into: box)
|
|
687
|
+
sender.stop()
|
|
688
|
+
sender.delegate = nil
|
|
689
|
+
guard watch.known[box.identityKey] === box else { return true }
|
|
690
|
+
watchDelegate?.mdns(self, didFindService: box.result)
|
|
691
|
+
return true
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private func watchResolveFailed(_ sender: NetService) -> Bool {
|
|
695
|
+
guard let watch = watchSession, let box = watch.resolvers.removeValue(forKey: sender) else { return false }
|
|
696
|
+
watch.retries.removeValue(forKey: box.identityKey)?.cancel()
|
|
697
|
+
sender.delegate = nil
|
|
698
|
+
sender.stop()
|
|
699
|
+
let key = box.identityKey
|
|
700
|
+
if watch.attempts[key, default: 0] < 4 {
|
|
701
|
+
let retry = DispatchWorkItem { [weak self, weak watch] in
|
|
702
|
+
guard let self = self, let watch = watch else { return }
|
|
703
|
+
self.resolveWatched(box, watch: watch)
|
|
704
|
+
}
|
|
705
|
+
watch.retries[key] = retry
|
|
706
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150), execute: retry)
|
|
707
|
+
} else {
|
|
708
|
+
watch.known.removeValue(forKey: key)
|
|
709
|
+
watch.attempts.removeValue(forKey: key)
|
|
710
|
+
watchDelegate?.mdns(self, watchError: box.type, message: "Failed to resolve \(box.name)", watching: true)
|
|
711
|
+
}
|
|
712
|
+
return true
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private func readResolved(_ sender: NetService, into box: ServiceBox) {
|
|
716
|
+
box.port = Int(sender.port)
|
|
717
|
+
box.hosts = parseHosts(sender.addresses)
|
|
718
|
+
box.hostname = sender.hostName
|
|
719
|
+
if let txt = sender.txtRecordData() {
|
|
720
|
+
box.txt = NetService.dictionary(fromTXTRecord: txt).mapValues { String(data: $0, encoding: .utf8) ?? "" }
|
|
721
|
+
}
|
|
722
|
+
box.resolved = !box.hosts.isEmpty
|
|
376
723
|
}
|
|
377
724
|
}
|
|
378
725
|
|
|
@@ -386,6 +733,7 @@ private final class ServiceBox: Hashable {
|
|
|
386
733
|
|
|
387
734
|
var port: Int = 0
|
|
388
735
|
var hosts: [String] = []
|
|
736
|
+
var hostname: String?
|
|
389
737
|
var txt: [String: String] = [:]
|
|
390
738
|
var resolved: Bool = false
|
|
391
739
|
|
|
@@ -395,6 +743,13 @@ private final class ServiceBox: Hashable {
|
|
|
395
743
|
self.domain = domain
|
|
396
744
|
}
|
|
397
745
|
|
|
746
|
+
var result: [String: Any] {
|
|
747
|
+
var value: [String: Any] = ["name": name, "type": type, "domain": domain, "port": port, "hosts": hosts]
|
|
748
|
+
if let hostname = hostname { value["hostname"] = hostname }
|
|
749
|
+
if !txt.isEmpty { value["txt"] = txt }
|
|
750
|
+
return value
|
|
751
|
+
}
|
|
752
|
+
|
|
398
753
|
var identityKey: String { "\(name)|\(type)|\(domain)" }
|
|
399
754
|
|
|
400
755
|
static func == (lhs: ServiceBox, rhs: ServiceBox) -> Bool {
|
|
@@ -406,31 +761,33 @@ private final class ServiceBox: Hashable {
|
|
|
406
761
|
// MARK: - Address utilities
|
|
407
762
|
|
|
408
763
|
/// Convert `NetService.addresses` to numeric IPv4/IPv6 strings.
|
|
409
|
-
|
|
764
|
+
func parseHosts(_ addrs: [Data]?) -> [String] {
|
|
410
765
|
guard let addrs = addrs, !addrs.isEmpty else { return [] }
|
|
411
766
|
var out: [String] = []
|
|
412
767
|
out.reserveCapacity(addrs.count)
|
|
413
768
|
|
|
414
769
|
for data in addrs {
|
|
415
770
|
data.withUnsafeBytes { raw in
|
|
416
|
-
guard let base = raw.baseAddress else { return }
|
|
771
|
+
guard raw.count >= MemoryLayout<sockaddr>.size, let base = raw.baseAddress else { return }
|
|
417
772
|
let sa = base.assumingMemoryBound(to: sockaddr.self)
|
|
418
773
|
let family = Int32(sa.pointee.sa_family)
|
|
419
774
|
|
|
420
|
-
if family == AF_INET {
|
|
775
|
+
if family == AF_INET, raw.count >= MemoryLayout<sockaddr_in>.size {
|
|
421
776
|
var addr = sa.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { $0.pointee.sin_addr }
|
|
422
777
|
var buf = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN))
|
|
423
778
|
if inet_ntop(AF_INET, &addr, &buf, socklen_t(INET_ADDRSTRLEN)) != nil {
|
|
424
779
|
out.append(String(cString: buf))
|
|
425
780
|
}
|
|
426
|
-
} else if family == AF_INET6 {
|
|
427
|
-
|
|
781
|
+
} else if family == AF_INET6, raw.count >= MemoryLayout<sockaddr_in6>.size {
|
|
782
|
+
let address = sa.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { $0.pointee }
|
|
783
|
+
var addr6 = address.sin6_addr
|
|
428
784
|
var buf6 = [CChar](repeating: 0, count: Int(INET6_ADDRSTRLEN))
|
|
429
785
|
if inet_ntop(AF_INET6, &addr6, &buf6, socklen_t(INET6_ADDRSTRLEN)) != nil {
|
|
430
|
-
|
|
786
|
+
let host = String(cString: buf6)
|
|
787
|
+
out.append(address.sin6_scope_id == 0 ? host : "\(host)%\(address.sin6_scope_id)")
|
|
431
788
|
}
|
|
432
789
|
}
|
|
433
790
|
}
|
|
434
791
|
}
|
|
435
|
-
return out
|
|
792
|
+
return out.filter { !$0.contains(":") } + out.filter { $0.contains(":") }
|
|
436
793
|
}
|