Skip to content

Instantly share code, notes, and snippets.

@opichals
Last active November 24, 2021 11:24
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save opichals/1be27a74571b6ca2e1cc02b3587b78b8 to your computer and use it in GitHub Desktop.
Save opichals/1be27a74571b6ca2e1cc02b3587b78b8 to your computer and use it in GitHub Desktop.
Espruino experimental mDNS module
function init(hostname, ip) {
const dgram = require('dgram');
const srv = dgram.createSocket({ type: 'udp4', reuseAddr: true, recvBufferSize: 2000 });
srv.addMembership('224.0.0.251', ip); // Bounjour link-local multicast IP
const socket = srv.bind(5353, function(bound) {
function onDatagram(msg, rinfo) {
//console.log('>MEM', JSON.stringify(process.memory()));
//console.log('MSG', msg.length, rinfo.address, rinfo.port);
try {
processMessage(E.toArrayBuffer(msg), {
rinfo: rinfo,
socket: bound,
responders: responders
});
} catch(e) {
console.log('Error', e);
}
// console.log('<MEM', JSON.stringify(process.memory()));
}
bound.on('message', onDatagram);
bound.on('close', function(err) {
console.log('s:close', err);
});
});
function hostnameResponder(ctx, packet, r) {
if (!r || r.name !== hostname+'.local' || r.data) return;
return mDNSAnswer(packet.tid, hostname+'.local', ip);
}
responder( 1, hostnameResponder); // A `${hostname}.local`
return {
send: function(data) { return socket.send(data, 5353, '224.0.0.251'); },
resolvePTR: function(ptr, callback) {
const res = {};
function ptrResolveResponder(ctx, packet, r) {
if (!r || (!packet.answerRRs && !packet.additionalRRs)) {
// end of packet (or no answers)
if (res.found) {
// unsub!
delete res.found;
// console.log('REF', res);
const ptrs = res['\x0c'+ptr];
res.data = ptrs.map(function(p) {
const ref = p.data;
const data = {
ptr: ref
};
const target = res['\x21'+ref] && res['\x21'+ref][0].data;
if (target) {
target.ip = res['\x01'+target.host][0].data;
data.text = res['\x10'+ref][0].data;
data.target = target;
}
return data;
});
callback(res);
}
return;
}
var id = String.fromCharCode(r.type) + r.name;
res[id] = res[id] || [];
res[id].push(r);
if (r.name === ptr && r.type === 12) {
res.found = true;
}
}
responder(0, ptrResolveResponder);
this.send(qmDNSQuery(12, ptr));
}
};
}
const responders = {};
function responder(type, handler) {
responders[type] = responders[type] || [];
responders[type].push(handler);
}
function servicesResponder(ctx, packet, r) {
if (!r || r.name !== '_services._dns-sd._udp.local' || r.data) return;
return mDNSSvcAnswer(packet.tid, r.name, '_http._tcp.local');
}
responder(12, servicesResponder); // PTR `_services._dns-sd._udp.local`
const fromCharCode = String.fromCharCode;
function bytesToString(bytes, offset, len) {
return bytes.slice(offset, offset+len).map(function(n) { return fromCharCode(n); }).join('');
}
function toDnsName(name) { // no 0xc0 LABELs
return name.split('.').map(function(s) { return fromCharCode(s.length) + s; }).join('') + '\x00';
}
function mDNSAnswer(tid, hostname, ip) {
var ipBuff = ip.split('.').map(function(n) { return fromCharCode(parseInt(n, 10)); }).join('');
return fromCharCode((tid >> 8) & 0xff)+fromCharCode(tid & 0xff)+'\x84\x00'+'\x00\x00'+'\x00\x01'+'\x00\x00'+'\x00\x00' + toDnsName(hostname) + '\x00\x01\x00\x01'+'\x00\x00\x00\x0a'+'\x00'+fromCharCode(ipBuff.length)+ipBuff;
}
function mDNSSvcAnswer(tid, hostname, domainName) {
var ipBuff = toDnsName(domainName);
return fromCharCode((tid >> 8) & 0xff)+fromCharCode(tid & 0xff)+'\x84\x00'+'\x00\x00'+'\x00\x01'+'\x00\x00'+'\x00\x00' + toDnsName(hostname) + '\x00\x0c\x00\x01'+'\x00\x00\x00\x0a'+'\x00'+fromCharCode(ipBuff.length)+ipBuff;
}
function quDNSQuery(hostname) {
return '\x00\x00'+'\x00\x00'+'\x00\x01'+'\x00\x00'+'\x00\x00'+'\x00\x00' + toDnsName(hostname) + '\x00\x01\x80\x01';
}
function qmDNSQuery(type, hostname) {
return '\x00\x00'+'\x00\x00'+'\x00\x01'+'\x00\x00'+'\x00\x00'+'\x00\x00' + toDnsName(hostname) + '\x00'+fromCharCode(type)+'\x00\x01';
}
function parseDnsName(bytes, offset) {
var end = 0;
var start = offset;
var name = '';
do {
var len = bytes[offset++];
//console.log('Name', len, offset, bytes.length);
if (!len) break;
if (len >= 0xc0) {
bytes = new Uint8Array(bytes.buffer, bytes[offset++]);
if (!end) end = offset;
offset = 0;
continue;
}
name += '.'+bytesToString(bytes, offset, len);
offset += len;
//console.log('name', name, len, offset);
if (offset >= bytes.length) {
console.log('wlen', offset, bytes.length, JSON.stringify(name));
offset = bytes.length;
break;
}
} while(len);
if (!end) end = offset;
return { name: name.substring(1), len: end-start };
}
function parseRecord(bytes, offset, isAnswer) {
// console.log('BYTES', offset, bytes.slice(offset, bytes.length));
var dnsName = parseDnsName(bytes, offset);
offset += dnsName.len;
// console.log('REC-NAME', JSON.stringify(dnsName));
var recordInfo = new DataView(bytes.buffer, offset + bytes.byteOffset);
const getUint16 = recordInfo.getUint16.bind(recordInfo);
var rec = {
name: dnsName.name,
type: getUint16(0),
clazz: getUint16(2),
len: dnsName.len + 4
}
if (isAnswer) {
offset += 10;
rec.ttl = (getUint16(4) << 16) | getUint16(6);
var len = getUint16(8);
rec.len += 6 + len;
if (rec.type === 12) { // PTR
var n = parseDnsName(bytes, offset);
rec.data = n.name;
} else
if (rec.type === 33) { // SRV
var n = parseDnsName(bytes, offset + 6);
rec.data = { port: getUint16(14), host: n.name };
} else
if (rec.type === 16) { // TXT
rec.data = [];
while(len) {
var slen = bytes[offset];
if (!slen) break;
offset++;
var field = bytesToString(bytes, offset, slen);
rec.data.push(field);
offset+=slen;
len-=slen+1;
}
} else
{ // generic
rec.data = bytes.slice(offset, offset + len);
}
}
// console.log('REC', rec);
return rec;
}
function handleRecord(ctx, r) {
var rs = (ctx.responders[r && r.type] || []).concat(ctx.responders[0] || []);
rs.forEach(function(responder) {
const reply = responder(ctx, ctx.packet, r);
if (!reply) return;
console.log('reply', r && r.type, r && r.name, r && !r.data);
const rinfo = ctx.rinfo;
ctx.socket.send(reply, rinfo.port, rinfo.address);
});
}
function handleRecords(ctx, count, isAnswer) {
var record;
for (var q=0; q<count; q++) {
record = parseRecord(ctx.bytes, ctx.offset, isAnswer);
handleRecord(ctx, record);
// console.log('REC-LEN', record.len);
ctx.offset += record.len;
}
}
function processMessage(messageBuffer, ctx) {
var record;
var buffer = new DataView(messageBuffer);
const getUint16 = buffer.getUint16.bind(buffer);
// console.log('MBYTES', messageBuffer.length, new Uint8Array(messageBuffer).slice(0, messageBuffer.length));
var packet = {
tid: getUint16(0),
flags: getUint16(2),
questions: getUint16(4),
answerRRs: getUint16(6),
authority: getUint16(8),
additionalRRs: getUint16(10)
}
//console.log('PKT', packet);
ctx.bytes = new Uint8Array(messageBuffer, 12);
ctx.offset = 0;
ctx.packet = packet;
handleRecords(ctx, packet.questions, 0);
handleRecords(ctx, packet.answerRRs, 1);
handleRecords(ctx, packet.authority, 0);
handleRecords(ctx, packet.additionalRRs, 1);
handleRecord(ctx); // end of packet
return packet;
}
exports.init = init;
exports.responder = responder;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment