forked from coolaj86/goldilocks.js
204 lines
5.3 KiB
JavaScript
204 lines
5.3 KiB
JavaScript
'use strict';
|
|
|
|
var PromiseA = require('bluebird');
|
|
var queryName = '_cloud._tcp.local';
|
|
var dnsSuite = require('dns-suite');
|
|
|
|
function createResponse(name, ownerIds, packet, ttl, mainPort) {
|
|
var rpacket = {
|
|
header: {
|
|
id: packet.header.id
|
|
, qr: 1
|
|
, opcode: 0
|
|
, aa: 1
|
|
, tc: 0
|
|
, rd: 0
|
|
, ra: 0
|
|
, res1: 0
|
|
, res2: 0
|
|
, res3: 0
|
|
, rcode: 0
|
|
, }
|
|
, question: packet.question
|
|
, answer: []
|
|
, authority: []
|
|
, additional: []
|
|
, edns_options: []
|
|
};
|
|
|
|
rpacket.answer.push({
|
|
name: queryName
|
|
, typeName: 'PTR'
|
|
, ttl: ttl
|
|
, className: 'IN'
|
|
, data: name + '.' + queryName
|
|
});
|
|
|
|
var ifaces = require('./local-ip').find();
|
|
Object.keys(ifaces).forEach(function (iname) {
|
|
var iface = ifaces[iname];
|
|
|
|
iface.ipv4.forEach(function (addr) {
|
|
rpacket.additional.push({
|
|
name: name + '.local'
|
|
, typeName: 'A'
|
|
, ttl: ttl
|
|
, className: 'IN'
|
|
, address: addr.address
|
|
});
|
|
});
|
|
|
|
iface.ipv6.forEach(function (addr) {
|
|
rpacket.additional.push({
|
|
name: name + '.local'
|
|
, typeName: 'AAAA'
|
|
, ttl: ttl
|
|
, className: 'IN'
|
|
, address: addr.address
|
|
});
|
|
});
|
|
});
|
|
|
|
rpacket.additional.push({
|
|
name: name + '.' + queryName
|
|
, typeName: 'SRV'
|
|
, ttl: ttl
|
|
, className: 'IN'
|
|
, priority: 1
|
|
, weight: 0
|
|
, port: mainPort
|
|
, target: name + ".local"
|
|
});
|
|
rpacket.additional.push({
|
|
name: name + '._device-info.' + queryName
|
|
, typeName: 'TXT'
|
|
, ttl: ttl
|
|
, className: 'IN'
|
|
, data: ["model=CloudHome1,1", "dappsvers=1"]
|
|
});
|
|
ownerIds.forEach(function (id) {
|
|
rpacket.additional.push({
|
|
name: name + '._owner-id.' + queryName
|
|
, typeName: 'TXT'
|
|
, ttl: ttl
|
|
, className: 'IN'
|
|
, data: [id]
|
|
});
|
|
});
|
|
|
|
return dnsSuite.DNSPacket.write(rpacket);
|
|
}
|
|
|
|
module.exports.create = function (deps, config) {
|
|
var socket;
|
|
var nextBroadcast = -1;
|
|
|
|
function handlePacket(message, rinfo) {
|
|
// console.log('Received %d bytes from %s:%d', message.length, rinfo.address, rinfo.port);
|
|
|
|
var packet;
|
|
try {
|
|
packet = dnsSuite.DNSPacket.parse(message);
|
|
}
|
|
catch (er) {
|
|
// `dns-suite` actually errors on a lot of the packets floating around in our network,
|
|
// so don't bother logging any errors. (We still use `dns-suite` because unlike `dns-js`
|
|
// it can successfully craft the one packet we want to send.)
|
|
return;
|
|
}
|
|
|
|
// Only respond to queries.
|
|
if (packet.header.qr !== 0) { return; }
|
|
// Only respond if they were asking for cloud devices.
|
|
if (packet.question.length !== 1) { return; }
|
|
if (packet.question[0].name !== queryName) { return; }
|
|
if (packet.question[0].typeName !== 'PTR') { return; }
|
|
if (packet.question[0].className !== 'IN' ) { return; }
|
|
|
|
var proms = [
|
|
deps.storage.mdnsId.get()
|
|
, deps.storage.owners.all().then(function (owners) {
|
|
// The ID is the sha256 hash of the PPID, which shouldn't be reversible and therefore
|
|
// should be safe to expose without needing authentication.
|
|
return owners.map(function (owner) {
|
|
return owner.id;
|
|
});
|
|
})
|
|
];
|
|
|
|
PromiseA.all(proms).then(function (results) {
|
|
var resp = createResponse(results[0], results[1], packet, config.mdns.ttl, deps.tcp.mainPort);
|
|
var now = Date.now();
|
|
if (now > nextBroadcast) {
|
|
socket.send(resp, config.mdns.port, config.mdns.broadcast);
|
|
nextBroadcast = now + config.mdns.ttl * 1000;
|
|
} else {
|
|
socket.send(resp, rinfo.port, rinfo.address);
|
|
}
|
|
});
|
|
}
|
|
|
|
function start() {
|
|
socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true });
|
|
socket.on('message', handlePacket);
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
socket.once('error', reject);
|
|
|
|
socket.bind(config.mdns.port, function () {
|
|
var addr = this.address();
|
|
console.log('bound on UDP %s:%d for mDNS', addr.address, addr.port);
|
|
|
|
socket.setBroadcast(true);
|
|
socket.addMembership(config.mdns.broadcast);
|
|
// This is supposed to be a local device discovery mechanism, so we shouldn't
|
|
// need to hop through any gateways. This helps with security by making it
|
|
// much more difficult for someone to use us as part of a DDoS attack by
|
|
// spoofing the UDP address a request came from.
|
|
socket.setTTL(1);
|
|
|
|
socket.removeListener('error', reject);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
function stop() {
|
|
return new Promise(function (resolve, reject) {
|
|
socket.once('error', reject);
|
|
|
|
socket.close(function () {
|
|
socket.removeListener('error', reject);
|
|
socket = null;
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateConf() {
|
|
var promise;
|
|
if (config.mdns.disabled) {
|
|
if (socket) {
|
|
promise = stop();
|
|
}
|
|
} else {
|
|
if (!socket) {
|
|
promise = start();
|
|
} else if (socket.address().port !== config.mdns.port) {
|
|
promise = stop().then(start);
|
|
} else {
|
|
// Can't check membership, so just add the current broadcast address to make sure
|
|
// it's set. If it's already set it will throw an exception (at least on linux).
|
|
try {
|
|
socket.addMembership(config.mdns.broadcast);
|
|
} catch (e) {}
|
|
promise = Promise.resolve();
|
|
}
|
|
}
|
|
}
|
|
updateConf();
|
|
|
|
return {
|
|
updateConf
|
|
};
|
|
};
|