forked from mirrors/gecko-dev
Differential Revision: https://phabricator.services.mozilla.com/D3733 --HG-- extra : rebase_source : c0fac176d7b3d840c4dbb14f8d95ccfc7f83a5a8 extra : histedit_source : a92c40117d0808a3ad68c972f622a7a42c9ae8ba
871 lines
25 KiB
JavaScript
871 lines
25 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'];
|
|
|
|
ChromeUtils.import('resource://gre/modules/Services.jsm');
|
|
ChromeUtils.import('resource://gre/modules/Timer.jsm');
|
|
ChromeUtils.import('resource://gre/modules/XPCOMUtils.jsm');
|
|
|
|
ChromeUtils.import('resource://gre/modules/DNSPacket.jsm');
|
|
ChromeUtils.import('resource://gre/modules/DNSRecord.jsm');
|
|
ChromeUtils.import('resource://gre/modules/DNSResourceRecord.jsm');
|
|
ChromeUtils.import('resource://gre/modules/DNSTypes.jsm');
|
|
|
|
const NS_NETWORK_LINK_TOPIC = 'network:link-status-changed';
|
|
|
|
let observerService = Cc["@mozilla.org/observer-service;1"]
|
|
.getService(Ci.nsIObserverService);
|
|
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);
|
|
observerService.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);
|
|
observerService.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, data.length);
|
|
} 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 [serviceKey, 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: 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, data.length);
|
|
});
|
|
|
|
// 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);
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
|
|
_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: serviceName,
|
|
serviceType: serviceType,
|
|
host: host,
|
|
address: address,
|
|
port: port,
|
|
domainName: 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: serviceInfo,
|
|
expireTime: 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;
|
|
}
|