// mdns-sync.js // A single-file mDNS sync tool acting as client or server // Uses mdns-server for mDNS and Express for HTTP /********************************* * * 1. LIBRARY FUNCTIONS * *********************************/ // 0. HELPER FUNCTIONS function convertBufferToString(a) {if (a.data) for (i in a.data) a.data[i] = a.data[i].toString(); return a;} // 1. Database functions function saveDbToFile(data, filename) {return fs.writeFileSync(filename,typeof data === 'object'?JSON.stringify(data):data);} function loadDbFromFile(filename) {if (!fs.existsSync(filename)) return {local:{},remote:{}}; else return JSON.parse(fs.readFileSync(filename));} // 2. Network functions function getLocalNetworkAddressesIPs() { const os = require('os'); const interfaces = os.networkInterfaces(); const addresses = []; for (const name of Object.keys(interfaces)) { for (const iface of interfaces[name]) { if (iface.family === 'IPv4' && !iface.internal) { addresses.push(iface.address); } } } return addresses; } function sniffmDNSLocalPackets() { // 1. Listen for responses mdns.on('response', function(response) { // console.log("got a new mDNS response.",response); for (k in response.answers) { let answer = response.answers[k]; // 1. Handle DEVICES for a given SERVICE if (answer.type == "PTR") { // 1.1 Filter by RELEVANT SERVICE TYPES if (!["in-addr.arpa","_googlecast._tcp.local","_tcp.local"].some(suffix => answer.name.endsWith(suffix))) return; // 1.2 Initialize POINTERS (services) sub-database if (!db.local.PTR) db.local.PTR = {}; // 1.3 Initialize this SERVICE TYPE (e.g. _googlecast._tcp.local), if needed if (!db.local.PTR[answer.name]) { db.local.PTR[answer.name] = [answer.data]; } else { // Add a new DEVICE to this SERVICE TYPE if needed if (!db.local.PTR[answer.name].includes(answer.data)) db.local.PTR[answer.name].push(answer.data); } } // 2. Handle DEVICE IP resolution if (answer.type == "A") { // 2.1 Initialize ADDRESSES sub-database if (!db.local.A) db.local.A = {}; db.local.A[answer.name] = answer.data; } if (["TXT","SRV"].includes(answer.type)) { // Initialize this device / entry, if needed if (db.local[answer.name] === undefined) db.local[answer.name] = {"TXT":null,"SRV":null}; db.local[answer.name][answer.type] = answer.data; } saveDbToFile(db,DB_FILENAME); } }) // 2. Listen for queries mdns.on('query', function(query) {/* console.log("query requested",query);*/}); // 3. Handle the server being destroyed mdns.on('destroyed', function () {console.log('Server destroyed.');process.exit(0);}); // 4. Handle the onReady event mdns.on('ready', function () { console.log("mDNS server is ready..."); // mdns.query({questions:[{ name: '_:googlecast._tcp.local', type: 'PTR', class: 'IN'}]} ); }) // initialize the server now that we are watching for events mdns.initServer() } function injectmDNSPacketLocally(type,name,data,flush = true,_class = 'IN',ttl = 120) { console.log(`sending packet ${name} of ${type} with data: ${data}`); mdns.respond({answers:[ { name: name, type: type, ttl: ttl, class: _class, flush: flush, data: data } ]});} /********************************* * * 100. MAIN / IMPERATIVE CODE * *********************************/ const fs = require("fs"); const DB_FILENAME = 'database.json'; const db = loadDbFromFile(DB_FILENAME); const LOCAL_IP_ADDR = getLocalNetworkAddressesIPs()[0]; const mdns = require('mdns-server')({interface: LOCAL_IP_ADDR,reuseAddr: true,loopback: false,noInit: true}); sniffmDNSLocalPackets(); setInterval(function(){ // INJECT PTR - Pointers services for (name in db.remote.PTR) { for (i in db.remote.PTR[name]) injectmDNSPacketLocally('PTR',name,db.remote.PTR[name][i]); } // INJECT A - Addresses for (name in db.remote.A) { injectmDNSPacketLocally('A',name,db.remote.A[name]); } // INJECT SRV,TXT for (name in db.remote) { if (["PTR","A"].includes(name)) continue; if (db.remote[name].SRV !== null) injectmDNSPacketLocally('SRV',name,db.remote[name].SRV); if (db.remote[name].TXT !== null) injectmDNSPacketLocally('TXT',name,Buffer.from(db.remote[name].TXT, 'utf8')); } },5000);