diff --git a/.cirrus.yml b/.cirrus.yml index aaf45ec62156..946535f59914 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -10,8 +10,12 @@ task: - git fetch origin master activate_script: pub global activate flutter_plugin_tools matrix: + - name: analyze + script: ./script/incremental_build.sh analyze - name: publishable script: ./script/check_publish.sh + depends_on: + - analyze - name: test+format install_script: - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - @@ -20,8 +24,8 @@ task: - sudo apt-get install -y --allow-unauthenticated clang-format-5.0 format_script: ./script/incremental_build.sh format --travis --clang-format=clang-format-5.0 test_script: ./script/incremental_build.sh test - - name: analyze - script: ./script/incremental_build.sh analyze + depends_on: + - analyze - name: build-apks+java-test env: matrix: @@ -30,6 +34,8 @@ task: script: - ./script/incremental_build.sh build-examples --apk - ./script/incremental_build.sh java-test # must come after apk build + depends_on: + - analyze task: use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' diff --git a/.gitignore b/.gitignore index c57b12a34b3b..851f68f5e2a3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ .idea .packages .pub/ +.dart_tool/ + pubspec.lock Podfile.lock diff --git a/packages/multicast_dns/CHANGELOG.md b/packages/multicast_dns/CHANGELOG.md new file mode 100644 index 000000000000..9d666838e129 --- /dev/null +++ b/packages/multicast_dns/CHANGELOG.md @@ -0,0 +1,5 @@ +## 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. \ No newline at end of file diff --git a/packages/multicast_dns/LICENSE b/packages/multicast_dns/LICENSE new file mode 100644 index 000000000000..73e6b6ec6754 --- /dev/null +++ b/packages/multicast_dns/LICENSE @@ -0,0 +1,27 @@ +Copyright 2019 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/packages/multicast_dns/README.md b/packages/multicast_dns/README.md new file mode 100644 index 000000000000..17dc254a720b --- /dev/null +++ b/packages/multicast_dns/README.md @@ -0,0 +1,21 @@ +# Multicast DNS package + +[![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 +To use this package, add `multicast_dns` as a +[dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). + +## Example + +Import the library via +``` dart +import 'package:multicast_dns/mdns_client.dart'; +``` + +Then use the `MDnsClient` Dart class in your code. To see how this is done, +check out the [example app](example/main.dart) or the sample implementations in +the [bin](bin/) directory. diff --git a/packages/multicast_dns/example/main.dart b/packages/multicast_dns/example/main.dart new file mode 100644 index 000000000000..c88a79ea19ab --- /dev/null +++ b/packages/multicast_dns/example/main.dart @@ -0,0 +1,37 @@ +// Copyright 2018, the Flutter project authors. Please see the AUTHORS file +// for details. 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. + +import 'package:multicast_dns/multicast_dns.dart'; + +void 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 recod for the service. + await for (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 (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/packages/multicast_dns/example/mdns-resolve.dart b/packages/multicast_dns/example/mdns-resolve.dart new file mode 100644 index 000000000000..9db513ccbc10 --- /dev/null +++ b/packages/multicast_dns/example/mdns-resolve.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. 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. + +import 'package:multicast_dns/multicast_dns.dart'; + +void 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 (IPAddressResourceRecord record in client + .lookup(ResourceRecordQuery.addressIPv4(name))) { + print('Found address (${record.address}).'); + } + + await for (IPAddressResourceRecord record in client + .lookup(ResourceRecordQuery.addressIPv6(name))) { + print('Found address (${record.address}).'); + } + client.stop(); +} diff --git a/packages/multicast_dns/example/mdns-sd.dart b/packages/multicast_dns/example/mdns-sd.dart new file mode 100644 index 000000000000..22d6c45858b6 --- /dev/null +++ b/packages/multicast_dns/example/mdns-sd.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. 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. + +import 'package:multicast_dns/multicast_dns.dart'; + +void 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 (PtrResourceRecord ptr in client + .lookup(ResourceRecordQuery.serverPointer(name))) { + if (verbose) { + print(ptr); + } + await for (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 (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 (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/packages/multicast_dns/lib/multicast_dns.dart b/packages/multicast_dns/lib/multicast_dns.dart new file mode 100644 index 000000000000..eeb71d126243 --- /dev/null +++ b/packages/multicast_dns/lib/multicast_dns.dart @@ -0,0 +1,203 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. 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/src/constants.dart'; +import 'package:multicast_dns/src/lookup_resolver.dart'; +import 'package:multicast_dns/src/native_protocol_client.dart'; +import 'package:multicast_dns/src/packet.dart'; +import 'package:multicast_dns/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); + +/// 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 { + bool _starting = false; + bool _started = false; + RawDatagramSocket _incoming; + final List _sockets = []; + final LookupResolver _resolver = LookupResolver(); + final ResourceRecordCache _cache = ResourceRecordCache(); + InternetAddress _mDnsAddress; + + /// Find all network interfaces with an the [InternetAddressType] specified. + static NetworkInterfacesFactory allInterfacesFactory = + (InternetAddressType type) => 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]. + Future start({ + InternetAddress listenAddress, + NetworkInterfacesFactory interfacesFactory, + }) async { + listenAddress ??= InternetAddress.anyIPv4; + interfacesFactory ??= allInterfacesFactory; + + assert(listenAddress.address == InternetAddress.anyIPv4.address || + listenAddress.address == InternetAddress.anyIPv6.address); + + if (_started || _starting) { + return; + } + _starting = true; + + // Listen on all addresses. + _incoming = await RawDatagramSocket.bind( + listenAddress.address, + mDnsPort, + reuseAddress: true, + reusePort: true, + ttl: 255, + ); + + // Can't send to IPv6 any address. + if (_incoming.address != InternetAddress.anyIPv6) { + _sockets.add(_incoming); + } + + _mDnsAddress = _incoming.address.type == InternetAddressType.IPv4 + ? mDnsAddressIPv4 + : mDnsAddressIPv6; + + final List interfaces = + await interfacesFactory(listenAddress.type); + + for (NetworkInterface interface in interfaces) { + // Create a socket for sending on each adapter. + final InternetAddress targetAddress = interface.addresses[0]; + final RawDatagramSocket socket = await RawDatagramSocket.bind( + targetAddress, + mDnsPort, + reuseAddress: true, + reusePort: true, + ttl: 255, + ); + _sockets.add(socket); + // Ensure that we're using this address/interface for multicast. + if (targetAddress.type == InternetAddressType.IPv4) { + socket.setRawOption(RawSocketOption( + RawSocketOption.levelIPv4, + RawSocketOption.IPv4MulticastInterface, + targetAddress.rawAddress, + )); + } else { + socket.setRawOption(RawSocketOption.fromInt( + RawSocketOption.levelIPv6, + RawSocketOption.IPv6MulticastInterface, + interface.index, + )); + } + // Join multicast on this interface. + _incoming.joinMulticast(_mDnsAddress, interface); + } + _incoming.listen(_handleIncoming); + _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.'); + } + + for (RawDatagramSocket socket in _sockets) { + socket.close(); + } + + _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), + }) { + if (!_started) { + 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); + + // Send the request on all interfaces. + final List packet = query.encode(); + for (RawDatagramSocket socket in _sockets) { + socket.send(packet, _mDnsAddress, mDnsPort); + } + return results; + } + + // Process incoming datagrams. + void _handleIncoming(RawSocketEvent event) { + if (event == RawSocketEvent.read) { + final Datagram datagram = _incoming.receive(); + + // 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/packages/multicast_dns/lib/src/constants.dart b/packages/multicast_dns/lib/src/constants.dart new file mode 100644 index 000000000000..2a222649b64d --- /dev/null +++ b/packages/multicast_dns/lib/src/constants.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. 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. +class ResourceRecordClass { + // This class is intended to be used as a namespace, and should not be + // extended directly. + factory ResourceRecordClass._() => null; + + /// Internet address class ("IN"). + static const int internet = 1; +} + +/// Enumeration of DNS question types. +class QuestionType { + // This class is intended to be used as a namespace, and should not be + // extended directly. + factory QuestionType._() => null; + + /// "QU" Question. + static const int unicast = 0x8000; + + /// "QM" Question. + static const int multicast = 0x0000; +} diff --git a/packages/multicast_dns/lib/src/lookup_resolver.dart b/packages/multicast_dns/lib/src/lookup_resolver.dart new file mode 100644 index 000000000000..b374e9f0449a --- /dev/null +++ b/packages/multicast_dns/lib/src/lookup_resolver.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. 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 'package:multicast_dns/src/resource_record.dart'; + +/// Class for maintaining state about pending mDNS requests. +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 (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 (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/packages/multicast_dns/lib/src/native_protocol_client.dart b/packages/multicast_dns/lib/src/native_protocol_client.dart new file mode 100644 index 000000000000..bef02bca762b --- /dev/null +++ b/packages/multicast_dns/lib/src/native_protocol_client.dart @@ -0,0 +1,76 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. 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 'package:multicast_dns/src/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 (ResourceRecord record in records) { + seenRecordTypes[record.resourceRecordType] ??= Set(); + 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/packages/multicast_dns/lib/src/packet.dart b/packages/multicast_dns/lib/src/packet.dart new file mode 100644 index 000000000000..8614f4597a0d --- /dev/null +++ b/packages/multicast_dns/lib/src/packet.dart @@ -0,0 +1,375 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. 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:multicast_dns/src/constants.dart'; +import 'package:multicast_dns/src/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) { + assert(name != null); + 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(name != null); + assert(ResourceRecordType.debugAssertValid(type)); + assert(multicast != null); + + 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; + 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. + checkLength(offset + 2); + + // A compressed FQDN has a new offset in the lower 14 bits. + final _FQDNReadResult result = _readFQDN( + data, byteData, byteData.getUint16(offset) & ~0xc000, length); + parts.addAll(result.fqdnParts); + 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; + parts.add(utf8.decode(partBytes)); + } else { + break; + } + } + } + return _FQDNReadResult(parts, offset - prevOffset); +} + +/// Decode an mDNS query packet. +/// +/// If decoding fails (e.g. due to an invalid packet), `null` is returned. +/// +/// See https://tools.ietf.org/html/rfc1035 for format. +ResourceRecordQuery decodeMDnsQuery(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); + + // Check whether it's a query. + final int flags = packetBytes.getUint16(_kFlagsOffset); + if (flags != 0) { + return null; + } + final int questionCount = packetBytes.getUint16(_kQdcountOffset); + if (questionCount == 0) { + return null; + } + + final _FQDNReadResult fqdn = + _readFQDN(data, packetBytes, _kHeaderSize, data.length); + + int offset = _kHeaderSize + fqdn.bytesRead; + final int type = packetBytes.getUint16(offset); + offset += 2; + final int queryType = packetBytes.getUint16(offset) & 0x8000; + return ResourceRecordQuery(type, fqdn.fqdn, queryType); +} + +/// 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); + if (answerCount == 0) { + return null; + } + + final int answerRecordCount = packetBytes.getUint16(_kArcountOffset); + 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; 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; 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, + ); + break; + 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); + final Uint8List rawText = Uint8List.view( + data.buffer, + offset, + readDataLength, + ); + final String text = utf8.decode(rawText); + offset += readDataLength; + return TxtResourceRecord(fqdn, validUntil, text: text); + 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 < answerCount + answerRecordCount; 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) : assert(offset != null); + + /// The offset in the packet at which the exception occurred. + final int offset; + + @override + String toString() => 'Decoding error at $offset'; +} diff --git a/packages/multicast_dns/lib/src/resource_record.dart b/packages/multicast_dns/lib/src/resource_record.dart new file mode 100644 index 000000000000..e4be5f30d6de --- /dev/null +++ b/packages/multicast_dns/lib/src/resource_record.dart @@ -0,0 +1,403 @@ +// Copyright 2018 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 'package:multicast_dns/src/constants.dart'; +import 'package:multicast_dns/src/packet.dart'; + +// TODO(dnfield): Probably should go with a real hashing function here +// when https://github.com/dart-lang/sdk/issues/11617 is figured out. +const int _seedHashPrime = 2166136261; +const int _multipleHashPrime = 16777619; + +int _combineHash(int current, int hash) => + (current & _multipleHashPrime) ^ hash; + +int _hashValues(List values) { + assert(values != null); + assert(values.isNotEmpty); + + return values.fold( + _seedHashPrime, + (int current, int next) => _combineHash(current, next), + ); +} + +/// Enumeration of support resource record types. +class ResourceRecordType { + // This class is intended to be used as a namespace, and should not be + // extended directly. + factory ResourceRecordType._() => null; + + /// 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. +class ResourceRecordQuery { + /// Creates a new ResourceRecordQuery. + /// + /// Most callers should prefer one of the named constructors. + ResourceRecordQuery( + this.resourceRecordType, + this.fullyQualifiedName, + this.questionType, + ) : assert(fullyQualifiedName != null), + 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 => _hashValues( + [resourceRecordType, fullyQualifiedName.hashCode, questionType]); + + @override + bool operator ==(Object other) { + return other is ResourceRecordQuery && + other.resourceRecordType == resourceRecordType && + other.fullyQualifiedName == fullyQualifiedName && + other.questionType == questionType; + } + + @override + String toString() => + '$runtimeType{$fullyQualifiedName, type: ${ResourceRecordType.toDebugString(resourceRecordType)}, isMulticast: $isMulticast}'; +} + +/// Base implementation of DNS resource records (RRs). +abstract class ResourceRecord { + /// Creates a new ResourceRecord. + const ResourceRecord(this.resourceRecordType, this.name, this.validUntil) + : assert(name != null); + + /// 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 ?? 0)}, $_additionalInfo}'; + + @override + bool operator ==(Object other) { + return other.runtimeType == runtimeType && _equals(other); + } + + @protected + bool _equals(ResourceRecord other) { + return other.name == name && + other.validUntil == validUntil && + other.resourceRecordType == resourceRecordType; + } + + @override + int get hashCode { + return _hashValues([ + name.hashCode, + validUntil.hashCode, + resourceRecordType.hashCode, + _hashCode, + ]); + } + + // Subclasses of this class should use _hashValues to create a hash code + // that will then get hashed in with the common values on this class. + @protected + int get _hashCode; + + /// 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. + PtrResourceRecord( + String name, + int validUntil, { + @required this.domainName, + }) : assert(domainName != null), + super(ResourceRecordType.serverPointer, name, validUntil); + + /// The FQDN for this record. + final String domainName; + + @override + String get _additionalInfo => 'domainName: $domainName'; + + @override + bool _equals(ResourceRecord other) { + return other is PtrResourceRecord && + other.domainName == domainName && + super._equals(other); + } + + @override + int get _hashCode => _combineHash(_seedHashPrime, domainName.hashCode); + + @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 + bool _equals(ResourceRecord other) { + return other is IPAddressResourceRecord && other.address == address; + } + + @override + int get _hashCode => _combineHash(_seedHashPrime, address.hashCode); + + @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. + SrvResourceRecord( + String name, + int validUntil, { + @required this.target, + @required this.port, + @required this.priority, + @required this.weight, + }) : assert(target != null), + assert(port != null), + assert(priority != null), + assert(weight != null), + 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 + bool _equals(ResourceRecord other) { + return other is SrvResourceRecord && + other.target == target && + other.port == port && + other.priority == priority && + other.weight == weight; + } + + @override + int get _hashCode => _hashValues([ + target.hashCode, + port.hashCode, + priority.hashCode, + weight.hashCode, + ]); + + @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. + TxtResourceRecord( + String name, + int validUntil, { + @required this.text, + }) : assert(text != null), + super(ResourceRecordType.text, name, validUntil); + + /// The raw text from this record. + final String text; + + @override + String get _additionalInfo => 'text: $text'; + + @override + bool _equals(ResourceRecord other) { + return other is TxtResourceRecord && other.text == text; + } + + @override + int get _hashCode => _combineHash(_seedHashPrime, text.hashCode); + + @override + Uint8List encodeResponseRecord() { + return Uint8List.fromList(utf8.encode(text)); + } +} diff --git a/packages/multicast_dns/pubspec.yaml b/packages/multicast_dns/pubspec.yaml new file mode 100644 index 000000000000..cc35f1511efb --- /dev/null +++ b/packages/multicast_dns/pubspec.yaml @@ -0,0 +1,14 @@ +name: multicast_dns +description: Dart package for mDNS queries (e.g. Bonjour, Avahi). +author: Flutter Team +homepage: https://github.com/flutter/packages/tree/master/packages/mdns +version: 0.1.0 + +dependencies: + meta: ^1.1.6 + +dev_dependencies: + test: "^1.3.4" + +environment: + sdk: ">=2.1.1-dev.2.0 <3.0.0" diff --git a/packages/multicast_dns/test/decode_test.dart b/packages/multicast_dns/test/decode_test.dart new file mode 100644 index 000000000000..5790278c9d3a --- /dev/null +++ b/packages/multicast_dns/test/decode_test.dart @@ -0,0 +1,834 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. 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:test/test.dart'; +import 'package:multicast_dns/src/packet.dart'; +import 'package:multicast_dns/src/resource_record.dart'; + +const int _kSrvHeaderSize = 6; + +void main() { + testValidPackages(); + testBadPackages(); + // 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]; + expect(ipResult.name, 'raspberrypi.local'); + expect(ipResult.address.address, '192.168.1.191'); + + result = decodeMDnsResponse(package2); + expect(result.length, 2); + ipResult = result[0]; + expect(ipResult.name, 'raspberrypi.local'); + expect(ipResult.address.address, '192.168.1.191'); + ipResult = result[1]; + 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: '\x00', + ), + 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: '\x00', + ), + 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: '\x00', + ), + 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'), + ), + ]); + }); +} + +void testBadPackages() { + test('Returns null for invalid packets', () { + for (List p in >[package1, package2, package3]) { + for (int i = 0; i < p.length; i++) { + expect(decodeMDnsResponse(p.sublist(0, i)), 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)); + }); +} + +// 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 +]; + +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 +]; diff --git a/packages/multicast_dns/test/lookup_resolver_test.dart b/packages/multicast_dns/test/lookup_resolver_test.dart new file mode 100644 index 000000000000..6a311fd1bad8 --- /dev/null +++ b/packages/multicast_dns/test/lookup_resolver_test.dart @@ -0,0 +1,97 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. 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:test/test.dart'; +import 'package:multicast_dns/src/lookup_resolver.dart'; +import 'package:multicast_dns/src/resource_record.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; + 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; + final IPAddressResourceRecord result2 = await futureResult2.first; + 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; + final IPAddressResourceRecord result2 = await futureResult2.first; + expect('1.2.3.4', result1.address.address); + expect('2.3.4.5', result2.address.address); + resolver.clearPendingRequests(); + }); +} diff --git a/packages/multicast_dns/test/resource_record_cache_test.dart b/packages/multicast_dns/test/resource_record_cache_test.dart new file mode 100644 index 000000000000..1b9130fa6eba --- /dev/null +++ b/packages/multicast_dns/test/resource_record_cache_test.dart @@ -0,0 +1,84 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. 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:test/test.dart'; +import 'package:multicast_dns/src/resource_record.dart'; +import 'package:multicast_dns/src/native_protocol_client.dart' + show ResourceRecordCache; + +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/packages/multicast_dns/tool/packet_gen.dart b/packages/multicast_dns/tool/packet_gen.dart new file mode 100644 index 000000000000..36486be1e010 --- /dev/null +++ b/packages/multicast_dns/tool/packet_gen.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// 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(''); +} diff --git a/packages/palette_generator/lib/palette_generator.dart b/packages/palette_generator/lib/palette_generator.dart index 397625835c29..fe26e7016ebc 100644 --- a/packages/palette_generator/lib/palette_generator.dart +++ b/packages/palette_generator/lib/palette_generator.dart @@ -912,9 +912,9 @@ class _ColorVolumeBox { _population = count; } - /// Split this color box at the mid-point along it's longest dimension + /// Split this color box at the mid-point along it's longest dimension. /// - /// Returns the new ColorBox + /// Returns the new ColorBox. _ColorVolumeBox splitBox() { assert(canSplit(), "Can't split a box with only 1 color"); // find median along the longest dimension