@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/DevioartsCapacitorMdns.podspec +3 -3
  3. package/Package.swift +1 -1
  4. package/README.md +221 -37
  5. package/android/build.gradle +3 -9
  6. package/android/src/main/java/com/devioarts/capacitor/mdns/AndroidNsdBackend.kt +145 -0
  7. package/android/src/main/java/com/devioarts/capacitor/mdns/MdnsOptions.kt +32 -0
  8. package/android/src/main/java/com/devioarts/capacitor/mdns/MdnsValidation.kt +26 -0
  9. package/android/src/main/java/com/devioarts/capacitor/mdns/NsdBackend.kt +59 -0
  10. package/android/src/main/java/com/devioarts/capacitor/mdns/mDNS.kt +467 -243
  11. package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +80 -57
  12. package/dist/docs.json +250 -14
  13. package/dist/esm/definitions.d.ts +64 -17
  14. package/dist/esm/definitions.js.map +1 -1
  15. package/dist/esm/index.js +22 -2
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/web.d.ts +6 -1
  18. package/dist/esm/web.js +23 -0
  19. package/dist/esm/web.js.map +1 -1
  20. package/dist/plugin.cjs.js +45 -2
  21. package/dist/plugin.cjs.js.map +1 -1
  22. package/dist/plugin.js +45 -2
  23. package/dist/plugin.js.map +1 -1
  24. package/docs/electron.md +29 -3
  25. package/docs/platform-setup.md +6 -0
  26. package/docs/playground.md +33 -2
  27. package/docs/runtime-behavior.md +68 -2
  28. package/electron/dist/plugin-settings.js +10 -3
  29. package/electron/dist/plugin-settings.js.map +1 -1
  30. package/electron/dist/plugin.cjs.js +519 -89
  31. package/electron/dist/plugin.cjs.js.map +1 -1
  32. package/electron/mdns-bridge.cjs +25 -2
  33. package/electron/mdns.ts +514 -81
  34. package/electron/rollup.config.mjs +1 -1
  35. package/electron/src/index.ts +1 -0
  36. package/electron/src/plugin-settings.ts +10 -3
  37. package/electron/types-public/index.d.ts +25 -3
  38. package/ios/Sources/mDNSPlugin/mDNS.swift +407 -117
  39. package/ios/Sources/mDNSPlugin/mDNSPlugin.swift +77 -24
  40. package/ios/Tests/mDNSPluginTests/BridgeTests.swift +46 -0
  41. package/ios/Tests/mDNSPluginTests/MDNSTests.swift +490 -0
  42. package/package.json +11 -4
  43. package/ios/Tests/mDNSPluginTests/mDNSPluginTests.swift +0 -9
@@ -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
+ }