Skip to content

Commit a062a28

Browse files
committed
Initial refactor/adding of mdns
1 parent 372e481 commit a062a28

31 files changed

+1984
-1233
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
.idea
44
.packages
55
.pub/
6+
.dart_tool/
7+
68
pubspec.lock
79

810
Podfile.lock
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## 0.1.0
2+
3+
* Initial Open Source release.
4+
* Migrates the dartino-sdk's mDNS client to Dart 2.0 and Flutter's analysis rules
5+
* Breaks from original Dartino code, as it does not use native libraries for macOS and overhauls the `ResourceRecord` class.

packages/multicast_dns/LICENSE

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Copyright 2014 The Chromium Authors. All rights reserved.
2+
3+
Redistribution and use in source and binary forms, with or without
4+
modification, are permitted provided that the following conditions are
5+
met:
6+
7+
* Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above
10+
copyright notice, this list of conditions and the following
11+
disclaimer in the documentation and/or other materials provided
12+
with the distribution.
13+
* Neither the name of Google Inc. nor the names of its
14+
contributors may be used to endorse or promote products derived
15+
from this software without specific prior written permission.
16+
17+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

pkg/mdns/bin/mdns-resolve.dart renamed to packages/multicast_dns/example/mdns-resolve.dart

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file
22
// for details. All rights reserved. Use of this source code is governed by a
3-
// BSD-style license that can be found in the LICENSE.md file.
3+
// BSD-style license that can be found in the LICENSE file.
44

55
// Example script to illustrate how to use the mdns package to lookup names
66
// on the local network.
77

88
import 'package:args/args.dart';
99

10-
import '../lib/mdns.dart';
10+
import 'package:multicast_dns/mdns_client.dart';
1111

