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
 | |
|   };
 | |
| };
 |