From 47bc4685b05d18861cbb54f05b549ca81b90cc79 Mon Sep 17 00:00:00 2001 From: Nick Vance Date: Wed, 17 Sep 2025 17:51:01 -0700 Subject: [PATCH 1/3] adding multicast_dns_source_ref file --- multicast_dns_source_ref/AUTHORS | 7 + multicast_dns_source_ref/CHANGELOG.md | 80 + multicast_dns_source_ref/LICENSE | 25 + multicast_dns_source_ref/README.md | 13 + multicast_dns_source_ref/dart_test.yaml | 1 + multicast_dns_source_ref/example/main.dart | 43 + .../example/mdns_resolve.dart | 36 + multicast_dns_source_ref/example/mdns_sd.dart | 71 + .../lib/multicast_dns.dart | 272 +++ .../lib/src/constants.dart | 37 + .../lib/src/lookup_resolver.dart | 99 + .../lib/src/native_protocol_client.dart | 83 + multicast_dns_source_ref/lib/src/packet.dart | 396 ++++ .../lib/src/resource_record.dart | 346 ++++ multicast_dns_source_ref/pubspec.yaml | 19 + .../test/client_test.dart | 295 +++ .../test/decode_test.dart | 1648 +++++++++++++++++ .../test/lookup_resolver_test.dart | 132 ++ .../test/resource_record_cache_test.dart | 85 + multicast_dns_source_ref/tool/packet_gen.dart | 72 + 20 files changed, 3760 insertions(+) create mode 100644 multicast_dns_source_ref/AUTHORS create mode 100644 multicast_dns_source_ref/CHANGELOG.md create mode 100644 multicast_dns_source_ref/LICENSE create mode 100644 multicast_dns_source_ref/README.md create mode 100644 multicast_dns_source_ref/dart_test.yaml create mode 100644 multicast_dns_source_ref/example/main.dart create mode 100644 multicast_dns_source_ref/example/mdns_resolve.dart create mode 100644 multicast_dns_source_ref/example/mdns_sd.dart create mode 100644 multicast_dns_source_ref/lib/multicast_dns.dart create mode 100644 multicast_dns_source_ref/lib/src/constants.dart create mode 100644 multicast_dns_source_ref/lib/src/lookup_resolver.dart create mode 100644 multicast_dns_source_ref/lib/src/native_protocol_client.dart create mode 100644 multicast_dns_source_ref/lib/src/packet.dart create mode 100644 multicast_dns_source_ref/lib/src/resource_record.dart create mode 100644 multicast_dns_source_ref/pubspec.yaml create mode 100644 multicast_dns_source_ref/test/client_test.dart create mode 100644 multicast_dns_source_ref/test/decode_test.dart create mode 100644 multicast_dns_source_ref/test/lookup_resolver_test.dart create mode 100644 multicast_dns_source_ref/test/resource_record_cache_test.dart create mode 100644 multicast_dns_source_ref/tool/packet_gen.dart diff --git a/multicast_dns_source_ref/AUTHORS b/multicast_dns_source_ref/AUTHORS new file mode 100644 index 00000000..add10412 --- /dev/null +++ b/multicast_dns_source_ref/AUTHORS @@ -0,0 +1,7 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Dartino project authors diff --git a/multicast_dns_source_ref/CHANGELOG.md b/multicast_dns_source_ref/CHANGELOG.md new file mode 100644 index 00000000..6c4cbfa7 --- /dev/null +++ b/multicast_dns_source_ref/CHANGELOG.md @@ -0,0 +1,80 @@ +## NEXT + +* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. + +## 0.3.3 + +* Adds an optional error callback for `MDnsClient::start` to prevent uncaught exceptions. + +## 0.3.2+8 + +* Fixes stack overflows ocurring during the parsing of domain names in MDNS messages. +* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. + +## 0.3.2+7 + +* Optimized Socket Binding: Always bind to 0.0.0.0 for simplicity and efficiency. +* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2. + +## 0.3.2+6 + +* Improves links in README.md. + +## 0.3.2+5 + +* Updates `PendingRequest` to be a `base class` for Dart 3.0 compatibility. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + +## 0.3.2+4 + +* Adds pub topics to package metadata. +* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19. + +## 0.3.2+3 + +* Removes use of `runtimeType.toString()`. +* Updates minimum SDK version to Flutter 3.0. + +## 0.3.2+2 + +* Fixes lints warnings. + +## 0.3.2+1 + +* Migrates from `ui.hash*` to `Object.hash*`. + +## 0.3.2 + +* Updates package description. +* Make [MDnsClient.start] idempotent. + +## 0.3.1 + +* Close IPv6 sockets on [MDnsClient.stop]. + +## 0.3.0+1 + +* Removed redundant link in README.md file. + +## 0.3.0 + +* Migrate package to null safety. + +## 0.2.2 +* Fixes parsing of TXT records. Continues parsing on non-utf8 strings. + +## 0.2.1 +* Fixes the handling of packets containing non-utf8 strings. + +## 0.2.0 +* Allow configuration of the port and address the mdns query is performed on. + +## 0.1.1 + +* Fixes [flutter/issue/31854](https://github.com/flutter/flutter/issues/31854) where `decodeMDnsResponse` advanced to incorrect code points and ignored some records. + +## 0.1.0 + +* Initial Open Source release. +* Migrates the dartino-sdk's mDNS client to Dart 2.0 and Flutter's analysis rules +* Breaks from original Dartino code, as it does not use native libraries for macOS and overhauls the `ResourceRecord` class. diff --git a/multicast_dns_source_ref/LICENSE b/multicast_dns_source_ref/LICENSE new file mode 100644 index 00000000..c6823b81 --- /dev/null +++ b/multicast_dns_source_ref/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/multicast_dns_source_ref/README.md b/multicast_dns_source_ref/README.md new file mode 100644 index 00000000..f30ce169 --- /dev/null +++ b/multicast_dns_source_ref/README.md @@ -0,0 +1,13 @@ +# Multicast DNS package + +Based on [RFC 6762 Multicast DNS](https://datatracker.ietf.org/doc/html/rfc6762). + +[![pub package](https://img.shields.io/pub/v/multicast_dns.svg)]( +https://pub.dartlang.org/packages/multicast_dns) + +A Dart package to do service discovery over multicast DNS (mDNS), Bonjour, and Avahi. + +## Usage + +[The example](https://pub.dev/packages/multicast_dns/example) demonstrates how +to use the `MDnsClient` Dart class in your code. diff --git a/multicast_dns_source_ref/dart_test.yaml b/multicast_dns_source_ref/dart_test.yaml new file mode 100644 index 00000000..91ec220b --- /dev/null +++ b/multicast_dns_source_ref/dart_test.yaml @@ -0,0 +1 @@ +test_on: vm diff --git a/multicast_dns_source_ref/example/main.dart b/multicast_dns_source_ref/example/main.dart new file mode 100644 index 00000000..81c7697c --- /dev/null +++ b/multicast_dns_source_ref/example/main.dart @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Example script to illustrate how to use the mdns package to discover the port +// of a Dart observatory over mDNS. + +// ignore_for_file: avoid_print + +import 'package:multicast_dns/multicast_dns.dart'; + +Future main() async { + // Parse the command line arguments. + + const String name = '_dartobservatory._tcp.local'; + final MDnsClient client = MDnsClient(); + // Start the client with default options. + await client.start(); + + // Get the PTR record for the service. + await for (final PtrResourceRecord ptr in client.lookup( + ResourceRecordQuery.serverPointer(name), + )) { + // Use the domainName from the PTR record to get the SRV record, + // which will have the port and local hostname. + // Note that duplicate messages may come through, especially if any + // other mDNS queries are running elsewhere on the machine. + await for (final SrvResourceRecord srv in client.lookup( + ResourceRecordQuery.service(ptr.domainName), + )) { + // Domain name will be something like "io.flutter.example@some-iphone.local._dartobservatory._tcp.local" + final String bundleId = + ptr.domainName; //.substring(0, ptr.domainName.indexOf('@')); + print( + 'Dart observatory instance found at ' + '${srv.target}:${srv.port} for "$bundleId".', + ); + } + } + client.stop(); + + print('Done.'); +} diff --git a/multicast_dns_source_ref/example/mdns_resolve.dart b/multicast_dns_source_ref/example/mdns_resolve.dart new file mode 100644 index 00000000..5246997c --- /dev/null +++ b/multicast_dns_source_ref/example/mdns_resolve.dart @@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Example script to illustrate how to use the mdns package to lookup names +// on the local network. + +// ignore_for_file: avoid_print + +import 'package:multicast_dns/multicast_dns.dart'; + +Future main(List args) async { + if (args.length != 1) { + print(''' +Please provide an address as argument. + +For example: + dart mdns_resolve.dart dartino.local'''); + return; + } + + final String name = args[0]; + + final MDnsClient client = MDnsClient(); + await client.start(); + await for (final IPAddressResourceRecord record in client + .lookup(ResourceRecordQuery.addressIPv4(name))) { + print('Found address (${record.address}).'); + } + + await for (final IPAddressResourceRecord record in client + .lookup(ResourceRecordQuery.addressIPv6(name))) { + print('Found address (${record.address}).'); + } + client.stop(); +} diff --git a/multicast_dns_source_ref/example/mdns_sd.dart b/multicast_dns_source_ref/example/mdns_sd.dart new file mode 100644 index 00000000..b728b612 --- /dev/null +++ b/multicast_dns_source_ref/example/mdns_sd.dart @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Example script to illustrate how to use the mdns package to discover services +// on the local network. + +// ignore_for_file: avoid_print + +import 'package:multicast_dns/multicast_dns.dart'; + +Future main(List args) async { + if (args.isEmpty) { + print(''' +Please provide the name of a service as argument. + +For example: + dart mdns_sd.dart [--verbose] _workstation._tcp.local'''); + return; + } + + final bool verbose = args.contains('--verbose') || args.contains('-v'); + final String name = args.last; + final MDnsClient client = MDnsClient(); + await client.start(); + + await for (final PtrResourceRecord ptr in client.lookup( + ResourceRecordQuery.serverPointer(name), + )) { + if (verbose) { + print(ptr); + } + await for (final SrvResourceRecord srv in client.lookup( + ResourceRecordQuery.service(ptr.domainName), + )) { + if (verbose) { + print(srv); + } + if (verbose) { + await client + .lookup(ResourceRecordQuery.text(ptr.domainName)) + .forEach(print); + } + await for (final IPAddressResourceRecord ip in client + .lookup( + ResourceRecordQuery.addressIPv4(srv.target), + )) { + if (verbose) { + print(ip); + } + print( + 'Service instance found at ' + '${srv.target}:${srv.port} with ${ip.address}.', + ); + } + await for (final IPAddressResourceRecord ip in client + .lookup( + ResourceRecordQuery.addressIPv6(srv.target), + )) { + if (verbose) { + print(ip); + } + print( + 'Service instance found at ' + '${srv.target}:${srv.port} with ${ip.address}.', + ); + } + } + } + client.stop(); +} diff --git a/multicast_dns_source_ref/lib/multicast_dns.dart b/multicast_dns_source_ref/lib/multicast_dns.dart new file mode 100644 index 00000000..18674fb2 --- /dev/null +++ b/multicast_dns_source_ref/lib/multicast_dns.dart @@ -0,0 +1,272 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'src/constants.dart'; +import 'src/lookup_resolver.dart'; +import 'src/native_protocol_client.dart'; +import 'src/packet.dart'; +import 'src/resource_record.dart'; + +export 'package:multicast_dns/src/resource_record.dart'; + +/// A callback type for [MDnsQuerier.start] to iterate available network +/// interfaces. +/// +/// Implementations must ensure they return interfaces appropriate for the +/// [type] parameter. +/// +/// See also: +/// * [MDnsQuerier.allInterfacesFactory] +typedef NetworkInterfacesFactory = + Future> Function(InternetAddressType type); + +/// A factory for construction of datagram sockets. +/// +/// This can be injected into the [MDnsClient] to provide alternative +/// implementations of [RawDatagramSocket.bind]. +typedef RawDatagramSocketFactory = + Future Function( + dynamic host, + int port, { + bool reuseAddress, + bool reusePort, + int ttl, + }); + +/// Client for DNS lookup and publishing using the mDNS protocol. +/// +/// Users should call [MDnsQuerier.start] when ready to start querying and +/// listening. [MDnsQuerier.stop] must be called when done to clean up +/// resources. +/// +/// This client only supports "One-Shot Multicast DNS Queries" as described in +/// section 5.1 of [RFC 6762](https://tools.ietf.org/html/rfc6762). +class MDnsClient { + /// Create a new [MDnsClient]. + MDnsClient({ + RawDatagramSocketFactory rawDatagramSocketFactory = RawDatagramSocket.bind, + }) : _rawDatagramSocketFactory = rawDatagramSocketFactory; + + bool _starting = false; + bool _started = false; + RawDatagramSocket? _incomingIPv4; + final List _ipv6InterfaceSockets = []; + final LookupResolver _resolver = LookupResolver(); + final ResourceRecordCache _cache = ResourceRecordCache(); + final RawDatagramSocketFactory _rawDatagramSocketFactory; + + InternetAddress? _mDnsAddress; + int? _mDnsPort; + + /// Find all network interfaces with an the [InternetAddressType] specified. + Future> allInterfacesFactory( + InternetAddressType type, + ) { + return NetworkInterface.list( + includeLinkLocal: true, + type: type, + includeLoopback: true, + ); + } + + /// Start the mDNS client. + /// + /// With no arguments, this method will listen on the IPv4 multicast address + /// on all IPv4 network interfaces. + /// + /// The [listenAddress] parameter must be either [InternetAddress.anyIPv4] or + /// [InternetAddress.anyIPv6], and will default to anyIPv4. + /// + /// The [interfaceFactory] defaults to [allInterfacesFactory]. + /// + /// The [mDnsPort] allows configuring what port is used for the mDNS + /// query. If not provided, defaults to `5353`. + /// + /// The [mDnsAddress] allows configuring what internet address is used + /// for the mDNS query. If not provided, defaults to either `224.0.0.251` or + /// or `FF02::FB`. + /// + /// If provided, [onError] will be called in case of a stream error. If + /// omitted any errors on the stream are considered unhandled, and will be + /// passed to the current [Zone]'s error handler. + /// + /// Subsequent calls to this method are ignored while the mDNS client is in + /// started state. + Future start({ + InternetAddress? listenAddress, + NetworkInterfacesFactory? interfacesFactory, + int mDnsPort = mDnsPort, + InternetAddress? mDnsAddress, + Function? onError, + }) async { + listenAddress ??= InternetAddress.anyIPv4; + interfacesFactory ??= allInterfacesFactory; + + assert( + listenAddress.address == InternetAddress.anyIPv4.address || + listenAddress.address == InternetAddress.anyIPv6.address, + ); + + if (_started || _starting) { + return; + } + _starting = true; + + final int selectedMDnsPort = _mDnsPort = mDnsPort; + _mDnsAddress = mDnsAddress; + + // Listen on all addresses. + final RawDatagramSocket incoming = await _rawDatagramSocketFactory( + listenAddress.address, + selectedMDnsPort, + reuseAddress: true, + reusePort: true, + ttl: 255, + ); + + // Can't send to IPv6 any address. + if (incoming.address != InternetAddress.anyIPv6) { + _incomingIPv4 = incoming; + } else { + _ipv6InterfaceSockets.add(incoming); + } + + _mDnsAddress ??= + incoming.address.type == InternetAddressType.IPv4 + ? mDnsAddressIPv4 + : mDnsAddressIPv6; + + final List interfaces = + (await interfacesFactory(listenAddress.type)).toList(); + + for (final NetworkInterface interface in interfaces) { + final InternetAddress targetAddress = interface.addresses[0]; + + // Ensure that we're using this address/interface for multicast. + if (targetAddress.type == InternetAddressType.IPv6) { + final RawDatagramSocket socket = await _rawDatagramSocketFactory( + targetAddress, + selectedMDnsPort, + reuseAddress: true, + reusePort: true, + ttl: 255, + ); + _ipv6InterfaceSockets.add(socket); + socket.setRawOption( + RawSocketOption.fromInt( + RawSocketOption.levelIPv6, + RawSocketOption.IPv6MulticastInterface, + interface.index, + ), + ); + } + + // Join multicast on this interface. + incoming.joinMulticast(_mDnsAddress!, interface); + } + incoming.listen( + (RawSocketEvent event) => _handleIncoming(event, incoming), + onError: onError, + ); + _started = true; + _starting = false; + } + + /// Stop the client and close any associated sockets. + void stop() { + if (!_started) { + return; + } + if (_starting) { + throw StateError('Cannot stop mDNS client while it is starting.'); + } + + _incomingIPv4?.close(); + _incomingIPv4 = null; + + for (final RawDatagramSocket socket in _ipv6InterfaceSockets) { + socket.close(); + } + _ipv6InterfaceSockets.clear(); + + _resolver.clearPendingRequests(); + + _started = false; + } + + /// Lookup a [ResourceRecord], potentially from the cache. + /// + /// The [type] parameter must be a valid [ResourceRecordType]. The [fullyQualifiedName] + /// parameter is the name of the service to lookup, and must not be null. The + /// [timeout] parameter specifies how long the internal cache should hold on + /// to the record. The [multicast] parameter specifies whether the query + /// should be sent as unicast (QU) or multicast (QM). + /// + /// Some publishers have been observed to not respond to unicast requests + /// properly, so the default is true. + Stream lookup( + ResourceRecordQuery query, { + Duration timeout = const Duration(seconds: 5), + }) { + final int? selectedMDnsPort = _mDnsPort; + if (!_started || selectedMDnsPort == null) { + throw StateError('mDNS client must be started before calling lookup.'); + } + // Look for entries in the cache. + final List cached = []; + _cache.lookup( + query.fullyQualifiedName, + query.resourceRecordType, + cached, + ); + if (cached.isNotEmpty) { + final StreamController controller = StreamController(); + cached.forEach(controller.add); + controller.close(); + return controller.stream; + } + + // Add the pending request before sending the query. + final Stream results = _resolver.addPendingRequest( + query.resourceRecordType, + query.fullyQualifiedName, + timeout, + ); + + final List packet = query.encode(); + + if (_mDnsAddress?.type == InternetAddressType.IPv4) { + // Send and listen on same "ANY" interface + _incomingIPv4?.send(packet, _mDnsAddress!, selectedMDnsPort); + } else { + for (final RawDatagramSocket socket in _ipv6InterfaceSockets) { + socket.send(packet, _mDnsAddress!, selectedMDnsPort); + } + } + + return results; + } + + // Process incoming datagrams. + void _handleIncoming(RawSocketEvent event, RawDatagramSocket incoming) { + if (event == RawSocketEvent.read) { + final Datagram? datagram = incoming.receive(); + if (datagram == null) { + return; + } + + // Check for published responses. + final List? response = decodeMDnsResponse(datagram.data); + if (response != null) { + _cache.updateRecords(response); + _resolver.handleResponse(response); + return; + } + // TODO(dnfield): Support queries coming in for published entries. + } + } +} diff --git a/multicast_dns_source_ref/lib/src/constants.dart b/multicast_dns_source_ref/lib/src/constants.dart new file mode 100644 index 00000000..76d8c6a8 --- /dev/null +++ b/multicast_dns_source_ref/lib/src/constants.dart @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +/// The IPv4 mDNS Address. +final InternetAddress mDnsAddressIPv4 = InternetAddress('224.0.0.251'); + +/// The IPv6 mDNS Address. +final InternetAddress mDnsAddressIPv6 = InternetAddress('FF02::FB'); + +/// The mDNS port. +const int mDnsPort = 5353; + +/// Enumeration of supported resource record class types. +abstract class ResourceRecordClass { + // This class is intended to be used as a namespace, and should not be + // extended directly. + ResourceRecordClass._(); + + /// Internet address class ("IN"). + static const int internet = 1; +} + +/// Enumeration of DNS question types. +abstract class QuestionType { + // This class is intended to be used as a namespace, and should not be + // extended directly. + QuestionType._(); + + /// "QU" Question. + static const int unicast = 0x8000; + + /// "QM" Question. + static const int multicast = 0x0000; +} diff --git a/multicast_dns_source_ref/lib/src/lookup_resolver.dart b/multicast_dns_source_ref/lib/src/lookup_resolver.dart new file mode 100644 index 00000000..a12c3e10 --- /dev/null +++ b/multicast_dns_source_ref/lib/src/lookup_resolver.dart @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; + +import 'resource_record.dart'; + +/// Class for maintaining state about pending mDNS requests. +base class PendingRequest extends LinkedListEntry { + /// Creates a new PendingRequest. + PendingRequest(this.type, this.domainName, this.controller); + + /// The [ResourceRecordType] of the request. + final int type; + + /// The domain name to look up via mDNS. + /// + /// For example, `'_http._tcp.local` to look up HTTP services on the local + /// domain. + final String domainName; + + /// A StreamController managing the request. + final StreamController controller; + + /// The timer for the request. + Timer? timer; +} + +/// Class for keeping track of pending lookups and processing incoming +/// query responses. +class LookupResolver { + final LinkedList _pendingRequests = + LinkedList(); + + /// Adds a request and returns a [Stream] of [ResourceRecord] responses. + Stream addPendingRequest( + int type, + String name, + Duration timeout, + ) { + final StreamController controller = StreamController(); + final PendingRequest request = PendingRequest(type, name, controller); + final Timer timer = Timer(timeout, () { + request.unlink(); + controller.close(); + }); + request.timer = timer; + _pendingRequests.add(request); + return controller.stream; + } + + /// Parses [ResoureRecord]s received and delivers them to the appropriate + /// listener(s) added via [addPendingRequest]. + void handleResponse(List response) { + for (final ResourceRecord r in response) { + final int type = r.resourceRecordType; + String name = r.name.toLowerCase(); + if (name.endsWith('.')) { + name = name.substring(0, name.length - 1); + } + + bool responseMatches(PendingRequest request) { + String requestName = request.domainName.toLowerCase(); + // make, e.g. "_http" become "_http._tcp.local". + if (!requestName.endsWith('local')) { + if (!requestName.endsWith('._tcp.local') && + !requestName.endsWith('._udp.local') && + !requestName.endsWith('._tcp') && + !requestName.endsWith('.udp')) { + requestName += '._tcp'; + } + requestName += '.local'; + } + return requestName == name && request.type == type; + } + + for (final PendingRequest pendingRequest in _pendingRequests) { + if (responseMatches(pendingRequest)) { + if (pendingRequest.controller.isClosed) { + return; + } + pendingRequest.controller.add(r); + } + } + } + } + + /// Removes any pending requests and ends processing. + void clearPendingRequests() { + while (_pendingRequests.isNotEmpty) { + final PendingRequest request = _pendingRequests.first; + request.unlink(); + request.timer?.cancel(); + request.controller.close(); + } + } +} diff --git a/multicast_dns_source_ref/lib/src/native_protocol_client.dart b/multicast_dns_source_ref/lib/src/native_protocol_client.dart new file mode 100644 index 00000000..fb94343e --- /dev/null +++ b/multicast_dns_source_ref/lib/src/native_protocol_client.dart @@ -0,0 +1,83 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:collection'; + +import 'resource_record.dart'; + +/// Cache for resource records that have been received. +/// +/// There can be multiple entries for the same name and type. +/// +/// The cache is updated with a list of records, because it needs to remove +/// all entries that correspond to the name and type of the name/type +/// combinations of records that should be updated. For example, a host may +/// remove one of its IP addresses and report the remaining address as a +/// response - then we need to clear all previous entries for that host before +/// updating the cache. +class ResourceRecordCache { + /// Creates a new ResourceRecordCache. + ResourceRecordCache(); + + final Map>> _cache = + >>{}; + + /// The number of entries in the cache. + int get entryCount { + int count = 0; + for (final SplayTreeMap> map + in _cache.values) { + for (final List records in map.values) { + count += records.length; + } + } + return count; + } + + /// Update the records in this cache. + void updateRecords(List records) { + // TODO(karlklose): include flush bit in the record and only flush if + // necessary. + // Clear the cache for all name/type combinations to be updated. + final Map> seenRecordTypes = >{}; + for (final ResourceRecord record in records) { + // TODO(dnfield): Update this to use set literal syntax when we're able to bump the SDK constraint. + seenRecordTypes[record.resourceRecordType] ??= + Set(); // ignore: prefer_collection_literals + if (seenRecordTypes[record.resourceRecordType]!.add(record.name)) { + _cache[record.resourceRecordType] ??= + SplayTreeMap>(); + + _cache[record.resourceRecordType]![record.name] = [ + record, + ]; + } else { + _cache[record.resourceRecordType]![record.name]!.add(record); + } + } + } + + /// Get a record from this cache. + void lookup( + String name, + int type, + List results, + ) { + assert(ResourceRecordType.debugAssertValid(type)); + final int time = DateTime.now().millisecondsSinceEpoch; + final SplayTreeMap>? candidates = _cache[type]; + if (candidates == null) { + return; + } + + final List? candidateRecords = candidates[name]; + if (candidateRecords == null) { + return; + } + candidateRecords.removeWhere( + (ResourceRecord candidate) => candidate.validUntil < time, + ); + results.addAll(candidateRecords.cast()); + } +} diff --git a/multicast_dns_source_ref/lib/src/packet.dart b/multicast_dns_source_ref/lib/src/packet.dart new file mode 100644 index 00000000..aa41cefd --- /dev/null +++ b/multicast_dns_source_ref/lib/src/packet.dart @@ -0,0 +1,396 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'constants.dart'; +import 'resource_record.dart'; + +// Offsets into the header. See https://tools.ietf.org/html/rfc1035. +const int _kIdOffset = 0; +const int _kFlagsOffset = 2; +const int _kQdcountOffset = 4; +const int _kAncountOffset = 6; +const int _kNscountOffset = 8; +const int _kArcountOffset = 10; +const int _kHeaderSize = 12; + +/// Processes a DNS query name into a list of parts. +/// +/// Will attempt to append 'local' if the name is something like '_http._tcp', +/// and '._tcp.local' if name is something like '_http'. +List processDnsNameParts(String name) { + final List parts = name.split('.'); + if (parts.length == 1) { + return [parts[0], '_tcp', 'local']; + } else if (parts.length == 2 && parts[1].startsWith('_')) { + return [parts[0], parts[1], 'local']; + } + + return parts; +} + +/// Encode an mDNS query packet. +/// +/// The [type] parameter must be a valid [ResourceRecordType] value. The +/// [multicast] parameter must not be null. +/// +/// This is a low level API; most consumers should prefer +/// [ResourceRecordQuery.encode], which offers some convenience wrappers around +/// selecting the correct [type] and setting the [name] parameter correctly. +List encodeMDnsQuery( + String name, { + int type = ResourceRecordType.addressIPv4, + bool multicast = true, +}) { + assert(ResourceRecordType.debugAssertValid(type)); + + final List nameParts = processDnsNameParts(name); + final List> rawNameParts = + nameParts.map>((String part) => utf8.encode(part)).toList(); + + // Calculate the size of the packet. + int size = _kHeaderSize; + for (int i = 0; i < rawNameParts.length; i++) { + size += 1 + rawNameParts[i].length; + } + + size += 1; // End with empty part + size += 4; // Trailer (QTYPE and QCLASS). + final Uint8List data = Uint8List(size); + final ByteData packetByteData = ByteData.view(data.buffer); + // Query identifier - just use 0. + packetByteData.setUint16(_kIdOffset, 0); + // Flags - 0 for query. + packetByteData.setUint16(_kFlagsOffset, 0); + // Query count. + packetByteData.setUint16(_kQdcountOffset, 1); + // Number of answers - 0 for query. + packetByteData.setUint16(_kAncountOffset, 0); + // Number of name server records - 0 for query. + packetByteData.setUint16(_kNscountOffset, 0); + // Number of resource records - 0 for query. + packetByteData.setUint16(_kArcountOffset, 0); + int offset = _kHeaderSize; + for (int i = 0; i < rawNameParts.length; i++) { + data[offset++] = rawNameParts[i].length; + data.setRange(offset, offset + rawNameParts[i].length, rawNameParts[i]); + offset += rawNameParts[i].length; + } + + data[offset] = 0; // Empty part. + offset++; + packetByteData.setUint16(offset, type); // QTYPE. + offset += 2; + packetByteData.setUint16( + offset, + ResourceRecordClass.internet | + (multicast ? QuestionType.multicast : QuestionType.unicast), + ); + + return data; +} + +/// Result of reading a Fully Qualified Domain Name (FQDN). +class _FQDNReadResult { + /// Creates a new FQDN read result. + _FQDNReadResult(this.fqdnParts, this.bytesRead); + + /// The raw parts of the FQDN. + final List fqdnParts; + + /// The bytes consumed from the packet for this FQDN. + final int bytesRead; + + /// Returns the Fully Qualified Domain Name. + String get fqdn => fqdnParts.join('.'); + + @override + String toString() => fqdn; +} + +/// Reads a FQDN from raw packet data. +String readFQDN(List packet, [int offset = 0]) { + final Uint8List data = + packet is Uint8List ? packet : Uint8List.fromList(packet); + final ByteData byteData = ByteData.view(data.buffer); + + return _readFQDN(data, byteData, offset, data.length).fqdn; +} + +// Read a FQDN at the given offset. Returns a pair with the FQDN +// parts and the number of bytes consumed. +// +// If decoding fails (e.g. due to an invalid packet) `null` is returned. +_FQDNReadResult _readFQDN( + Uint8List data, + ByteData byteData, + int offset, + int length, +) { + void checkLength(int required) { + if (length < required) { + throw MDnsDecodeException(required); + } + } + + final List parts = []; + final int prevOffset = offset; + final List offsetsToVisit = [offset]; + int upperLimitOffset = offset; + int highestOffsetRead = offset; + + while (offsetsToVisit.isNotEmpty) { + offset = offsetsToVisit.removeLast(); + + while (true) { + // At least one byte is required. + checkLength(offset + 1); + // Check for compressed. + if (data[offset] & 0xc0 == 0xc0) { + // At least two bytes are required for a compressed FQDN (see RFC1035 section 4.1.4). + checkLength(offset + 2); + + // A compressed FQDN has a new offset in the lower 14 bits. + final int pointerDest = byteData.getUint16(offset) & ~0xc000; + // Pointers can only point to prior occurances of some name. + // This check also guards against pointers that form loops. + if (pointerDest >= upperLimitOffset) { + throw MDnsDecodeException(offset); + } + upperLimitOffset = pointerDest; + offsetsToVisit.add(pointerDest); + highestOffsetRead = max(highestOffsetRead, offset + 2); + break; + } else { + // A normal FQDN part has a length and a UTF-8 encoded name + // part. If the length is 0 this is the end of the FQDN. + final int partLength = data[offset]; + offset++; + if (partLength > 0) { + checkLength(offset + partLength); + final Uint8List partBytes = Uint8List.view( + data.buffer, + offset, + partLength, + ); + offset += partLength; + // According to the RFC, this is supposed to be utf-8 encoded, but + // we should continue decoding even if it isn't to avoid dropping the + // rest of the data, which might still be useful. + parts.add(utf8.decode(partBytes, allowMalformed: true)); + highestOffsetRead = max(highestOffsetRead, offset); + } else { + highestOffsetRead = max(highestOffsetRead, offset); + break; + } + } + } + } + return _FQDNReadResult(parts, highestOffsetRead - prevOffset); +} + +/// Decode an mDNS response packet. +/// +/// If decoding fails (e.g. due to an invalid packet) `null` is returned. +/// +/// See https://tools.ietf.org/html/rfc1035 for the format. +List? decodeMDnsResponse(List packet) { + final int length = packet.length; + if (length < _kHeaderSize) { + return null; + } + + final Uint8List data = + packet is Uint8List ? packet : Uint8List.fromList(packet); + final ByteData packetBytes = ByteData.view(data.buffer); + + final int answerCount = packetBytes.getUint16(_kAncountOffset); + final int authorityCount = packetBytes.getUint16(_kNscountOffset); + final int additionalCount = packetBytes.getUint16(_kArcountOffset); + final int remainingCount = answerCount + authorityCount + additionalCount; + + if (remainingCount == 0) { + return null; + } + + final int questionCount = packetBytes.getUint16(_kQdcountOffset); + int offset = _kHeaderSize; + + void checkLength(int required) { + if (length < required) { + throw MDnsDecodeException(required); + } + } + + ResourceRecord? readResourceRecord() { + // First read the FQDN. + final _FQDNReadResult result = _readFQDN(data, packetBytes, offset, length); + final String fqdn = result.fqdn; + offset += result.bytesRead; + checkLength(offset + 2); + final int type = packetBytes.getUint16(offset); + offset += 2; + // The first bit of the rrclass field is set to indicate that the answer is + // unique and the querier should flush the cached answer for this name + // (RFC 6762, Sec. 10.2). We ignore it for now since we don't cache answers. + checkLength(offset + 2); + final int resourceRecordClass = packetBytes.getUint16(offset) & 0x7fff; + + if (resourceRecordClass != ResourceRecordClass.internet) { + // We do not support other classes. + return null; + } + + offset += 2; + checkLength(offset + 4); + final int ttl = packetBytes.getInt32(offset); + offset += 4; + + checkLength(offset + 2); + final int readDataLength = packetBytes.getUint16(offset); + offset += 2; + final int validUntil = DateTime.now().millisecondsSinceEpoch + ttl * 1000; + switch (type) { + case ResourceRecordType.addressIPv4: + checkLength(offset + readDataLength); + final StringBuffer addr = StringBuffer(); + final int stop = offset + readDataLength; + addr.write(packetBytes.getUint8(offset)); + offset++; + for (; offset < stop; offset++) { + addr.write('.'); + addr.write(packetBytes.getUint8(offset)); + } + return IPAddressResourceRecord( + fqdn, + validUntil, + address: InternetAddress(addr.toString()), + ); + case ResourceRecordType.addressIPv6: + checkLength(offset + readDataLength); + final StringBuffer addr = StringBuffer(); + final int stop = offset + readDataLength; + addr.write(packetBytes.getUint16(offset).toRadixString(16)); + offset += 2; + for (; offset < stop; offset += 2) { + addr.write(':'); + addr.write(packetBytes.getUint16(offset).toRadixString(16)); + } + return IPAddressResourceRecord( + fqdn, + validUntil, + address: InternetAddress(addr.toString()), + ); + case ResourceRecordType.service: + checkLength(offset + 2); + final int priority = packetBytes.getUint16(offset); + offset += 2; + checkLength(offset + 2); + final int weight = packetBytes.getUint16(offset); + offset += 2; + checkLength(offset + 2); + final int port = packetBytes.getUint16(offset); + offset += 2; + final _FQDNReadResult result = _readFQDN( + data, + packetBytes, + offset, + length, + ); + offset += result.bytesRead; + return SrvResourceRecord( + fqdn, + validUntil, + target: result.fqdn, + port: port, + priority: priority, + weight: weight, + ); + case ResourceRecordType.serverPointer: + checkLength(offset + readDataLength); + final _FQDNReadResult result = _readFQDN( + data, + packetBytes, + offset, + length, + ); + offset += readDataLength; + return PtrResourceRecord(fqdn, validUntil, domainName: result.fqdn); + case ResourceRecordType.text: + checkLength(offset + readDataLength); + // The first byte of the buffer is the length of the first string of + // the TXT record. Further length-prefixed strings may follow. We + // concatenate them with newlines. + final StringBuffer strings = StringBuffer(); + int index = 0; + while (index < readDataLength) { + final int txtLength = data[offset + index]; + index++; + if (txtLength == 0) { + continue; + } + final String text = utf8.decode( + Uint8List.view(data.buffer, offset + index, txtLength), + allowMalformed: true, + ); + strings.writeln(text); + index += txtLength; + } + offset += readDataLength; + return TxtResourceRecord(fqdn, validUntil, text: strings.toString()); + default: + checkLength(offset + readDataLength); + offset += readDataLength; + return null; + } + } + + // This list can't be fixed length right now because we might get + // resource record types we don't support, and consumers expect this list + // to not have null entries. + final List result = []; + + try { + for (int i = 0; i < questionCount; i++) { + final _FQDNReadResult result = _readFQDN( + data, + packetBytes, + offset, + length, + ); + offset += result.bytesRead; + checkLength(offset + 4); + offset += 4; + } + for (int i = 0; i < remainingCount; i++) { + final ResourceRecord? record = readResourceRecord(); + if (record != null) { + result.add(record); + } + } + } on MDnsDecodeException { + // If decoding fails return null. + return null; + } + return result; +} + +/// This exception is thrown by the decoder when the packet is invalid. +class MDnsDecodeException implements Exception { + /// Creates a new MDnsDecodeException, indicating an error in decoding at the + /// specified [offset]. + /// + /// The [offset] parameter should not be null. + const MDnsDecodeException(this.offset); + + /// The offset in the packet at which the exception occurred. + final int offset; + + @override + String toString() => 'Decoding error at $offset'; +} diff --git a/multicast_dns_source_ref/lib/src/resource_record.dart b/multicast_dns_source_ref/lib/src/resource_record.dart new file mode 100644 index 00000000..8c59ab2c --- /dev/null +++ b/multicast_dns_source_ref/lib/src/resource_record.dart @@ -0,0 +1,346 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; +import 'constants.dart'; +import 'packet.dart'; + +/// Enumeration of support resource record types. +abstract class ResourceRecordType { + // This class is intended to be used as a namespace, and should not be + // extended directly. + ResourceRecordType._(); + + /// An IPv4 Address record, also known as an "A" record. It has a value of 1. + static const int addressIPv4 = 1; + + /// An IPv6 Address record, also known as an "AAAA" record. It has a vaule of + /// 28. + static const int addressIPv6 = 28; + + /// An IP Address reverse map record, also known as a "PTR" recored. It has a + /// value of 12. + static const int serverPointer = 12; + + /// An available service record, also known as an "SRV" record. It has a + /// value of 33. + static const int service = 33; + + /// A text record, also known as a "TXT" record. It has a value of 16. + static const int text = 16; + + // TODO(dnfield): Support ANY in some meaningful way. Might be server only. + // /// A query for all records of all types known to the name server. + // static const int any = 255; + + /// Checks that a given int is a valid ResourceRecordType. + /// + /// This method is intended to be called only from an `assert()`. + static bool debugAssertValid(int resourceRecordType) { + return resourceRecordType == addressIPv4 || + resourceRecordType == addressIPv6 || + resourceRecordType == serverPointer || + resourceRecordType == service || + resourceRecordType == text; + } + + /// Prints a debug-friendly version of the resource record type value. + static String toDebugString(int resourceRecordType) { + switch (resourceRecordType) { + case addressIPv4: + return 'A (IPv4 Address)'; + case addressIPv6: + return 'AAAA (IPv6 Address)'; + case serverPointer: + return 'PTR (Domain Name Pointer)'; + case service: + return 'SRV (Service record)'; + case text: + return 'TXT (Text)'; + } + return 'Unknown ($resourceRecordType)'; + } +} + +/// Represents a DNS query. +@immutable +class ResourceRecordQuery { + /// Creates a new ResourceRecordQuery. + /// + /// Most callers should prefer one of the named constructors. + ResourceRecordQuery( + this.resourceRecordType, + this.fullyQualifiedName, + this.questionType, + ) : assert(ResourceRecordType.debugAssertValid(resourceRecordType)); + + /// An A (IPv4) query. + ResourceRecordQuery.addressIPv4(String name, {bool isMulticast = true}) + : this( + ResourceRecordType.addressIPv4, + name, + isMulticast ? QuestionType.multicast : QuestionType.unicast, + ); + + /// An AAAA (IPv6) query. + ResourceRecordQuery.addressIPv6(String name, {bool isMulticast = true}) + : this( + ResourceRecordType.addressIPv6, + name, + isMulticast ? QuestionType.multicast : QuestionType.unicast, + ); + + /// A PTR (Server pointer) query. + ResourceRecordQuery.serverPointer(String name, {bool isMulticast = true}) + : this( + ResourceRecordType.serverPointer, + name, + isMulticast ? QuestionType.multicast : QuestionType.unicast, + ); + + /// An SRV (Service) query. + ResourceRecordQuery.service(String name, {bool isMulticast = true}) + : this( + ResourceRecordType.service, + name, + isMulticast ? QuestionType.multicast : QuestionType.unicast, + ); + + /// A TXT (Text record) query. + ResourceRecordQuery.text(String name, {bool isMulticast = true}) + : this( + ResourceRecordType.text, + name, + isMulticast ? QuestionType.multicast : QuestionType.unicast, + ); + + /// Tye type of resource record - one of [ResourceRecordType]'s values. + final int resourceRecordType; + + /// The Fully Qualified Domain Name associated with the request. + final String fullyQualifiedName; + + /// The [QuestionType], i.e. multicast or unicast. + final int questionType; + + /// Convenience accessor to determine whether the question type is multicast. + bool get isMulticast => questionType == QuestionType.multicast; + + /// Convenience accessor to determine whether the question type is unicast. + bool get isUnicast => questionType == QuestionType.unicast; + + /// Encodes this query to the raw wire format. + List encode() { + return encodeMDnsQuery( + fullyQualifiedName, + type: resourceRecordType, + multicast: isMulticast, + ); + } + + @override + int get hashCode => + Object.hash(resourceRecordType, fullyQualifiedName, questionType); + + @override + bool operator ==(Object other) { + return other is ResourceRecordQuery && + other.resourceRecordType == resourceRecordType && + other.fullyQualifiedName == fullyQualifiedName && + other.questionType == questionType; + } + + @override + String toString() => + 'ResourceRecordQuery{$fullyQualifiedName, type: ${ResourceRecordType.toDebugString(resourceRecordType)}, isMulticast: $isMulticast}'; +} + +/// Base implementation of DNS resource records (RRs). +@immutable +abstract class ResourceRecord { + /// Creates a new ResourceRecord. + const ResourceRecord(this.resourceRecordType, this.name, this.validUntil); + + /// The FQDN for this record. + final String name; + + /// The epoch time at which point this record is valid for in the cache. + final int validUntil; + + /// The raw resource record value. See [ResourceRecordType] for supported values. + final int resourceRecordType; + + String get _additionalInfo; + + @override + String toString() => + '$runtimeType{$name, validUntil: ${DateTime.fromMillisecondsSinceEpoch(validUntil)}, $_additionalInfo}'; + + @override + int get hashCode => Object.hash(name, validUntil, resourceRecordType); + + @override + bool operator ==(Object other) { + return other is ResourceRecord && + other.name == name && + other.validUntil == validUntil && + other.resourceRecordType == resourceRecordType; + } + + /// Low level method for encoding this record into an mDNS packet. + /// + /// Subclasses should provide the packet format of their encapsulated data + /// into a `Uint8List`, which could then be used to write a pakcet to send + /// as a response for this record type. + Uint8List encodeResponseRecord(); +} + +/// A Service Pointer for reverse mapping an IP address (DNS "PTR"). +class PtrResourceRecord extends ResourceRecord { + /// Creates a new PtrResourceRecord. + const PtrResourceRecord( + String name, + int validUntil, { + required this.domainName, + }) : super(ResourceRecordType.serverPointer, name, validUntil); + + /// The FQDN for this record. + final String domainName; + + @override + String get _additionalInfo => 'domainName: $domainName'; + + @override + int get hashCode => Object.hash(domainName.hashCode, super.hashCode); + + @override + bool operator ==(Object other) { + return super == other && + other is PtrResourceRecord && + other.domainName == domainName; + } + + @override + Uint8List encodeResponseRecord() { + return Uint8List.fromList(utf8.encode(domainName)); + } +} + +/// An IP Address record for IPv4 (DNS "A") or IPv6 (DNS "AAAA") records. +class IPAddressResourceRecord extends ResourceRecord { + /// Creates a new IPAddressResourceRecord. + IPAddressResourceRecord(String name, int validUntil, {required this.address}) + : super( + address.type == InternetAddressType.IPv4 + ? ResourceRecordType.addressIPv4 + : ResourceRecordType.addressIPv6, + name, + validUntil, + ); + + /// The [InternetAddress] for this record. + final InternetAddress address; + + @override + String get _additionalInfo => 'address: $address'; + + @override + int get hashCode => Object.hash(address.hashCode, super.hashCode); + + @override + bool operator ==(Object other) { + return super == other && + other is IPAddressResourceRecord && + other.address == address; + } + + @override + Uint8List encodeResponseRecord() { + return Uint8List.fromList(address.rawAddress); + } +} + +/// A Service record, capturing a host target and port (DNS "SRV"). +class SrvResourceRecord extends ResourceRecord { + /// Creates a new service record. + const SrvResourceRecord( + String name, + int validUntil, { + required this.target, + required this.port, + required this.priority, + required this.weight, + }) : super(ResourceRecordType.service, name, validUntil); + + /// The hostname for this record. + final String target; + + /// The port for this record. + final int port; + + /// The relative priority of this service. + final int priority; + + /// The weight (used when multiple services have the same priority). + final int weight; + + @override + String get _additionalInfo => + 'target: $target, port: $port, priority: $priority, weight: $weight'; + + @override + int get hashCode => + Object.hash(target, port, priority, weight, super.hashCode); + + @override + bool operator ==(Object other) { + return super == other && + other is SrvResourceRecord && + other.target == target && + other.port == port && + other.priority == priority && + other.weight == weight; + } + + @override + Uint8List encodeResponseRecord() { + final List data = utf8.encode(target); + final Uint8List result = Uint8List(data.length + 7); + final ByteData resultData = ByteData.view(result.buffer); + resultData.setUint16(0, priority); + resultData.setUint16(2, weight); + resultData.setUint16(4, port); + result[6] = data.length; + return result..setRange(7, data.length, data); + } +} + +/// A Text record, contianing additional textual data (DNS "TXT"). +class TxtResourceRecord extends ResourceRecord { + /// Creates a new text record. + const TxtResourceRecord(String name, int validUntil, {required this.text}) + : super(ResourceRecordType.text, name, validUntil); + + /// The raw text from this record. + final String text; + + @override + String get _additionalInfo => 'text: $text'; + + @override + int get hashCode => Object.hash(text.hashCode, super.hashCode); + + @override + bool operator ==(Object other) => + super == other && other is TxtResourceRecord && other.text == text; + + @override + Uint8List encodeResponseRecord() { + return Uint8List.fromList(utf8.encode(text)); + } +} diff --git a/multicast_dns_source_ref/pubspec.yaml b/multicast_dns_source_ref/pubspec.yaml new file mode 100644 index 00000000..38de03aa --- /dev/null +++ b/multicast_dns_source_ref/pubspec.yaml @@ -0,0 +1,19 @@ +name: multicast_dns +description: Dart package for performing mDNS queries (e.g. Bonjour, Avahi). +repository: https://github.com/flutter/packages/tree/main/packages/multicast_dns +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+multicast_dns%22 +version: 0.3.3 + +environment: + sdk: ^3.7.0 + +dependencies: + meta: ^1.3.0 + +dev_dependencies: + test: "^1.16.5" + +topics: + - bonjour + - mdns + - network diff --git a/multicast_dns_source_ref/test/client_test.dart b/multicast_dns_source_ref/test/client_test.dart new file mode 100644 index 00000000..b5fef509 --- /dev/null +++ b/multicast_dns_source_ref/test/client_test.dart @@ -0,0 +1,295 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:multicast_dns/multicast_dns.dart'; +import 'package:test/fake.dart'; +import 'package:test/test.dart'; + +void main() { + test('Can inject datagram socket factory and configure mdns port', () async { + late int lastPort; + final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); + final MDnsClient client = MDnsClient( + rawDatagramSocketFactory: ( + dynamic host, + int port, { + bool reuseAddress = true, + bool reusePort = true, + int ttl = 1, + }) async { + lastPort = port; + return datagramSocket; + }, + ); + + await client.start( + mDnsPort: 1234, + interfacesFactory: + (InternetAddressType type) async => [], + ); + + expect(lastPort, 1234); + }); + + test('Closes IPv4 sockets', () async { + final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); + final MDnsClient client = MDnsClient( + rawDatagramSocketFactory: ( + dynamic host, + int port, { + bool reuseAddress = true, + bool reusePort = true, + int ttl = 1, + }) async { + return datagramSocket; + }, + ); + + await client.start( + mDnsPort: 1234, + interfacesFactory: + (InternetAddressType type) async => [], + ); + expect(datagramSocket.closed, false); + client.stop(); + expect(datagramSocket.closed, true); + }); + + test('Closes IPv6 sockets', () async { + final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); + datagramSocket.address = InternetAddress.anyIPv6; + final MDnsClient client = MDnsClient( + rawDatagramSocketFactory: ( + dynamic host, + int port, { + bool reuseAddress = true, + bool reusePort = true, + int ttl = 1, + }) async { + return datagramSocket; + }, + ); + + await client.start( + mDnsPort: 1234, + interfacesFactory: + (InternetAddressType type) async => [], + ); + expect(datagramSocket.closed, false); + client.stop(); + expect(datagramSocket.closed, true); + }); + + test('start() is idempotent', () async { + final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); + datagramSocket.address = InternetAddress.anyIPv4; + final MDnsClient client = MDnsClient( + rawDatagramSocketFactory: ( + dynamic host, + int port, { + bool reuseAddress = true, + bool reusePort = true, + int ttl = 1, + }) async { + return datagramSocket; + }, + ); + + await client.start( + interfacesFactory: + (InternetAddressType type) async => [], + ); + await client.start(); + await client.lookup(ResourceRecordQuery.serverPointer('_')).toList(); + }); + + group('Bind a single socket to ANY IPv4 and more than one when IPv6', () { + final List> testCases = >[ + { + 'name': 'IPv4', + 'datagramSocketType': InternetAddress.anyIPv4, + 'interfacePrefix': '192.168.2.', + }, + { + 'name': 'IPv6', + 'datagramSocketType': InternetAddress.anyIPv6, + 'interfacePrefix': '2001:0db8:85a3:0000:0000:8a2e:7335:030', + }, + ]; + + for (final Map testCase in testCases) { + test('Bind a single socket to ANY ${testCase["name"]}', () async { + final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); + + datagramSocket.address = + testCase['datagramSocketType']! as InternetAddress; + + final List selectedInterfacesForSendingPackets = []; + final MDnsClient client = MDnsClient( + rawDatagramSocketFactory: ( + dynamic host, + int port, { + bool reuseAddress = true, + bool reusePort = true, + int ttl = 1, + }) async { + selectedInterfacesForSendingPackets.add(host); + return datagramSocket; + }, + ); + + const int numberOfFakeInterfaces = 10; + Future> fakeNetworkInterfacesFactory( + InternetAddressType type, + ) async { + final List fakeInterfaces = []; + + // Generate "fake" interfaces + for (int i = 0; i < numberOfFakeInterfaces; i++) { + fakeInterfaces.add( + FakeNetworkInterface('inetfake$i', [ + InternetAddress("${testCase['interfacePrefix']! as String}$i"), + ], 0), + ); + } + + // ignore: always_specify_types + return Future.value(fakeInterfaces); + } + + final InternetAddress listenAddress = + testCase['datagramSocketType']! as InternetAddress; + + await client.start( + listenAddress: listenAddress, + mDnsPort: 1234, + interfacesFactory: fakeNetworkInterfacesFactory, + ); + client.stop(); + + if (testCase['datagramSocketType'] == InternetAddress.anyIPv4) { + expect(selectedInterfacesForSendingPackets.length, 1); + } else { + // + 1 because of unspecified address (::) + expect( + selectedInterfacesForSendingPackets.length, + numberOfFakeInterfaces + 1, + ); + } + expect(selectedInterfacesForSendingPackets[0], listenAddress.address); + }); + } + }); + + test('Calls onError callback in case of socket error', () async { + final FakeRawDatagramSocketThatSendsError datagramSocket = + FakeRawDatagramSocketThatSendsError(); + final MDnsClient client = MDnsClient( + rawDatagramSocketFactory: ( + dynamic host, + int port, { + bool reuseAddress = true, + bool reusePort = true, + int ttl = 1, + }) async { + return datagramSocket; + }, + ); + + final Completer onErrorCalledCompleter = Completer(); + await client.start( + mDnsPort: 1234, + interfacesFactory: + (InternetAddressType type) async => [], + onError: (Object e) { + expect(e, 'Error'); + onErrorCalledCompleter.complete(); + }, + ); + + await onErrorCalledCompleter.future.timeout(const Duration(seconds: 5)); + }); +} + +class FakeRawDatagramSocket extends Fake implements RawDatagramSocket { + @override + InternetAddress address = InternetAddress.anyIPv4; + + @override + StreamSubscription listen( + void Function(RawSocketEvent event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return const Stream.empty().listen( + onData, + onError: onError, + cancelOnError: cancelOnError, + onDone: onDone, + ); + } + + bool closed = false; + + @override + void close() { + closed = true; + } + + @override + int send(List buffer, InternetAddress address, int port) { + return buffer.length; + } + + @override + void joinMulticast(InternetAddress group, [NetworkInterface? interface]) { + // nothing to do here + } + @override + void setRawOption(RawSocketOption option) { + // nothing to do here + } +} + +class FakeRawDatagramSocketThatSendsError extends Fake + implements RawDatagramSocket { + @override + InternetAddress address = InternetAddress.anyIPv4; + + @override + StreamSubscription listen( + void Function(RawSocketEvent event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return Stream.error('Error').listen( + onData, + onError: onError, + cancelOnError: cancelOnError, + onDone: onDone, + ); + } +} + +class FakeNetworkInterface implements NetworkInterface { + FakeNetworkInterface(this._name, this._addresses, this._index); + + final String _name; + final List _addresses; + final int _index; + + @override + List get addresses => _addresses; + + @override + String get name => _name; + + @override + int get index => _index; +} diff --git a/multicast_dns_source_ref/test/decode_test.dart b/multicast_dns_source_ref/test/decode_test.dart new file mode 100644 index 00000000..7d5d16b8 --- /dev/null +++ b/multicast_dns_source_ref/test/decode_test.dart @@ -0,0 +1,1648 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:multicast_dns/src/packet.dart'; +import 'package:multicast_dns/src/resource_record.dart'; +import 'package:test/test.dart'; + +const int _kSrvHeaderSize = 6; + +void main() { + testValidPackages(); + testBadPackages(); + testNonUtf8DomainName(); + // testHexDumpList(); + testPTRRData(); + testSRVRData(); +} + +void testValidPackages() { + test('Can decode valid packets', () { + List result = decodeMDnsResponse(package1)!; + expect(result, isNotNull); + expect(result.length, 1); + IPAddressResourceRecord ipResult = result[0] as IPAddressResourceRecord; + expect(ipResult.name, 'raspberrypi.local'); + expect(ipResult.address.address, '192.168.1.191'); + + result = decodeMDnsResponse(package2)!; + expect(result.length, 2); + ipResult = result[0] as IPAddressResourceRecord; + expect(ipResult.name, 'raspberrypi.local'); + expect(ipResult.address.address, '192.168.1.191'); + ipResult = result[1] as IPAddressResourceRecord; + expect(ipResult.name, 'raspberrypi.local'); + expect(ipResult.address.address, '169.254.95.83'); + + result = decodeMDnsResponse(package3)!; + expect(result.length, 8); + expect(result, [ + TxtResourceRecord( + 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local', + result[0].validUntil, + text: '', + ), + PtrResourceRecord( + '_udisks-ssh._tcp.local', + result[1].validUntil, + domainName: 'raspberrypi._udisks-ssh._tcp.local', + ), + SrvResourceRecord( + 'raspberrypi._udisks-ssh._tcp.local', + result[2].validUntil, + target: 'raspberrypi.local', + port: 22, + priority: 0, + weight: 0, + ), + TxtResourceRecord( + 'raspberrypi._udisks-ssh._tcp.local', + result[3].validUntil, + text: '', + ), + PtrResourceRecord( + '_services._dns-sd._udp.local', + result[4].validUntil, + domainName: '_udisks-ssh._tcp.local', + ), + PtrResourceRecord( + '_workstation._tcp.local', + result[5].validUntil, + domainName: 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local', + ), + SrvResourceRecord( + 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local', + result[6].validUntil, + target: 'raspberrypi.local', + port: 9, + priority: 0, + weight: 0, + ), + PtrResourceRecord( + '_services._dns-sd._udp.local', + result[7].validUntil, + domainName: '_workstation._tcp.local', + ), + ]); + + result = decodeMDnsResponse(packagePtrResponse)!; + expect(6, result.length); + expect(result, [ + PtrResourceRecord( + '_fletch_agent._tcp.local', + result[0].validUntil, + domainName: 'fletch-agent on raspberrypi._fletch_agent._tcp.local', + ), + TxtResourceRecord( + 'fletch-agent on raspberrypi._fletch_agent._tcp.local', + result[1].validUntil, + text: '', + ), + SrvResourceRecord( + 'fletch-agent on raspberrypi._fletch_agent._tcp.local', + result[2].validUntil, + target: 'raspberrypi.local', + port: 12121, + priority: 0, + weight: 0, + ), + IPAddressResourceRecord( + 'raspberrypi.local', + result[3].validUntil, + address: InternetAddress('fe80:0000:0000:0000:ba27:ebff:fe69:6e3a'), + ), + IPAddressResourceRecord( + 'raspberrypi.local', + result[4].validUntil, + address: InternetAddress('192.168.1.1'), + ), + IPAddressResourceRecord( + 'raspberrypi.local', + result[5].validUntil, + address: InternetAddress('169.254.167.172'), + ), + ]); + }); + + // Fixes https://github.com/flutter/flutter/issues/31854 + test('Can decode packages with question, answer and additional', () { + final List result = + decodeMDnsResponse(packetWithQuestionAnArCount)!; + expect(result, isNotNull); + expect(result.length, 2); + expect(result, [ + PtrResourceRecord( + '_______________.____._____', + result[0].validUntil, + domainName: '_______________________._______________.____._____', + ), + PtrResourceRecord( + '_______________.____._____', + result[1].validUntil, + domainName: '____________________________._______________.____._____', + ), + ]); + }); + + // Fixes https://github.com/flutter/flutter/issues/31854 + test( + 'Can decode packages without question and with answer and additional', + () { + final List result = + decodeMDnsResponse(packetWithoutQuestionWithAnArCount)!; + expect(result, isNotNull); + expect(result.length, 2); + expect(result, [ + PtrResourceRecord( + '_______________.____._____', + result[0].validUntil, + domainName: '______________________._______________.____._____', + ), + TxtResourceRecord( + '______________________.____________.____._____', + result[1].validUntil, + text: 'model=MacBookPro14,3\nosxvers=18\necolor=225,225,223\n', + ), + ]); + }, + ); + + test('Can decode packages with a long text resource', () { + final List result = decodeMDnsResponse(packetWithLongTxt)!; + expect(result, isNotNull); + expect(result.length, 2); + expect(result, [ + PtrResourceRecord( + '_______________.____._____', + result[0].validUntil, + domainName: '______________________._______________.____._____', + ), + TxtResourceRecord( + '______________________.____________.____._____', + result[1].validUntil, + text: '${')' * 129}\n', + ), + ]); + }); +} + +void testBadPackages() { + test('Returns null for invalid packets', () { + for (final List p in >[package1, package2, package3]) { + for (int i = 0; i < p.length; i++) { + expect(decodeMDnsResponse(p.sublist(0, i)), isNull); + } + } + }); + + test('Detects cyclic pointers and returns null', () { + expect(decodeMDnsResponse(cycle), isNull); + }); +} + +void testPTRRData() { + test('Can read FQDN from PTR data', () { + expect( + 'sgjesse-macbookpro2 [78:31:c1:b8:55:38]._workstation._tcp.local', + readFQDN(ptrRData), + ); + expect('fletch-agent._fletch_agent._tcp.local', readFQDN(ptrRData2)); + }); +} + +void testSRVRData() { + test('Can read FQDN from SRV data', () { + expect('fletch.local', readFQDN(srvRData, _kSrvHeaderSize)); + }); +} + +void testNonUtf8DomainName() { + test('Returns non-null for non-utf8 domain name', () { + final List result = decodeMDnsResponse(nonUtf8Package)!; + expect(result, isNotNull); + expect(result[0] is TxtResourceRecord, isTrue); + final TxtResourceRecord txt = result[0] as TxtResourceRecord; + expect(txt.name, contains('�')); + }); +} + +// One address. +const List package1 = [ + 0x00, + 0x00, + 0x84, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0b, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0x05, + 0x6c, + 0x6f, + 0x63, + 0x61, + 0x6c, + 0x00, + 0x00, + 0x01, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x04, + 0xc0, + 0xa8, + 0x01, + 0xbf, +]; + +// Two addresses. +const List package2 = [ + 0x00, + 0x00, + 0x84, + 0x00, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0b, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0x05, + 0x6c, + 0x6f, + 0x63, + 0x61, + 0x6c, + 0x00, + 0x00, + 0x01, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x04, + 0xc0, + 0xa8, + 0x01, + 0xbf, + 0xc0, + 0x0c, + 0x00, + 0x01, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x04, + 0xa9, + 0xfe, + 0x5f, + 0x53, +]; + +// Eight mixed answers. +const List package3 = [ + 0x00, + 0x00, + 0x84, + 0x00, + 0x00, + 0x00, + 0x00, + 0x08, + 0x00, + 0x00, + 0x00, + 0x00, + 0x1f, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0x20, + 0x5b, + 0x62, + 0x38, + 0x3a, + 0x32, + 0x37, + 0x3a, + 0x65, + 0x62, + 0x3a, + 0x30, + 0x33, + 0x3a, + 0x39, + 0x32, + 0x3a, + 0x34, + 0x62, + 0x5d, + 0x0c, + 0x5f, + 0x77, + 0x6f, + 0x72, + 0x6b, + 0x73, + 0x74, + 0x61, + 0x74, + 0x69, + 0x6f, + 0x6e, + 0x04, + 0x5f, + 0x74, + 0x63, + 0x70, + 0x05, + 0x6c, + 0x6f, + 0x63, + 0x61, + 0x6c, + 0x00, + 0x00, + 0x10, + 0x80, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x01, + 0x00, + 0x0b, + 0x5f, + 0x75, + 0x64, + 0x69, + 0x73, + 0x6b, + 0x73, + 0x2d, + 0x73, + 0x73, + 0x68, + 0xc0, + 0x39, + 0x00, + 0x0c, + 0x00, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x0e, + 0x0b, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0xc0, + 0x50, + 0xc0, + 0x68, + 0x00, + 0x21, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x14, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x16, + 0x0b, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0xc0, + 0x3e, + 0xc0, + 0x68, + 0x00, + 0x10, + 0x80, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x01, + 0x00, + 0x09, + 0x5f, + 0x73, + 0x65, + 0x72, + 0x76, + 0x69, + 0x63, + 0x65, + 0x73, + 0x07, + 0x5f, + 0x64, + 0x6e, + 0x73, + 0x2d, + 0x73, + 0x64, + 0x04, + 0x5f, + 0x75, + 0x64, + 0x70, + 0xc0, + 0x3e, + 0x00, + 0x0c, + 0x00, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x02, + 0xc0, + 0x50, + 0xc0, + 0x2c, + 0x00, + 0x0c, + 0x00, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x02, + 0xc0, + 0x0c, + 0xc0, + 0x0c, + 0x00, + 0x21, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x08, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x09, + 0xc0, + 0x88, + 0xc0, + 0xa3, + 0x00, + 0x0c, + 0x00, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x02, + 0xc0, + 0x2c, +]; + +/// Contains compressed domain names where a there is a cycle amongst the +/// offset pointers. +const List cycle = [ + 0x00, + 0x00, + 0x84, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // "example" + 0xC0, 0x16, // Pointer to "com" + 0x03, 0x63, 0x6f, 0x6d, // "com" + 0xC0, 0x0c, // Pointer to "example" + 0x00, + 0x00, + 0x01, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x04, + 0xc0, + 0xa8, + 0x01, + 0xbf, +]; + +const List packagePtrResponse = [ + 0x00, + 0x00, + 0x84, + 0x00, + 0x00, + 0x00, + 0x00, + 0x06, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0d, + 0x5f, + 0x66, + 0x6c, + 0x65, + 0x74, + 0x63, + 0x68, + 0x5f, + 0x61, + 0x67, + 0x65, + 0x6e, + 0x74, + 0x04, + 0x5f, + 0x74, + 0x63, + 0x70, + 0x05, + 0x6c, + 0x6f, + 0x63, + 0x61, + 0x6c, + 0x00, + 0x00, + 0x0c, + 0x00, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x1e, + 0x1b, + 0x66, + 0x6c, + 0x65, + 0x74, + 0x63, + 0x68, + 0x2d, + 0x61, + 0x67, + 0x65, + 0x6e, + 0x74, + 0x20, + 0x6f, + 0x6e, + 0x20, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0xc0, + 0x0c, + 0xc0, + 0x30, + 0x00, + 0x10, + 0x80, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x01, + 0x00, + 0xc0, + 0x30, + 0x00, + 0x21, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x14, + 0x00, + 0x00, + 0x00, + 0x00, + 0x2f, + 0x59, + 0x0b, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0xc0, + 0x1f, + 0xc0, + 0x6d, + 0x00, + 0x1c, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x10, + 0xfe, + 0x80, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xba, + 0x27, + 0xeb, + 0xff, + 0xfe, + 0x69, + 0x6e, + 0x3a, + 0xc0, + 0x6d, + 0x00, + 0x01, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x04, + 0xc0, + 0xa8, + 0x01, + 0x01, + 0xc0, + 0x6d, + 0x00, + 0x01, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x04, + 0xa9, + 0xfe, + 0xa7, + 0xac, +]; + +const List ptrRData = [ + 0x27, + 0x73, + 0x67, + 0x6a, + 0x65, + 0x73, + 0x73, + 0x65, + 0x2d, + 0x6d, + 0x61, + 0x63, + 0x62, + 0x6f, + 0x6f, + 0x6b, + 0x70, + 0x72, + 0x6f, + 0x32, + 0x20, + 0x5b, + 0x37, + 0x38, + 0x3a, + 0x33, + 0x31, + 0x3a, + 0x63, + 0x31, + 0x3a, + 0x62, + 0x38, + 0x3a, + 0x35, + 0x35, + 0x3a, + 0x33, + 0x38, + 0x5d, + 0x0c, + 0x5f, + 0x77, + 0x6f, + 0x72, + 0x6b, + 0x73, + 0x74, + 0x61, + 0x74, + 0x69, + 0x6f, + 0x6e, + 0x04, + 0x5f, + 0x74, + 0x63, + 0x70, + 0x05, + 0x6c, + 0x6f, + 0x63, + 0x61, + 0x6c, + 0x00, +]; + +const List ptrRData2 = [ + 0x0c, + 0x66, + 0x6c, + 0x65, + 0x74, + 0x63, + 0x68, + 0x2d, + 0x61, + 0x67, + 0x65, + 0x6e, + 0x74, + 0x0d, + 0x5f, + 0x66, + 0x6c, + 0x65, + 0x74, + 0x63, + 0x68, + 0x5f, + 0x61, + 0x67, + 0x65, + 0x6e, + 0x74, + 0x04, + 0x5f, + 0x74, + 0x63, + 0x70, + 0x05, + 0x6c, + 0x6f, + 0x63, + 0x61, + 0x6c, + 0x00, +]; + +const List srvRData = [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x2f, + 0x59, + 0x06, + 0x66, + 0x6c, + 0x65, + 0x74, + 0x63, + 0x68, + 0x05, + 0x6c, + 0x6f, + 0x63, + 0x61, + 0x6c, + 0x00, +]; + +const List packetWithQuestionAnArCount = [ + 0, + 0, + 2, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 15, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 4, + 95, + 95, + 95, + 95, + 5, + 95, + 95, + 95, + 95, + 95, + 0, + 0, + 12, + 0, + 1, + 192, + 12, + 0, + 12, + 0, + 1, + 0, + 0, + 14, + 13, + 0, + 26, + 23, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 192, + 12, + 192, + 12, + 0, + 12, + 0, + 1, + 0, + 0, + 14, + 13, + 0, + 31, + 28, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 192, + 12, +]; + +const List packetWithoutQuestionWithAnArCount = [ + 0, + 0, + 132, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 15, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 4, + 95, + 95, + 95, + 95, + 5, + 95, + 95, + 95, + 95, + 95, + 0, + 0, + 12, + 0, + 1, + 0, + 0, + 17, + 148, + 0, + 25, + 22, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 192, + 12, + 22, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 12, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 192, + 28, + 0, + 16, + 0, + 1, + 0, + 0, + 17, + 148, + 0, + 51, + 20, + 109, + 111, + 100, + 101, + 108, + 61, + 77, + 97, + 99, + 66, + 111, + 111, + 107, + 80, + 114, + 111, + 49, + 52, + 44, + 51, + 10, + 111, + 115, + 120, + 118, + 101, + 114, + 115, + 61, + 49, + 56, + 18, + 101, + 99, + 111, + 108, + 111, + 114, + 61, + 50, + 50, + 53, + 44, + 50, + 50, + 53, + 44, + 50, + 50, + 51, +]; + +// This is the same as packetWithoutQuestionWithAnArCount, but the text +// resource just has a single long string. If the length isn't decoded +// separately from the string, there will be utf8 decoding failures. +const List packetWithLongTxt = [ + 0, + 0, + 132, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 15, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 4, + 95, + 95, + 95, + 95, + 5, + 95, + 95, + 95, + 95, + 95, + 0, + 0, + 12, + 0, + 1, + 0, + 0, + 17, + 148, + 0, + 25, + 22, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 192, + 12, + 22, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 12, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 95, + 192, + 28, + 0, + 16, + 0, + 1, + 0, + 0, + 17, + 148, + 0, + 51, + // Long string starts here. + 129, + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 16 + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 32 + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 64 + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 128, + 41, // 129 +]; + +// Package with a domain name that is not valid utf-8. +const List nonUtf8Package = [ + 0x00, + 0x00, + 0x84, + 0x00, + 0x00, + 0x00, + 0x00, + 0x08, + 0x00, + 0x00, + 0x00, + 0x00, + 0x1f, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0x20, + 0x5b, + 0x62, + 0x38, + 0x3a, + 0x32, + 0x37, + 0x3a, + 0x65, + 0x62, + 0xd2, + 0x30, + 0x33, + 0x3a, + 0x39, + 0x32, + 0x3a, + 0x34, + 0x62, + 0x5d, + 0x0c, + 0x5f, + 0x77, + 0x6f, + 0x72, + 0x6b, + 0x73, + 0x74, + 0x61, + 0x74, + 0x69, + 0x6f, + 0x6e, + 0x04, + 0x5f, + 0x74, + 0x63, + 0x70, + 0x05, + 0x6c, + 0x6f, + 0x63, + 0x61, + 0x6c, + 0x00, + 0x00, + 0x10, + 0x80, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x01, + 0x00, + 0x0b, + 0x5f, + 0x75, + 0x64, + 0x69, + 0x73, + 0x6b, + 0x73, + 0x2d, + 0x73, + 0x73, + 0x68, + 0xc0, + 0x39, + 0x00, + 0x0c, + 0x00, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x0e, + 0x0b, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0xc0, + 0x50, + 0xc0, + 0x68, + 0x00, + 0x21, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x14, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x16, + 0x0b, + 0x72, + 0x61, + 0x73, + 0x70, + 0x62, + 0x65, + 0x72, + 0x72, + 0x79, + 0x70, + 0x69, + 0xc0, + 0x3e, + 0xc0, + 0x68, + 0x00, + 0x10, + 0x80, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x01, + 0x00, + 0x09, + 0x5f, + 0x73, + 0x65, + 0x72, + 0x76, + 0x69, + 0x63, + 0x65, + 0x73, + 0x07, + 0x5f, + 0x64, + 0x6e, + 0x73, + 0x2d, + 0x73, + 0x64, + 0x04, + 0x5f, + 0x75, + 0x64, + 0x70, + 0xc0, + 0x3e, + 0x00, + 0x0c, + 0x00, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x02, + 0xc0, + 0x50, + 0xc0, + 0x2c, + 0x00, + 0x0c, + 0x00, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x02, + 0xc0, + 0x0c, + 0xc0, + 0x0c, + 0x00, + 0x21, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x78, + 0x00, + 0x08, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x09, + 0xc0, + 0x88, + 0xc0, + 0xa3, + 0x00, + 0x0c, + 0x00, + 0x01, + 0x00, + 0x00, + 0x11, + 0x94, + 0x00, + 0x02, + 0xc0, + 0x2c, +]; diff --git a/multicast_dns_source_ref/test/lookup_resolver_test.dart b/multicast_dns_source_ref/test/lookup_resolver_test.dart new file mode 100644 index 00000000..1188ec26 --- /dev/null +++ b/multicast_dns_source_ref/test/lookup_resolver_test.dart @@ -0,0 +1,132 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:multicast_dns/src/lookup_resolver.dart'; +import 'package:multicast_dns/src/resource_record.dart'; +import 'package:test/test.dart'; + +void main() { + testTimeout(); + testResult(); + testResult2(); + testResult3(); +} + +ResourceRecord ip4Result(String name, InternetAddress address) { + final int validUntil = DateTime.now().millisecondsSinceEpoch + 2000; + return IPAddressResourceRecord(name, validUntil, address: address); +} + +void testTimeout() { + test('Resolver does not return with short timeout', () async { + const Duration shortTimeout = Duration(milliseconds: 1); + final LookupResolver resolver = LookupResolver(); + final Stream result = resolver.addPendingRequest( + ResourceRecordType.addressIPv4, + 'xxx', + shortTimeout, + ); + expect(await result.isEmpty, isTrue); + }); +} + +// One pending request and one response. +void testResult() { + test('One pending request and one response', () async { + const Duration noTimeout = Duration(days: 1); + final LookupResolver resolver = LookupResolver(); + final Stream futureResult = resolver.addPendingRequest( + ResourceRecordType.addressIPv4, + 'xxx.local', + noTimeout, + ); + final ResourceRecord response = ip4Result( + 'xxx.local', + InternetAddress('1.2.3.4'), + ); + resolver.handleResponse([response]); + final IPAddressResourceRecord result = + await futureResult.first as IPAddressResourceRecord; + expect('1.2.3.4', result.address.address); + resolver.clearPendingRequests(); + }); +} + +void testResult2() { + test('Two requests', () async { + const Duration noTimeout = Duration(days: 1); + final LookupResolver resolver = LookupResolver(); + final Stream futureResult1 = resolver.addPendingRequest( + ResourceRecordType.addressIPv4, + 'xxx.local', + noTimeout, + ); + final Stream futureResult2 = resolver.addPendingRequest( + ResourceRecordType.addressIPv4, + 'yyy.local', + noTimeout, + ); + final ResourceRecord response1 = ip4Result( + 'xxx.local', + InternetAddress('1.2.3.4'), + ); + final ResourceRecord response2 = ip4Result( + 'yyy.local', + InternetAddress('2.3.4.5'), + ); + resolver.handleResponse([response2, response1]); + final IPAddressResourceRecord result1 = + await futureResult1.first as IPAddressResourceRecord; + final IPAddressResourceRecord result2 = + await futureResult2.first as IPAddressResourceRecord; + expect('1.2.3.4', result1.address.address); + expect('2.3.4.5', result2.address.address); + resolver.clearPendingRequests(); + }); +} + +void testResult3() { + test('Multiple requests', () async { + const Duration noTimeout = Duration(days: 1); + final LookupResolver resolver = LookupResolver(); + final ResourceRecord response0 = ip4Result( + 'zzz.local', + InternetAddress('2.3.4.5'), + ); + resolver.handleResponse([response0]); + final Stream futureResult1 = resolver.addPendingRequest( + ResourceRecordType.addressIPv4, + 'xxx.local', + noTimeout, + ); + resolver.handleResponse([response0]); + final Stream futureResult2 = resolver.addPendingRequest( + ResourceRecordType.addressIPv4, + 'yyy.local', + noTimeout, + ); + resolver.handleResponse([response0]); + final ResourceRecord response1 = ip4Result( + 'xxx.local', + InternetAddress('1.2.3.4'), + ); + resolver.handleResponse([response0]); + final ResourceRecord response2 = ip4Result( + 'yyy.local', + InternetAddress('2.3.4.5'), + ); + resolver.handleResponse([response0]); + resolver.handleResponse([response2, response1]); + resolver.handleResponse([response0]); + final IPAddressResourceRecord result1 = + await futureResult1.first as IPAddressResourceRecord; + final IPAddressResourceRecord result2 = + await futureResult2.first as IPAddressResourceRecord; + expect('1.2.3.4', result1.address.address); + expect('2.3.4.5', result2.address.address); + resolver.clearPendingRequests(); + }); +} diff --git a/multicast_dns_source_ref/test/resource_record_cache_test.dart b/multicast_dns_source_ref/test/resource_record_cache_test.dart new file mode 100644 index 00000000..11be6276 --- /dev/null +++ b/multicast_dns_source_ref/test/resource_record_cache_test.dart @@ -0,0 +1,85 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Test that the resource record cache works correctly. In particular, make +// sure that it removes all entries for a name before insertingrecords +// of that name. + +import 'dart:io'; + +import 'package:multicast_dns/src/native_protocol_client.dart' + show ResourceRecordCache; +import 'package:multicast_dns/src/resource_record.dart'; +import 'package:test/test.dart'; + +void main() { + testOverwrite(); + testTimeout(); +} + +void testOverwrite() { + test('Cache can overwrite entries', () { + final InternetAddress ip1 = InternetAddress('192.168.1.1'); + final InternetAddress ip2 = InternetAddress('192.168.1.2'); + final int valid = DateTime.now().millisecondsSinceEpoch + 86400 * 1000; + + final ResourceRecordCache cache = ResourceRecordCache(); + + // Add two different records. + cache.updateRecords([ + IPAddressResourceRecord('hest', valid, address: ip1), + IPAddressResourceRecord('fisk', valid, address: ip2), + ]); + expect(cache.entryCount, 2); + + // Update these records. + cache.updateRecords([ + IPAddressResourceRecord('hest', valid, address: ip1), + IPAddressResourceRecord('fisk', valid, address: ip2), + ]); + expect(cache.entryCount, 2); + + // Add two records with the same name (should remove the old one + // with that name only.) + cache.updateRecords([ + IPAddressResourceRecord('hest', valid, address: ip1), + IPAddressResourceRecord('hest', valid, address: ip2), + ]); + expect(cache.entryCount, 3); + + // Overwrite the two cached entries with one with the same name. + cache.updateRecords([ + IPAddressResourceRecord('hest', valid, address: ip1), + ]); + expect(cache.entryCount, 2); + }); +} + +void testTimeout() { + test('Cache can evict records after timeout', () { + final InternetAddress ip1 = InternetAddress('192.168.1.1'); + final int valid = DateTime.now().millisecondsSinceEpoch + 86400 * 1000; + final int notValid = DateTime.now().millisecondsSinceEpoch - 1; + + final ResourceRecordCache cache = ResourceRecordCache(); + + cache.updateRecords([ + IPAddressResourceRecord('hest', valid, address: ip1), + ]); + expect(cache.entryCount, 1); + + cache.updateRecords([ + IPAddressResourceRecord('fisk', notValid, address: ip1), + ]); + + List results = []; + cache.lookup('hest', ResourceRecordType.addressIPv4, results); + expect(results.isEmpty, isFalse); + + results = []; + cache.lookup('fisk', ResourceRecordType.addressIPv4, results); + expect(results.isEmpty, isTrue); + expect(cache.entryCount, 1); + }); +} diff --git a/multicast_dns_source_ref/tool/packet_gen.dart b/multicast_dns_source_ref/tool/packet_gen.dart new file mode 100644 index 00000000..6e1e278b --- /dev/null +++ b/multicast_dns_source_ref/tool/packet_gen.dart @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +// Support code to generate the hex-lists in test/decode_test.dart from +// a hex-stream. +import 'dart:io'; + +void formatHexStream(String hexStream) { + String s = ''; + for (int i = 0; i < hexStream.length / 2; i++) { + if (s.isNotEmpty) { + s += ', '; + } + s += '0x'; + final String x = hexStream.substring(i * 2, i * 2 + 2); + s += x; + if (((i + 1) % 8) == 0) { + s += ','; + print(s); + s = ''; + } + } + if (s.isNotEmpty) { + print(s); + } +} + +// Support code for generating the hex-lists in test/decode_test.dart. +void hexDumpList(List package) { + String s = ''; + for (int i = 0; i < package.length; i++) { + if (s.isNotEmpty) { + s += ', '; + } + s += '0x'; + final String x = package[i].toRadixString(16); + if (x.length == 1) { + s += '0'; + } + s += x; + if (((i + 1) % 8) == 0) { + s += ','; + print(s); + s = ''; + } + } + if (s.isNotEmpty) { + print(s); + } +} + +void dumpDatagram(Datagram datagram) { + String toHex(List ints) { + final StringBuffer buffer = StringBuffer(); + for (int i = 0; i < ints.length; i++) { + buffer.write(ints[i].toRadixString(16).padLeft(2, '0')); + if ((i + 1) % 10 == 0) { + buffer.writeln(); + } else { + buffer.write(' '); + } + } + return buffer.toString(); + } + + print('${datagram.address.address}:${datagram.port}:'); + print(toHex(datagram.data)); + print(''); +} From a349fe7d8d59cc77fa6cffeba04e5a8fb10590d5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 02:03:44 +0000 Subject: [PATCH 2/3] feat: Implement Bonjour service discovery for iOS clients This change adds support for zero-configuration networking (Bonjour/mDNS) to allow iOS clients to automatically discover and connect to servers running on the local network. On the server side, a new `Bonjour` class has been added to handle the advertising of the service. It manually constructs and broadcasts mDNS packets containing the necessary PTR, SRV, and A records. This is done without modifying the `multicast_dns` package, by using a `RawDatagramSocket`. On the client side, a new server discovery dialog has been added to the settings menu. This dialog uses the `multicast_dns` package to discover servers advertising the `_frosthaven._tcp.local` service. When a user selects a server from the list, the IP address and port are automatically populated in the connection fields. --- .../Layout/menus/server_discovery_dialog.dart | 80 ++++++++++ .../lib/Layout/menus/settings_menu.dart | 41 +++-- .../lib/services/network/client.dart | 39 +++++ frosthaven_assistant/pubspec.yaml | 1 + frosthaven_assistant_server/lib/bonjour.dart | 148 ++++++++++++++++++ .../lib/game_server.dart | 4 + .../lib/standalone_server.dart | 15 +- frosthaven_assistant_server/pubspec.yaml | 1 + 8 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 frosthaven_assistant/lib/Layout/menus/server_discovery_dialog.dart create mode 100644 frosthaven_assistant_server/lib/bonjour.dart diff --git a/frosthaven_assistant/lib/Layout/menus/server_discovery_dialog.dart b/frosthaven_assistant/lib/Layout/menus/server_discovery_dialog.dart new file mode 100644 index 00000000..31e01070 --- /dev/null +++ b/frosthaven_assistant/lib/Layout/menus/server_discovery_dialog.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:frosthaven_assistant/services/network/client.dart'; +import 'package:frosthaven_assistant/services/service_locator.dart'; + +class ServerDiscoveryDialog extends StatefulWidget { + const ServerDiscoveryDialog({super.key}); + + @override + State createState() => _ServerDiscoveryDialogState(); +} + +class _ServerDiscoveryDialogState extends State { + final _client = getIt(); + List _servers = []; + bool _isDiscovering = false; + + @override + void initState() { + super.initState(); + _discoverServers(); + } + + void _discoverServers() { + setState(() { + _isDiscovering = true; + _servers = []; + }); + _client.discoverServers().listen((server) { + setState(() { + if (!_servers.any((s) => s.address == server.address && s.port == server.port)) { + _servers.add(server); + } + }); + }).onDone(() { + setState(() { + _isDiscovering = false; + }); + }); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Discover Servers'), + content: SizedBox( + width: 400, + height: 300, + child: _isDiscovering && _servers.isEmpty + ? const Center(child: CircularProgressIndicator()) + : _servers.isEmpty + ? const Center(child: Text('No servers found.')) + : ListView.builder( + itemCount: _servers.length, + itemBuilder: (context, index) { + final server = _servers[index]; + return ListTile( + title: Text(server.name), + subtitle: Text('${server.address}:${server.port}'), + onTap: () { + Navigator.of(context).pop(server); + }, + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: _isDiscovering ? null : _discoverServers, + child: const Text('Refresh'), + ), + ], + ); + } +} diff --git a/frosthaven_assistant/lib/Layout/menus/settings_menu.dart b/frosthaven_assistant/lib/Layout/menus/settings_menu.dart index f542aecd..38813633 100644 --- a/frosthaven_assistant/lib/Layout/menus/settings_menu.dart +++ b/frosthaven_assistant/lib/Layout/menus/settings_menu.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:frosthaven_assistant/Layout/menus/save_menu.dart'; +import 'package:frosthaven_assistant/Layout/menus/server_discovery_dialog.dart'; import 'package:frosthaven_assistant/Resource/commands/clear_unlocked_classes_command.dart'; import 'package:frosthaven_assistant/Resource/commands/set_ally_deck_in_og_gloom_command.dart'; import 'package:frosthaven_assistant/Resource/commands/track_standees_command.dart'; @@ -443,17 +444,35 @@ class SettingsMenuState extends State { }); }), - Container( - margin: const EdgeInsets.only(top: 10), - width: 200, - height: 40, - child: TextField( - controller: _serverTextController, - decoration: const InputDecoration( - counterText: "", - helperText: "server ip address", - ), - maxLength: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: const EdgeInsets.only(top: 10), + width: 200, + height: 40, + child: TextField( + controller: _serverTextController, + decoration: const InputDecoration( + counterText: "", + helperText: "server ip address", + ), + maxLength: 20), + ), + IconButton( + icon: const Icon(Icons.search), + onPressed: () async { + final result = await showDialog( + context: context, + builder: (context) => const ServerDiscoveryDialog(), + ); + if (result != null) { + _serverTextController.text = result.address; + _portTextController.text = result.port.toString(); + } + }, + ), + ], ), Container( diff --git a/frosthaven_assistant/lib/services/network/client.dart b/frosthaven_assistant/lib/services/network/client.dart index 602db647..bab62206 100644 --- a/frosthaven_assistant/lib/services/network/client.dart +++ b/frosthaven_assistant/lib/services/network/client.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:frosthaven_assistant/services/network/communication.dart'; import 'package:frosthaven_assistant/services/network/network.dart'; +import 'package:multicast_dns/multicast_dns.dart'; import '../../Resource/settings.dart'; import '../../Resource/state/game_state.dart'; @@ -179,4 +180,42 @@ class Client { } _serverResponsive = true; } + + Stream discoverServers() async* { + final MDnsClient client = MDnsClient(); + await client.start(); + + const String name = '_frosthaven._tcp.local'; + await for (final PtrResourceRecord ptr in client.lookup( + ResourceRecordQuery.serverPointer(name), + )) { + await for (final SrvResourceRecord srv in client.lookup( + ResourceRecordQuery.service(ptr.domainName), + )) { + await for (final IPAddressResourceRecord ip + in client.lookup( + ResourceRecordQuery.addressIPv4(srv.target), + )) { + yield DiscoveredServer( + name: ptr.domainName, + address: ip.address.address, + port: srv.port, + ); + } + } + } + client.stop(); + } +} + +class DiscoveredServer { + DiscoveredServer({ + required this.name, + required this.address, + required this.port, + }); + + final String name; + final String address; + final int port; } diff --git a/frosthaven_assistant/pubspec.yaml b/frosthaven_assistant/pubspec.yaml index 6d2e1a98..d43f9167 100644 --- a/frosthaven_assistant/pubspec.yaml +++ b/frosthaven_assistant/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: format: ^1.4.0 frosthaven_assistant_server: path: ../frosthaven_assistant_server + multicast_dns: ^0.3.3 diff --git a/frosthaven_assistant_server/lib/bonjour.dart b/frosthaven_assistant_server/lib/bonjour.dart new file mode 100644 index 00000000..24e24341 --- /dev/null +++ b/frosthaven_assistant_server/lib/bonjour.dart @@ -0,0 +1,148 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:convert'; + +import 'package:multicast_dns/src/constants.dart'; +import 'package:multicast_dns/src/resource_record.dart'; + +class Bonjour { + RawDatagramSocket? _socket; + + Future advertise(String ip, int port) async { + // Stop any existing advertising. + stop(); + + const String serviceName = '_frosthaven._tcp.local'; + final String serverName = 'Frosthaven Assistant Server.$serviceName'; + + _socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, mDnsPort); + _socket!.joinMulticast(mDnsAddressIPv4); + + final PtrResourceRecord ptr = PtrResourceRecord( + serviceName, + 3600, + domainName: serverName, + ); + + final SrvResourceRecord srv = SrvResourceRecord( + serverName, + 3600, + port: port, + target: ip, + weight: 0, + priority: 0, + ); + + final IPAddressResourceRecord ipAddress = IPAddressResourceRecord( + ip, + 3600, + address: InternetAddress(ip), + ); + + final List packet = _buildResponse([ptr, srv, ipAddress]); + _socket!.send(packet, mDnsAddressIPv4, mDnsPort); + } + + void stop() { + _socket?.close(); + _socket = null; + } + + List _buildResponse(List records) { + final writer = _MDnsWriter(isQuery: false); + writer.writeRecords(records); + return writer.toUint8List(); + } +} + +class _MDnsWriter { + _MDnsWriter({this.isQuery = true}) { + // ID is always 0 + _byteData.setUint16(0, 0); + // Flags + _byteData.setUint16(2, isQuery ? 0 : 0x8400); // Authoritative answer + // Question count + _byteData.setUint16(4, 0); + // Answer count + _byteData.setUint16(6, 0); + // Authority count + _byteData.setUint16(8, 0); + // Additional count + _byteData.setUint16(10, 0); + } + + final bool isQuery; + final Uint8List _data = Uint8List(4096); + late final ByteData _byteData = ByteData.view(_data.buffer); + int _offset = 12; + final Map _fqdnOffsets = {}; + + void writeFQDN(String fqdn) { + if (_fqdnOffsets.containsKey(fqdn)) { + final int fqdnOffset = _fqdnOffsets[fqdn]!; + _byteData.setUint16(_offset, 0xc000 | fqdnOffset); + _offset += 2; + return; + } + + _fqdnOffsets[fqdn] = _offset; + final List parts = fqdn.split('.'); + for (final String part in parts) { + final List partBytes = utf8.encode(part); + _data[_offset++] = partBytes.length; + _data.setRange(_offset, _offset + partBytes.length, partBytes); + _offset += partBytes.length; + } + _data[_offset++] = 0; + } + + void writeRecord(ResourceRecord record) { + writeFQDN(record.fullyQualifiedName); + _byteData.setUint16(_offset, record.resourceRecordType); + _offset += 2; + _byteData.setUint16(_offset, ResourceRecordClass.internet | 0x8000); // Cache flush + _offset += 2; + _byteData.setUint32(_offset, record.ttl); + _offset += 4; + + final int dataLengthOffset = _offset; + _offset += 2; + + final int dataStartOffset = _offset; + + if (record is IPAddressResourceRecord) { + final List addressBytes = record.address.rawAddress; + _data.setRange(_offset, _offset + addressBytes.length, addressBytes); + _offset += addressBytes.length; + } else if (record is PtrResourceRecord) { + writeFQDN(record.domainName); + } else if (record is SrvResourceRecord) { + _byteData.setUint16(_offset, record.priority); + _offset += 2; + _byteData.setUint16(_offset, record.weight); + _offset += 2; + _byteData.setUint16(_offset, record.port); + _offset += 2; + writeFQDN(record.target); + } else if (record is TxtResourceRecord) { + final List textBytes = utf8.encode(record.text); + _data[_offset++] = textBytes.length; + _data.setRange(_offset, _offset + textBytes.length, textBytes); + _offset += textBytes.length; + } + + final int dataLength = _offset - dataStartOffset; + _byteData.setUint16(dataLengthOffset, dataLength); + } + + void writeRecords(List records) { + _byteData.setUint16(6, records.length); + for (final ResourceRecord record in records) { + writeRecord(record); + } + } + + Uint8List toUint8List() { + return Uint8List.view(_data.buffer, 0, _offset); + } +} diff --git a/frosthaven_assistant_server/lib/game_server.dart b/frosthaven_assistant_server/lib/game_server.dart index 94906a05..4a5686fa 100644 --- a/frosthaven_assistant_server/lib/game_server.dart +++ b/frosthaven_assistant_server/lib/game_server.dart @@ -48,6 +48,8 @@ abstract class GameServer { void send(String data); String currentStateMessage(String commandDescription); Future getConnectToIP(); + void startAdvertising(String ip, int port); + void stopAdvertising(); void sendPing(); void addClientConnection(Socket client); @@ -84,6 +86,7 @@ abstract class GameServer { 'Server Online: IP: $connectTo, Port: ${server.port.toString()}'; log(info); setNetworkMessage(info); + startAdvertising(connectTo, server.port); resetState(); send(currentStateMessage("")); var subscriptions = server.listen((Socket client) { @@ -119,6 +122,7 @@ abstract class GameServer { serverEnabled = false; leftOverMessage = ""; + stopAdvertising(); resetState(); } diff --git a/frosthaven_assistant_server/lib/standalone_server.dart b/frosthaven_assistant_server/lib/standalone_server.dart index 4aa71b8f..b8391481 100644 --- a/frosthaven_assistant_server/lib/standalone_server.dart +++ b/frosthaven_assistant_server/lib/standalone_server.dart @@ -2,11 +2,15 @@ import 'dart:developer'; import 'dart:io'; +import 'dart:io'; + +import 'package:frosthaven_assistant_server/bonjour.dart'; import 'package:frosthaven_assistant_server/connection_health.dart'; import 'package:frosthaven_assistant_server/game_server.dart'; import 'package:frosthaven_assistant_server/server_state.dart'; class StandaloneServer extends GameServer { + final Bonjour _bonjour = Bonjour(); final List _clientConnections = List.empty(growable: true); final ServerState _state = ServerState(); @@ -243,5 +247,14 @@ class StandaloneServer extends GameServer { return "Closed client: "; } } - + + @override + void startAdvertising(String ip, int port) { + _bonjour.advertise(ip, port); + } + + @override + void stopAdvertising() { + _bonjour.stop(); + } } diff --git a/frosthaven_assistant_server/pubspec.yaml b/frosthaven_assistant_server/pubspec.yaml index 2e128374..dff8ad0c 100644 --- a/frosthaven_assistant_server/pubspec.yaml +++ b/frosthaven_assistant_server/pubspec.yaml @@ -8,6 +8,7 @@ environment: # Add regular dependencies here. dependencies: + multicast_dns: ^0.3.3 # path: ^1.8.0 dev_dependencies: From a01dd2a311f03b40672dedf795afdb5b6e1f6f6f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 03:46:57 +0000 Subject: [PATCH 3/3] feat: Implement Bonjour service discovery for iOS clients This change adds support for zero-configuration networking (Bonjour/mDNS) to allow iOS clients to automatically discover and connect to servers running on the local network. On the server side, a new `Bonjour` class has been added to handle the advertising of the service. It manually constructs and broadcasts mDNS packets containing the necessary PTR, SRV, and A records. This is done without modifying the `multicast_dns` package, by using a `RawDatagramSocket`. On the client side, a new server discovery dialog has been added to the settings menu. This dialog uses the `multicast_dns` package to discover servers advertising the `_frosthaven._tcp.local` service. When a user selects a server from the list, the IP address and port are automatically populated in the connection fields. --- multicast_dns_source_ref/AUTHORS | 7 - multicast_dns_source_ref/CHANGELOG.md | 80 - multicast_dns_source_ref/LICENSE | 25 - multicast_dns_source_ref/README.md | 13 - multicast_dns_source_ref/dart_test.yaml | 1 - multicast_dns_source_ref/example/main.dart | 43 - .../example/mdns_resolve.dart | 36 - multicast_dns_source_ref/example/mdns_sd.dart | 71 - .../lib/multicast_dns.dart | 272 --- .../lib/src/constants.dart | 37 - .../lib/src/lookup_resolver.dart | 99 - .../lib/src/native_protocol_client.dart | 83 - multicast_dns_source_ref/lib/src/packet.dart | 396 ---- .../lib/src/resource_record.dart | 346 ---- multicast_dns_source_ref/pubspec.yaml | 19 - .../test/client_test.dart | 295 --- .../test/decode_test.dart | 1648 ----------------- .../test/lookup_resolver_test.dart | 132 -- .../test/resource_record_cache_test.dart | 85 - multicast_dns_source_ref/tool/packet_gen.dart | 72 - 20 files changed, 3760 deletions(-) delete mode 100644 multicast_dns_source_ref/AUTHORS delete mode 100644 multicast_dns_source_ref/CHANGELOG.md delete mode 100644 multicast_dns_source_ref/LICENSE delete mode 100644 multicast_dns_source_ref/README.md delete mode 100644 multicast_dns_source_ref/dart_test.yaml delete mode 100644 multicast_dns_source_ref/example/main.dart delete mode 100644 multicast_dns_source_ref/example/mdns_resolve.dart delete mode 100644 multicast_dns_source_ref/example/mdns_sd.dart delete mode 100644 multicast_dns_source_ref/lib/multicast_dns.dart delete mode 100644 multicast_dns_source_ref/lib/src/constants.dart delete mode 100644 multicast_dns_source_ref/lib/src/lookup_resolver.dart delete mode 100644 multicast_dns_source_ref/lib/src/native_protocol_client.dart delete mode 100644 multicast_dns_source_ref/lib/src/packet.dart delete mode 100644 multicast_dns_source_ref/lib/src/resource_record.dart delete mode 100644 multicast_dns_source_ref/pubspec.yaml delete mode 100644 multicast_dns_source_ref/test/client_test.dart delete mode 100644 multicast_dns_source_ref/test/decode_test.dart delete mode 100644 multicast_dns_source_ref/test/lookup_resolver_test.dart delete mode 100644 multicast_dns_source_ref/test/resource_record_cache_test.dart delete mode 100644 multicast_dns_source_ref/tool/packet_gen.dart diff --git a/multicast_dns_source_ref/AUTHORS b/multicast_dns_source_ref/AUTHORS deleted file mode 100644 index add10412..00000000 --- a/multicast_dns_source_ref/AUTHORS +++ /dev/null @@ -1,7 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Dartino project authors diff --git a/multicast_dns_source_ref/CHANGELOG.md b/multicast_dns_source_ref/CHANGELOG.md deleted file mode 100644 index 6c4cbfa7..00000000 --- a/multicast_dns_source_ref/CHANGELOG.md +++ /dev/null @@ -1,80 +0,0 @@ -## NEXT - -* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. - -## 0.3.3 - -* Adds an optional error callback for `MDnsClient::start` to prevent uncaught exceptions. - -## 0.3.2+8 - -* Fixes stack overflows ocurring during the parsing of domain names in MDNS messages. -* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. - -## 0.3.2+7 - -* Optimized Socket Binding: Always bind to 0.0.0.0 for simplicity and efficiency. -* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2. - -## 0.3.2+6 - -* Improves links in README.md. - -## 0.3.2+5 - -* Updates `PendingRequest` to be a `base class` for Dart 3.0 compatibility. -* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. - -## 0.3.2+4 - -* Adds pub topics to package metadata. -* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19. - -## 0.3.2+3 - -* Removes use of `runtimeType.toString()`. -* Updates minimum SDK version to Flutter 3.0. - -## 0.3.2+2 - -* Fixes lints warnings. - -## 0.3.2+1 - -* Migrates from `ui.hash*` to `Object.hash*`. - -## 0.3.2 - -* Updates package description. -* Make [MDnsClient.start] idempotent. - -## 0.3.1 - -* Close IPv6 sockets on [MDnsClient.stop]. - -## 0.3.0+1 - -* Removed redundant link in README.md file. - -## 0.3.0 - -* Migrate package to null safety. - -## 0.2.2 -* Fixes parsing of TXT records. Continues parsing on non-utf8 strings. - -## 0.2.1 -* Fixes the handling of packets containing non-utf8 strings. - -## 0.2.0 -* Allow configuration of the port and address the mdns query is performed on. - -## 0.1.1 - -* Fixes [flutter/issue/31854](https://github.com/flutter/flutter/issues/31854) where `decodeMDnsResponse` advanced to incorrect code points and ignored some records. - -## 0.1.0 - -* Initial Open Source release. -* Migrates the dartino-sdk's mDNS client to Dart 2.0 and Flutter's analysis rules -* Breaks from original Dartino code, as it does not use native libraries for macOS and overhauls the `ResourceRecord` class. diff --git a/multicast_dns_source_ref/LICENSE b/multicast_dns_source_ref/LICENSE deleted file mode 100644 index c6823b81..00000000 --- a/multicast_dns_source_ref/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/multicast_dns_source_ref/README.md b/multicast_dns_source_ref/README.md deleted file mode 100644 index f30ce169..00000000 --- a/multicast_dns_source_ref/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Multicast DNS package - -Based on [RFC 6762 Multicast DNS](https://datatracker.ietf.org/doc/html/rfc6762). - -[![pub package](https://img.shields.io/pub/v/multicast_dns.svg)]( -https://pub.dartlang.org/packages/multicast_dns) - -A Dart package to do service discovery over multicast DNS (mDNS), Bonjour, and Avahi. - -## Usage - -[The example](https://pub.dev/packages/multicast_dns/example) demonstrates how -to use the `MDnsClient` Dart class in your code. diff --git a/multicast_dns_source_ref/dart_test.yaml b/multicast_dns_source_ref/dart_test.yaml deleted file mode 100644 index 91ec220b..00000000 --- a/multicast_dns_source_ref/dart_test.yaml +++ /dev/null @@ -1 +0,0 @@ -test_on: vm diff --git a/multicast_dns_source_ref/example/main.dart b/multicast_dns_source_ref/example/main.dart deleted file mode 100644 index 81c7697c..00000000 --- a/multicast_dns_source_ref/example/main.dart +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Example script to illustrate how to use the mdns package to discover the port -// of a Dart observatory over mDNS. - -// ignore_for_file: avoid_print - -import 'package:multicast_dns/multicast_dns.dart'; - -Future main() async { - // Parse the command line arguments. - - const String name = '_dartobservatory._tcp.local'; - final MDnsClient client = MDnsClient(); - // Start the client with default options. - await client.start(); - - // Get the PTR record for the service. - await for (final PtrResourceRecord ptr in client.lookup( - ResourceRecordQuery.serverPointer(name), - )) { - // Use the domainName from the PTR record to get the SRV record, - // which will have the port and local hostname. - // Note that duplicate messages may come through, especially if any - // other mDNS queries are running elsewhere on the machine. - await for (final SrvResourceRecord srv in client.lookup( - ResourceRecordQuery.service(ptr.domainName), - )) { - // Domain name will be something like "io.flutter.example@some-iphone.local._dartobservatory._tcp.local" - final String bundleId = - ptr.domainName; //.substring(0, ptr.domainName.indexOf('@')); - print( - 'Dart observatory instance found at ' - '${srv.target}:${srv.port} for "$bundleId".', - ); - } - } - client.stop(); - - print('Done.'); -} diff --git a/multicast_dns_source_ref/example/mdns_resolve.dart b/multicast_dns_source_ref/example/mdns_resolve.dart deleted file mode 100644 index 5246997c..00000000 --- a/multicast_dns_source_ref/example/mdns_resolve.dart +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Example script to illustrate how to use the mdns package to lookup names -// on the local network. - -// ignore_for_file: avoid_print - -import 'package:multicast_dns/multicast_dns.dart'; - -Future main(List args) async { - if (args.length != 1) { - print(''' -Please provide an address as argument. - -For example: - dart mdns_resolve.dart dartino.local'''); - return; - } - - final String name = args[0]; - - final MDnsClient client = MDnsClient(); - await client.start(); - await for (final IPAddressResourceRecord record in client - .lookup(ResourceRecordQuery.addressIPv4(name))) { - print('Found address (${record.address}).'); - } - - await for (final IPAddressResourceRecord record in client - .lookup(ResourceRecordQuery.addressIPv6(name))) { - print('Found address (${record.address}).'); - } - client.stop(); -} diff --git a/multicast_dns_source_ref/example/mdns_sd.dart b/multicast_dns_source_ref/example/mdns_sd.dart deleted file mode 100644 index b728b612..00000000 --- a/multicast_dns_source_ref/example/mdns_sd.dart +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Example script to illustrate how to use the mdns package to discover services -// on the local network. - -// ignore_for_file: avoid_print - -import 'package:multicast_dns/multicast_dns.dart'; - -Future main(List args) async { - if (args.isEmpty) { - print(''' -Please provide the name of a service as argument. - -For example: - dart mdns_sd.dart [--verbose] _workstation._tcp.local'''); - return; - } - - final bool verbose = args.contains('--verbose') || args.contains('-v'); - final String name = args.last; - final MDnsClient client = MDnsClient(); - await client.start(); - - await for (final PtrResourceRecord ptr in client.lookup( - ResourceRecordQuery.serverPointer(name), - )) { - if (verbose) { - print(ptr); - } - await for (final SrvResourceRecord srv in client.lookup( - ResourceRecordQuery.service(ptr.domainName), - )) { - if (verbose) { - print(srv); - } - if (verbose) { - await client - .lookup(ResourceRecordQuery.text(ptr.domainName)) - .forEach(print); - } - await for (final IPAddressResourceRecord ip in client - .lookup( - ResourceRecordQuery.addressIPv4(srv.target), - )) { - if (verbose) { - print(ip); - } - print( - 'Service instance found at ' - '${srv.target}:${srv.port} with ${ip.address}.', - ); - } - await for (final IPAddressResourceRecord ip in client - .lookup( - ResourceRecordQuery.addressIPv6(srv.target), - )) { - if (verbose) { - print(ip); - } - print( - 'Service instance found at ' - '${srv.target}:${srv.port} with ${ip.address}.', - ); - } - } - } - client.stop(); -} diff --git a/multicast_dns_source_ref/lib/multicast_dns.dart b/multicast_dns_source_ref/lib/multicast_dns.dart deleted file mode 100644 index 18674fb2..00000000 --- a/multicast_dns_source_ref/lib/multicast_dns.dart +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io'; - -import 'src/constants.dart'; -import 'src/lookup_resolver.dart'; -import 'src/native_protocol_client.dart'; -import 'src/packet.dart'; -import 'src/resource_record.dart'; - -export 'package:multicast_dns/src/resource_record.dart'; - -/// A callback type for [MDnsQuerier.start] to iterate available network -/// interfaces. -/// -/// Implementations must ensure they return interfaces appropriate for the -/// [type] parameter. -/// -/// See also: -/// * [MDnsQuerier.allInterfacesFactory] -typedef NetworkInterfacesFactory = - Future> Function(InternetAddressType type); - -/// A factory for construction of datagram sockets. -/// -/// This can be injected into the [MDnsClient] to provide alternative -/// implementations of [RawDatagramSocket.bind]. -typedef RawDatagramSocketFactory = - Future Function( - dynamic host, - int port, { - bool reuseAddress, - bool reusePort, - int ttl, - }); - -/// Client for DNS lookup and publishing using the mDNS protocol. -/// -/// Users should call [MDnsQuerier.start] when ready to start querying and -/// listening. [MDnsQuerier.stop] must be called when done to clean up -/// resources. -/// -/// This client only supports "One-Shot Multicast DNS Queries" as described in -/// section 5.1 of [RFC 6762](https://tools.ietf.org/html/rfc6762). -class MDnsClient { - /// Create a new [MDnsClient]. - MDnsClient({ - RawDatagramSocketFactory rawDatagramSocketFactory = RawDatagramSocket.bind, - }) : _rawDatagramSocketFactory = rawDatagramSocketFactory; - - bool _starting = false; - bool _started = false; - RawDatagramSocket? _incomingIPv4; - final List _ipv6InterfaceSockets = []; - final LookupResolver _resolver = LookupResolver(); - final ResourceRecordCache _cache = ResourceRecordCache(); - final RawDatagramSocketFactory _rawDatagramSocketFactory; - - InternetAddress? _mDnsAddress; - int? _mDnsPort; - - /// Find all network interfaces with an the [InternetAddressType] specified. - Future> allInterfacesFactory( - InternetAddressType type, - ) { - return NetworkInterface.list( - includeLinkLocal: true, - type: type, - includeLoopback: true, - ); - } - - /// Start the mDNS client. - /// - /// With no arguments, this method will listen on the IPv4 multicast address - /// on all IPv4 network interfaces. - /// - /// The [listenAddress] parameter must be either [InternetAddress.anyIPv4] or - /// [InternetAddress.anyIPv6], and will default to anyIPv4. - /// - /// The [interfaceFactory] defaults to [allInterfacesFactory]. - /// - /// The [mDnsPort] allows configuring what port is used for the mDNS - /// query. If not provided, defaults to `5353`. - /// - /// The [mDnsAddress] allows configuring what internet address is used - /// for the mDNS query. If not provided, defaults to either `224.0.0.251` or - /// or `FF02::FB`. - /// - /// If provided, [onError] will be called in case of a stream error. If - /// omitted any errors on the stream are considered unhandled, and will be - /// passed to the current [Zone]'s error handler. - /// - /// Subsequent calls to this method are ignored while the mDNS client is in - /// started state. - Future start({ - InternetAddress? listenAddress, - NetworkInterfacesFactory? interfacesFactory, - int mDnsPort = mDnsPort, - InternetAddress? mDnsAddress, - Function? onError, - }) async { - listenAddress ??= InternetAddress.anyIPv4; - interfacesFactory ??= allInterfacesFactory; - - assert( - listenAddress.address == InternetAddress.anyIPv4.address || - listenAddress.address == InternetAddress.anyIPv6.address, - ); - - if (_started || _starting) { - return; - } - _starting = true; - - final int selectedMDnsPort = _mDnsPort = mDnsPort; - _mDnsAddress = mDnsAddress; - - // Listen on all addresses. - final RawDatagramSocket incoming = await _rawDatagramSocketFactory( - listenAddress.address, - selectedMDnsPort, - reuseAddress: true, - reusePort: true, - ttl: 255, - ); - - // Can't send to IPv6 any address. - if (incoming.address != InternetAddress.anyIPv6) { - _incomingIPv4 = incoming; - } else { - _ipv6InterfaceSockets.add(incoming); - } - - _mDnsAddress ??= - incoming.address.type == InternetAddressType.IPv4 - ? mDnsAddressIPv4 - : mDnsAddressIPv6; - - final List interfaces = - (await interfacesFactory(listenAddress.type)).toList(); - - for (final NetworkInterface interface in interfaces) { - final InternetAddress targetAddress = interface.addresses[0]; - - // Ensure that we're using this address/interface for multicast. - if (targetAddress.type == InternetAddressType.IPv6) { - final RawDatagramSocket socket = await _rawDatagramSocketFactory( - targetAddress, - selectedMDnsPort, - reuseAddress: true, - reusePort: true, - ttl: 255, - ); - _ipv6InterfaceSockets.add(socket); - socket.setRawOption( - RawSocketOption.fromInt( - RawSocketOption.levelIPv6, - RawSocketOption.IPv6MulticastInterface, - interface.index, - ), - ); - } - - // Join multicast on this interface. - incoming.joinMulticast(_mDnsAddress!, interface); - } - incoming.listen( - (RawSocketEvent event) => _handleIncoming(event, incoming), - onError: onError, - ); - _started = true; - _starting = false; - } - - /// Stop the client and close any associated sockets. - void stop() { - if (!_started) { - return; - } - if (_starting) { - throw StateError('Cannot stop mDNS client while it is starting.'); - } - - _incomingIPv4?.close(); - _incomingIPv4 = null; - - for (final RawDatagramSocket socket in _ipv6InterfaceSockets) { - socket.close(); - } - _ipv6InterfaceSockets.clear(); - - _resolver.clearPendingRequests(); - - _started = false; - } - - /// Lookup a [ResourceRecord], potentially from the cache. - /// - /// The [type] parameter must be a valid [ResourceRecordType]. The [fullyQualifiedName] - /// parameter is the name of the service to lookup, and must not be null. The - /// [timeout] parameter specifies how long the internal cache should hold on - /// to the record. The [multicast] parameter specifies whether the query - /// should be sent as unicast (QU) or multicast (QM). - /// - /// Some publishers have been observed to not respond to unicast requests - /// properly, so the default is true. - Stream lookup( - ResourceRecordQuery query, { - Duration timeout = const Duration(seconds: 5), - }) { - final int? selectedMDnsPort = _mDnsPort; - if (!_started || selectedMDnsPort == null) { - throw StateError('mDNS client must be started before calling lookup.'); - } - // Look for entries in the cache. - final List cached = []; - _cache.lookup( - query.fullyQualifiedName, - query.resourceRecordType, - cached, - ); - if (cached.isNotEmpty) { - final StreamController controller = StreamController(); - cached.forEach(controller.add); - controller.close(); - return controller.stream; - } - - // Add the pending request before sending the query. - final Stream results = _resolver.addPendingRequest( - query.resourceRecordType, - query.fullyQualifiedName, - timeout, - ); - - final List packet = query.encode(); - - if (_mDnsAddress?.type == InternetAddressType.IPv4) { - // Send and listen on same "ANY" interface - _incomingIPv4?.send(packet, _mDnsAddress!, selectedMDnsPort); - } else { - for (final RawDatagramSocket socket in _ipv6InterfaceSockets) { - socket.send(packet, _mDnsAddress!, selectedMDnsPort); - } - } - - return results; - } - - // Process incoming datagrams. - void _handleIncoming(RawSocketEvent event, RawDatagramSocket incoming) { - if (event == RawSocketEvent.read) { - final Datagram? datagram = incoming.receive(); - if (datagram == null) { - return; - } - - // Check for published responses. - final List? response = decodeMDnsResponse(datagram.data); - if (response != null) { - _cache.updateRecords(response); - _resolver.handleResponse(response); - return; - } - // TODO(dnfield): Support queries coming in for published entries. - } - } -} diff --git a/multicast_dns_source_ref/lib/src/constants.dart b/multicast_dns_source_ref/lib/src/constants.dart deleted file mode 100644 index 76d8c6a8..00000000 --- a/multicast_dns_source_ref/lib/src/constants.dart +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -/// The IPv4 mDNS Address. -final InternetAddress mDnsAddressIPv4 = InternetAddress('224.0.0.251'); - -/// The IPv6 mDNS Address. -final InternetAddress mDnsAddressIPv6 = InternetAddress('FF02::FB'); - -/// The mDNS port. -const int mDnsPort = 5353; - -/// Enumeration of supported resource record class types. -abstract class ResourceRecordClass { - // This class is intended to be used as a namespace, and should not be - // extended directly. - ResourceRecordClass._(); - - /// Internet address class ("IN"). - static const int internet = 1; -} - -/// Enumeration of DNS question types. -abstract class QuestionType { - // This class is intended to be used as a namespace, and should not be - // extended directly. - QuestionType._(); - - /// "QU" Question. - static const int unicast = 0x8000; - - /// "QM" Question. - static const int multicast = 0x0000; -} diff --git a/multicast_dns_source_ref/lib/src/lookup_resolver.dart b/multicast_dns_source_ref/lib/src/lookup_resolver.dart deleted file mode 100644 index a12c3e10..00000000 --- a/multicast_dns_source_ref/lib/src/lookup_resolver.dart +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:collection'; - -import 'resource_record.dart'; - -/// Class for maintaining state about pending mDNS requests. -base class PendingRequest extends LinkedListEntry { - /// Creates a new PendingRequest. - PendingRequest(this.type, this.domainName, this.controller); - - /// The [ResourceRecordType] of the request. - final int type; - - /// The domain name to look up via mDNS. - /// - /// For example, `'_http._tcp.local` to look up HTTP services on the local - /// domain. - final String domainName; - - /// A StreamController managing the request. - final StreamController controller; - - /// The timer for the request. - Timer? timer; -} - -/// Class for keeping track of pending lookups and processing incoming -/// query responses. -class LookupResolver { - final LinkedList _pendingRequests = - LinkedList(); - - /// Adds a request and returns a [Stream] of [ResourceRecord] responses. - Stream addPendingRequest( - int type, - String name, - Duration timeout, - ) { - final StreamController controller = StreamController(); - final PendingRequest request = PendingRequest(type, name, controller); - final Timer timer = Timer(timeout, () { - request.unlink(); - controller.close(); - }); - request.timer = timer; - _pendingRequests.add(request); - return controller.stream; - } - - /// Parses [ResoureRecord]s received and delivers them to the appropriate - /// listener(s) added via [addPendingRequest]. - void handleResponse(List response) { - for (final ResourceRecord r in response) { - final int type = r.resourceRecordType; - String name = r.name.toLowerCase(); - if (name.endsWith('.')) { - name = name.substring(0, name.length - 1); - } - - bool responseMatches(PendingRequest request) { - String requestName = request.domainName.toLowerCase(); - // make, e.g. "_http" become "_http._tcp.local". - if (!requestName.endsWith('local')) { - if (!requestName.endsWith('._tcp.local') && - !requestName.endsWith('._udp.local') && - !requestName.endsWith('._tcp') && - !requestName.endsWith('.udp')) { - requestName += '._tcp'; - } - requestName += '.local'; - } - return requestName == name && request.type == type; - } - - for (final PendingRequest pendingRequest in _pendingRequests) { - if (responseMatches(pendingRequest)) { - if (pendingRequest.controller.isClosed) { - return; - } - pendingRequest.controller.add(r); - } - } - } - } - - /// Removes any pending requests and ends processing. - void clearPendingRequests() { - while (_pendingRequests.isNotEmpty) { - final PendingRequest request = _pendingRequests.first; - request.unlink(); - request.timer?.cancel(); - request.controller.close(); - } - } -} diff --git a/multicast_dns_source_ref/lib/src/native_protocol_client.dart b/multicast_dns_source_ref/lib/src/native_protocol_client.dart deleted file mode 100644 index fb94343e..00000000 --- a/multicast_dns_source_ref/lib/src/native_protocol_client.dart +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:collection'; - -import 'resource_record.dart'; - -/// Cache for resource records that have been received. -/// -/// There can be multiple entries for the same name and type. -/// -/// The cache is updated with a list of records, because it needs to remove -/// all entries that correspond to the name and type of the name/type -/// combinations of records that should be updated. For example, a host may -/// remove one of its IP addresses and report the remaining address as a -/// response - then we need to clear all previous entries for that host before -/// updating the cache. -class ResourceRecordCache { - /// Creates a new ResourceRecordCache. - ResourceRecordCache(); - - final Map>> _cache = - >>{}; - - /// The number of entries in the cache. - int get entryCount { - int count = 0; - for (final SplayTreeMap> map - in _cache.values) { - for (final List records in map.values) { - count += records.length; - } - } - return count; - } - - /// Update the records in this cache. - void updateRecords(List records) { - // TODO(karlklose): include flush bit in the record and only flush if - // necessary. - // Clear the cache for all name/type combinations to be updated. - final Map> seenRecordTypes = >{}; - for (final ResourceRecord record in records) { - // TODO(dnfield): Update this to use set literal syntax when we're able to bump the SDK constraint. - seenRecordTypes[record.resourceRecordType] ??= - Set(); // ignore: prefer_collection_literals - if (seenRecordTypes[record.resourceRecordType]!.add(record.name)) { - _cache[record.resourceRecordType] ??= - SplayTreeMap>(); - - _cache[record.resourceRecordType]![record.name] = [ - record, - ]; - } else { - _cache[record.resourceRecordType]![record.name]!.add(record); - } - } - } - - /// Get a record from this cache. - void lookup( - String name, - int type, - List results, - ) { - assert(ResourceRecordType.debugAssertValid(type)); - final int time = DateTime.now().millisecondsSinceEpoch; - final SplayTreeMap>? candidates = _cache[type]; - if (candidates == null) { - return; - } - - final List? candidateRecords = candidates[name]; - if (candidateRecords == null) { - return; - } - candidateRecords.removeWhere( - (ResourceRecord candidate) => candidate.validUntil < time, - ); - results.addAll(candidateRecords.cast()); - } -} diff --git a/multicast_dns_source_ref/lib/src/packet.dart b/multicast_dns_source_ref/lib/src/packet.dart deleted file mode 100644 index aa41cefd..00000000 --- a/multicast_dns_source_ref/lib/src/packet.dart +++ /dev/null @@ -1,396 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; - -import 'constants.dart'; -import 'resource_record.dart'; - -// Offsets into the header. See https://tools.ietf.org/html/rfc1035. -const int _kIdOffset = 0; -const int _kFlagsOffset = 2; -const int _kQdcountOffset = 4; -const int _kAncountOffset = 6; -const int _kNscountOffset = 8; -const int _kArcountOffset = 10; -const int _kHeaderSize = 12; - -/// Processes a DNS query name into a list of parts. -/// -/// Will attempt to append 'local' if the name is something like '_http._tcp', -/// and '._tcp.local' if name is something like '_http'. -List processDnsNameParts(String name) { - final List parts = name.split('.'); - if (parts.length == 1) { - return [parts[0], '_tcp', 'local']; - } else if (parts.length == 2 && parts[1].startsWith('_')) { - return [parts[0], parts[1], 'local']; - } - - return parts; -} - -/// Encode an mDNS query packet. -/// -/// The [type] parameter must be a valid [ResourceRecordType] value. The -/// [multicast] parameter must not be null. -/// -/// This is a low level API; most consumers should prefer -/// [ResourceRecordQuery.encode], which offers some convenience wrappers around -/// selecting the correct [type] and setting the [name] parameter correctly. -List encodeMDnsQuery( - String name, { - int type = ResourceRecordType.addressIPv4, - bool multicast = true, -}) { - assert(ResourceRecordType.debugAssertValid(type)); - - final List nameParts = processDnsNameParts(name); - final List> rawNameParts = - nameParts.map>((String part) => utf8.encode(part)).toList(); - - // Calculate the size of the packet. - int size = _kHeaderSize; - for (int i = 0; i < rawNameParts.length; i++) { - size += 1 + rawNameParts[i].length; - } - - size += 1; // End with empty part - size += 4; // Trailer (QTYPE and QCLASS). - final Uint8List data = Uint8List(size); - final ByteData packetByteData = ByteData.view(data.buffer); - // Query identifier - just use 0. - packetByteData.setUint16(_kIdOffset, 0); - // Flags - 0 for query. - packetByteData.setUint16(_kFlagsOffset, 0); - // Query count. - packetByteData.setUint16(_kQdcountOffset, 1); - // Number of answers - 0 for query. - packetByteData.setUint16(_kAncountOffset, 0); - // Number of name server records - 0 for query. - packetByteData.setUint16(_kNscountOffset, 0); - // Number of resource records - 0 for query. - packetByteData.setUint16(_kArcountOffset, 0); - int offset = _kHeaderSize; - for (int i = 0; i < rawNameParts.length; i++) { - data[offset++] = rawNameParts[i].length; - data.setRange(offset, offset + rawNameParts[i].length, rawNameParts[i]); - offset += rawNameParts[i].length; - } - - data[offset] = 0; // Empty part. - offset++; - packetByteData.setUint16(offset, type); // QTYPE. - offset += 2; - packetByteData.setUint16( - offset, - ResourceRecordClass.internet | - (multicast ? QuestionType.multicast : QuestionType.unicast), - ); - - return data; -} - -/// Result of reading a Fully Qualified Domain Name (FQDN). -class _FQDNReadResult { - /// Creates a new FQDN read result. - _FQDNReadResult(this.fqdnParts, this.bytesRead); - - /// The raw parts of the FQDN. - final List fqdnParts; - - /// The bytes consumed from the packet for this FQDN. - final int bytesRead; - - /// Returns the Fully Qualified Domain Name. - String get fqdn => fqdnParts.join('.'); - - @override - String toString() => fqdn; -} - -/// Reads a FQDN from raw packet data. -String readFQDN(List packet, [int offset = 0]) { - final Uint8List data = - packet is Uint8List ? packet : Uint8List.fromList(packet); - final ByteData byteData = ByteData.view(data.buffer); - - return _readFQDN(data, byteData, offset, data.length).fqdn; -} - -// Read a FQDN at the given offset. Returns a pair with the FQDN -// parts and the number of bytes consumed. -// -// If decoding fails (e.g. due to an invalid packet) `null` is returned. -_FQDNReadResult _readFQDN( - Uint8List data, - ByteData byteData, - int offset, - int length, -) { - void checkLength(int required) { - if (length < required) { - throw MDnsDecodeException(required); - } - } - - final List parts = []; - final int prevOffset = offset; - final List offsetsToVisit = [offset]; - int upperLimitOffset = offset; - int highestOffsetRead = offset; - - while (offsetsToVisit.isNotEmpty) { - offset = offsetsToVisit.removeLast(); - - while (true) { - // At least one byte is required. - checkLength(offset + 1); - // Check for compressed. - if (data[offset] & 0xc0 == 0xc0) { - // At least two bytes are required for a compressed FQDN (see RFC1035 section 4.1.4). - checkLength(offset + 2); - - // A compressed FQDN has a new offset in the lower 14 bits. - final int pointerDest = byteData.getUint16(offset) & ~0xc000; - // Pointers can only point to prior occurances of some name. - // This check also guards against pointers that form loops. - if (pointerDest >= upperLimitOffset) { - throw MDnsDecodeException(offset); - } - upperLimitOffset = pointerDest; - offsetsToVisit.add(pointerDest); - highestOffsetRead = max(highestOffsetRead, offset + 2); - break; - } else { - // A normal FQDN part has a length and a UTF-8 encoded name - // part. If the length is 0 this is the end of the FQDN. - final int partLength = data[offset]; - offset++; - if (partLength > 0) { - checkLength(offset + partLength); - final Uint8List partBytes = Uint8List.view( - data.buffer, - offset, - partLength, - ); - offset += partLength; - // According to the RFC, this is supposed to be utf-8 encoded, but - // we should continue decoding even if it isn't to avoid dropping the - // rest of the data, which might still be useful. - parts.add(utf8.decode(partBytes, allowMalformed: true)); - highestOffsetRead = max(highestOffsetRead, offset); - } else { - highestOffsetRead = max(highestOffsetRead, offset); - break; - } - } - } - } - return _FQDNReadResult(parts, highestOffsetRead - prevOffset); -} - -/// Decode an mDNS response packet. -/// -/// If decoding fails (e.g. due to an invalid packet) `null` is returned. -/// -/// See https://tools.ietf.org/html/rfc1035 for the format. -List? decodeMDnsResponse(List packet) { - final int length = packet.length; - if (length < _kHeaderSize) { - return null; - } - - final Uint8List data = - packet is Uint8List ? packet : Uint8List.fromList(packet); - final ByteData packetBytes = ByteData.view(data.buffer); - - final int answerCount = packetBytes.getUint16(_kAncountOffset); - final int authorityCount = packetBytes.getUint16(_kNscountOffset); - final int additionalCount = packetBytes.getUint16(_kArcountOffset); - final int remainingCount = answerCount + authorityCount + additionalCount; - - if (remainingCount == 0) { - return null; - } - - final int questionCount = packetBytes.getUint16(_kQdcountOffset); - int offset = _kHeaderSize; - - void checkLength(int required) { - if (length < required) { - throw MDnsDecodeException(required); - } - } - - ResourceRecord? readResourceRecord() { - // First read the FQDN. - final _FQDNReadResult result = _readFQDN(data, packetBytes, offset, length); - final String fqdn = result.fqdn; - offset += result.bytesRead; - checkLength(offset + 2); - final int type = packetBytes.getUint16(offset); - offset += 2; - // The first bit of the rrclass field is set to indicate that the answer is - // unique and the querier should flush the cached answer for this name - // (RFC 6762, Sec. 10.2). We ignore it for now since we don't cache answers. - checkLength(offset + 2); - final int resourceRecordClass = packetBytes.getUint16(offset) & 0x7fff; - - if (resourceRecordClass != ResourceRecordClass.internet) { - // We do not support other classes. - return null; - } - - offset += 2; - checkLength(offset + 4); - final int ttl = packetBytes.getInt32(offset); - offset += 4; - - checkLength(offset + 2); - final int readDataLength = packetBytes.getUint16(offset); - offset += 2; - final int validUntil = DateTime.now().millisecondsSinceEpoch + ttl * 1000; - switch (type) { - case ResourceRecordType.addressIPv4: - checkLength(offset + readDataLength); - final StringBuffer addr = StringBuffer(); - final int stop = offset + readDataLength; - addr.write(packetBytes.getUint8(offset)); - offset++; - for (; offset < stop; offset++) { - addr.write('.'); - addr.write(packetBytes.getUint8(offset)); - } - return IPAddressResourceRecord( - fqdn, - validUntil, - address: InternetAddress(addr.toString()), - ); - case ResourceRecordType.addressIPv6: - checkLength(offset + readDataLength); - final StringBuffer addr = StringBuffer(); - final int stop = offset + readDataLength; - addr.write(packetBytes.getUint16(offset).toRadixString(16)); - offset += 2; - for (; offset < stop; offset += 2) { - addr.write(':'); - addr.write(packetBytes.getUint16(offset).toRadixString(16)); - } - return IPAddressResourceRecord( - fqdn, - validUntil, - address: InternetAddress(addr.toString()), - ); - case ResourceRecordType.service: - checkLength(offset + 2); - final int priority = packetBytes.getUint16(offset); - offset += 2; - checkLength(offset + 2); - final int weight = packetBytes.getUint16(offset); - offset += 2; - checkLength(offset + 2); - final int port = packetBytes.getUint16(offset); - offset += 2; - final _FQDNReadResult result = _readFQDN( - data, - packetBytes, - offset, - length, - ); - offset += result.bytesRead; - return SrvResourceRecord( - fqdn, - validUntil, - target: result.fqdn, - port: port, - priority: priority, - weight: weight, - ); - case ResourceRecordType.serverPointer: - checkLength(offset + readDataLength); - final _FQDNReadResult result = _readFQDN( - data, - packetBytes, - offset, - length, - ); - offset += readDataLength; - return PtrResourceRecord(fqdn, validUntil, domainName: result.fqdn); - case ResourceRecordType.text: - checkLength(offset + readDataLength); - // The first byte of the buffer is the length of the first string of - // the TXT record. Further length-prefixed strings may follow. We - // concatenate them with newlines. - final StringBuffer strings = StringBuffer(); - int index = 0; - while (index < readDataLength) { - final int txtLength = data[offset + index]; - index++; - if (txtLength == 0) { - continue; - } - final String text = utf8.decode( - Uint8List.view(data.buffer, offset + index, txtLength), - allowMalformed: true, - ); - strings.writeln(text); - index += txtLength; - } - offset += readDataLength; - return TxtResourceRecord(fqdn, validUntil, text: strings.toString()); - default: - checkLength(offset + readDataLength); - offset += readDataLength; - return null; - } - } - - // This list can't be fixed length right now because we might get - // resource record types we don't support, and consumers expect this list - // to not have null entries. - final List result = []; - - try { - for (int i = 0; i < questionCount; i++) { - final _FQDNReadResult result = _readFQDN( - data, - packetBytes, - offset, - length, - ); - offset += result.bytesRead; - checkLength(offset + 4); - offset += 4; - } - for (int i = 0; i < remainingCount; i++) { - final ResourceRecord? record = readResourceRecord(); - if (record != null) { - result.add(record); - } - } - } on MDnsDecodeException { - // If decoding fails return null. - return null; - } - return result; -} - -/// This exception is thrown by the decoder when the packet is invalid. -class MDnsDecodeException implements Exception { - /// Creates a new MDnsDecodeException, indicating an error in decoding at the - /// specified [offset]. - /// - /// The [offset] parameter should not be null. - const MDnsDecodeException(this.offset); - - /// The offset in the packet at which the exception occurred. - final int offset; - - @override - String toString() => 'Decoding error at $offset'; -} diff --git a/multicast_dns_source_ref/lib/src/resource_record.dart b/multicast_dns_source_ref/lib/src/resource_record.dart deleted file mode 100644 index 8c59ab2c..00000000 --- a/multicast_dns_source_ref/lib/src/resource_record.dart +++ /dev/null @@ -1,346 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:meta/meta.dart'; -import 'constants.dart'; -import 'packet.dart'; - -/// Enumeration of support resource record types. -abstract class ResourceRecordType { - // This class is intended to be used as a namespace, and should not be - // extended directly. - ResourceRecordType._(); - - /// An IPv4 Address record, also known as an "A" record. It has a value of 1. - static const int addressIPv4 = 1; - - /// An IPv6 Address record, also known as an "AAAA" record. It has a vaule of - /// 28. - static const int addressIPv6 = 28; - - /// An IP Address reverse map record, also known as a "PTR" recored. It has a - /// value of 12. - static const int serverPointer = 12; - - /// An available service record, also known as an "SRV" record. It has a - /// value of 33. - static const int service = 33; - - /// A text record, also known as a "TXT" record. It has a value of 16. - static const int text = 16; - - // TODO(dnfield): Support ANY in some meaningful way. Might be server only. - // /// A query for all records of all types known to the name server. - // static const int any = 255; - - /// Checks that a given int is a valid ResourceRecordType. - /// - /// This method is intended to be called only from an `assert()`. - static bool debugAssertValid(int resourceRecordType) { - return resourceRecordType == addressIPv4 || - resourceRecordType == addressIPv6 || - resourceRecordType == serverPointer || - resourceRecordType == service || - resourceRecordType == text; - } - - /// Prints a debug-friendly version of the resource record type value. - static String toDebugString(int resourceRecordType) { - switch (resourceRecordType) { - case addressIPv4: - return 'A (IPv4 Address)'; - case addressIPv6: - return 'AAAA (IPv6 Address)'; - case serverPointer: - return 'PTR (Domain Name Pointer)'; - case service: - return 'SRV (Service record)'; - case text: - return 'TXT (Text)'; - } - return 'Unknown ($resourceRecordType)'; - } -} - -/// Represents a DNS query. -@immutable -class ResourceRecordQuery { - /// Creates a new ResourceRecordQuery. - /// - /// Most callers should prefer one of the named constructors. - ResourceRecordQuery( - this.resourceRecordType, - this.fullyQualifiedName, - this.questionType, - ) : assert(ResourceRecordType.debugAssertValid(resourceRecordType)); - - /// An A (IPv4) query. - ResourceRecordQuery.addressIPv4(String name, {bool isMulticast = true}) - : this( - ResourceRecordType.addressIPv4, - name, - isMulticast ? QuestionType.multicast : QuestionType.unicast, - ); - - /// An AAAA (IPv6) query. - ResourceRecordQuery.addressIPv6(String name, {bool isMulticast = true}) - : this( - ResourceRecordType.addressIPv6, - name, - isMulticast ? QuestionType.multicast : QuestionType.unicast, - ); - - /// A PTR (Server pointer) query. - ResourceRecordQuery.serverPointer(String name, {bool isMulticast = true}) - : this( - ResourceRecordType.serverPointer, - name, - isMulticast ? QuestionType.multicast : QuestionType.unicast, - ); - - /// An SRV (Service) query. - ResourceRecordQuery.service(String name, {bool isMulticast = true}) - : this( - ResourceRecordType.service, - name, - isMulticast ? QuestionType.multicast : QuestionType.unicast, - ); - - /// A TXT (Text record) query. - ResourceRecordQuery.text(String name, {bool isMulticast = true}) - : this( - ResourceRecordType.text, - name, - isMulticast ? QuestionType.multicast : QuestionType.unicast, - ); - - /// Tye type of resource record - one of [ResourceRecordType]'s values. - final int resourceRecordType; - - /// The Fully Qualified Domain Name associated with the request. - final String fullyQualifiedName; - - /// The [QuestionType], i.e. multicast or unicast. - final int questionType; - - /// Convenience accessor to determine whether the question type is multicast. - bool get isMulticast => questionType == QuestionType.multicast; - - /// Convenience accessor to determine whether the question type is unicast. - bool get isUnicast => questionType == QuestionType.unicast; - - /// Encodes this query to the raw wire format. - List encode() { - return encodeMDnsQuery( - fullyQualifiedName, - type: resourceRecordType, - multicast: isMulticast, - ); - } - - @override - int get hashCode => - Object.hash(resourceRecordType, fullyQualifiedName, questionType); - - @override - bool operator ==(Object other) { - return other is ResourceRecordQuery && - other.resourceRecordType == resourceRecordType && - other.fullyQualifiedName == fullyQualifiedName && - other.questionType == questionType; - } - - @override - String toString() => - 'ResourceRecordQuery{$fullyQualifiedName, type: ${ResourceRecordType.toDebugString(resourceRecordType)}, isMulticast: $isMulticast}'; -} - -/// Base implementation of DNS resource records (RRs). -@immutable -abstract class ResourceRecord { - /// Creates a new ResourceRecord. - const ResourceRecord(this.resourceRecordType, this.name, this.validUntil); - - /// The FQDN for this record. - final String name; - - /// The epoch time at which point this record is valid for in the cache. - final int validUntil; - - /// The raw resource record value. See [ResourceRecordType] for supported values. - final int resourceRecordType; - - String get _additionalInfo; - - @override - String toString() => - '$runtimeType{$name, validUntil: ${DateTime.fromMillisecondsSinceEpoch(validUntil)}, $_additionalInfo}'; - - @override - int get hashCode => Object.hash(name, validUntil, resourceRecordType); - - @override - bool operator ==(Object other) { - return other is ResourceRecord && - other.name == name && - other.validUntil == validUntil && - other.resourceRecordType == resourceRecordType; - } - - /// Low level method for encoding this record into an mDNS packet. - /// - /// Subclasses should provide the packet format of their encapsulated data - /// into a `Uint8List`, which could then be used to write a pakcet to send - /// as a response for this record type. - Uint8List encodeResponseRecord(); -} - -/// A Service Pointer for reverse mapping an IP address (DNS "PTR"). -class PtrResourceRecord extends ResourceRecord { - /// Creates a new PtrResourceRecord. - const PtrResourceRecord( - String name, - int validUntil, { - required this.domainName, - }) : super(ResourceRecordType.serverPointer, name, validUntil); - - /// The FQDN for this record. - final String domainName; - - @override - String get _additionalInfo => 'domainName: $domainName'; - - @override - int get hashCode => Object.hash(domainName.hashCode, super.hashCode); - - @override - bool operator ==(Object other) { - return super == other && - other is PtrResourceRecord && - other.domainName == domainName; - } - - @override - Uint8List encodeResponseRecord() { - return Uint8List.fromList(utf8.encode(domainName)); - } -} - -/// An IP Address record for IPv4 (DNS "A") or IPv6 (DNS "AAAA") records. -class IPAddressResourceRecord extends ResourceRecord { - /// Creates a new IPAddressResourceRecord. - IPAddressResourceRecord(String name, int validUntil, {required this.address}) - : super( - address.type == InternetAddressType.IPv4 - ? ResourceRecordType.addressIPv4 - : ResourceRecordType.addressIPv6, - name, - validUntil, - ); - - /// The [InternetAddress] for this record. - final InternetAddress address; - - @override - String get _additionalInfo => 'address: $address'; - - @override - int get hashCode => Object.hash(address.hashCode, super.hashCode); - - @override - bool operator ==(Object other) { - return super == other && - other is IPAddressResourceRecord && - other.address == address; - } - - @override - Uint8List encodeResponseRecord() { - return Uint8List.fromList(address.rawAddress); - } -} - -/// A Service record, capturing a host target and port (DNS "SRV"). -class SrvResourceRecord extends ResourceRecord { - /// Creates a new service record. - const SrvResourceRecord( - String name, - int validUntil, { - required this.target, - required this.port, - required this.priority, - required this.weight, - }) : super(ResourceRecordType.service, name, validUntil); - - /// The hostname for this record. - final String target; - - /// The port for this record. - final int port; - - /// The relative priority of this service. - final int priority; - - /// The weight (used when multiple services have the same priority). - final int weight; - - @override - String get _additionalInfo => - 'target: $target, port: $port, priority: $priority, weight: $weight'; - - @override - int get hashCode => - Object.hash(target, port, priority, weight, super.hashCode); - - @override - bool operator ==(Object other) { - return super == other && - other is SrvResourceRecord && - other.target == target && - other.port == port && - other.priority == priority && - other.weight == weight; - } - - @override - Uint8List encodeResponseRecord() { - final List data = utf8.encode(target); - final Uint8List result = Uint8List(data.length + 7); - final ByteData resultData = ByteData.view(result.buffer); - resultData.setUint16(0, priority); - resultData.setUint16(2, weight); - resultData.setUint16(4, port); - result[6] = data.length; - return result..setRange(7, data.length, data); - } -} - -/// A Text record, contianing additional textual data (DNS "TXT"). -class TxtResourceRecord extends ResourceRecord { - /// Creates a new text record. - const TxtResourceRecord(String name, int validUntil, {required this.text}) - : super(ResourceRecordType.text, name, validUntil); - - /// The raw text from this record. - final String text; - - @override - String get _additionalInfo => 'text: $text'; - - @override - int get hashCode => Object.hash(text.hashCode, super.hashCode); - - @override - bool operator ==(Object other) => - super == other && other is TxtResourceRecord && other.text == text; - - @override - Uint8List encodeResponseRecord() { - return Uint8List.fromList(utf8.encode(text)); - } -} diff --git a/multicast_dns_source_ref/pubspec.yaml b/multicast_dns_source_ref/pubspec.yaml deleted file mode 100644 index 38de03aa..00000000 --- a/multicast_dns_source_ref/pubspec.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: multicast_dns -description: Dart package for performing mDNS queries (e.g. Bonjour, Avahi). -repository: https://github.com/flutter/packages/tree/main/packages/multicast_dns -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+multicast_dns%22 -version: 0.3.3 - -environment: - sdk: ^3.7.0 - -dependencies: - meta: ^1.3.0 - -dev_dependencies: - test: "^1.16.5" - -topics: - - bonjour - - mdns - - network diff --git a/multicast_dns_source_ref/test/client_test.dart b/multicast_dns_source_ref/test/client_test.dart deleted file mode 100644 index b5fef509..00000000 --- a/multicast_dns_source_ref/test/client_test.dart +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io'; - -import 'package:multicast_dns/multicast_dns.dart'; -import 'package:test/fake.dart'; -import 'package:test/test.dart'; - -void main() { - test('Can inject datagram socket factory and configure mdns port', () async { - late int lastPort; - final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); - final MDnsClient client = MDnsClient( - rawDatagramSocketFactory: ( - dynamic host, - int port, { - bool reuseAddress = true, - bool reusePort = true, - int ttl = 1, - }) async { - lastPort = port; - return datagramSocket; - }, - ); - - await client.start( - mDnsPort: 1234, - interfacesFactory: - (InternetAddressType type) async => [], - ); - - expect(lastPort, 1234); - }); - - test('Closes IPv4 sockets', () async { - final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); - final MDnsClient client = MDnsClient( - rawDatagramSocketFactory: ( - dynamic host, - int port, { - bool reuseAddress = true, - bool reusePort = true, - int ttl = 1, - }) async { - return datagramSocket; - }, - ); - - await client.start( - mDnsPort: 1234, - interfacesFactory: - (InternetAddressType type) async => [], - ); - expect(datagramSocket.closed, false); - client.stop(); - expect(datagramSocket.closed, true); - }); - - test('Closes IPv6 sockets', () async { - final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); - datagramSocket.address = InternetAddress.anyIPv6; - final MDnsClient client = MDnsClient( - rawDatagramSocketFactory: ( - dynamic host, - int port, { - bool reuseAddress = true, - bool reusePort = true, - int ttl = 1, - }) async { - return datagramSocket; - }, - ); - - await client.start( - mDnsPort: 1234, - interfacesFactory: - (InternetAddressType type) async => [], - ); - expect(datagramSocket.closed, false); - client.stop(); - expect(datagramSocket.closed, true); - }); - - test('start() is idempotent', () async { - final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); - datagramSocket.address = InternetAddress.anyIPv4; - final MDnsClient client = MDnsClient( - rawDatagramSocketFactory: ( - dynamic host, - int port, { - bool reuseAddress = true, - bool reusePort = true, - int ttl = 1, - }) async { - return datagramSocket; - }, - ); - - await client.start( - interfacesFactory: - (InternetAddressType type) async => [], - ); - await client.start(); - await client.lookup(ResourceRecordQuery.serverPointer('_')).toList(); - }); - - group('Bind a single socket to ANY IPv4 and more than one when IPv6', () { - final List> testCases = >[ - { - 'name': 'IPv4', - 'datagramSocketType': InternetAddress.anyIPv4, - 'interfacePrefix': '192.168.2.', - }, - { - 'name': 'IPv6', - 'datagramSocketType': InternetAddress.anyIPv6, - 'interfacePrefix': '2001:0db8:85a3:0000:0000:8a2e:7335:030', - }, - ]; - - for (final Map testCase in testCases) { - test('Bind a single socket to ANY ${testCase["name"]}', () async { - final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket(); - - datagramSocket.address = - testCase['datagramSocketType']! as InternetAddress; - - final List selectedInterfacesForSendingPackets = []; - final MDnsClient client = MDnsClient( - rawDatagramSocketFactory: ( - dynamic host, - int port, { - bool reuseAddress = true, - bool reusePort = true, - int ttl = 1, - }) async { - selectedInterfacesForSendingPackets.add(host); - return datagramSocket; - }, - ); - - const int numberOfFakeInterfaces = 10; - Future> fakeNetworkInterfacesFactory( - InternetAddressType type, - ) async { - final List fakeInterfaces = []; - - // Generate "fake" interfaces - for (int i = 0; i < numberOfFakeInterfaces; i++) { - fakeInterfaces.add( - FakeNetworkInterface('inetfake$i', [ - InternetAddress("${testCase['interfacePrefix']! as String}$i"), - ], 0), - ); - } - - // ignore: always_specify_types - return Future.value(fakeInterfaces); - } - - final InternetAddress listenAddress = - testCase['datagramSocketType']! as InternetAddress; - - await client.start( - listenAddress: listenAddress, - mDnsPort: 1234, - interfacesFactory: fakeNetworkInterfacesFactory, - ); - client.stop(); - - if (testCase['datagramSocketType'] == InternetAddress.anyIPv4) { - expect(selectedInterfacesForSendingPackets.length, 1); - } else { - // + 1 because of unspecified address (::) - expect( - selectedInterfacesForSendingPackets.length, - numberOfFakeInterfaces + 1, - ); - } - expect(selectedInterfacesForSendingPackets[0], listenAddress.address); - }); - } - }); - - test('Calls onError callback in case of socket error', () async { - final FakeRawDatagramSocketThatSendsError datagramSocket = - FakeRawDatagramSocketThatSendsError(); - final MDnsClient client = MDnsClient( - rawDatagramSocketFactory: ( - dynamic host, - int port, { - bool reuseAddress = true, - bool reusePort = true, - int ttl = 1, - }) async { - return datagramSocket; - }, - ); - - final Completer onErrorCalledCompleter = Completer(); - await client.start( - mDnsPort: 1234, - interfacesFactory: - (InternetAddressType type) async => [], - onError: (Object e) { - expect(e, 'Error'); - onErrorCalledCompleter.complete(); - }, - ); - - await onErrorCalledCompleter.future.timeout(const Duration(seconds: 5)); - }); -} - -class FakeRawDatagramSocket extends Fake implements RawDatagramSocket { - @override - InternetAddress address = InternetAddress.anyIPv4; - - @override - StreamSubscription listen( - void Function(RawSocketEvent event)? onData, { - Function? onError, - void Function()? onDone, - bool? cancelOnError, - }) { - return const Stream.empty().listen( - onData, - onError: onError, - cancelOnError: cancelOnError, - onDone: onDone, - ); - } - - bool closed = false; - - @override - void close() { - closed = true; - } - - @override - int send(List buffer, InternetAddress address, int port) { - return buffer.length; - } - - @override - void joinMulticast(InternetAddress group, [NetworkInterface? interface]) { - // nothing to do here - } - @override - void setRawOption(RawSocketOption option) { - // nothing to do here - } -} - -class FakeRawDatagramSocketThatSendsError extends Fake - implements RawDatagramSocket { - @override - InternetAddress address = InternetAddress.anyIPv4; - - @override - StreamSubscription listen( - void Function(RawSocketEvent event)? onData, { - Function? onError, - void Function()? onDone, - bool? cancelOnError, - }) { - return Stream.error('Error').listen( - onData, - onError: onError, - cancelOnError: cancelOnError, - onDone: onDone, - ); - } -} - -class FakeNetworkInterface implements NetworkInterface { - FakeNetworkInterface(this._name, this._addresses, this._index); - - final String _name; - final List _addresses; - final int _index; - - @override - List get addresses => _addresses; - - @override - String get name => _name; - - @override - int get index => _index; -} diff --git a/multicast_dns_source_ref/test/decode_test.dart b/multicast_dns_source_ref/test/decode_test.dart deleted file mode 100644 index 7d5d16b8..00000000 --- a/multicast_dns_source_ref/test/decode_test.dart +++ /dev/null @@ -1,1648 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:multicast_dns/src/packet.dart'; -import 'package:multicast_dns/src/resource_record.dart'; -import 'package:test/test.dart'; - -const int _kSrvHeaderSize = 6; - -void main() { - testValidPackages(); - testBadPackages(); - testNonUtf8DomainName(); - // testHexDumpList(); - testPTRRData(); - testSRVRData(); -} - -void testValidPackages() { - test('Can decode valid packets', () { - List result = decodeMDnsResponse(package1)!; - expect(result, isNotNull); - expect(result.length, 1); - IPAddressResourceRecord ipResult = result[0] as IPAddressResourceRecord; - expect(ipResult.name, 'raspberrypi.local'); - expect(ipResult.address.address, '192.168.1.191'); - - result = decodeMDnsResponse(package2)!; - expect(result.length, 2); - ipResult = result[0] as IPAddressResourceRecord; - expect(ipResult.name, 'raspberrypi.local'); - expect(ipResult.address.address, '192.168.1.191'); - ipResult = result[1] as IPAddressResourceRecord; - expect(ipResult.name, 'raspberrypi.local'); - expect(ipResult.address.address, '169.254.95.83'); - - result = decodeMDnsResponse(package3)!; - expect(result.length, 8); - expect(result, [ - TxtResourceRecord( - 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local', - result[0].validUntil, - text: '', - ), - PtrResourceRecord( - '_udisks-ssh._tcp.local', - result[1].validUntil, - domainName: 'raspberrypi._udisks-ssh._tcp.local', - ), - SrvResourceRecord( - 'raspberrypi._udisks-ssh._tcp.local', - result[2].validUntil, - target: 'raspberrypi.local', - port: 22, - priority: 0, - weight: 0, - ), - TxtResourceRecord( - 'raspberrypi._udisks-ssh._tcp.local', - result[3].validUntil, - text: '', - ), - PtrResourceRecord( - '_services._dns-sd._udp.local', - result[4].validUntil, - domainName: '_udisks-ssh._tcp.local', - ), - PtrResourceRecord( - '_workstation._tcp.local', - result[5].validUntil, - domainName: 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local', - ), - SrvResourceRecord( - 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local', - result[6].validUntil, - target: 'raspberrypi.local', - port: 9, - priority: 0, - weight: 0, - ), - PtrResourceRecord( - '_services._dns-sd._udp.local', - result[7].validUntil, - domainName: '_workstation._tcp.local', - ), - ]); - - result = decodeMDnsResponse(packagePtrResponse)!; - expect(6, result.length); - expect(result, [ - PtrResourceRecord( - '_fletch_agent._tcp.local', - result[0].validUntil, - domainName: 'fletch-agent on raspberrypi._fletch_agent._tcp.local', - ), - TxtResourceRecord( - 'fletch-agent on raspberrypi._fletch_agent._tcp.local', - result[1].validUntil, - text: '', - ), - SrvResourceRecord( - 'fletch-agent on raspberrypi._fletch_agent._tcp.local', - result[2].validUntil, - target: 'raspberrypi.local', - port: 12121, - priority: 0, - weight: 0, - ), - IPAddressResourceRecord( - 'raspberrypi.local', - result[3].validUntil, - address: InternetAddress('fe80:0000:0000:0000:ba27:ebff:fe69:6e3a'), - ), - IPAddressResourceRecord( - 'raspberrypi.local', - result[4].validUntil, - address: InternetAddress('192.168.1.1'), - ), - IPAddressResourceRecord( - 'raspberrypi.local', - result[5].validUntil, - address: InternetAddress('169.254.167.172'), - ), - ]); - }); - - // Fixes https://github.com/flutter/flutter/issues/31854 - test('Can decode packages with question, answer and additional', () { - final List result = - decodeMDnsResponse(packetWithQuestionAnArCount)!; - expect(result, isNotNull); - expect(result.length, 2); - expect(result, [ - PtrResourceRecord( - '_______________.____._____', - result[0].validUntil, - domainName: '_______________________._______________.____._____', - ), - PtrResourceRecord( - '_______________.____._____', - result[1].validUntil, - domainName: '____________________________._______________.____._____', - ), - ]); - }); - - // Fixes https://github.com/flutter/flutter/issues/31854 - test( - 'Can decode packages without question and with answer and additional', - () { - final List result = - decodeMDnsResponse(packetWithoutQuestionWithAnArCount)!; - expect(result, isNotNull); - expect(result.length, 2); - expect(result, [ - PtrResourceRecord( - '_______________.____._____', - result[0].validUntil, - domainName: '______________________._______________.____._____', - ), - TxtResourceRecord( - '______________________.____________.____._____', - result[1].validUntil, - text: 'model=MacBookPro14,3\nosxvers=18\necolor=225,225,223\n', - ), - ]); - }, - ); - - test('Can decode packages with a long text resource', () { - final List result = decodeMDnsResponse(packetWithLongTxt)!; - expect(result, isNotNull); - expect(result.length, 2); - expect(result, [ - PtrResourceRecord( - '_______________.____._____', - result[0].validUntil, - domainName: '______________________._______________.____._____', - ), - TxtResourceRecord( - '______________________.____________.____._____', - result[1].validUntil, - text: '${')' * 129}\n', - ), - ]); - }); -} - -void testBadPackages() { - test('Returns null for invalid packets', () { - for (final List p in >[package1, package2, package3]) { - for (int i = 0; i < p.length; i++) { - expect(decodeMDnsResponse(p.sublist(0, i)), isNull); - } - } - }); - - test('Detects cyclic pointers and returns null', () { - expect(decodeMDnsResponse(cycle), isNull); - }); -} - -void testPTRRData() { - test('Can read FQDN from PTR data', () { - expect( - 'sgjesse-macbookpro2 [78:31:c1:b8:55:38]._workstation._tcp.local', - readFQDN(ptrRData), - ); - expect('fletch-agent._fletch_agent._tcp.local', readFQDN(ptrRData2)); - }); -} - -void testSRVRData() { - test('Can read FQDN from SRV data', () { - expect('fletch.local', readFQDN(srvRData, _kSrvHeaderSize)); - }); -} - -void testNonUtf8DomainName() { - test('Returns non-null for non-utf8 domain name', () { - final List result = decodeMDnsResponse(nonUtf8Package)!; - expect(result, isNotNull); - expect(result[0] is TxtResourceRecord, isTrue); - final TxtResourceRecord txt = result[0] as TxtResourceRecord; - expect(txt.name, contains('�')); - }); -} - -// One address. -const List package1 = [ - 0x00, - 0x00, - 0x84, - 0x00, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, - 0x00, - 0x00, - 0x0b, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0x05, - 0x6c, - 0x6f, - 0x63, - 0x61, - 0x6c, - 0x00, - 0x00, - 0x01, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x04, - 0xc0, - 0xa8, - 0x01, - 0xbf, -]; - -// Two addresses. -const List package2 = [ - 0x00, - 0x00, - 0x84, - 0x00, - 0x00, - 0x00, - 0x00, - 0x02, - 0x00, - 0x00, - 0x00, - 0x00, - 0x0b, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0x05, - 0x6c, - 0x6f, - 0x63, - 0x61, - 0x6c, - 0x00, - 0x00, - 0x01, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x04, - 0xc0, - 0xa8, - 0x01, - 0xbf, - 0xc0, - 0x0c, - 0x00, - 0x01, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x04, - 0xa9, - 0xfe, - 0x5f, - 0x53, -]; - -// Eight mixed answers. -const List package3 = [ - 0x00, - 0x00, - 0x84, - 0x00, - 0x00, - 0x00, - 0x00, - 0x08, - 0x00, - 0x00, - 0x00, - 0x00, - 0x1f, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0x20, - 0x5b, - 0x62, - 0x38, - 0x3a, - 0x32, - 0x37, - 0x3a, - 0x65, - 0x62, - 0x3a, - 0x30, - 0x33, - 0x3a, - 0x39, - 0x32, - 0x3a, - 0x34, - 0x62, - 0x5d, - 0x0c, - 0x5f, - 0x77, - 0x6f, - 0x72, - 0x6b, - 0x73, - 0x74, - 0x61, - 0x74, - 0x69, - 0x6f, - 0x6e, - 0x04, - 0x5f, - 0x74, - 0x63, - 0x70, - 0x05, - 0x6c, - 0x6f, - 0x63, - 0x61, - 0x6c, - 0x00, - 0x00, - 0x10, - 0x80, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x01, - 0x00, - 0x0b, - 0x5f, - 0x75, - 0x64, - 0x69, - 0x73, - 0x6b, - 0x73, - 0x2d, - 0x73, - 0x73, - 0x68, - 0xc0, - 0x39, - 0x00, - 0x0c, - 0x00, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x0e, - 0x0b, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0xc0, - 0x50, - 0xc0, - 0x68, - 0x00, - 0x21, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x14, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x16, - 0x0b, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0xc0, - 0x3e, - 0xc0, - 0x68, - 0x00, - 0x10, - 0x80, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x01, - 0x00, - 0x09, - 0x5f, - 0x73, - 0x65, - 0x72, - 0x76, - 0x69, - 0x63, - 0x65, - 0x73, - 0x07, - 0x5f, - 0x64, - 0x6e, - 0x73, - 0x2d, - 0x73, - 0x64, - 0x04, - 0x5f, - 0x75, - 0x64, - 0x70, - 0xc0, - 0x3e, - 0x00, - 0x0c, - 0x00, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x02, - 0xc0, - 0x50, - 0xc0, - 0x2c, - 0x00, - 0x0c, - 0x00, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x02, - 0xc0, - 0x0c, - 0xc0, - 0x0c, - 0x00, - 0x21, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x08, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x09, - 0xc0, - 0x88, - 0xc0, - 0xa3, - 0x00, - 0x0c, - 0x00, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x02, - 0xc0, - 0x2c, -]; - -/// Contains compressed domain names where a there is a cycle amongst the -/// offset pointers. -const List cycle = [ - 0x00, - 0x00, - 0x84, - 0x00, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, - 0x00, - 0x00, - 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // "example" - 0xC0, 0x16, // Pointer to "com" - 0x03, 0x63, 0x6f, 0x6d, // "com" - 0xC0, 0x0c, // Pointer to "example" - 0x00, - 0x00, - 0x01, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x04, - 0xc0, - 0xa8, - 0x01, - 0xbf, -]; - -const List packagePtrResponse = [ - 0x00, - 0x00, - 0x84, - 0x00, - 0x00, - 0x00, - 0x00, - 0x06, - 0x00, - 0x00, - 0x00, - 0x00, - 0x0d, - 0x5f, - 0x66, - 0x6c, - 0x65, - 0x74, - 0x63, - 0x68, - 0x5f, - 0x61, - 0x67, - 0x65, - 0x6e, - 0x74, - 0x04, - 0x5f, - 0x74, - 0x63, - 0x70, - 0x05, - 0x6c, - 0x6f, - 0x63, - 0x61, - 0x6c, - 0x00, - 0x00, - 0x0c, - 0x00, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x1e, - 0x1b, - 0x66, - 0x6c, - 0x65, - 0x74, - 0x63, - 0x68, - 0x2d, - 0x61, - 0x67, - 0x65, - 0x6e, - 0x74, - 0x20, - 0x6f, - 0x6e, - 0x20, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0xc0, - 0x0c, - 0xc0, - 0x30, - 0x00, - 0x10, - 0x80, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x01, - 0x00, - 0xc0, - 0x30, - 0x00, - 0x21, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x14, - 0x00, - 0x00, - 0x00, - 0x00, - 0x2f, - 0x59, - 0x0b, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0xc0, - 0x1f, - 0xc0, - 0x6d, - 0x00, - 0x1c, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x10, - 0xfe, - 0x80, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xba, - 0x27, - 0xeb, - 0xff, - 0xfe, - 0x69, - 0x6e, - 0x3a, - 0xc0, - 0x6d, - 0x00, - 0x01, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x04, - 0xc0, - 0xa8, - 0x01, - 0x01, - 0xc0, - 0x6d, - 0x00, - 0x01, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x04, - 0xa9, - 0xfe, - 0xa7, - 0xac, -]; - -const List ptrRData = [ - 0x27, - 0x73, - 0x67, - 0x6a, - 0x65, - 0x73, - 0x73, - 0x65, - 0x2d, - 0x6d, - 0x61, - 0x63, - 0x62, - 0x6f, - 0x6f, - 0x6b, - 0x70, - 0x72, - 0x6f, - 0x32, - 0x20, - 0x5b, - 0x37, - 0x38, - 0x3a, - 0x33, - 0x31, - 0x3a, - 0x63, - 0x31, - 0x3a, - 0x62, - 0x38, - 0x3a, - 0x35, - 0x35, - 0x3a, - 0x33, - 0x38, - 0x5d, - 0x0c, - 0x5f, - 0x77, - 0x6f, - 0x72, - 0x6b, - 0x73, - 0x74, - 0x61, - 0x74, - 0x69, - 0x6f, - 0x6e, - 0x04, - 0x5f, - 0x74, - 0x63, - 0x70, - 0x05, - 0x6c, - 0x6f, - 0x63, - 0x61, - 0x6c, - 0x00, -]; - -const List ptrRData2 = [ - 0x0c, - 0x66, - 0x6c, - 0x65, - 0x74, - 0x63, - 0x68, - 0x2d, - 0x61, - 0x67, - 0x65, - 0x6e, - 0x74, - 0x0d, - 0x5f, - 0x66, - 0x6c, - 0x65, - 0x74, - 0x63, - 0x68, - 0x5f, - 0x61, - 0x67, - 0x65, - 0x6e, - 0x74, - 0x04, - 0x5f, - 0x74, - 0x63, - 0x70, - 0x05, - 0x6c, - 0x6f, - 0x63, - 0x61, - 0x6c, - 0x00, -]; - -const List srvRData = [ - 0x00, - 0x00, - 0x00, - 0x00, - 0x2f, - 0x59, - 0x06, - 0x66, - 0x6c, - 0x65, - 0x74, - 0x63, - 0x68, - 0x05, - 0x6c, - 0x6f, - 0x63, - 0x61, - 0x6c, - 0x00, -]; - -const List packetWithQuestionAnArCount = [ - 0, - 0, - 2, - 0, - 0, - 1, - 0, - 1, - 0, - 0, - 0, - 1, - 15, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 4, - 95, - 95, - 95, - 95, - 5, - 95, - 95, - 95, - 95, - 95, - 0, - 0, - 12, - 0, - 1, - 192, - 12, - 0, - 12, - 0, - 1, - 0, - 0, - 14, - 13, - 0, - 26, - 23, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 192, - 12, - 192, - 12, - 0, - 12, - 0, - 1, - 0, - 0, - 14, - 13, - 0, - 31, - 28, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 192, - 12, -]; - -const List packetWithoutQuestionWithAnArCount = [ - 0, - 0, - 132, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 15, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 4, - 95, - 95, - 95, - 95, - 5, - 95, - 95, - 95, - 95, - 95, - 0, - 0, - 12, - 0, - 1, - 0, - 0, - 17, - 148, - 0, - 25, - 22, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 192, - 12, - 22, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 12, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 192, - 28, - 0, - 16, - 0, - 1, - 0, - 0, - 17, - 148, - 0, - 51, - 20, - 109, - 111, - 100, - 101, - 108, - 61, - 77, - 97, - 99, - 66, - 111, - 111, - 107, - 80, - 114, - 111, - 49, - 52, - 44, - 51, - 10, - 111, - 115, - 120, - 118, - 101, - 114, - 115, - 61, - 49, - 56, - 18, - 101, - 99, - 111, - 108, - 111, - 114, - 61, - 50, - 50, - 53, - 44, - 50, - 50, - 53, - 44, - 50, - 50, - 51, -]; - -// This is the same as packetWithoutQuestionWithAnArCount, but the text -// resource just has a single long string. If the length isn't decoded -// separately from the string, there will be utf8 decoding failures. -const List packetWithLongTxt = [ - 0, - 0, - 132, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 15, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 4, - 95, - 95, - 95, - 95, - 5, - 95, - 95, - 95, - 95, - 95, - 0, - 0, - 12, - 0, - 1, - 0, - 0, - 17, - 148, - 0, - 25, - 22, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 192, - 12, - 22, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 12, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 95, - 192, - 28, - 0, - 16, - 0, - 1, - 0, - 0, - 17, - 148, - 0, - 51, - // Long string starts here. - 129, - 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 16 - 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 32 - 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // - 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 64 - 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // - 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // - 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // - 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 128, - 41, // 129 -]; - -// Package with a domain name that is not valid utf-8. -const List nonUtf8Package = [ - 0x00, - 0x00, - 0x84, - 0x00, - 0x00, - 0x00, - 0x00, - 0x08, - 0x00, - 0x00, - 0x00, - 0x00, - 0x1f, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0x20, - 0x5b, - 0x62, - 0x38, - 0x3a, - 0x32, - 0x37, - 0x3a, - 0x65, - 0x62, - 0xd2, - 0x30, - 0x33, - 0x3a, - 0x39, - 0x32, - 0x3a, - 0x34, - 0x62, - 0x5d, - 0x0c, - 0x5f, - 0x77, - 0x6f, - 0x72, - 0x6b, - 0x73, - 0x74, - 0x61, - 0x74, - 0x69, - 0x6f, - 0x6e, - 0x04, - 0x5f, - 0x74, - 0x63, - 0x70, - 0x05, - 0x6c, - 0x6f, - 0x63, - 0x61, - 0x6c, - 0x00, - 0x00, - 0x10, - 0x80, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x01, - 0x00, - 0x0b, - 0x5f, - 0x75, - 0x64, - 0x69, - 0x73, - 0x6b, - 0x73, - 0x2d, - 0x73, - 0x73, - 0x68, - 0xc0, - 0x39, - 0x00, - 0x0c, - 0x00, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x0e, - 0x0b, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0xc0, - 0x50, - 0xc0, - 0x68, - 0x00, - 0x21, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x14, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x16, - 0x0b, - 0x72, - 0x61, - 0x73, - 0x70, - 0x62, - 0x65, - 0x72, - 0x72, - 0x79, - 0x70, - 0x69, - 0xc0, - 0x3e, - 0xc0, - 0x68, - 0x00, - 0x10, - 0x80, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x01, - 0x00, - 0x09, - 0x5f, - 0x73, - 0x65, - 0x72, - 0x76, - 0x69, - 0x63, - 0x65, - 0x73, - 0x07, - 0x5f, - 0x64, - 0x6e, - 0x73, - 0x2d, - 0x73, - 0x64, - 0x04, - 0x5f, - 0x75, - 0x64, - 0x70, - 0xc0, - 0x3e, - 0x00, - 0x0c, - 0x00, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x02, - 0xc0, - 0x50, - 0xc0, - 0x2c, - 0x00, - 0x0c, - 0x00, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x02, - 0xc0, - 0x0c, - 0xc0, - 0x0c, - 0x00, - 0x21, - 0x80, - 0x01, - 0x00, - 0x00, - 0x00, - 0x78, - 0x00, - 0x08, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x09, - 0xc0, - 0x88, - 0xc0, - 0xa3, - 0x00, - 0x0c, - 0x00, - 0x01, - 0x00, - 0x00, - 0x11, - 0x94, - 0x00, - 0x02, - 0xc0, - 0x2c, -]; diff --git a/multicast_dns_source_ref/test/lookup_resolver_test.dart b/multicast_dns_source_ref/test/lookup_resolver_test.dart deleted file mode 100644 index 1188ec26..00000000 --- a/multicast_dns_source_ref/test/lookup_resolver_test.dart +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:multicast_dns/src/lookup_resolver.dart'; -import 'package:multicast_dns/src/resource_record.dart'; -import 'package:test/test.dart'; - -void main() { - testTimeout(); - testResult(); - testResult2(); - testResult3(); -} - -ResourceRecord ip4Result(String name, InternetAddress address) { - final int validUntil = DateTime.now().millisecondsSinceEpoch + 2000; - return IPAddressResourceRecord(name, validUntil, address: address); -} - -void testTimeout() { - test('Resolver does not return with short timeout', () async { - const Duration shortTimeout = Duration(milliseconds: 1); - final LookupResolver resolver = LookupResolver(); - final Stream result = resolver.addPendingRequest( - ResourceRecordType.addressIPv4, - 'xxx', - shortTimeout, - ); - expect(await result.isEmpty, isTrue); - }); -} - -// One pending request and one response. -void testResult() { - test('One pending request and one response', () async { - const Duration noTimeout = Duration(days: 1); - final LookupResolver resolver = LookupResolver(); - final Stream futureResult = resolver.addPendingRequest( - ResourceRecordType.addressIPv4, - 'xxx.local', - noTimeout, - ); - final ResourceRecord response = ip4Result( - 'xxx.local', - InternetAddress('1.2.3.4'), - ); - resolver.handleResponse([response]); - final IPAddressResourceRecord result = - await futureResult.first as IPAddressResourceRecord; - expect('1.2.3.4', result.address.address); - resolver.clearPendingRequests(); - }); -} - -void testResult2() { - test('Two requests', () async { - const Duration noTimeout = Duration(days: 1); - final LookupResolver resolver = LookupResolver(); - final Stream futureResult1 = resolver.addPendingRequest( - ResourceRecordType.addressIPv4, - 'xxx.local', - noTimeout, - ); - final Stream futureResult2 = resolver.addPendingRequest( - ResourceRecordType.addressIPv4, - 'yyy.local', - noTimeout, - ); - final ResourceRecord response1 = ip4Result( - 'xxx.local', - InternetAddress('1.2.3.4'), - ); - final ResourceRecord response2 = ip4Result( - 'yyy.local', - InternetAddress('2.3.4.5'), - ); - resolver.handleResponse([response2, response1]); - final IPAddressResourceRecord result1 = - await futureResult1.first as IPAddressResourceRecord; - final IPAddressResourceRecord result2 = - await futureResult2.first as IPAddressResourceRecord; - expect('1.2.3.4', result1.address.address); - expect('2.3.4.5', result2.address.address); - resolver.clearPendingRequests(); - }); -} - -void testResult3() { - test('Multiple requests', () async { - const Duration noTimeout = Duration(days: 1); - final LookupResolver resolver = LookupResolver(); - final ResourceRecord response0 = ip4Result( - 'zzz.local', - InternetAddress('2.3.4.5'), - ); - resolver.handleResponse([response0]); - final Stream futureResult1 = resolver.addPendingRequest( - ResourceRecordType.addressIPv4, - 'xxx.local', - noTimeout, - ); - resolver.handleResponse([response0]); - final Stream futureResult2 = resolver.addPendingRequest( - ResourceRecordType.addressIPv4, - 'yyy.local', - noTimeout, - ); - resolver.handleResponse([response0]); - final ResourceRecord response1 = ip4Result( - 'xxx.local', - InternetAddress('1.2.3.4'), - ); - resolver.handleResponse([response0]); - final ResourceRecord response2 = ip4Result( - 'yyy.local', - InternetAddress('2.3.4.5'), - ); - resolver.handleResponse([response0]); - resolver.handleResponse([response2, response1]); - resolver.handleResponse([response0]); - final IPAddressResourceRecord result1 = - await futureResult1.first as IPAddressResourceRecord; - final IPAddressResourceRecord result2 = - await futureResult2.first as IPAddressResourceRecord; - expect('1.2.3.4', result1.address.address); - expect('2.3.4.5', result2.address.address); - resolver.clearPendingRequests(); - }); -} diff --git a/multicast_dns_source_ref/test/resource_record_cache_test.dart b/multicast_dns_source_ref/test/resource_record_cache_test.dart deleted file mode 100644 index 11be6276..00000000 --- a/multicast_dns_source_ref/test/resource_record_cache_test.dart +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Test that the resource record cache works correctly. In particular, make -// sure that it removes all entries for a name before insertingrecords -// of that name. - -import 'dart:io'; - -import 'package:multicast_dns/src/native_protocol_client.dart' - show ResourceRecordCache; -import 'package:multicast_dns/src/resource_record.dart'; -import 'package:test/test.dart'; - -void main() { - testOverwrite(); - testTimeout(); -} - -void testOverwrite() { - test('Cache can overwrite entries', () { - final InternetAddress ip1 = InternetAddress('192.168.1.1'); - final InternetAddress ip2 = InternetAddress('192.168.1.2'); - final int valid = DateTime.now().millisecondsSinceEpoch + 86400 * 1000; - - final ResourceRecordCache cache = ResourceRecordCache(); - - // Add two different records. - cache.updateRecords([ - IPAddressResourceRecord('hest', valid, address: ip1), - IPAddressResourceRecord('fisk', valid, address: ip2), - ]); - expect(cache.entryCount, 2); - - // Update these records. - cache.updateRecords([ - IPAddressResourceRecord('hest', valid, address: ip1), - IPAddressResourceRecord('fisk', valid, address: ip2), - ]); - expect(cache.entryCount, 2); - - // Add two records with the same name (should remove the old one - // with that name only.) - cache.updateRecords([ - IPAddressResourceRecord('hest', valid, address: ip1), - IPAddressResourceRecord('hest', valid, address: ip2), - ]); - expect(cache.entryCount, 3); - - // Overwrite the two cached entries with one with the same name. - cache.updateRecords([ - IPAddressResourceRecord('hest', valid, address: ip1), - ]); - expect(cache.entryCount, 2); - }); -} - -void testTimeout() { - test('Cache can evict records after timeout', () { - final InternetAddress ip1 = InternetAddress('192.168.1.1'); - final int valid = DateTime.now().millisecondsSinceEpoch + 86400 * 1000; - final int notValid = DateTime.now().millisecondsSinceEpoch - 1; - - final ResourceRecordCache cache = ResourceRecordCache(); - - cache.updateRecords([ - IPAddressResourceRecord('hest', valid, address: ip1), - ]); - expect(cache.entryCount, 1); - - cache.updateRecords([ - IPAddressResourceRecord('fisk', notValid, address: ip1), - ]); - - List results = []; - cache.lookup('hest', ResourceRecordType.addressIPv4, results); - expect(results.isEmpty, isFalse); - - results = []; - cache.lookup('fisk', ResourceRecordType.addressIPv4, results); - expect(results.isEmpty, isTrue); - expect(cache.entryCount, 1); - }); -} diff --git a/multicast_dns_source_ref/tool/packet_gen.dart b/multicast_dns_source_ref/tool/packet_gen.dart deleted file mode 100644 index 6e1e278b..00000000 --- a/multicast_dns_source_ref/tool/packet_gen.dart +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: avoid_print - -// Support code to generate the hex-lists in test/decode_test.dart from -// a hex-stream. -import 'dart:io'; - -void formatHexStream(String hexStream) { - String s = ''; - for (int i = 0; i < hexStream.length / 2; i++) { - if (s.isNotEmpty) { - s += ', '; - } - s += '0x'; - final String x = hexStream.substring(i * 2, i * 2 + 2); - s += x; - if (((i + 1) % 8) == 0) { - s += ','; - print(s); - s = ''; - } - } - if (s.isNotEmpty) { - print(s); - } -} - -// Support code for generating the hex-lists in test/decode_test.dart. -void hexDumpList(List package) { - String s = ''; - for (int i = 0; i < package.length; i++) { - if (s.isNotEmpty) { - s += ', '; - } - s += '0x'; - final String x = package[i].toRadixString(16); - if (x.length == 1) { - s += '0'; - } - s += x; - if (((i + 1) % 8) == 0) { - s += ','; - print(s); - s = ''; - } - } - if (s.isNotEmpty) { - print(s); - } -} - -void dumpDatagram(Datagram datagram) { - String toHex(List ints) { - final StringBuffer buffer = StringBuffer(); - for (int i = 0; i < ints.length; i++) { - buffer.write(ints[i].toRadixString(16).padLeft(2, '0')); - if ((i + 1) % 10 == 0) { - buffer.writeln(); - } else { - buffer.write(' '); - } - } - return buffer.toString(); - } - - print('${datagram.address.address}:${datagram.port}:'); - print(toHex(datagram.data)); - print(''); -}