blob: e670c4a425734286fefc980736718dfe54b035f1 [file] [log] [blame]
// 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.md file.
library mdns.src.native_protocol_client;
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:mdns/mdns.dart';
import 'package:mdns/src/constants.dart';
import 'package:mdns/src/lookup_resolver.dart';
import 'package:mdns/src/packet.dart';
/// Cache for resource records that have been received.
///
/// There can be multiple entries for the same name and type.
///
/// The cached is updated with a list of records, because it needs to remove
/// all entries that correspond to 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 {
final List buffer;
final int size;
int position;
ResourceRecordCache({int size: 32})
: buffer = new List(size),
size = size,
position = 0;
void updateRecords(List<ResourceRecord> 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.
for (int i = 0; i < size; i++) {
ResourceRecord r = buffer[i % size];
if (r == null) continue;
String name = r.name;
int type = r.type;
for (ResourceRecord record in records) {
if (name == record.name && type == record.type) {
buffer[i % size] = null;
break;
}
}
}
// Add the new records.
for (ResourceRecord record in records) {
buffer[position] = record;
position = (position + 1) % size;
}
}
void lookup(String name, int type, List results) {
int time = new DateTime.now().millisecondsSinceEpoch;
for (int i = position + size; i >= position; i--) {
int index = i % size;
ResourceRecord record = buffer[index];
if (record == null) continue;
if (record.validUntil < time) {
buffer[index] = null;
} else if (record.name == name && record.type == type) {
results.add(record);
}
}
}
}
// Implementation of mDNS client using the native protocol.
class NativeProtocolMDnsClient implements MDnsClient {
bool _starting = false;
bool _started = false;
RawDatagramSocket _incoming;
final List<RawDatagramSocket> _sockets = <RawDatagramSocket>[];
final LookupResolver _resolver = new LookupResolver();
ResourceRecordCache cache = new ResourceRecordCache();
/// Start the mDNS client.
Future start() async {
if (_started && _starting) {
throw new StateError('mDNS client already started');
}
_starting = true;
// Listen on all addresses.
_incoming = await RawDatagramSocket.bind(
InternetAddress.ANY_IP_V4, mDnsPort, reuseAddress: true);
// Find all network interfaces with an IPv4 address.
var interfaces =
await NetworkInterface.list(type: InternetAddressType.IP_V4);
for (NetworkInterface interface in interfaces) {
// Create a socket for sending on each adapter.
var socket = await RawDatagramSocket.bind(
interface.addresses[0], mDnsPort, reuseAddress: true);
_sockets.add(socket);
// Join multicast on this interface.
(_incoming as dynamic).joinMulticast(mDnsAddress, interface);
}
_incoming.listen(_handleIncoming);
_starting = false;
_started = true;
}
void stop() {
if (!_started) return;
if (_starting) {
throw new StateError('Cannot stop mDNS client wile it is starting');
}
_sockets.forEach((socket) => socket.close());
_incoming.close();
_resolver.clearPendingRequests();
_started = false;
}
Stream<ResourceRecord> lookup(
int type,
String name,
{Duration timeout: const Duration(seconds: 5)}) {
if (!_started) {
throw new StateError('mDNS client is not started');
}
// Look for entries in the cache.
List<ResourceRecord> cached = <ResourceRecord>[];
cache.lookup(name, type, cached);
if (cached.isNotEmpty) {
StreamController controller = new StreamController();
cached.forEach(controller.add);
controller.close();
return controller.stream;
}
// Add the pending request before sending the query.
var results = _resolver.addPendingRequest(type, name, timeout);
// Send the request on all interfaces.
List<int> packet = encodeMDnsQuery(name, type);
for (int i = 0; i < _sockets.length; i++) {
_sockets[i].send(packet, mDnsAddress, mDnsPort);
}
return results;
}
// Process incoming datagrams.
_handleIncoming(event) {
if (event == RawSocketEvent.READ) {
Datagram datagram = _incoming.receive();
List<ResourceRecord> response = decodeMDnsResponse(datagram.data);
if (response != null) {
cache.updateRecords(response);
_resolver.handleResponse(response);
}
}
}
}