goldilocks.js/lib/mdns.js

186 lines
4.9 KiB
JavaScript
Raw Normal View History

2017-05-23 22:23:43 +00:00
'use strict';
var PromiseA = require('bluebird');
var fs = PromiseA.promisifyAll(require('fs'));
var path = require('path');
var idFilename = path.join(__dirname, '..', 'var', 'mdns-id');
2017-05-23 22:23:43 +00:00
var queryName = '_cloud._tcp.local';
var randomId = {
get: function () {
return fs.readFileAsync(idFilename)
.catch(function (err) {
if (err.code !== 'ENOENT') {
return PromiseA.reject(err);
}
var id = require('crypto').randomBytes(5).toString('hex');
return randomId.set(id);
});
}
, set: function (value) {
return fs.mkdirAsync(path.dirname(idFilename)).catch(function (err) {
if (err.code !== 'EEXIST') {
console.error('failed to mkdir', path.dirname(idFilename), err.toString());
}
}).then(function () {
return fs.writeFileAsync(idFilename, value).then(function () {
2017-05-23 22:23:43 +00:00
return value;
});
});
2017-05-23 22:23:43 +00:00
}
};
function createResponse(name, ownerIds, packet, ttl, mainPort) {
2017-05-23 22:23:43 +00:00
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
2017-06-08 19:21:58 +00:00
, port: mainPort
2017-05-23 22:23:43 +00:00
, target: name + ".local"
});
rpacket.additional.push({
name: name + '._device-info.' + queryName
2017-05-23 22:23:43 +00:00
, 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]
});
});
2017-05-23 22:23:43 +00:00
return require('dns-suite').DNSPacket.write(rpacket);
}
2017-06-08 19:21:58 +00:00
module.exports.start = function (deps, config, mainPort) {
2017-05-23 22:23:43 +00:00
var socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true });
var dns = require('dns-suite');
var nextBroadcast = -1;
2017-05-23 22:23:43 +00:00
socket.on('message', function (message, rinfo) {
// console.log('Received %d bytes from %s:%d', message.length, rinfo.address, rinfo.port);
var packet;
try {
packet = dns.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 || packet.question[0].name !== queryName) {
return;
}
if (packet.question[0].typeName !== 'PTR' || packet.question[0].className !== 'IN' ) {
return;
}
2017-05-23 22:23:43 +00:00
var proms = [
randomId.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, mainPort);
2017-06-16 19:21:20 +00:00
var now = Date.now();
if (now > nextBroadcast) {
socket.send(resp, config.mdns.port, config.mdns.broadcast);
2017-06-16 19:21:20 +00:00
nextBroadcast = now + config.mdns.ttl * 1000;
} else {
socket.send(resp, rinfo.port, rinfo.address);
}
2017-05-23 22:23:43 +00:00
});
});
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);
2017-06-16 19:21:20 +00:00
// 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);
2017-05-23 22:23:43 +00:00
});
};