// 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'; }