Skip to content

Commit 6636613

Browse files
issue #85: added native IP resolution for Android, macOS and iOS
- added native macOS IP addresses (inspired by #83) - extended example app to display multiple addresses (taken from #84) - extended Android side to use new getHostAddresses instead of deprecate host (since API 34) - Android side now always returns multiple addresses - IpLookupType functionality internally uses natively supplied addresses (if available) - added filtering of addresses for IpLookupType to maintain exisiting behavior
1 parent af3209e commit 6636613

File tree

6 files changed

+251
-30
lines changed

6 files changed

+251
-30
lines changed

nsd/example/lib/main.dart

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22
import 'dart:convert';
3+
import 'dart:io';
34
import 'dart:typed_data';
45

56
import 'package:flutter/material.dart';
@@ -178,22 +179,24 @@ class DiscoveryState extends State<DiscoveryWidget> {
178179

179180
Widget buildDataTable(
180181
BuildContext context, Discovery discovery, Widget? child) {
181-
return DataTable(
182-
headingRowHeight: 24,
183-
dataRowMinHeight: 24,
184-
dataRowMaxHeight: 24,
185-
dataTextStyle: const TextStyle(color: Colors.black, fontSize: 12),
186-
columnSpacing: 8,
187-
horizontalMargin: 0,
188-
headingTextStyle: const TextStyle(
189-
color: Colors.black, fontSize: 12, fontWeight: FontWeight.w600),
190-
columns: <DataColumn>[
191-
buildDataColumn('Name'),
192-
buildDataColumn('Type'),
193-
buildDataColumn('Host'),
194-
buildDataColumn('Port'),
195-
],
196-
rows: buildDataRows(discovery),
182+
return SingleChildScrollView(
183+
scrollDirection: Axis.horizontal,
184+
child: DataTable(
185+
dataRowMinHeight: 24,
186+
dataTextStyle: const TextStyle(color: Colors.black, fontSize: 12),
187+
columnSpacing: 8,
188+
horizontalMargin: 0,
189+
headingTextStyle: const TextStyle(
190+
color: Colors.black, fontSize: 12, fontWeight: FontWeight.w600),
191+
columns: <DataColumn>[
192+
buildDataColumn('Name'),
193+
buildDataColumn('Type'),
194+
buildDataColumn('Host'),
195+
buildDataColumn('Port'),
196+
buildDataColumn('Addresses')
197+
],
198+
rows: buildDataRows(discovery),
199+
),
197200
);
198201
}
199202

@@ -212,7 +215,10 @@ class DiscoveryState extends State<DiscoveryWidget> {
212215
DataCell(Text(e.name ?? 'unknown')),
213216
DataCell(Text(e.type ?? 'unknown')),
214217
DataCell(Text(e.host ?? 'unknown')),
215-
DataCell(Text(e.port != null ? '${e.port}' : 'unknown'))
218+
DataCell(Text(e.port != null ? '${e.port}' : 'unknown')),
219+
DataCell(Text(e.addresses != null
220+
? renderAddresses(e.addresses)
221+
: 'unknown')),
216222
],
217223
))
218224
.toList();
@@ -265,3 +271,11 @@ Map<String, Uint8List?> createTxt() {
265271
'a-null': null,
266272
};
267273
}
274+
275+
String renderAddresses(List<InternetAddress>? addresses) {
276+
if (addresses == null || addresses.isEmpty) {
277+
return '';
278+
}
279+
280+
return addresses.map((a) => a.address).join('\n');
281+
}

nsd_android/android/src/main/kotlin/com/haberey/flutter/nsd_android/Serialization.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.haberey.flutter.nsd_android
22

33
import android.net.nsd.NsdServiceInfo
4+
import android.os.Build
5+
import java.net.Inet4Address
6+
import java.net.Inet6Address
47
import java.net.InetAddress
58
import java.nio.ByteBuffer
69
import java.nio.charset.CharsetDecoder
@@ -107,16 +110,38 @@ private fun assertValidUtf8(key: String, value: ByteArray) {
107110
}
108111

