From dfdaff628fc257f8b52eea856ed3d2c288b9cda4 Mon Sep 17 00:00:00 2001 From: git Date: Thu, 26 Jun 2025 10:01:19 +0100 Subject: [PATCH] removes the local-file persistence removes the local-file persistence (saveDbToFile, loadDbFromFile and related fs/database.json bits) and relies solely on MariaDB for storing and fetching both local and remote mDNS records --- main.js | 336 ++++++++++++++++++++------------------------------------ 1 file changed, 118 insertions(+), 218 deletions(-) diff --git a/main.js b/main.js index 53c90f0..0026291 100644 --- a/main.js +++ b/main.js @@ -2,270 +2,174 @@ // A single-file mDNS sync tool acting as client or server // Uses mdns-server for mDNS and Express for HTTP - /********************************* * * 1. LIBRARY FUNCTIONS * *********************************/ -const os = require('os'); +const os = require('os'); const mysql = require('mysql2/promise'); +const mdns = require('mdns-server'); // we'll initialize below - -// 0. HELPER FUNCTIONS +// 0. HELPER FUNCTIONS // Extract password from ~/.ssh/id_rsa.pub (last 6 characters of base64) function getDbPassword() { - const pubKeyContent = fs.readFileSync(`${os.homedir()}/.ssh/id_rsa.pub`, "utf8"); - const base64Part = pubKeyContent.split(" ")[1]; - return base64Part.slice(-7,-1); + const pubKeyContent = require('fs').readFileSync( + `${os.homedir()}/.ssh/id_rsa.pub`, + 'utf8' + ); + const base64Part = pubKeyContent.split(' ')[1]; + return base64Part.slice(-7, -1); } -// Insert data into MariaDB -async function updateMariaDB(record) { - if (!['A','PTR','SRV','TXT'].includes(record.Type)) return; - const host = DB_USER; +// Insert/update data into MariaDB +async function updateMariaDB(record) { const { Type, Name } = record; + if (!['A','PTR','SRV','TXT'].includes(Type)) return; + + const host = DB_USER; const dataStr = typeof record.Data === 'object' ? JSON.stringify(record.Data) : record.Data; try { - // INSERT new row, or if (Host,Type,Name) already exists, just update Data const sql = ` INSERT INTO ${DB_TABLE} (Host, Type, Name, Data) VALUES (?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - Data = VALUES(Data) + ON DUPLICATE KEY UPDATE Data = VALUES(Data) `; await dbPool.execute(sql, [host, Type, Name, dataStr]); } catch (err) { - console.error("DB Upsert Error:", err); + console.error('DB Upsert Error:', err); } } -// 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));} +// 1. Network functions -// 2. Network functions function getLocalNetworkAddressesIPs() { - - 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; - + const interfaces = os.networkInterfaces(); + return Object.values(interfaces) + .flat() + .filter(iface => iface.family === 'IPv4' && !iface.internal) + .map(iface => iface.address); } function sniffmDNSLocalPackets() { + mdns.on('response', async response => { + const answers = response.answers.concat(response.additionals); - // 1. Listen for responses - mdns.on('response', async function(response) { - // console.log("got a new mDNS response.",response); - let all_answers = response.answers.concat(response.additionals); - for (let k in all_answers) { - let answer = all_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))) continue; - - // 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); - await updateMariaDB({Type: answer.type,Name: answer.name,Data: db.local.PTR[answer.name]}); - } - } - // 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; - await updateMariaDB({Type: answer.type,Name: answer.name,Data: db.local.A[answer.name]}); - } - - if (answer.type == "SRV") { - // 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; - await updateMariaDB({Type: answer.type,Name: answer.name,Data: db.local[answer.name][answer.type]}); - } - if (answer.type == "TXT") { - // 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; - await updateMariaDB({Type: answer.type,Name: answer.name,Data: db.local[answer.name][answer.type]}); - } - - saveDbToFile(db,DB_FILENAME); - } - }) + for (const ans of answers) { + // only care about standard types + if (!['A','PTR','SRV','TXT'].includes(ans.type)) continue; - - // 2. Handle the server being destroyed - mdns.on('destroyed', function () {console.log('Server destroyed.');process.exit(0);}); + // For each answer, immediately upsert to MariaDB + await updateMariaDB({ + Type: ans.type, + Name: ans.name, + Data: ans.data + }); + } + }); - // 3. 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'}]} ); - }) + mdns.on('destroyed', () => { + console.log('Server destroyed.'); process.exit(0); + }); - + mdns.on('ready', () => { + console.log('mDNS server is ready...'); + }); } -function replyLocallyWithRemoteDevicesData(name,type,query) { - const answers = []; - if (!db.remote) db.remote = {}; - if (!db.remote.PTR) db.remote.PTR = {}; - if (!db.remote.A) db.remote.A = {}; +function replyLocallyWithRemoteDevicesData(name, type) { + const answers = []; + if (type === 'PTR' && db.remote.PTR[name]) { + db.remote.PTR[name].forEach(ptrData => + answers.push({ name, type:'PTR', class:'IN', ttl:120, data: ptrData }) + ); + } + if (type === 'A' && db.remote.A[name]) { + answers.push({ name, type:'A', class:'IN', ttl:120, data: db.remote.A[name] }); + } + if (type === 'SRV' && db.remote.services[name]?.SRV) { + answers.push({ name, type:'SRV', class:'IN', ttl:120, data: db.remote.services[name].SRV }); + } + if (type === 'TXT' && db.remote.services[name]?.TXT) { + answers.push({ + name, type:'TXT', class:'IN', ttl:120, + data: db.remote.services[name].TXT.map(e => Buffer.from(e.data, 'utf8')) + }); + } + if (type === 'ANY') { + // same as above but for ANY + ['PTR','A','SRV','TXT'].forEach(t => + replyLocallyWithRemoteDevicesData(name, t) + ); + } - // 1. If the query is asking for a PTR (pointer to a service) - if (type === 'PTR' && db.remote.PTR && db.remote.PTR[name]) { - for (let ptrData of db.remote.PTR[name]) { - answers.push({ - name, // The service name being queried - type: 'PTR', // Type of DNS record - class: 'IN', // Internet class - ttl: 120, // Time-to-live (how long to cache) - data: ptrData // The actual pointer data (e.g. device instance name) - }); - } - } - - // 2. If the query is for an A record (IPv4 address of a device) - if (type === 'A' && db.remote.A && db.remote.A[name]) { - answers.push({ - name, - type: 'A', - class: 'IN', - ttl: 120, - data: db.remote.A[name] // The IPv4 address - }); - } - - // 3. If the query is for an SRV record (hostname + port of a service) - if (type === 'SRV' && db.remote.services[name] && db.remote.services[name].SRV !== null) { - answers.push({ - name, - type: 'SRV', - class: 'IN', - ttl: 120, - data: db.remote.services[name].SRV // Must be an object like { port, target, priority, weight } - }); - } - - // 4. If the query is for a TXT record (extra metadata) - if (type === 'TXT' && db.remote.services[name] && db.remote.services[name].TXT !== null) { - answers.push({ - name, - type: 'TXT', - class: 'IN', - ttl: 120, - // Convert plain string to buffer; required by mdns-server - data: db.remote.services[name].TXT.map(entry=>Buffer.from(entry.data,'utf8')) - }); - } - // 5. Many mDNS tools (like Avahi or Bonjour) send type: 'ANY' queries to discover all records for a name. - if (type === 'ANY') { - // Respond with everything you know about this name - if (db.remote.PTR && db.remote.PTR[name]) { - for (let ptrData of db.remote.PTR[name]) { - answers.push({ name, type: 'PTR', class: 'IN', ttl: 120, data: ptrData }); - } - } - if (db.remote.A && db.remote.A[name]) { - answers.push({ name, type: 'A', class: 'IN', ttl: 120, data: db.remote.A[name] }); - } - if (db.remote.services[name]) { - const r = db.remote.services[name]; - if (r.SRV) answers.push({ name, type: 'SRV', class: 'IN', ttl: 120, data: r.SRV }); - if (r.TXT) answers.push({ name, type: 'TXT', class: 'IN', ttl: 120, data: r.TXT.map(entry=>Buffer.from(entry.data,'utf8')) }); - } - } - - // 6. If we prepared any answers, respond to the query - if (answers.length > 0) { - console.log(`Responding with ${answers.length} answer(s) for ${name}`); - mdns.respond({ answers }); - } - + if (answers.length) { + console.log(`Responding with ${answers.length} answer(s) for ${name}`); + mdns.respond({ answers }); + } } + async function fetchRemoteDevicesData() { try { const [rows] = await dbPool.execute( - `SELECT Type, Name, Data FROM ${DB_TABLE} WHERE Host <> ? AND MakeAvailableEverywhere = 1`, + `SELECT Type, Name, Data + FROM ${DB_TABLE} + WHERE Host <> ? + AND MakeAvailableEverywhere = 1`, [DB_USER] ); + // reinitialize remote cache db.remote = { A: {}, PTR: {}, services: {} }; - - rows.forEach(row => { - const { Type, Name, Data } = row; - + for (const { Type, Name, Data } of rows) { + const parsed = ['[','{'].includes(Data[0]) ? JSON.parse(Data) : Data; if (Type === 'A') { - db.remote.A[Name] = Data; - } - else if (Type === 'PTR') { - db.remote.PTR[Name] = (["[","{"].includes(Data.substr(0,1))?JSON.parse(Data):Data); - } - else if (['TXT', 'SRV'].includes(Type)) { - if (!db.remote.services[Name]) db.remote.services[Name] = { TXT: null, SRV: null }; - db.remote.services[Name][Type] = (["[","{"].includes(Data.substr(0,1))?JSON.parse(Data):Data); + db.remote.A[Name] = parsed; + } else if (Type === 'PTR') { + db.remote.PTR[Name] = parsed; + } else { // SRV or TXT + db.remote.services[Name] ||= { TXT:null, SRV:null }; + db.remote.services[Name][Type] = parsed; } - }); - - console.log(`Remote DB updated. A records: ${Object.keys(db.remote.A).length} PTR records: ${Object.keys(db.remote.PTR).length}`); - + } + console.log( + `Remote DB updated. A: ${Object.keys(db.remote.A).length}, PTR: ${Object.keys(db.remote.PTR).length}` + ); } catch (err) { console.error('Error fetching remote DB:', err); } } - - - - /********************************* * * 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}); // MariaDB Configuration -const DB_HOST = "10.10.8.1"; -const DB_NAME = "NETWORK"; -const DB_TABLE = "mDNS"; -const DB_USER = os.hostname(); +const DB_HOST = '10.10.8.1'; +const DB_NAME = 'NETWORK'; +const DB_TABLE = 'mDNS'; +const DB_USER = os.hostname(); +// Initialize in-memory cache +const db = { remote: { A: {}, PTR: {}, services: {} } }; -// Initialize MariaDB connection pool +// Pick first non-internal v4 address +const LOCAL_IP_ADDR = getLocalNetworkAddressesIPs()[0]; + +const mdnsServer = mdns({ + interface: LOCAL_IP_ADDR, + reuseAddr: true, + loopback: false, + noInit: true +}); + +// Init MariaDB connection pool const dbPool = mysql.createPool({ host: DB_HOST, user: DB_USER, @@ -273,27 +177,23 @@ const dbPool = mysql.createPool({ database: DB_NAME, connectionLimit: 5 }); -console.log(`Trying to connect to central SQL server with host: ${DB_HOST} user: ${DB_USER} pass: ${getDbPassword()} db_name: ${DB_NAME}`); - +console.log( + `Connecting to SQL ${DB_HOST} as ${DB_USER}, DB=${DB_NAME}` +); sniffmDNSLocalPackets(); - -mdns.on('query', function (query) { - // Loop through each question in the mDNS query - query.questions.forEach(q => { - const { name, type } = q; - console.log(`Received mDNS query for ${name} (${type})`); - replyLocallyWithRemoteDevicesData(name,type,q); - - - }); +mdnsServer.on('query', ({ questions }) => { + for (const { name, type } of questions) { + console.log(`Query for ${name} (${type})`); + replyLocallyWithRemoteDevicesData(name, type); + } }); -// every 15 seconds -setInterval(fetchRemoteDevicesData, 15000); -fetchRemoteDevicesData(); // initial immediate call +// fetch remote every 15s +setInterval(fetchRemoteDevicesData, 15_000); +fetchRemoteDevicesData(); -// initialize the server now that we are watching for events -mdns.initServer() +// now start the server +mdnsServer.initServer(); \ No newline at end of file