@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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
package com.devioarts.capacitor.mdns
|
|
2
|
+
// English-only code and comments.
|
|
3
|
+
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.net.nsd.NsdManager
|
|
6
|
+
import android.net.nsd.NsdServiceInfo
|
|
7
|
+
import android.net.wifi.WifiManager
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.os.Handler
|
|
10
|
+
import android.os.Looper
|
|
11
|
+
import android.os.ext.SdkExtensions
|
|
12
|
+
import java.util.concurrent.Executor
|
|
13
|
+
|
|
14
|
+
/** Production [MainThread] backed by the application's main looper. */
|
|
15
|
+
internal class AndroidMainThread : MainThread {
|
|
16
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
17
|
+
override fun isCurrent(): Boolean = Looper.myLooper() === Looper.getMainLooper()
|
|
18
|
+
override fun post(block: () -> Unit) {
|
|
19
|
+
handler.post { block() }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Production [NsdBackend] on top of Android's `NsdManager`.
|
|
25
|
+
*
|
|
26
|
+
* Callbacks of the legacy overloads are delivered on NsdManager's internal thread, so this class
|
|
27
|
+
* does not touch any shared state from them: it only translates and forwards.
|
|
28
|
+
*/
|
|
29
|
+
internal class AndroidNsdBackend(context: Context) : NsdBackend {
|
|
30
|
+
|
|
31
|
+
private val nsd: NsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
|
32
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
33
|
+
private val mainExecutor = Executor { command -> mainHandler.post(command) }
|
|
34
|
+
private val multicastLock by lazy {
|
|
35
|
+
(context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager)
|
|
36
|
+
?.createMulticastLock("capacitor-mdns")
|
|
37
|
+
}
|
|
38
|
+
private val needsMulticastLock = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
|
39
|
+
SdkExtensions.getExtensionVersion(Build.VERSION_CODES.TIRAMISU) < 7
|
|
40
|
+
|
|
41
|
+
override fun acquireMulticastLock() {
|
|
42
|
+
if (needsMulticastLock) multicastLock?.acquire()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun releaseMulticastLock() {
|
|
46
|
+
if (needsMulticastLock) multicastLock?.release()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override val supportsConcurrentResolve: Boolean
|
|
50
|
+
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
|
51
|
+
|
|
52
|
+
override fun registerService(
|
|
53
|
+
type: String,
|
|
54
|
+
name: String,
|
|
55
|
+
port: Int,
|
|
56
|
+
txt: Map<String, String>,
|
|
57
|
+
listener: NsdBackend.RegistrationListener
|
|
58
|
+
): Any {
|
|
59
|
+
val info = NsdServiceInfo().apply {
|
|
60
|
+
serviceType = type
|
|
61
|
+
serviceName = name
|
|
62
|
+
setPort(port)
|
|
63
|
+
txt.forEach { (key, value) -> setAttribute(key, value) }
|
|
64
|
+
}
|
|
65
|
+
val nsdListener = object : NsdManager.RegistrationListener {
|
|
66
|
+
override fun onServiceRegistered(nsi: NsdServiceInfo) = listener.onRegistered(nsi.serviceName)
|
|
67
|
+
override fun onRegistrationFailed(nsi: NsdServiceInfo, errorCode: Int) = listener.onFailed(errorCode)
|
|
68
|
+
override fun onServiceUnregistered(nsi: NsdServiceInfo) = listener.onUnregistered()
|
|
69
|
+
override fun onUnregistrationFailed(nsi: NsdServiceInfo, errorCode: Int) = listener.onUnregistrationFailed(errorCode)
|
|
70
|
+
}
|
|
71
|
+
nsd.registerService(info, NsdManager.PROTOCOL_DNS_SD, nsdListener)
|
|
72
|
+
return nsdListener
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override fun unregisterService(token: Any) {
|
|
76
|
+
nsd.unregisterService(token as NsdManager.RegistrationListener)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override fun startDiscovery(type: String, listener: NsdBackend.DiscoveryListener): Any {
|
|
80
|
+
val nsdListener = object : NsdManager.DiscoveryListener {
|
|
81
|
+
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) = listener.onStartFailed(errorCode)
|
|
82
|
+
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) = listener.onStopFailed(errorCode)
|
|
83
|
+
override fun onDiscoveryStarted(serviceType: String) { /* no-op */ }
|
|
84
|
+
override fun onDiscoveryStopped(serviceType: String) = listener.onStopped()
|
|
85
|
+
override fun onServiceFound(si: NsdServiceInfo) =
|
|
86
|
+
listener.onFound(DiscoveredService(si.serviceName, si.serviceType, si))
|
|
87
|
+
override fun onServiceLost(si: NsdServiceInfo) =
|
|
88
|
+
listener.onLost(DiscoveredService(si.serviceName, si.serviceType, si))
|
|
89
|
+
}
|
|
90
|
+
nsd.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, nsdListener)
|
|
91
|
+
return nsdListener
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override fun stopDiscovery(token: Any) {
|
|
95
|
+
nsd.stopServiceDiscovery(token as NsdManager.DiscoveryListener)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Suppress("DEPRECATION")
|
|
99
|
+
override fun resolve(service: DiscoveredService, listener: NsdBackend.ResolveListener): Any? {
|
|
100
|
+
val si = service.handle as NsdServiceInfo
|
|
101
|
+
|
|
102
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
103
|
+
val callback = object : NsdManager.ServiceInfoCallback {
|
|
104
|
+
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) = listener.onFailed(errorCode)
|
|
105
|
+
override fun onServiceInfoCallbackUnregistered() { /* no-op */ }
|
|
106
|
+
override fun onServiceLost() { /* no-op */ }
|
|
107
|
+
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) =
|
|
108
|
+
listener.onResolved(toMdnsService(serviceInfo))
|
|
109
|
+
}
|
|
110
|
+
nsd.registerServiceInfoCallback(si, mainExecutor, callback)
|
|
111
|
+
return callback
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
nsd.resolveService(si, object : NsdManager.ResolveListener {
|
|
115
|
+
override fun onResolveFailed(s: NsdServiceInfo, errorCode: Int) = listener.onFailed(errorCode)
|
|
116
|
+
override fun onServiceResolved(s: NsdServiceInfo) = listener.onResolved(toMdnsService(s))
|
|
117
|
+
})
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
override fun cancelResolve(token: Any) {
|
|
122
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
|
|
123
|
+
token is NsdManager.ServiceInfoCallback
|
|
124
|
+
) {
|
|
125
|
+
nsd.unregisterServiceInfoCallback(token)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private fun toMdnsService(s: NsdServiceInfo): mDNS.MdnsService =
|
|
130
|
+
mDNS.MdnsService(
|
|
131
|
+
name = s.serviceName,
|
|
132
|
+
type = s.serviceType,
|
|
133
|
+
hosts = hostAddresses(s),
|
|
134
|
+
port = s.port,
|
|
135
|
+
txt = s.attributes.mapValues { (_, value) -> value?.let { String(it, Charsets.UTF_8) } ?: "" }
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
private fun hostAddresses(s: NsdServiceInfo): List<String> =
|
|
139
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
140
|
+
s.hostAddresses.mapNotNull { it.hostAddress }
|
|
141
|
+
} else {
|
|
142
|
+
@Suppress("DEPRECATION")
|
|
143
|
+
s.host?.hostAddress?.let { listOf(it) } ?: emptyList()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.devioarts.capacitor.mdns
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
|
|
5
|
+
/** Parses the raw values supplied by the Capacitor JSON bridge. */
|
|
6
|
+
internal object MdnsOptions {
|
|
7
|
+
fun string(raw: Any?, key: String): String? {
|
|
8
|
+
require(raw == null || raw === JSONObject.NULL || raw is String) { "Invalid $key" }
|
|
9
|
+
return raw as? String
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fun port(raw: Any?): Int {
|
|
13
|
+
require(raw is Number && raw.toDouble() in 1.0..65535.0 && raw.toDouble() % 1.0 == 0.0) { "Missing/invalid port" }
|
|
14
|
+
return raw.toInt()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fun txt(raw: Any?): Map<String, String> {
|
|
18
|
+
if (raw == null || raw === JSONObject.NULL) return emptyMap()
|
|
19
|
+
require(raw is JSONObject) { "TXT must be an object of strings" }
|
|
20
|
+
return raw.keys().asSequence().associateWith { key ->
|
|
21
|
+
val value = raw.get(key)
|
|
22
|
+
require(value is String) { "TXT values must be strings" }
|
|
23
|
+
value
|
|
24
|
+
}.also { MdnsValidation.txt(it) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun timeout(raw: Any?): Int {
|
|
28
|
+
if (raw == null || raw === JSONObject.NULL) return 3000
|
|
29
|
+
require(raw is Number && raw.toDouble().isFinite()) { "Invalid timeout" }
|
|
30
|
+
return raw.toDouble().coerceIn(0.0, Int.MAX_VALUE.toDouble()).toInt()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.devioarts.capacitor.mdns
|
|
2
|
+
|
|
3
|
+
internal object MdnsValidation {
|
|
4
|
+
fun type(raw: String?): String {
|
|
5
|
+
val type = raw?.trim()?.takeIf { it.isNotEmpty() }?.removeSuffix(".")?.lowercase() ?: "_http._tcp"
|
|
6
|
+
val match = Regex("^_([a-z0-9](?:[a-z0-9-]{0,13}[a-z0-9])?)\\._(tcp|udp)$").matchEntire(type)
|
|
7
|
+
require(match != null && match.groupValues[1].any { it in 'a'..'z' } && !match.groupValues[1].contains("--")) { "Invalid service type: $raw" }
|
|
8
|
+
return "$type."
|
|
9
|
+
}
|
|
10
|
+
fun name(raw: String?): String {
|
|
11
|
+
val name = raw?.trim().orEmpty()
|
|
12
|
+
require(name.toByteArray(Charsets.UTF_8).size <= 63 && raw.orEmpty().none { it < ' ' || it == '\u007f' }) { "Invalid instance name (maximum 63 UTF-8 bytes, no control characters)" }
|
|
13
|
+
return name
|
|
14
|
+
}
|
|
15
|
+
fun txt(txt: Map<String, String>) {
|
|
16
|
+
val keys = mutableSetOf<String>()
|
|
17
|
+
var totalBytes = 0
|
|
18
|
+
txt.forEach { (key, value) ->
|
|
19
|
+
require(key.isNotEmpty() && key.all { it in ' '..'~' && it != '=' } && keys.add(key.lowercase())) { "Invalid or duplicate TXT key" }
|
|
20
|
+
val bytes = "$key=$value".toByteArray(Charsets.UTF_8).size
|
|
21
|
+
totalBytes += bytes + 1
|
|
22
|
+
require(totalBytes <= 1300) { "TXT record must fit in 1300 bytes including length prefixes" }
|
|
23
|
+
require(bytes <= 255) { "TXT entries must be at most 255 UTF-8 bytes including key=" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package com.devioarts.capacitor.mdns
|
|
2
|
+
// English-only code and comments.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thin seam over `NsdManager` so the discovery/registration logic can be unit-tested on the JVM.
|
|
6
|
+
*
|
|
7
|
+
* Contract:
|
|
8
|
+
* - Listener callbacks MAY be invoked from any thread (the real NSD service uses its own thread for
|
|
9
|
+
* the legacy overloads). The consumer ([mDNS]) is responsible for hopping to its main thread.
|
|
10
|
+
* - Methods may throw; registration/stop failures must reach the caller.
|
|
11
|
+
* - Returned tokens are opaque and only meaningful when handed back to the same backend.
|
|
12
|
+
*/
|
|
13
|
+
internal interface NsdBackend {
|
|
14
|
+
|
|
15
|
+
/** True when several resolves can be in flight at once (API 34+ `ServiceInfoCallback`). */
|
|
16
|
+
val supportsConcurrentResolve: Boolean
|
|
17
|
+
|
|
18
|
+
fun acquireMulticastLock()
|
|
19
|
+
fun releaseMulticastLock()
|
|
20
|
+
|
|
21
|
+
fun registerService(type: String, name: String, port: Int, txt: Map<String, String>, listener: RegistrationListener): Any
|
|
22
|
+
fun unregisterService(token: Any)
|
|
23
|
+
|
|
24
|
+
fun startDiscovery(type: String, listener: DiscoveryListener): Any
|
|
25
|
+
fun stopDiscovery(token: Any)
|
|
26
|
+
|
|
27
|
+
/** Returns a token to hand to [cancelResolve], or null when there is nothing to cancel. */
|
|
28
|
+
fun resolve(service: DiscoveredService, listener: ResolveListener): Any?
|
|
29
|
+
fun cancelResolve(token: Any)
|
|
30
|
+
|
|
31
|
+
interface RegistrationListener {
|
|
32
|
+
fun onRegistered(publishedName: String)
|
|
33
|
+
fun onFailed(errorCode: Int)
|
|
34
|
+
fun onUnregistered()
|
|
35
|
+
fun onUnregistrationFailed(errorCode: Int)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface DiscoveryListener {
|
|
39
|
+
fun onStartFailed(errorCode: Int)
|
|
40
|
+
fun onFound(service: DiscoveredService)
|
|
41
|
+
fun onLost(service: DiscoveredService) {}
|
|
42
|
+
fun onStopFailed(errorCode: Int) {}
|
|
43
|
+
fun onStopped() {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ResolveListener {
|
|
47
|
+
fun onResolved(service: mDNS.MdnsService)
|
|
48
|
+
fun onFailed(errorCode: Int)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A service reported by discovery but not resolved yet. [handle] is backend-specific. */
|
|
53
|
+
internal class DiscoveredService(val name: String, val type: String, val handle: Any? = null)
|
|
54
|
+
|
|
55
|
+
/** Abstraction over "the main thread" so tests can substitute their own single thread. */
|
|
56
|
+
internal interface MainThread {
|
|
57
|
+
fun isCurrent(): Boolean
|
|
58
|
+
fun post(block: () -> Unit)
|
|
59
|
+
}
|