12-
main(List<String> args) async {
12+
void main(List<String> args) async {
1313
// Parse the command line arguments.
14-
var parser = new ArgParser();
14+
final ArgParser parser = ArgParser();
1515
parser.addOption('timeout', abbr: 't', defaultsTo: '5');
16-
var arguments = parser.parse(args);
16+
final ArgResults arguments = parser.parse(args);
1717

1818
if (arguments.rest.length != 1) {
1919
print('''
@@ -24,14 +24,13 @@ For example:
2424
return;
2525
}
2626

27-
var name = arguments.rest[0];
27+
final String name = arguments.rest[0];
2828

29-
MDnsClient client = new MDnsClient();
29+
final MDnsClient client = MDnsClient();
3030
await client.start();
31-
var timeout;
32-
timeout = new Duration(seconds: int.parse(arguments['timeout']));
33-
await for (ResourceRecord record in
34-
client.lookup(RRType.A, name, timeout: timeout)) {
31+
final Duration timeout = Duration(seconds: int.parse(arguments['timeout']));
32+
await for (IPAddressResourceRecord record
33+
in client.lookup(RRType.a, name, timeout: timeout)) {
3534
print('Found address (${record.address}).');
3635
}
3736
client.stop();
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Example script to illustrate how to use the mdns package to discover services
6+
// on the local network.
7+
8+
import 'package:args/args.dart';
9+
10+
import 'package:multicast_dns/mdns_client.dart';
11+
12+
void main(List<String> args) async {
13+
// Parse the command line arguments.
14+
final ArgParser parser = ArgParser();
15+
parser.addOption('timeout', abbr: 't', defaultsTo: '5');
16+
final ArgResults arguments = parser.parse(args);
17+
18+
if (arguments.rest.length != 1) {
19+
print('''
20+
Please provide the name of a service as argument.
21+
22+
For example:
23+
dart mdns-sd.dart [--timeout <timeout>] _workstation._tcp.local''');
24+
return;
25+
}
26+
27+
final String name = arguments.rest[0];
28+
29+
final MDnsClient client = MDnsClient();
30+
await client.start();
31+
await for (PtrResourceRecord ptr in client.lookup(RRType.ptr, name)) {
32+
final String domain = ptr.domainName;
33+
print(ptr);
34+
await for (SrvResourceRecord srv in client.lookup(RRType.srv, domain)) {
35+
final String target = srv.target;
36+
print(srv);
37+
await client.lookup(RRType.txt, domain).forEach(print);
38+
await for (IPAddressResourceRecord ip
39+
in client.lookup(RRType.a, target)) {
40+
print(ip);
41+
print('Service instance found at $target (${ip.address}).');
42+
}
43+
}
44+
}
45+
client.stop();
46+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// ignore_for_file: undefined_named_parameter
2+
// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file
3+
// for details. All rights reserved. Use of this source code is governed by a
4+
// BSD-style license that can be found in the LICENSE file.
5+
6+
import 'dart:async';
7+
import 'dart:io';
8+
9+
import 'package:multicast_dns/src/constants.dart';
10+
import 'package:multicast_dns/src/lookup_resolver.dart';
11+
import 'package:multicast_dns/src/native_protocol_client.dart';
12+
import 'package:multicast_dns/src/packet.dart';
13+
import 'package:multicast_dns/src/resource_record.dart';
14+
15+
export 'package:multicast_dns/src/resource_record.dart';
16+
17+
/// Client for DNS lookup using the mDNS protocol.
18+
///
19+
/// This client only support "One-Shot Multicast DNS Queries" as described in
20+
/// section 5.1 of https://tools.ietf.org/html/rfc6762
21+
class MDnsClient {
22+
bool _starting = false;
23+
bool _started = false;
24+
RawDatagramSocket _incoming;
25+
final List<RawDatagramSocket> _sockets = <RawDatagramSocket>[];
26+
final LookupResolver _resolver = LookupResolver();
27+
final ResourceRecordCache _cache = ResourceRecordCache();
28+
29+
/// Start the mDNS client.
30+
Future<void> start() async {
31+
if (_started && _starting) {
32+
throw StateError('mDNS client already started');
33+
}
34+
_starting = true;
35+
36+
// Listen on all addresses.
37+
_incoming = await RawDatagramSocket.bind(
38+
InternetAddress.anyIPv4,
39+
mDnsPort,
40+
reuseAddress: true,
41+
reusePort: true,
42+
ttl: 255,
43+
);
44+
// Find all network interfaces with an IPv4 address.
45+
final List<NetworkInterface> interfaces = await NetworkInterface.list(
46+
includeLinkLocal: true,
47+
type: InternetAddressType.IPv4,
48+
includeLoopback: true,
49+
);
50+
51+
_sockets.add(_incoming);
52+
53+
for (NetworkInterface interface in interfaces) {
54+
// Create a socket for sending on each adapter.
55+
final RawDatagramSocket socket = await RawDatagramSocket.bind(
56+
interface.addresses[0],
57+
mDnsPort,
58+
reuseAddress: true,
59+
reusePort: true,
60+
ttl: 255,
61+
);
62+
_sockets.add(socket);
63+
64+
// Join multicast on this interface.
65+
_incoming.joinMulticast(mDnsAddress, interface);
66+
}
67+
_incoming.listen(_handleIncoming);
68+
69+
_starting = false;
70+
_started = true;
71+
}
72+
73+
/// Stop the client and close any associated sockets.
74+
void stop() {
75+
if (!_started) {
76+
return;
77+
}
78+
if (_starting) {
79+
throw StateError('Cannot stop mDNS client wile it is starting');
80+
}
81+
82+
for (RawDatagramSocket socket in _sockets) {
83+
socket.close();
84+
}
85+
86+
_resolver.clearPendingRequests();
87+
88+
_started = false;
89+
}
90+
91+
/// Lookup a [ResourceRecord], potentially from cache.
92+
Stream<ResourceRecord> lookup(int type, String name,
93+
{Duration timeout = const Duration(seconds: 5)}) {
94+
if (!_started) {
95+
throw StateError('mDNS client is not started');
96+
}
97+
// Look for entries in the cache.
98+
final List<ResourceRecord> cached = <ResourceRecord>[];
99+
_cache.lookup(name, type, cached);
100+
if (cached.isNotEmpty) {
101+
final StreamController<ResourceRecord> controller =
102+
StreamController<ResourceRecord>();
103+
cached.forEach(controller.add);
104+
controller.close();
105+
return controller.stream;
106+
}
107+
108+
// Add the pending request before sending the query.
109+
final Stream<ResourceRecord> results =
110+
_resolver.addPendingRequest(type, name, timeout);
111+
112+
// Send the request on all interfaces.
113+
final List<int> packet = encodeMDnsQuery(name, type: type);
114+
for (RawDatagramSocket socket in _sockets) {
115+
socket.send(packet, mDnsAddress, mDnsPort);
116+
}
117+
return results;
118+
}
119+
120+
// Process incoming datagrams.
121+
void _handleIncoming(RawSocketEvent event) {
122+
if (event == RawSocketEvent.read) {
123+
final Datagram datagram = _incoming.receive();
124+
125+
final List<ResourceRecord> response = decodeMDnsResponse(datagram.data);
126+
if (response != null) {
127+
_cache.updateRecords(response);
128+
_resolver.handleResponse(response);
129+
}
130+
}
131+
}
132+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export 'package:multicast_dns/src/resource_record.dart';
2+
3+
export 'mdns_client.dart';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
/// The IPv4 mDNS Address.
8+
InternetAddress mDnsAddress = InternetAddress('224.0.0.251');
9+
10+
/// The mDNS port.
11+
const int mDnsPort = 5353;
12+
13+
/// Enumeration of supported resource record class types.
14+
class RRClass {
15+
/// Internet address class ("IN").
16+
static const int internet = 1;
17+
}
18+
19+
/// Enumeration of DNS question types.
20+
class QuestionType {
21+
/// "QU" Question.
22+
static const int unicast = 0x8000;
23+
24+
/// "QM" Question.
25+
static const int multicast = 0x0000;
26+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:collection';
7+
8+
import 'package:multicast_dns/src/resource_record.dart';
9+
10+
/// Class for maintaining state about pending mDNS requests.
11+
class PendingRequest extends LinkedListEntry<PendingRequest> {
12+
/// Creates a new PendingRequest.
13+
PendingRequest(this.type, this.name, this.controller);
14+
15+
/// The [RRType] of the request.
16+
final int type;
17+
18+
/// The domain name.
19+
final String name;
20+
21+
/// A StreamController managing the request.
22+
final StreamController<ResourceRecord> controller;
23+
24+
/// The timer for the request.
25+
Timer timer;
26+
}
27+
28+
/// Class for keeping track of pending lookups and process incoming
29+
/// query responses.
30+
class LookupResolver {
31+
/// The requests the process.
32+
final LinkedList<PendingRequest> pendingRequests =
33+
LinkedList<PendingRequest>();
34+
35+
/// Adds a request and returns a [Stream] of [ResourceRecord] responses.
36+
Stream<ResourceRecord> addPendingRequest(
37+
int type, String name, Duration timeout) {
38+
final StreamController<ResourceRecord> controller =
39+
StreamController<ResourceRecord>();
40+
final PendingRequest request = PendingRequest(type, name, controller);
41+
final Timer timer = Timer(timeout, () {
42+
request.unlink();
43+
controller.close();
44+
});
45+
request.timer = timer;
46+
pendingRequests.add(request);
47+
return controller.stream;
48+
}
49+
50+
/// Processes responses back to the caller.
51+
void handleResponse(List<ResourceRecord> response) {
52+
for (ResourceRecord r in response) {
53+
final int type = r.rrValue;
54+
String name = r.name.toLowerCase();
55+
if (name.endsWith('.')) {
56+
name = name.substring(0, name.length - 1);
57+
}
58+
59+
bool responseMatches(PendingRequest request) {
60+
return request.name.toLowerCase() == name && request.type == type;
61+
}
62+
63+
for (PendingRequest pendingRequest in pendingRequests) {
64+
if (responseMatches(pendingRequest)) {
65+
if (pendingRequest.controller.isClosed) {
66+
return;
67+
}
68+
pendingRequest.controller.add(r);
69+
}
70+
}
71+
}
72+
}
73+
74+
/// Removes any pending requests and ends processing.
75+
void clearPendingRequests() {
76+
while (pendingRequests.isNotEmpty) {
77+
final PendingRequest request = pendingRequests.first;
78+
request.unlink();
79+
request.timer.cancel();
80+
request.controller.close();
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)