Last active
November 24, 2021 11:24
-
-
Save opichals/1be27a74571b6ca2e1cc02b3587b78b8 to your computer and use it in GitHub Desktop.
Espruino experimental mDNS module
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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