109112
internal fun serializeServiceInfo(nsdServiceInfo: NsdServiceInfo): Map<String, Any?> {
113+
val inetAddresses = when {
114+
Build.VERSION.SDK_INT >= 34 -> nsdServiceInfo.hostAddresses
115+
else -> listOfNotNull(nsdServiceInfo.host)
116+
}
117+
118+
val addressEntries = inetAddresses.mapNotNull { getAddressEntry(it) }
119+
110120
return mapOf(
111121
Key.SERVICE_NAME.serializeKey to nsdServiceInfo.serviceName,
112122
Key.SERVICE_TYPE.serializeKey to removeLeadingAndTrailingDots(serviceType = nsdServiceInfo.serviceType),
113123
Key.SERVICE_HOST.serializeKey to nsdServiceInfo.host?.canonicalHostName,
114-
Key.SERVICE_ADDRESSES.serializeKey to nsdServiceInfo.host?.hostAddress,
124+
Key.SERVICE_ADDRESSES.serializeKey to addressEntries,
115125
Key.SERVICE_PORT.serializeKey to if (nsdServiceInfo.port == 0) null else nsdServiceInfo.port,
116126
Key.SERVICE_TXT.serializeKey to nsdServiceInfo.attributes,
117127
)
118128
}
119129

130+
private fun getAddressEntry(addr: InetAddress): Map<String, Any?>? {
131+
val addressStr = addr.hostAddress ?: return null
132+
133+
val typeStr = when (addr) {
134+
is Inet4Address -> "ipv4"
135+
is Inet6Address -> "ipv6"
136+
else -> null
137+
}
138+
139+
return mapOf(
140+
"address" to addressStr,
141+
"type" to typeStr,
142+
)
143+
}
144+
120145
// In the specification http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt 4.1.2 / 7.
121146
// it looks like leading and trailing dots do not belong to the <Service> portion but separate it
122147
// from the surrounding <Name> and <Domain> portions. These dots are removed here to allow

nsd_ios/ios/Classes/Serialization.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ func serializeService(_ netService: NetService) -> [String: Any?] {
3131
}
3232
}
3333

34+
if let addressDataList = netService.addresses {
35+
36+
let addressEntries = addressDataList.compactMap { getAddressEntry(from: $0) }
37+
38+
if !addressEntries.isEmpty {
39+
service["service.addresses"] = addressEntries
40+
}
41+
}
42+
3443
return service
3544
}
3645

@@ -121,3 +130,55 @@ func serializeErrorCause(_ value: ErrorCause) -> [String: Any?] {
121130
func serializeErrorMessage(_ value: String) -> [String: Any?] {
122131
["error.message": value]
123132
}
133+
134+
func getAddressEntry(from addressData: Data) -> [String: Any]? {
135+
136+
// temporary access to raw bytes in data
137+
addressData.withUnsafeBytes { rawBuffer in
138+
139+
// rawBuffer.baseAddress can be nil if data is empty
140+
guard let baseAddress = rawBuffer.baseAddress else {
141+
return nil
142+
}
143+
144+
// get sockaddr pointer from baseAddress
145+
let sockaddrPtr = baseAddress.assumingMemoryBound(to: sockaddr.self)
146+
147+
// prepare C-style character buffer
148+
var addressData = [CChar](repeating: 0, count: Int(NI_MAXHOST))
149+
150+
// length of sockaddr struct
151+
let length = socklen_t(addressData.count)
152+
153+
// get human-readable strings from sockaddr
154+
guard getnameinfo(
155+
sockaddrPtr,
156+
length,
157+
&addressData,
158+
socklen_t(addressData.count),
159+
nil,
160+
0,
161+
NI_NUMERICHOST // request numeric address e.g. "192.168.1.10" or "fe80::1234"
162+
) == 0 else {
163+
return nil
164+
}
165+
166+
// convert C-string buffer into Swift string
167+
let address = String(cString: addressData)
168+
169+
switch Int32(sockaddrPtr.pointee.sa_family) {
170+
case AF_INET:
171+
return [
172+
"address": address,
173+
"type": "ipv4"
174+
]
175+
case AF_INET6:
176+
return [
177+
"address": address,
178+
"type": "ipv6"
179+
]
180+
default:
181+
return nil
182+
}
183+
}
184+
}

nsd_macos/macos/Classes/Serialization.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ func serializeService(_ netService: NetService) -> [String: Any?] {
3131
}
3232
}
3333

34+
if let addressDataList = netService.addresses {
35+
36+
let addressEntries = addressDataList.compactMap { getAddressEntry(from: $0) }
37+
38+
if !addressEntries.isEmpty {
39+
service["service.addresses"] = addressEntries
40+
}
41+
}
42+
3443
return service
3544
}
3645

