diff options
Diffstat (limited to '')
-rw-r--r-- | netwerk/dns/mdns/libmdns/fallback/DNSPacket.jsm | 304 | ||||
-rw-r--r-- | netwerk/dns/mdns/libmdns/fallback/DNSRecord.jsm | 71 | ||||
-rw-r--r-- | netwerk/dns/mdns/libmdns/fallback/DNSResourceRecord.jsm | 221 | ||||
-rw-r--r-- | netwerk/dns/mdns/libmdns/fallback/DNSTypes.jsm | 99 | ||||
-rw-r--r-- | netwerk/dns/mdns/libmdns/fallback/DataReader.jsm | 134 | ||||
-rw-r--r-- | netwerk/dns/mdns/libmdns/fallback/DataWriter.jsm | 97 | ||||
-rw-r--r-- | netwerk/dns/mdns/libmdns/fallback/MulticastDNS.jsm | 985 |
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; +} |