summaryrefslogtreecommitdiffstats
path: root/netwerk/dns/mdns/libmdns/fallback
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--netwerk/dns/mdns/libmdns/fallback/DNSPacket.jsm304
-rw-r--r--netwerk/dns/mdns/libmdns/fallback/DNSRecord.jsm71
-rw-r--r--netwerk/dns/mdns/libmdns/fallback/DNSResourceRecord.jsm221
-rw-r--r--netwerk/dns/mdns/libmdns/fallback/DNSTypes.jsm99
-rw-r--r--netwerk/dns/mdns/libmdns/fallback/DataReader.jsm134
-rw-r--r--netwerk/dns/mdns/libmdns/fallback/DataWriter.jsm97
-rw-r--r--netwerk/dns/mdns/libmdns/fallback/MulticastDNS.jsm985
7 files changed, 1911 insertions, 0 deletions
diff --git a/netwerk/dns/mdns/libmdns/fallback/DNSPacket.jsm b/netwerk/dns/mdns/libmdns/fallback/DNSPacket.jsm
new file mode 100644
index 0000000000..1dd859c6d0
--- /dev/null
+++ b/netwerk/dns/mdns/libmdns/fallback/DNSPacket.jsm
@@ -0,0 +1,304 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["DNSPacket"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { DataReader } = ChromeUtils.import(
+ "resource://gre/modules/DataReader.jsm"
+);
+const { DataWriter } = ChromeUtils.import(
+ "resource://gre/modules/DataWriter.jsm"
+);
+const { DNSRecord } = ChromeUtils.import(
+ "resource://gre/modules/DNSRecord.jsm"
+);
+const { DNSResourceRecord } = ChromeUtils.import(
+ "resource://gre/modules/DNSResourceRecord.jsm"
+);
+
+const DEBUG = true;
+
+function debug(msg) {
+ Services.console.logStringMessage("DNSPacket: " + msg);
+}
+
+let DNS_PACKET_SECTION_TYPES = [
+ "QD", // Question
+ "AN", // Answer
+ "NS", // Authority
+ "AR", // Additional
+];
+
+/**
+ * DNS Packet Structure
+ * *************************************************
+ *
+ * Header
+ * ======
+ *
+ * 00 2-Bytes 15
+ * -------------------------------------------------
+ * |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|
+ * -------------------------------------------------
+ * |<==================== ID =====================>|
+ * |QR|<== OP ===>|AA|TC|RD|RA|UN|AD|CD|<== RC ===>|
+ * |<================== QDCOUNT ==================>|
+ * |<================== ANCOUNT ==================>|
+ * |<================== NSCOUNT ==================>|
+ * |<================== ARCOUNT ==================>|
+ * -------------------------------------------------
+ *
+ * ID: 2-Bytes
+ * FLAGS: 2-Bytes
+ * - QR: 1-Bit
+ * - OP: 4-Bits
+ * - AA: 1-Bit
+ * - TC: 1-Bit
+ * - RD: 1-Bit
+ * - RA: 1-Bit
+ * - UN: 1-Bit
+ * - AD: 1-Bit
+ * - CD: 1-Bit
+ * - RC: 4-Bits
+ * QDCOUNT: 2-Bytes
+ * ANCOUNT: 2-Bytes
+ * NSCOUNT: 2-Bytes
+ * ARCOUNT: 2-Bytes
+ *
+ *
+ * Data
+ * ====
+ *
+ * 00 2-Bytes 15
+ * -------------------------------------------------
+ * |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|
+ * -------------------------------------------------
+ * |<???=============== QD[...] ===============???>|
+ * |<???=============== AN[...] ===============???>|
+ * |<???=============== NS[...] ===============???>|
+ * |<???=============== AR[...] ===============???>|
+ * -------------------------------------------------
+ *
+ * QD: ??-Bytes
+ * AN: ??-Bytes
+ * NS: ??-Bytes
+ * AR: ??-Bytes
+ *
+ *
+ * Question Record
+ * ===============
+ *
+ * 00 2-Bytes 15
+ * -------------------------------------------------
+ * |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|
+ * -------------------------------------------------
+ * |<???================ NAME =================???>|
+ * |<=================== TYPE ====================>|
+ * |<=================== CLASS ===================>|
+ * -------------------------------------------------
+ *
+ * NAME: ??-Bytes
+ * TYPE: 2-Bytes
+ * CLASS: 2-Bytes
+ *
+ *
+ * Resource Record
+ * ===============
+ *
+ * 00 4-Bytes 31
+ * -------------------------------------------------
+ * |00|02|04|06|08|10|12|14|16|18|20|22|24|26|28|30|
+ * -------------------------------------------------
+ * |<???================ NAME =================???>|
+ * |<======= TYPE ========>|<======= CLASS =======>|
+ * |<==================== TTL ====================>|
+ * |<====== DATALEN ======>|<???==== DATA =====???>|
+ * -------------------------------------------------
+ *
+ * NAME: ??-Bytes
+ * TYPE: 2-Bytes
+ * CLASS: 2-Bytes
+ * DATALEN: 2-Bytes
+ * DATA: ??-Bytes (Specified By DATALEN)
+ */
+class DNSPacket {
+ constructor() {
+ this._flags = _valueToFlags(0x0000);
+ this._records = {};
+
+ DNS_PACKET_SECTION_TYPES.forEach(sectionType => {
+ this._records[sectionType] = [];
+ });
+ }
+
+ static parse(data) {
+ let reader = new DataReader(data);
+ if (reader.getValue(2) !== 0x0000) {
+ throw new Error("Packet must start with 0x0000");
+ }
+
+ let packet = new DNSPacket();
+ packet._flags = _valueToFlags(reader.getValue(2));
+
+ let recordCounts = {};
+
+ // Parse the record counts.
+ DNS_PACKET_SECTION_TYPES.forEach(sectionType => {
+ recordCounts[sectionType] = reader.getValue(2);
+ });
+
+ // Parse the actual records.
+ DNS_PACKET_SECTION_TYPES.forEach(sectionType => {
+ let recordCount = recordCounts[sectionType];
+ for (let i = 0; i < recordCount; i++) {
+ if (sectionType === "QD") {
+ packet.addRecord(
+ sectionType,
+ DNSRecord.parseFromPacketReader(reader)
+ );
+ } else {
+ packet.addRecord(
+ sectionType,
+ DNSResourceRecord.parseFromPacketReader(reader)
+ );
+ }
+ }
+ });
+
+ if (!reader.eof) {
+ DEBUG && debug("Did not complete parsing packet data");
+ }
+
+ return packet;
+ }
+
+ getFlag(flag) {
+ return this._flags[flag];
+ }
+
+ setFlag(flag, value) {
+ this._flags[flag] = value;
+ }
+
+ addRecord(sectionType, record) {
+ this._records[sectionType].push(record);
+ }
+
+ getRecords(sectionTypes, recordType) {
+ let records = [];
+
+ sectionTypes.forEach(sectionType => {
+ records = records.concat(this._records[sectionType]);
+ });
+
+ if (!recordType) {
+ return records;
+ }
+
+ return records.filter(r => r.recordType === recordType);
+ }
+
+ serialize() {
+ let writer = new DataWriter();
+
+ // Write leading 0x0000 (2 bytes)
+ writer.putValue(0x0000, 2);
+
+ // Write `flags` (2 bytes)
+ writer.putValue(_flagsToValue(this._flags), 2);
+
+ // Write lengths of record sections (2 bytes each)
+ DNS_PACKET_SECTION_TYPES.forEach(sectionType => {
+ writer.putValue(this._records[sectionType].length, 2);
+ });
+
+ // Write records
+ DNS_PACKET_SECTION_TYPES.forEach(sectionType => {
+ this._records[sectionType].forEach(record => {
+ writer.putBytes(record.serialize());
+ });
+ });
+
+ return writer.data;
+ }
+
+ toJSON() {
+ return JSON.stringify(this.toJSONObject());
+ }
+
+ toJSONObject() {
+ let result = { flags: this._flags };
+ DNS_PACKET_SECTION_TYPES.forEach(sectionType => {
+ result[sectionType] = [];
+
+ let records = this._records[sectionType];
+ records.forEach(record => {
+ result[sectionType].push(record.toJSONObject());
+ });
+ });
+
+ return result;
+ }
+}
+
+/**
+ * @private
+ */
+function _valueToFlags(value) {
+ return {
+ QR: (value & 0x8000) >> 15,
+ OP: (value & 0x7800) >> 11,
+ AA: (value & 0x0400) >> 10,
+ TC: (value & 0x0200) >> 9,
+ RD: (value & 0x0100) >> 8,
+ RA: (value & 0x0080) >> 7,
+ UN: (value & 0x0040) >> 6,
+ AD: (value & 0x0020) >> 5,
+ CD: (value & 0x0010) >> 4,
+ RC: (value & 0x000f) >> 0,
+ };
+}
+
+/**
+ * @private
+ */
+function _flagsToValue(flags) {
+ let value = 0x0000;
+
+ value += flags.QR & 0x01;
+
+ value <<= 4;
+ value += flags.OP & 0x0f;
+
+ value <<= 1;
+ value += flags.AA & 0x01;
+
+ value <<= 1;
+ value += flags.TC & 0x01;
+
+ value <<= 1;
+ value += flags.RD & 0x01;
+
+ value <<= 1;
+ value += flags.RA & 0x01;
+
+ value <<= 1;
+ value += flags.UN & 0x01;
+
+ value <<= 1;
+ value += flags.AD & 0x01;
+
+ value <<= 1;
+ value += flags.CD & 0x01;
+
+ value <<= 4;
+ value += flags.RC & 0x0f;
+
+ return value;
+}
diff --git a/netwerk/dns/mdns/libmdns/fallback/DNSRecord.jsm b/netwerk/dns/mdns/libmdns/fallback/DNSRecord.jsm
new file mode 100644
index 0000000000..682fd8b103
--- /dev/null
+++ b/netwerk/dns/mdns/libmdns/fallback/DNSRecord.jsm
@@ -0,0 +1,71 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["DNSRecord"];
+
+const { DataWriter } = ChromeUtils.import(
+ "resource://gre/modules/DataWriter.jsm"
+);
+const { DNS_CLASS_CODES, DNS_RECORD_TYPES } = ChromeUtils.import(
+ "resource://gre/modules/DNSTypes.jsm"
+);
+
+class DNSRecord {
+ constructor(properties = {}) {
+ this.name = properties.name || "";
+ this.recordType = properties.recordType || DNS_RECORD_TYPES.ANY;
+ this.classCode = properties.classCode || DNS_CLASS_CODES.IN;
+ this.cacheFlush = properties.cacheFlush || false;
+ }
+
+ static parseFromPacketReader(reader) {
+ let name = reader.getLabel();
+ let recordType = reader.getValue(2);
+ let classCode = reader.getValue(2);
+ let cacheFlush = !!(classCode & 0x8000);
+ classCode &= 0xff;
+
+ return new this({
+ name,
+ recordType,
+ classCode,
+ cacheFlush,
+ });
+ }
+
+ serialize() {
+ let writer = new DataWriter();
+
+ // Write `name` (ends with trailing 0x00 byte)
+ writer.putLabel(this.name);
+
+ // Write `recordType` (2 bytes)
+ writer.putValue(this.recordType, 2);
+
+ // Write `classCode` (2 bytes)
+ let classCode = this.classCode;
+ if (this.cacheFlush) {
+ classCode |= 0x8000;
+ }
+ writer.putValue(classCode, 2);
+
+ return writer.data;
+ }
+
+ toJSON() {
+ return JSON.stringify(this.toJSONObject());
+ }
+
+ toJSONObject() {
+ return {
+ name: this.name,
+ recordType: this.recordType,
+ classCode: this.classCode,
+ cacheFlush: this.cacheFlush,
+ };
+ }
+}
diff --git a/netwerk/dns/mdns/libmdns/fallback/DNSResourceRecord.jsm b/netwerk/dns/mdns/libmdns/fallback/DNSResourceRecord.jsm
new file mode 100644
index 0000000000..e89f418410
--- /dev/null
+++ b/netwerk/dns/mdns/libmdns/fallback/DNSResourceRecord.jsm
@@ -0,0 +1,221 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["DNSResourceRecord"];
+
+const { DataReader } = ChromeUtils.import(
+ "resource://gre/modules/DataReader.jsm"
+);
+const { DataWriter } = ChromeUtils.import(
+ "resource://gre/modules/DataWriter.jsm"
+);
+const { DNSRecord } = ChromeUtils.import(
+ "resource://gre/modules/DNSRecord.jsm"
+);
+const { DNS_RECORD_TYPES } = ChromeUtils.import(
+ "resource://gre/modules/DNSTypes.jsm"
+);
+
+const DNS_RESOURCE_RECORD_DEFAULT_TTL = 120; // 120 seconds
+
+class DNSResourceRecord extends DNSRecord {
+ constructor(properties = {}) {
+ super(properties);
+
+ this.ttl = properties.ttl || DNS_RESOURCE_RECORD_DEFAULT_TTL;
+ this.data = properties.data || {};
+ }
+
+ static parseFromPacketReader(reader) {
+ let record = super.parseFromPacketReader(reader);
+
+ let ttl = reader.getValue(4);
+ let recordData = reader.getBytes(reader.getValue(2));
+ let packetData = reader.data;
+
+ let data;
+
+ switch (record.recordType) {
+ case DNS_RECORD_TYPES.A:
+ data = _parseA(recordData, packetData);
+ break;
+ case DNS_RECORD_TYPES.PTR:
+ data = _parsePTR(recordData, packetData);
+ break;
+ case DNS_RECORD_TYPES.TXT:
+ data = _parseTXT(recordData, packetData);
+ break;
+ case DNS_RECORD_TYPES.SRV:
+ data = _parseSRV(recordData, packetData);
+ break;
+ default:
+ data = null;
+ break;
+ }
+
+ record.ttl = ttl;
+ record.data = data;
+
+ return record;
+ }
+
+ serialize() {
+ let writer = new DataWriter(super.serialize());
+
+ // Write `ttl` (4 bytes)
+ writer.putValue(this.ttl, 4);
+
+ let data;
+
+ switch (this.recordType) {
+ case DNS_RECORD_TYPES.A:
+ data = _serializeA(this.data);
+ break;
+ case DNS_RECORD_TYPES.PTR:
+ data = _serializePTR(this.data);
+ break;
+ case DNS_RECORD_TYPES.TXT:
+ data = _serializeTXT(this.data);
+ break;
+ case DNS_RECORD_TYPES.SRV:
+ data = _serializeSRV(this.data);
+ break;
+ default:
+ data = new Uint8Array();
+ break;
+ }
+
+ // Write `data` length.
+ writer.putValue(data.length, 2);
+
+ // Write `data` (ends with trailing 0x00 byte)
+ writer.putBytes(data);
+
+ return writer.data;
+ }
+
+ toJSON() {
+ return JSON.stringify(this.toJSONObject());
+ }
+
+ toJSONObject() {
+ let result = super.toJSONObject();
+ result.ttl = this.ttl;
+ result.data = this.data;
+ return result;
+ }
+}
+
+/**
+ * @private
+ */
+function _parseA(recordData, packetData) {
+ let reader = new DataReader(recordData);
+
+ let parts = [];
+ for (let i = 0; i < 4; i++) {
+ parts.push(reader.getValue(1));
+ }
+
+ return parts.join(".");
+}
+
+/**
+ * @private
+ */
+function _parsePTR(recordData, packetData) {
+ let reader = new DataReader(recordData);
+
+ return reader.getLabel(packetData);
+}
+
+/**
+ * @private
+ */
+function _parseTXT(recordData, packetData) {
+ let reader = new DataReader(recordData);
+
+ let result = {};
+
+ let label = reader.getLabel(packetData);
+ if (label.length > 0) {
+ let parts = label.split(".");
+ parts.forEach(part => {
+ let [name] = part.split("=", 1);
+ let value = part.substr(name.length + 1);
+ result[name] = value;
+ });
+ }
+
+ return result;
+}
+
+/**
+ * @private
+ */
+function _parseSRV(recordData, packetData) {
+ let reader = new DataReader(recordData);
+
+ let priority = reader.getValue(2);
+ let weight = reader.getValue(2);
+ let port = reader.getValue(2);
+ let target = reader.getLabel(packetData);
+
+ return { priority, weight, port, target };
+}
+
+/**
+ * @private
+ */
+function _serializeA(data) {
+ let writer = new DataWriter();
+
+ let parts = data.split(".");
+ for (let i = 0; i < 4; i++) {
+ writer.putValue(parseInt(parts[i], 10) || 0);
+ }
+
+ return writer.data;
+}
+
+/**
+ * @private
+ */
+function _serializePTR(data) {
+ let writer = new DataWriter();
+
+ writer.putLabel(data);
+
+ return writer.data;
+}
+
+/**
+ * @private
+ */
+function _serializeTXT(data) {
+ let writer = new DataWriter();
+
+ for (let name in data) {
+ writer.putLengthString(name + "=" + data[name]);
+ }
+
+ return writer.data;
+}
+
+/**
+ * @private
+ */
+function _serializeSRV(data) {
+ let writer = new DataWriter();
+
+ writer.putValue(data.priority || 0, 2);
+ writer.putValue(data.weight || 0, 2);
+ writer.putValue(data.port || 0, 2);
+ writer.putLabel(data.target);
+
+ return writer.data;
+}
diff --git a/netwerk/dns/mdns/libmdns/fallback/DNSTypes.jsm b/netwerk/dns/mdns/libmdns/fallback/DNSTypes.jsm
new file mode 100644
index 0000000000..878e22917d
--- /dev/null
+++ b/netwerk/dns/mdns/libmdns/fallback/DNSTypes.jsm
@@ -0,0 +1,99 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "DNS_QUERY_RESPONSE_CODES",
+ "DNS_AUTHORITATIVE_ANSWER_CODES",
+ "DNS_CLASS_CODES",
+ "DNS_RECORD_TYPES",
+];
+
+let DNS_QUERY_RESPONSE_CODES = {
+ QUERY: 0, // RFC 1035 - Query
+ RESPONSE: 1, // RFC 1035 - Reponse
+};
+
+let DNS_AUTHORITATIVE_ANSWER_CODES = {
+ NO: 0, // RFC 1035 - Not Authoritative
+ YES: 1, // RFC 1035 - Is Authoritative
+};
+
+let DNS_CLASS_CODES = {
+ IN: 0x01, // RFC 1035 - Internet
+ CS: 0x02, // RFC 1035 - CSNET
+ CH: 0x03, // RFC 1035 - CHAOS
+ HS: 0x04, // RFC 1035 - Hesiod
+ NONE: 0xfe, // RFC 2136 - None
+ ANY: 0xff, // RFC 1035 - Any
+};
+
+let DNS_RECORD_TYPES = {
+ SIGZERO: 0, // RFC 2931
+ A: 1, // RFC 1035
+ NS: 2, // RFC 1035
+ MD: 3, // RFC 1035
+ MF: 4, // RFC 1035
+ CNAME: 5, // RFC 1035
+ SOA: 6, // RFC 1035
+ MB: 7, // RFC 1035
+ MG: 8, // RFC 1035
+ MR: 9, // RFC 1035
+ NULL: 10, // RFC 1035
+ WKS: 11, // RFC 1035
+ PTR: 12, // RFC 1035
+ HINFO: 13, // RFC 1035
+ MINFO: 14, // RFC 1035
+ MX: 15, // RFC 1035
+ TXT: 16, // RFC 1035
+ RP: 17, // RFC 1183
+ AFSDB: 18, // RFC 1183
+ X25: 19, // RFC 1183
+ ISDN: 20, // RFC 1183
+ RT: 21, // RFC 1183
+ NSAP: 22, // RFC 1706
+ NSAP_PTR: 23, // RFC 1348
+ SIG: 24, // RFC 2535
+ KEY: 25, // RFC 2535
+ PX: 26, // RFC 2163
+ GPOS: 27, // RFC 1712
+ AAAA: 28, // RFC 1886
+ LOC: 29, // RFC 1876
+ NXT: 30, // RFC 2535
+ EID: 31, // RFC ????
+ NIMLOC: 32, // RFC ????
+ SRV: 33, // RFC 2052
+ ATMA: 34, // RFC ????
+ NAPTR: 35, // RFC 2168
+ KX: 36, // RFC 2230
+ CERT: 37, // RFC 2538
+ DNAME: 39, // RFC 2672
+ OPT: 41, // RFC 2671
+ APL: 42, // RFC 3123
+ DS: 43, // RFC 4034
+ SSHFP: 44, // RFC 4255
+ IPSECKEY: 45, // RFC 4025
+ RRSIG: 46, // RFC 4034
+ NSEC: 47, // RFC 4034
+ DNSKEY: 48, // RFC 4034
+ DHCID: 49, // RFC 4701
+ NSEC3: 50, // RFC ????
+ NSEC3PARAM: 51, // RFC ????
+ HIP: 55, // RFC 5205
+ SPF: 99, // RFC 4408
+ UINFO: 100, // RFC ????
+ UID: 101, // RFC ????
+ GID: 102, // RFC ????
+ UNSPEC: 103, // RFC ????
+ TKEY: 249, // RFC 2930
+ TSIG: 250, // RFC 2931
+ IXFR: 251, // RFC 1995
+ AXFR: 252, // RFC 1035
+ MAILB: 253, // RFC 1035
+ MAILA: 254, // RFC 1035
+ ANY: 255, // RFC 1035
+ DLV: 32769, // RFC 4431
+};
diff --git a/netwerk/dns/mdns/libmdns/fallback/DataReader.jsm b/netwerk/dns/mdns/libmdns/fallback/DataReader.jsm
new file mode 100644
index 0000000000..978ab4dde0
--- /dev/null
+++ b/netwerk/dns/mdns/libmdns/fallback/DataReader.jsm
@@ -0,0 +1,134 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["DataReader"];
+
+class DataReader {
+ // `data` is `Uint8Array`
+ constructor(data, startByte = 0) {
+ this._data = data;
+ this._cursor = startByte;
+ }
+
+ get buffer() {
+ return this._data.buffer;
+ }
+
+ get data() {
+ return this._data;
+ }
+
+ get eof() {
+ return this._cursor >= this._data.length;
+ }
+
+ getBytes(length = 1) {
+ if (!length) {
+ return new Uint8Array();
+ }
+
+ let end = this._cursor + length;
+ if (end > this._data.length) {
+ return new Uint8Array();
+ }
+
+ let uint8Array = new Uint8Array(this.buffer.slice(this._cursor, end));
+ this._cursor += length;
+
+ return uint8Array;
+ }
+
+ getString(length) {
+ let uint8Array = this.getBytes(length);
+ return _uint8ArrayToString(uint8Array);
+ }
+
+ getValue(length) {
+ let uint8Array = this.getBytes(length);
+ return _uint8ArrayToValue(uint8Array);
+ }
+
+ getLabel(decompressData) {
+ let parts = [];
+ let partLength;
+
+ while ((partLength = this.getValue(1))) {
+ // If a length has been specified instead of a pointer,
+ // read the string of the specified length.
+ if (partLength !== 0xc0) {
+ parts.push(this.getString(partLength));
+ continue;
+ }
+
+ // TODO: Handle case where we have a pointer to the label
+ parts.push(String.fromCharCode(0xc0) + this.getString(1));
+ break;
+ }
+
+ let label = parts.join(".");
+
+ return _decompressLabel(label, decompressData || this._data);
+ }
+}
+
+/**
+ * @private
+ */
+function _uint8ArrayToValue(uint8Array) {
+ let length = uint8Array.length;
+ if (length === 0) {
+ return null;
+ }
+
+ let value = 0;
+ for (let i = 0; i < length; i++) {
+ value = value << 8;
+ value += uint8Array[i];
+ }
+
+ return value;
+}
+
+/**
+ * @private
+ */
+function _uint8ArrayToString(uint8Array) {
+ let length = uint8Array.length;
+ if (length === 0) {
+ return "";
+ }
+
+ let results = [];
+ for (let i = 0; i < length; i += 1024) {
+ results.push(
+ String.fromCharCode.apply(null, uint8Array.subarray(i, i + 1024))
+ );
+ }
+
+ return results.join("");
+}
+
+/**
+ * @private
+ */
+function _decompressLabel(label, decompressData) {
+ let result = "";
+
+ for (let i = 0, length = label.length; i < length; i++) {
+ if (label.charCodeAt(i) !== 0xc0) {
+ result += label.charAt(i);
+ continue;
+ }
+
+ i++;
+
+ let reader = new DataReader(decompressData, label.charCodeAt(i));
+ result += _decompressLabel(reader.getLabel(), decompressData);
+ }
+
+ return result;
+}
diff --git a/netwerk/dns/mdns/libmdns/fallback/DataWriter.jsm b/netwerk/dns/mdns/libmdns/fallback/DataWriter.jsm
new file mode 100644
index 0000000000..05949a1d05
--- /dev/null
+++ b/netwerk/dns/mdns/libmdns/fallback/DataWriter.jsm
@@ -0,0 +1,97 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["DataWriter"];
+
+class DataWriter {
+ constructor(data, maxBytes = 512) {
+ if (typeof data === "number") {
+ maxBytes = data;
+ data = undefined;
+ }
+
+ this._buffer = new ArrayBuffer(maxBytes);
+ this._data = new Uint8Array(this._buffer);
+ this._cursor = 0;
+
+ if (data) {
+ this.putBytes(data);
+ }
+ }
+
+ get buffer() {
+ return this._buffer.slice(0, this._cursor);
+ }
+
+ get data() {
+ return new Uint8Array(this.buffer);
+ }
+
+ // `data` is `Uint8Array`
+ putBytes(data) {
+ if (this._cursor + data.length > this._data.length) {
+ throw new Error("DataWriter buffer is exceeded");
+ }
+
+ for (let i = 0, length = data.length; i < length; i++) {
+ this._data[this._cursor] = data[i];
+ this._cursor++;
+ }
+ }
+
+ putByte(byte) {
+ if (this._cursor + 1 > this._data.length) {
+ throw new Error("DataWriter buffer is exceeded");
+ }
+
+ this._data[this._cursor] = byte;
+ this._cursor++;
+ }
+
+ putValue(value, length) {
+ length = length || 1;
+ if (length == 1) {
+ this.putByte(value);
+ } else {
+ this.putBytes(_valueToUint8Array(value, length));
+ }
+ }
+
+ putLabel(label) {
+ // Eliminate any trailing '.'s in the label (valid in text representation).
+ label = label.replace(/\.$/, "");
+ let parts = label.split(".");
+ parts.forEach(part => {
+ this.putLengthString(part);
+ });
+ this.putValue(0);
+ }
+
+ putLengthString(string) {
+ if (string.length > 0xff) {
+ throw new Error("String too long.");
+ }
+ this.putValue(string.length);
+ for (let i = 0; i < string.length; i++) {
+ this.putValue(string.charCodeAt(i));
+ }
+ }
+}
+
+/**
+ * @private
+ */
+function _valueToUint8Array(value, length) {
+ let arrayBuffer = new ArrayBuffer(length);
+ let uint8Array = new Uint8Array(arrayBuffer);
+ for (let i = length - 1; i >= 0; i--) {
+ uint8Array[i] = value & 0xff;
+ value = value >> 8;
+ }
+
+ return uint8Array;
+}
diff --git a/netwerk/dns/mdns/libmdns/fallback/MulticastDNS.jsm b/netwerk/dns/mdns/libmdns/fallback/MulticastDNS.jsm
new file mode 100644
index 0000000000..efc21fe523
--- /dev/null
+++ b/netwerk/dns/mdns/libmdns/fallback/MulticastDNS.jsm
@@ -0,0 +1,985 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["MulticastDNS"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { clearTimeout, setTimeout } = ChromeUtils.import(
+ "resource://gre/modules/Timer.jsm"
+);
+
+const { DNSPacket } = ChromeUtils.import(
+ "resource://gre/modules/DNSPacket.jsm"
+);
+const { DNSRecord } = ChromeUtils.import(
+ "resource://gre/modules/DNSRecord.jsm"
+);
+const { DNSResourceRecord } = ChromeUtils.import(
+ "resource://gre/modules/DNSResourceRecord.jsm"
+);
+const {
+ DNS_AUTHORITATIVE_ANSWER_CODES,
+ DNS_CLASS_CODES,
+ DNS_QUERY_RESPONSE_CODES,
+ DNS_RECORD_TYPES,
+} = ChromeUtils.import("resource://gre/modules/DNSTypes.jsm");
+
+const NS_NETWORK_LINK_TOPIC = "network:link-status-changed";
+
+let networkInfoService = Cc[
+ "@mozilla.org/network-info-service;1"
+].createInstance(Ci.nsINetworkInfoService);
+
+const DEBUG = true;
+
+const MDNS_MULTICAST_GROUP = "224.0.0.251";
+const MDNS_PORT = 5353;
+const DEFAULT_TTL = 120;
+
+function debug(msg) {
+ dump("MulticastDNS: " + msg + "\n");
+}
+
+function ServiceKey(svc) {
+ return (
+ "" +
+ svc.serviceType.length +
+ "/" +
+ svc.serviceType +
+ "|" +
+ svc.serviceName.length +
+ "/" +
+ svc.serviceName +
+ "|" +
+ svc.port
+ );
+}
+
+function TryGet(obj, name) {
+ try {
+ return obj[name];
+ } catch (err) {
+ return undefined;
+ }
+}
+
+function IsIpv4Address(addr) {
+ let parts = addr.split(".");
+ if (parts.length != 4) {
+ return false;
+ }
+ for (let part of parts) {
+ let partInt = Number.parseInt(part, 10);
+ if (partInt.toString() != part) {
+ return false;
+ }
+ if (partInt < 0 || partInt >= 256) {
+ return false;
+ }
+ }
+ return true;
+}
+
+class PublishedService {
+ constructor(attrs) {
+ this.serviceType = attrs.serviceType.replace(/\.$/, "");
+ this.serviceName = attrs.serviceName;
+ this.domainName = TryGet(attrs, "domainName") || "local";
+ this.address = TryGet(attrs, "address") || "0.0.0.0";
+ this.port = attrs.port;
+ this.serviceAttrs = _propertyBagToObject(TryGet(attrs, "attributes") || {});
+ this.host = TryGet(attrs, "host");
+ this.key = this.generateKey();
+ this.lastAdvertised = undefined;
+ this.advertiseTimer = undefined;
+ }
+
+ equals(svc) {
+ return (
+ this.port == svc.port &&
+ this.serviceName == svc.serviceName &&
+ this.serviceType == svc.serviceType
+ );
+ }
+
+ generateKey() {
+ return ServiceKey(this);
+ }
+
+ ptrMatch(name) {
+ return name == this.serviceType + "." + this.domainName;
+ }
+
+ clearAdvertiseTimer() {
+ if (!this.advertiseTimer) {
+ return;
+ }
+ clearTimeout(this.advertiseTimer);
+ this.advertiseTimer = undefined;
+ }
+}
+
+class MulticastDNS {
+ constructor() {
+ this._listeners = new Map();
+ this._sockets = new Map();
+ this._services = new Map();
+ this._discovered = new Map();
+ this._querySocket = undefined;
+ this._broadcastReceiverSocket = undefined;
+ this._broadcastTimer = undefined;
+
+ this._networkLinkObserver = {
+ observe: (subject, topic, data) => {
+ DEBUG &&
+ debug(
+ NS_NETWORK_LINK_TOPIC +
+ "(" +
+ data +
+ "); Clearing list of previously discovered services"
+ );
+ this._discovered.clear();
+ },
+ };
+ }
+
+ _attachNetworkLinkObserver() {
+ if (this._networkLinkObserverTimeout) {
+ clearTimeout(this._networkLinkObserverTimeout);
+ }
+
+ if (!this._isNetworkLinkObserverAttached) {
+ DEBUG && debug("Attaching observer " + NS_NETWORK_LINK_TOPIC);
+ Services.obs.addObserver(
+ this._networkLinkObserver,
+ NS_NETWORK_LINK_TOPIC
+ );
+ this._isNetworkLinkObserverAttached = true;
+ }
+ }
+
+ _detachNetworkLinkObserver() {
+ if (this._isNetworkLinkObserverAttached) {
+ if (this._networkLinkObserverTimeout) {
+ clearTimeout(this._networkLinkObserverTimeout);
+ }
+
+ this._networkLinkObserverTimeout = setTimeout(() => {
+ DEBUG && debug("Detaching observer " + NS_NETWORK_LINK_TOPIC);
+ Services.obs.removeObserver(
+ this._networkLinkObserver,
+ NS_NETWORK_LINK_TOPIC
+ );
+ this._isNetworkLinkObserverAttached = false;
+ this._networkLinkObserverTimeout = null;
+ }, 5000);
+ }
+ }
+
+ startDiscovery(aServiceType, aListener) {
+ DEBUG && debug('startDiscovery("' + aServiceType + '")');
+ let { serviceType } = _parseServiceDomainName(aServiceType);
+
+ this._attachNetworkLinkObserver();
+ this._addServiceListener(serviceType, aListener);
+
+ try {
+ this._query(serviceType + ".local");
+ aListener.onDiscoveryStarted(serviceType);
+ } catch (e) {
+ DEBUG && debug('startDiscovery("' + serviceType + '") FAILED: ' + e);
+ this._removeServiceListener(serviceType, aListener);
+ aListener.onStartDiscoveryFailed(serviceType, Cr.NS_ERROR_FAILURE);
+ }
+ }
+
+ stopDiscovery(aServiceType, aListener) {
+ DEBUG && debug('stopDiscovery("' + aServiceType + '")');
+ let { serviceType } = _parseServiceDomainName(aServiceType);
+
+ this._detachNetworkLinkObserver();
+ this._removeServiceListener(serviceType, aListener);
+
+ aListener.onDiscoveryStopped(serviceType);
+
+ this._checkCloseSockets();
+ }
+
+ resolveService(aServiceInfo, aListener) {
+ DEBUG && debug("resolveService(): " + aServiceInfo.serviceName);
+
+ // Address info is already resolved during discovery
+ setTimeout(() => aListener.onServiceResolved(aServiceInfo));
+ }
+
+ registerService(aServiceInfo, aListener) {
+ DEBUG && debug("registerService(): " + aServiceInfo.serviceName);
+
+ // Initialize the broadcast receiver socket in case it
+ // hasn't already been started so we can listen for
+ // multicast queries/announcements on all interfaces.
+ this._getBroadcastReceiverSocket();
+
+ for (let name of ["port", "serviceName", "serviceType"]) {
+ if (!TryGet(aServiceInfo, name)) {
+ aListener.onRegistrationFailed(aServiceInfo, Cr.NS_ERROR_FAILURE);
+ throw new Error('Invalid nsIDNSServiceInfo; Missing "' + name + '"');
+ }
+ }
+
+ let publishedService;
+ try {
+ publishedService = new PublishedService(aServiceInfo);
+ } catch (e) {
+ DEBUG &&
+ debug("Error constructing PublishedService: " + e + " - " + e.stack);
+ setTimeout(() =>
+ aListener.onRegistrationFailed(aServiceInfo, Cr.NS_ERROR_FAILURE)
+ );
+ return;
+ }
+
+ // Ensure such a service does not already exist.
+ if (this._services.get(publishedService.key)) {
+ setTimeout(() =>
+ aListener.onRegistrationFailed(aServiceInfo, Cr.NS_ERROR_FAILURE)
+ );
+ return;
+ }
+
+ // Make sure that the service addr is '0.0.0.0', or there is at least one
+ // socket open on the address the service is open on.
+ this._getSockets().then(sockets => {
+ if (
+ publishedService.address != "0.0.0.0" &&
+ !sockets.get(publishedService.address)
+ ) {
+ setTimeout(() =>
+ aListener.onRegistrationFailed(aServiceInfo, Cr.NS_ERROR_FAILURE)
+ );
+ return;
+ }
+
+ this._services.set(publishedService.key, publishedService);
+
+ // Service registered.. call onServiceRegistered on next tick.
+ setTimeout(() => aListener.onServiceRegistered(aServiceInfo));
+
+ // Set a timeout to start advertising the service too.
+ publishedService.advertiseTimer = setTimeout(() => {
+ this._advertiseService(publishedService.key, /* firstAdv = */ true);
+ });
+ });
+ }
+
+ unregisterService(aServiceInfo, aListener) {
+ DEBUG && debug("unregisterService(): " + aServiceInfo.serviceName);
+
+ let serviceKey;
+ try {
+ serviceKey = ServiceKey(aServiceInfo);
+ } catch (e) {
+ setTimeout(() =>
+ aListener.onUnregistrationFailed(aServiceInfo, Cr.NS_ERROR_FAILURE)
+ );
+ return;
+ }
+
+ let publishedService = this._services.get(serviceKey);
+ if (!publishedService) {
+ setTimeout(() =>
+ aListener.onUnregistrationFailed(aServiceInfo, Cr.NS_ERROR_FAILURE)
+ );
+ return;
+ }
+
+ // Clear any advertise timeout for this published service.
+ publishedService.clearAdvertiseTimer();
+
+ // Delete the service from the service map.
+ if (!this._services.delete(serviceKey)) {
+ setTimeout(() =>
+ aListener.onUnregistrationFailed(aServiceInfo, Cr.NS_ERROR_FAILURE)
+ );
+ return;
+ }
+
+ // Check the broadcast timer again to rejig when it should run next.
+ this._checkStartBroadcastTimer();
+
+ // Check to see if sockets should be closed, and if so close them.
+ this._checkCloseSockets();
+
+ aListener.onServiceUnregistered(aServiceInfo);
+ }
+
+ _respondToQuery(serviceKey, message) {
+ let address = message.fromAddr.address;
+ let port = message.fromAddr.port;
+ DEBUG &&
+ debug(
+ "_respondToQuery(): key=" +
+ serviceKey +
+ ", fromAddr=" +
+ address +
+ ":" +
+ port
+ );
+
+ let publishedService = this._services.get(serviceKey);
+ if (!publishedService) {
+ debug("_respondToQuery Could not find service (key=" + serviceKey + ")");
+ return;
+ }
+
+ DEBUG &&
+ debug("_respondToQuery(): key=" + serviceKey + ": SENDING RESPONSE");
+ this._advertiseServiceHelper(publishedService, { address, port });
+ }
+
+ _advertiseService(serviceKey, firstAdv) {
+ DEBUG && debug("_advertiseService(): key=" + serviceKey);
+ let publishedService = this._services.get(serviceKey);
+ if (!publishedService) {
+ debug(
+ "_advertiseService Could not find service to advertise (key=" +
+ serviceKey +
+ ")"
+ );
+ return;
+ }
+
+ publishedService.advertiseTimer = undefined;
+
+ this._advertiseServiceHelper(publishedService, null).then(() => {
+ // If first advertisement, re-advertise in 1 second.
+ // Otherwise, set the lastAdvertised time.
+ if (firstAdv) {
+ publishedService.advertiseTimer = setTimeout(() => {
+ this._advertiseService(serviceKey);
+ }, 1000);
+ } else {
+ publishedService.lastAdvertised = Date.now();
+ this._checkStartBroadcastTimer();
+ }
+ });
+ }
+
+ _advertiseServiceHelper(svc, target) {
+ if (!target) {
+ target = { address: MDNS_MULTICAST_GROUP, port: MDNS_PORT };
+ }
+
+ return this._getSockets().then(sockets => {
+ sockets.forEach((socket, address) => {
+ if (svc.address == "0.0.0.0" || address == svc.address) {
+ let packet = this._makeServicePacket(svc, [address]);
+ let data = packet.serialize();
+ try {
+ socket.send(target.address, target.port, data);
+ } catch (err) {
+ DEBUG &&
+ debug(
+ "Failed to send packet to " + target.address + ":" + target.port
+ );
+ }
+ }
+ });
+ });
+ }
+
+ _cancelBroadcastTimer() {
+ if (!this._broadcastTimer) {
+ return;
+ }
+ clearTimeout(this._broadcastTimer);
+ this._broadcastTimer = undefined;
+ }
+
+ _checkStartBroadcastTimer() {
+ DEBUG && debug("_checkStartBroadcastTimer()");
+ // Cancel any existing broadcasting timer.
+ this._cancelBroadcastTimer();
+
+ let now = Date.now();
+
+ // Go through services and find services to broadcast.
+ let bcastServices = [];
+ let nextBcastWait = undefined;
+ for (let [, publishedService] of this._services) {
+ // if lastAdvertised is undefined, service hasn't finished it's initial
+ // two broadcasts.
+ if (publishedService.lastAdvertised === undefined) {
+ continue;
+ }
+
+ // Otherwise, check lastAdvertised against now.
+ let msSinceAdv = now - publishedService.lastAdvertised;
+
+ // If msSinceAdv is more than 90% of the way to the TTL, advertise now.
+ if (msSinceAdv > DEFAULT_TTL * 1000 * 0.9) {
+ bcastServices.push(publishedService);
+ continue;
+ }
+
+ // Otherwise, calculate the next time to advertise for this service.
+ // We set that at 95% of the time to the TTL expiry.
+ let nextAdvWait = DEFAULT_TTL * 1000 * 0.95 - msSinceAdv;
+ if (nextBcastWait === undefined || nextBcastWait > nextAdvWait) {
+ nextBcastWait = nextAdvWait;
+ }
+ }
+
+ // Schedule an immediate advertisement of all services to be advertised now.
+ for (let svc of bcastServices) {
+ svc.advertiseTimer = setTimeout(() => this._advertiseService(svc.key));
+ }
+
+ // Schedule next broadcast check for the next bcast time.
+ if (nextBcastWait !== undefined) {
+ DEBUG &&
+ debug(
+ "_checkStartBroadcastTimer(): Scheduling next check in " +
+ nextBcastWait +
+ "ms"
+ );
+ this._broadcastTimer = setTimeout(
+ () => this._checkStartBroadcastTimer(),
+ nextBcastWait
+ );
+ }
+ }
+
+ _query(name) {
+ DEBUG && debug('query("' + name + '")');
+ let packet = new DNSPacket();
+ packet.setFlag("QR", DNS_QUERY_RESPONSE_CODES.QUERY);
+
+ // PTR Record
+ packet.addRecord(
+ "QD",
+ new DNSRecord({
+ name,
+ recordType: DNS_RECORD_TYPES.PTR,
+ classCode: DNS_CLASS_CODES.IN,
+ cacheFlush: true,
+ })
+ );
+
+ let data = packet.serialize();
+
+ // Initialize the broadcast receiver socket in case it
+ // hasn't already been started so we can listen for
+ // multicast queries/announcements on all interfaces.
+ this._getBroadcastReceiverSocket();
+
+ this._getQuerySocket().then(querySocket => {
+ DEBUG && debug('sending query on query socket ("' + name + '")');
+ querySocket.send(MDNS_MULTICAST_GROUP, MDNS_PORT, data);
+ });
+
+ // Automatically announce previously-discovered
+ // services that match and haven't expired yet.
+ setTimeout(() => {
+ DEBUG &&
+ debug('announcing previously discovered services ("' + name + '")');
+ let { serviceType } = _parseServiceDomainName(name);
+
+ this._clearExpiredDiscoveries();
+ this._discovered.forEach((discovery, key) => {
+ let serviceInfo = discovery.serviceInfo;
+ if (serviceInfo.serviceType !== serviceType) {
+ return;
+ }
+
+ let listeners = this._listeners.get(serviceInfo.serviceType) || [];
+ listeners.forEach(listener => {
+ listener.onServiceFound(serviceInfo);
+ });
+ });
+ });
+ }
+
+ _clearExpiredDiscoveries() {
+ this._discovered.forEach((discovery, key) => {
+ if (discovery.expireTime < Date.now()) {
+ this._discovered.delete(key);
+ }
+ });
+ }
+
+ _handleQueryPacket(packet, message) {
+ packet.getRecords(["QD"]).forEach(record => {
+ // Don't respond if the query's class code is not IN or ANY.
+ if (
+ record.classCode !== DNS_CLASS_CODES.IN &&
+ record.classCode !== DNS_CLASS_CODES.ANY
+ ) {
+ return;
+ }
+
+ // Don't respond if the query's record type is not PTR or ANY.
+ if (
+ record.recordType !== DNS_RECORD_TYPES.PTR &&
+ record.recordType !== DNS_RECORD_TYPES.ANY
+ ) {
+ return;
+ }
+
+ for (let [serviceKey, publishedService] of this._services) {
+ DEBUG && debug("_handleQueryPacket: " + packet.toJSON());
+ if (publishedService.ptrMatch(record.name)) {
+ this._respondToQuery(serviceKey, message);
+ }
+ }
+ });
+ }
+
+ _makeServicePacket(service, addresses) {
+ let packet = new DNSPacket();
+ packet.setFlag("QR", DNS_QUERY_RESPONSE_CODES.RESPONSE);
+ packet.setFlag("AA", DNS_AUTHORITATIVE_ANSWER_CODES.YES);
+
+ let host = service.host || _hostname;
+
+ // e.g.: foo-bar-service._http._tcp.local
+ let serviceDomainName =
+ service.serviceName + "." + service.serviceType + ".local";
+
+ // PTR Record
+ packet.addRecord(
+ "AN",
+ new DNSResourceRecord({
+ name: service.serviceType + ".local", // e.g.: _http._tcp.local
+ recordType: DNS_RECORD_TYPES.PTR,
+ data: serviceDomainName,
+ })
+ );
+
+ // SRV Record
+ packet.addRecord(
+ "AR",
+ new DNSResourceRecord({
+ name: serviceDomainName,
+ recordType: DNS_RECORD_TYPES.SRV,
+ classCode: DNS_CLASS_CODES.IN,
+ cacheFlush: true,
+ data: {
+ priority: 0,
+ weight: 0,
+ port: service.port,
+ target: host, // e.g.: My-Android-Phone.local
+ },
+ })
+ );
+
+ // A Records
+ for (let address of addresses) {
+ packet.addRecord(
+ "AR",
+ new DNSResourceRecord({
+ name: host,
+ recordType: DNS_RECORD_TYPES.A,
+ data: address,
+ })
+ );
+ }
+
+ // TXT Record
+ packet.addRecord(
+ "AR",
+ new DNSResourceRecord({
+ name: serviceDomainName,
+ recordType: DNS_RECORD_TYPES.TXT,
+ classCode: DNS_CLASS_CODES.IN,
+ cacheFlush: true,
+ data: service.serviceAttrs || {},
+ })
+ );
+
+ return packet;
+ }
+
+ _handleResponsePacket(packet, message) {
+ let services = {};
+ let hosts = {};
+
+ let srvRecords = packet.getRecords(["AN", "AR"], DNS_RECORD_TYPES.SRV);
+ let txtRecords = packet.getRecords(["AN", "AR"], DNS_RECORD_TYPES.TXT);
+ let ptrRecords = packet.getRecords(["AN", "AR"], DNS_RECORD_TYPES.PTR);
+ let aRecords = packet.getRecords(["AN", "AR"], DNS_RECORD_TYPES.A);
+
+ srvRecords.forEach(record => {
+ let data = record.data || {};
+
+ services[record.name] = {
+ host: data.target,
+ port: data.port,
+ ttl: record.ttl,
+ };
+ });
+
+ txtRecords.forEach(record => {
+ if (!services[record.name]) {
+ return;
+ }
+
+ services[record.name].attributes = record.data;
+ });
+
+ aRecords.forEach(record => {
+ if (IsIpv4Address(record.data)) {
+ hosts[record.name] = record.data;
+ }
+ });
+
+ ptrRecords.forEach(record => {
+ let name = record.data;
+ if (!services[name]) {
+ return;
+ }
+
+ let { host, port } = services[name];
+ if (!host || !port) {
+ return;
+ }
+
+ let { serviceName, serviceType, domainName } = _parseServiceDomainName(
+ name
+ );
+ if (!serviceName || !serviceType || !domainName) {
+ return;
+ }
+
+ let address = hosts[host];
+ if (!address) {
+ return;
+ }
+
+ let ttl = services[name].ttl || 0;
+ let serviceInfo = {
+ serviceName,
+ serviceType,
+ host,
+ address,
+ port,
+ domainName,
+ attributes: services[name].attributes || {},
+ };
+
+ this._onServiceFound(serviceInfo, ttl);
+ });
+ }
+
+ _onServiceFound(serviceInfo, ttl = 0) {
+ let expireTime = Date.now() + ttl * 1000;
+ let key =
+ serviceInfo.serviceName +
+ "." +
+ serviceInfo.serviceType +
+ "." +
+ serviceInfo.domainName +
+ " @" +
+ serviceInfo.address +
+ ":" +
+ serviceInfo.port;
+
+ // If this service was already discovered, just update
+ // its expiration time and don't re-emit it.
+ if (this._discovered.has(key)) {
+ this._discovered.get(key).expireTime = expireTime;
+ return;
+ }
+
+ this._discovered.set(key, {
+ serviceInfo,
+ expireTime,
+ });
+
+ let listeners = this._listeners.get(serviceInfo.serviceType) || [];
+ listeners.forEach(listener => {
+ listener.onServiceFound(serviceInfo);
+ });
+
+ DEBUG && debug("_onServiceFound()" + serviceInfo.serviceName);
+ }
+
+ /**
+ * Gets a non-exclusive socket on 0.0.0.0:{random} to send
+ * multicast queries on all interfaces. This socket does
+ * not need to join a multicast group since it is still
+ * able to *send* multicast queries, but it does not need
+ * to *listen* for multicast queries/announcements since
+ * the `_broadcastReceiverSocket` is already handling them.
+ */
+ _getQuerySocket() {
+ return new Promise((resolve, reject) => {
+ if (!this._querySocket) {
+ this._querySocket = _openSocket("0.0.0.0", 0, {
+ onPacketReceived: this._onPacketReceived.bind(this),
+ onStopListening: this._onStopListening.bind(this),
+ });
+ }
+ resolve(this._querySocket);
+ });
+ }
+
+ /**
+ * Gets a non-exclusive socket on 0.0.0.0:5353 to listen
+ * for multicast queries/announcements on all interfaces.
+ * Since this socket needs to listen for multicast queries
+ * and announcements, this socket joins the multicast
+ * group on *all* interfaces (0.0.0.0).
+ */
+ _getBroadcastReceiverSocket() {
+ return new Promise((resolve, reject) => {
+ if (!this._broadcastReceiverSocket) {
+ this._broadcastReceiverSocket = _openSocket(
+ "0.0.0.0",
+ MDNS_PORT,
+ {
+ onPacketReceived: this._onPacketReceived.bind(this),
+ onStopListening: this._onStopListening.bind(this),
+ },
+ /* multicastInterface = */ "0.0.0.0"
+ );
+ }
+ resolve(this._broadcastReceiverSocket);
+ });
+ }
+
+ /**
+ * Gets a non-exclusive socket for each interface on
+ * {iface-ip}:5353 for sending query responses as
+ * well as for listening for unicast queries. These
+ * sockets do not need to join a multicast group
+ * since they are still able to *send* multicast
+ * query responses, but they do not need to *listen*
+ * for multicast queries since the `_querySocket` is
+ * already handling them.
+ */
+ _getSockets() {
+ return new Promise(resolve => {
+ if (this._sockets.size > 0) {
+ resolve(this._sockets);
+ return;
+ }
+
+ Promise.all([getAddresses(), getHostname()]).then(() => {
+ _addresses.forEach(address => {
+ let socket = _openSocket(address, MDNS_PORT, null);
+ this._sockets.set(address, socket);
+ });
+
+ resolve(this._sockets);
+ });
+ });
+ }
+
+ _checkCloseSockets() {
+ // Nothing to do if no sockets to close.
+ if (this._sockets.size == 0) {
+ return;
+ }
+
+ // Don't close sockets if discovery listeners are still present.
+ if (this._listeners.size > 0) {
+ return;
+ }
+
+ // Don't close sockets if advertised services are present.
+ // Since we need to listen for service queries and respond to them.
+ if (this._services.size > 0) {
+ return;
+ }
+
+ this._closeSockets();
+ }
+
+ _closeSockets() {
+ this._sockets.forEach(socket => socket.close());
+ this._sockets.clear();
+ }
+
+ _onPacketReceived(socket, message) {
+ let packet = DNSPacket.parse(message.rawData);
+
+ switch (packet.getFlag("QR")) {
+ case DNS_QUERY_RESPONSE_CODES.QUERY:
+ this._handleQueryPacket(packet, message);
+ break;
+ case DNS_QUERY_RESPONSE_CODES.RESPONSE:
+ this._handleResponsePacket(packet, message);
+ break;
+ default:
+ break;
+ }
+ }
+
+ _onStopListening(socket, status) {
+ DEBUG && debug("_onStopListening() " + status);
+ }
+
+ _addServiceListener(serviceType, listener) {
+ let listeners = this._listeners.get(serviceType);
+ if (!listeners) {
+ listeners = [];
+ this._listeners.set(serviceType, listeners);
+ }
+
+ if (!listeners.find(l => l === listener)) {
+ listeners.push(listener);
+ }
+ }
+
+ _removeServiceListener(serviceType, listener) {
+ let listeners = this._listeners.get(serviceType);
+ if (!listeners) {
+ return;
+ }
+
+ let index = listeners.findIndex(l => l === listener);
+ if (index >= 0) {
+ listeners.splice(index, 1);
+ }
+
+ if (listeners.length === 0) {
+ this._listeners.delete(serviceType);
+ }
+ }
+}
+
+let _addresses;
+
+/**
+ * @private
+ */
+function getAddresses() {
+ return new Promise((resolve, reject) => {
+ if (_addresses) {
+ resolve(_addresses);
+ return;
+ }
+
+ networkInfoService.listNetworkAddresses({
+ onListedNetworkAddresses(aAddressArray) {
+ _addresses = aAddressArray.filter(address => {
+ return (
+ !address.includes("%p2p") && // No WiFi Direct interfaces
+ !address.includes(":") && // XXX: No IPv6 for now
+ address != "127.0.0.1"
+ ); // No ipv4 loopback addresses.
+ });
+
+ DEBUG && debug("getAddresses(): " + _addresses);
+ resolve(_addresses);
+ },
+
+ onListNetworkAddressesFailed() {
+ DEBUG && debug("getAddresses() FAILED!");
+ resolve([]);
+ },
+ });
+ });
+}
+
+let _hostname;
+
+/**
+ * @private
+ */
+function getHostname() {
+ return new Promise(resolve => {
+ if (_hostname) {
+ resolve(_hostname);
+ return;
+ }
+
+ networkInfoService.getHostname({
+ onGotHostname(aHostname) {
+ _hostname = aHostname.replace(/\s/g, "-") + ".local";
+
+ DEBUG && debug("getHostname(): " + _hostname);
+ resolve(_hostname);
+ },
+
+ onGetHostnameFailed() {
+ DEBUG && debug("getHostname() FAILED");
+ resolve("localhost");
+ },
+ });
+ });
+}
+
+/**
+ * Parse fully qualified domain name to service name, instance name,
+ * and domain name. See https://tools.ietf.org/html/rfc6763#section-7.
+ *
+ * Example: 'foo-bar-service._http._tcp.local' -> {
+ * serviceName: 'foo-bar-service',
+ * serviceType: '_http._tcp',
+ * domainName: 'local'
+ * }
+ *
+ * @private
+ */
+function _parseServiceDomainName(serviceDomainName) {
+ let parts = serviceDomainName.split(".");
+ let index = Math.max(parts.lastIndexOf("_tcp"), parts.lastIndexOf("_udp"));
+
+ return {
+ serviceName: parts.splice(0, index - 1).join("."),
+ serviceType: parts.splice(0, 2).join("."),
+ domainName: parts.join("."),
+ };
+}
+
+/**
+ * @private
+ */
+function _propertyBagToObject(propBag) {
+ let result = {};
+ if (propBag.QueryInterface) {
+ propBag.QueryInterface(Ci.nsIPropertyBag2);
+ for (let prop of propBag.enumerator) {
+ result[prop.name] = prop.value.toString();
+ }
+ } else {
+ for (let name in propBag) {
+ result[name] = propBag[name].toString();
+ }
+ }
+ return result;
+}
+
+/**
+ * @private
+ */
+function _openSocket(addr, port, handler, multicastInterface) {
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+ socket.init2(
+ addr,
+ port,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ true
+ );
+
+ if (handler) {
+ socket.asyncListen({
+ onPacketReceived: handler.onPacketReceived,
+ onStopListening: handler.onStopListening,
+ });
+ }
+
+ if (multicastInterface) {
+ socket.joinMulticast(MDNS_MULTICAST_GROUP, multicastInterface);
+ }
+
+ return socket;
+}