Fixed bug
This commit is contained in:
parent
dfdaff628f
commit
e470dc6c13
334
main.js
334
main.js
@ -2,174 +2,266 @@
|
|||||||
// A single-file mDNS sync tool acting as client or server
|
// A single-file mDNS sync tool acting as client or server
|
||||||
// Uses mdns-server for mDNS and Express for HTTP
|
// Uses mdns-server for mDNS and Express for HTTP
|
||||||
|
|
||||||
|
|
||||||
/*********************************
|
/*********************************
|
||||||
*
|
*
|
||||||
* 1. LIBRARY FUNCTIONS
|
* 1. LIBRARY FUNCTIONS
|
||||||
*
|
*
|
||||||
*********************************/
|
*********************************/
|
||||||
|
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const mysql = require('mysql2/promise');
|
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)
|
// Extract password from ~/.ssh/id_rsa.pub (last 6 characters of base64)
|
||||||
function getDbPassword() {
|
function getDbPassword() {
|
||||||
const pubKeyContent = require('fs').readFileSync(
|
const pubKeyContent = fs.readFileSync(`${os.homedir()}/.ssh/id_rsa.pub`, "utf8");
|
||||||
`${os.homedir()}/.ssh/id_rsa.pub`,
|
const base64Part = pubKeyContent.split(" ")[1];
|
||||||
'utf8'
|
return base64Part.slice(-7,-1);
|
||||||
);
|
|
||||||
const base64Part = pubKeyContent.split(' ')[1];
|
|
||||||
return base64Part.slice(-7, -1);
|
|
||||||
}
|
}
|
||||||
|
// Insert data into MariaDB
|
||||||
// Insert/update data into MariaDB
|
|
||||||
async function updateMariaDB(record) {
|
async function updateMariaDB(record) {
|
||||||
const { Type, Name } = record;
|
if (!['A','PTR','SRV','TXT'].includes(record.Type)) return;
|
||||||
if (!['A','PTR','SRV','TXT'].includes(Type)) return;
|
|
||||||
|
|
||||||
const host = DB_USER;
|
const host = DB_USER;
|
||||||
|
const { Type, Name } = record;
|
||||||
const dataStr = typeof record.Data === 'object'
|
const dataStr = typeof record.Data === 'object'
|
||||||
? JSON.stringify(record.Data)
|
? JSON.stringify(record.Data)
|
||||||
: record.Data;
|
: record.Data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// INSERT new row, or if (Host,Type,Name) already exists, just update Data
|
||||||
const sql = `
|
const sql = `
|
||||||
INSERT INTO ${DB_TABLE} (Host, Type, Name, Data)
|
INSERT INTO ${DB_TABLE} (Host, Type, Name, Data)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE Data = VALUES(Data)
|
ON DUPLICATE KEY UPDATE
|
||||||
|
Data = VALUES(Data)
|
||||||
`;
|
`;
|
||||||
await dbPool.execute(sql, [host, Type, Name, dataStr]);
|
await dbPool.execute(sql, [host, Type, Name, dataStr]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('DB Upsert Error:', err);
|
console.error("DB Upsert Error:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Network functions
|
// 1. Database functions
|
||||||
|
|
||||||
|
|
||||||
|
// 2. Network functions
|
||||||
function getLocalNetworkAddressesIPs() {
|
function getLocalNetworkAddressesIPs() {
|
||||||
const interfaces = os.networkInterfaces();
|
|
||||||
return Object.values(interfaces)
|
const interfaces = os.networkInterfaces();
|
||||||
.flat()
|
const addresses = [];
|
||||||
.filter(iface => iface.family === 'IPv4' && !iface.internal)
|
|
||||||
.map(iface => iface.address);
|
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() {
|
function sniffmDNSLocalPackets() {
|
||||||
mdns.on('response', async response => {
|
|
||||||
const answers = response.answers.concat(response.additionals);
|
|
||||||
|
|
||||||
for (const ans of answers) {
|
// 1. Listen for responses
|
||||||
// only care about standard types
|
mdns.on('response', async function(response) {
|
||||||
if (!['A','PTR','SRV','TXT'].includes(ans.type)) continue;
|
// 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]});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// For each answer, immediately upsert to MariaDB
|
|
||||||
await updateMariaDB({
|
// 2. Handle the server being destroyed
|
||||||
Type: ans.type,
|
mdns.on('destroyed', function () {console.log('Server destroyed.');process.exit(0);});
|
||||||
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'}]} );
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {};
|
||||||
|
|
||||||
|
// 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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
mdns.on('destroyed', () => {
|
// 3. If the query is for an SRV record (hostname + port of a service)
|
||||||
console.log('Server destroyed.'); process.exit(0);
|
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 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mdns.on('ready', () => {
|
// 4. If the query is for a TXT record (extra metadata)
|
||||||
console.log('mDNS server is ready...');
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (answers.length) {
|
|
||||||
console.log(`Responding with ${answers.length} answer(s) for ${name}`);
|
|
||||||
mdns.respond({ answers });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchRemoteDevicesData() {
|
async function fetchRemoteDevicesData() {
|
||||||
try {
|
try {
|
||||||
const [rows] = await dbPool.execute(
|
const [rows] = await dbPool.execute(
|
||||||
`SELECT Type, Name, Data
|
`SELECT Type, Name, Data FROM ${DB_TABLE} WHERE Host <> ? AND MakeAvailableEverywhere = 1`,
|
||||||
FROM ${DB_TABLE}
|
|
||||||
WHERE Host <> ?
|
|
||||||
AND MakeAvailableEverywhere = 1`,
|
|
||||||
[DB_USER]
|
[DB_USER]
|
||||||
);
|
);
|
||||||
|
|
||||||
// reinitialize remote cache
|
|
||||||
db.remote = { A: {}, PTR: {}, services: {} };
|
db.remote = { A: {}, PTR: {}, services: {} };
|
||||||
for (const { Type, Name, Data } of rows) {
|
|
||||||
const parsed = ['[','{'].includes(Data[0]) ? JSON.parse(Data) : Data;
|
rows.forEach(row => {
|
||||||
|
const { Type, Name, Data } = row;
|
||||||
|
|
||||||
if (Type === 'A') {
|
if (Type === 'A') {
|
||||||
db.remote.A[Name] = parsed;
|
db.remote.A[Name] = Data;
|
||||||
} else if (Type === 'PTR') {
|
}
|
||||||
db.remote.PTR[Name] = parsed;
|
else if (Type === 'PTR') {
|
||||||
} else { // SRV or TXT
|
db.remote.PTR[Name] = (["[","{"].includes(Data.substr(0,1))?JSON.parse(Data):Data);
|
||||||
db.remote.services[Name] ||= { TXT:null, SRV:null };
|
}
|
||||||
db.remote.services[Name][Type] = parsed;
|
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);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
console.log(
|
|
||||||
`Remote DB updated. A: ${Object.keys(db.remote.A).length}, PTR: ${Object.keys(db.remote.PTR).length}`
|
console.log(`Remote DB updated. A records: ${Object.keys(db.remote.A).length} PTR records: ${Object.keys(db.remote.PTR).length}`);
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching remote DB:', err);
|
console.error('Error fetching remote DB:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*********************************
|
/*********************************
|
||||||
*
|
*
|
||||||
* 100. MAIN / IMPERATIVE CODE
|
* 100. MAIN / IMPERATIVE CODE
|
||||||
*
|
*
|
||||||
*********************************/
|
*********************************/
|
||||||
|
|
||||||
// MariaDB Configuration
|
const fs = require("fs");
|
||||||
const DB_HOST = '10.10.8.1';
|
const db = {local:{},remote:{}};
|
||||||
const DB_NAME = 'NETWORK';
|
|
||||||
const DB_TABLE = 'mDNS';
|
|
||||||
const DB_USER = os.hostname();
|
|
||||||
|
|
||||||
// Initialize in-memory cache
|
|
||||||
const db = { remote: { A: {}, PTR: {}, services: {} } };
|
|
||||||
|
|
||||||
// Pick first non-internal v4 address
|
|
||||||
const LOCAL_IP_ADDR = getLocalNetworkAddressesIPs()[0];
|
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 mdnsServer = mdns({
|
|
||||||
interface: LOCAL_IP_ADDR,
|
|
||||||
reuseAddr: true,
|
|
||||||
loopback: false,
|
|
||||||
noInit: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Init MariaDB connection pool
|
// Initialize MariaDB connection pool
|
||||||
const dbPool = mysql.createPool({
|
const dbPool = mysql.createPool({
|
||||||
host: DB_HOST,
|
host: DB_HOST,
|
||||||
user: DB_USER,
|
user: DB_USER,
|
||||||
@ -177,23 +269,27 @@ const dbPool = mysql.createPool({
|
|||||||
database: DB_NAME,
|
database: DB_NAME,
|
||||||
connectionLimit: 5
|
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();
|
sniffmDNSLocalPackets();
|
||||||
|
|
||||||
mdnsServer.on('query', ({ questions }) => {
|
|
||||||
for (const { name, type } of questions) {
|
mdns.on('query', function (query) {
|
||||||
console.log(`Query for ${name} (${type})`);
|
// Loop through each question in the mDNS query
|
||||||
replyLocallyWithRemoteDevicesData(name, type);
|
query.questions.forEach(q => {
|
||||||
}
|
const { name, type } = q;
|
||||||
|
console.log(`Received mDNS query for ${name} (${type})`);
|
||||||
|
replyLocallyWithRemoteDevicesData(name,type,q);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// fetch remote every 15s
|
// every 15 seconds
|
||||||
setInterval(fetchRemoteDevicesData, 15_000);
|
setInterval(fetchRemoteDevicesData, 15000);
|
||||||
fetchRemoteDevicesData();
|
fetchRemoteDevicesData(); // initial immediate call
|
||||||
|
|
||||||
// now start the server
|
// initialize the server now that we are watching for events
|
||||||
mdnsServer.initServer();
|
mdns.initServer()
|
||||||
Loading…
x
Reference in New Issue
Block a user