forked from mirrors/gecko-dev
# ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D35919 --HG-- extra : source : afa5bd771feba466ba7670c58f3d93233a14e202
986 lines
26 KiB
JavaScript
986 lines
26 KiB
JavaScript
/* -*- 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/. */
|
|
/* jshint esnext: true, moz: true */
|
|
|
|
"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;
|
|
}
|