@@ -121,3 +130,55 @@ func serializeErrorCause(_ value: ErrorCause) -> [String: Any?] {
121130
func serializeErrorMessage(_ value: String) -> [String: Any?] {
122131
["error.message": value]
123132
}
133+
134+
func getAddressEntry(from addressData: Data) -> [String: Any]? {
135+
136+
// temporary access to raw bytes in data
137+
addressData.withUnsafeBytes { rawBuffer in
138+
139+
// rawBuffer.baseAddress can be nil if data is empty
140+
guard let baseAddress = rawBuffer.baseAddress else {
141+
return nil
142+
}
143+
144+
// get sockaddr pointer from baseAddress
145+
let sockaddrPtr = baseAddress.assumingMemoryBound(to: sockaddr.self)
146+
147+
// prepare C-style character buffer
148+
var addressData = [CChar](repeating: 0, count: Int(NI_MAXHOST))
149+
150+
// length of sockaddr struct
151+
let length = socklen_t(addressData.count)
152+
153+
// get human-readable strings from sockaddr
154+
guard getnameinfo(
155+
sockaddrPtr,
156+
length,
157+
&addressData,
158+
socklen_t(addressData.count),
159+
nil,
160+
0,
161+
NI_NUMERICHOST // request numeric address e.g. "192.168.1.10" or "fe80::1234"
162+
) == 0 else {
163+
return nil
164+
}
165+
166+
// convert C-string buffer into Swift string
167+
let address = String(cString: addressData)
168+
169+
switch Int32(sockaddrPtr.pointee.sa_family) {
170+
case AF_INET:
171+
return [
172+
"address": address,
173+
"type": "ipv4"
174+
]
175+
case AF_INET6:
176+
return [
177+
"address": address,
178+
"type": "ipv6"
179+
]
180+
default:
181+
return nil
182+
}
183+
}
184+
}

nsd_platform_interface/lib/src/method_channel_nsd_platform.dart

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ class MethodChannelNsdPlatform extends NsdPlatformInterface {
6161
if (autoResolve) {
6262
service = await resolve(service);
6363

64-
if (isIpLookupEnabled(ipLookupType) && service.addresses == null) {
65-
service = await performIpLookup(service, ipLookupType);
66-
}
64+
// TODO remove this and deprecate IpLookupType once Windows supports native addresses too
65+
service = await performIpLookup(service, ipLookupType);
6766
}
6867
discovery.add(service);
6968
});
@@ -77,6 +76,29 @@ class MethodChannelNsdPlatform extends NsdPlatformInterface {
7776
}).then((value) => completer.future);
7877
}
7978

79+
Future<Service> performIpLookup(
80+
Service service, IpLookupType ipLookupType) async {
81+
final internetAddressType = getInternetAddressType(ipLookupType);
82+
if (internetAddressType == null) {
83+
return service; // lookup not enabled -> leave service as is
84+
}
85+
86+
var addresses = service.addresses;
87+
if (addresses != null && internetAddressType != InternetAddressType.any) {
88+
// platform already supplied addresses -> no lookup, but respect user's choice of address type
89+
addresses =
90+
addresses.where((a) => a.type == internetAddressType).toList();
91+
} else {
92+
final host = service.host;
93+
if (host == null) {
94+
return service; // cannot look up addresses -> leave service as is
95+
}
96+
addresses = await InternetAddress.lookup(host, type: internetAddressType);
97+
}
98+
99+
return merge(service, Service(addresses: addresses));
100+
}
101+
80102
@override
81103
Future<void> stopDiscovery(Discovery discovery) async {
82104
final completer = Completer<void>();
@@ -230,15 +252,15 @@ class MethodChannelNsdPlatform extends NsdPlatformInterface {
230252
}
231253

232254
Future<Service> performIpLookup(
233-
Service service, IpLookupType ipLookupType) async {
255+
Service service, InternetAddressType internetAddressType) async {
234256
final host = service.host;
235257

236258
if (host == null) {
237259
return service;
238260
}
239261

240-
final addresses = await InternetAddress.lookup(host,
241-
type: getInternetAddressType(ipLookupType)!);
262+
final addresses =
263+
await InternetAddress.lookup(host, type: internetAddressType);
242264
return merge(service, Service(addresses: addresses));
243265
}
244266

0 commit comments

Comments
 (0)