diff --git a/lib/src/core/implementation/thing_discovery.dart b/lib/src/core/implementation/thing_discovery.dart index 662ef941..47e0052d 100644 --- a/lib/src/core/implementation/thing_discovery.dart +++ b/lib/src/core/implementation/thing_discovery.dart @@ -6,6 +6,7 @@ import "dart:async"; +import "package:basic_utils/basic_utils.dart"; import "package:coap/coap.dart"; import "package:collection/collection.dart"; import "package:multicast_dns/multicast_dns.dart"; @@ -222,7 +223,7 @@ class ThingDiscovery extends Stream if (dnsName.endsWith("local")) { yield* _discoverUsingMdnssd(dnsName); } else { - throw UnimplementedError("Only mDNS-SD is currently supported!"); + yield* _discoverUsingDnsSd(dnsName); } } @@ -242,6 +243,39 @@ class ThingDiscovery extends Stream ); } + Map _parseMdnsTxtRecords(String txtRecords) { + final recordsList = txtRecords + .split("\n") + .map((property) => property.split("=")) + .where((list) => list.length > 1) + .map((list) => MapEntry(list[0], list[1])); + + return Map.fromEntries(recordsList); + } + + static String _trimTxtRecord(String txtRecord) { + final startIndex = txtRecord.startsWith('"') ? 1 : 0; + + final length = txtRecord.length; + final endIndex = txtRecord.endsWith('"') ? length - 1 : length; + + return txtRecord.substring(startIndex, endIndex); + } + + Map _parseTxtRecords(List? txtRecords) { + final entries = txtRecords + ?.map((txtRecord) => txtRecord.data) + .map(_trimTxtRecord) + .map((e) => e.split("=")) + .where((element) => element.length == 2) + .map((txtRecord) { + return MapEntry(txtRecord[0], txtRecord[1]); + }) ?? + []; + + return Map.fromEntries(entries); + } + Future?> _lookupTxtRecords( MDnsClient client, String domainName, @@ -249,17 +283,81 @@ class ThingDiscovery extends Stream final txtRecords = await client .lookup(ResourceRecordQuery.text(domainName)) .toList(); - final recordsList = txtRecords.firstOrNull?.text - .split("\n") - .map((property) => property.split("=")) - .where((list) => list.length > 1) - .map((list) => MapEntry(list[0], list[1])); - if (recordsList == null) { + final firstTxtRecord = txtRecords.firstOrNull?.text; + + if (firstTxtRecord == null) { return null; } - return Map.fromEntries(recordsList); + return _parseMdnsTxtRecords(firstTxtRecord); + } + + Stream _discoverUsingDnsSd(String name) async* { + // TODO: Refactor + final ptrRecords = await DnsUtils.lookupRecord(name, RRecordType.PTR); + final defaultScheme = _isUdpDiscovery(name) ? "coap" : "http"; + final discoveredUris = {}; + const defaultType = "Thing"; + + for (final ptrRecord in ptrRecords ?? []) { + final srvRecords = await DnsUtils.lookupRecord( + ptrRecord.name, + RRecordType.SRV, + provider: DnsApiProvider.CLOUDFLARE, + ); + + for (final srvRecord in srvRecords ?? []) { + final serviceName = srvRecord.name; + final srvRecordEntries = srvRecord.data.split(" "); + + final validSrvRecord = srvRecordEntries.length == 4; + + if (!validSrvRecord) { + continue; + } + + final target = srvRecordEntries.last; + final port = + int.tryParse(srvRecordEntries[srvRecordEntries.length - 2]); + + if (port == null) { + continue; + } + + final txtRecords = await DnsUtils.lookupRecord( + serviceName, + RRecordType.TXT, + provider: DnsApiProvider.CLOUDFLARE, + ) ?? + []; + + final parsedTxtRecords = _parseTxtRecords(txtRecords); + + final uri = Uri( + host: target, + port: port, + path: parsedTxtRecords["td"], + scheme: parsedTxtRecords["scheme"] ?? defaultScheme, + ); + + final duplicate = !discoveredUris.add(uri); + + if (duplicate) { + continue; + } + + final type = parsedTxtRecords["type"] ?? defaultType; + + switch (type) { + case "Thing": + yield* _discoverDirectly(uri); + case "Directory": + // TODO(JKRhb): Implement directory discovery. + break; + } + } + } } Stream _discoverUsingMdnssd(String name) async* { diff --git a/pubspec.yaml b/pubspec.yaml index 63f47116..e3612325 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dev_dependencies: test: ^1.24.3 dependencies: + basic_utils: ^5.6.1 cbor: ^6.1.0 coap: ^9.0.0 collection: ^1.17.2