dns-setting
Version:
dns management: query, modify, shield corresponding domain name resolution
525 lines (515 loc) • 13.6 kB
JavaScript
;
var dns = require('native-dns');
var node_net = require('node:net');
var s3 = require('sqlite3');
var fs = require('fs');
require('assert');
require('util');
var express = require('express');
const evmt$1 = process.env;
const port = evmt$1.port || 53;
const address = evmt$1.address || "127.0.0.1";
const webPage = evmt$1.webPage === "false" ? false : true;
const httpPort = evmt$1.httpPort || 2048;
const apiUpdate = evmt$1.apiUpdate === "true" ? true : false;
console.log(
"port:",
port,
"address:",
address,
"webPage:",
webPage,
"httpPort:",
httpPort,
"apiUpdate:",
apiUpdate,
"\n"
);
let __dnsList = [];
function dnsList(newValue) {
if (Array.isArray(newValue)) __dnsList = newValue;
return __dnsList;
}
const dbp = "./db";
const sqlite3 = s3.verbose();
const dbName = `${dbp}/dns.db`;
function read$1(src) {
try {
return fs.readFileSync(src).toString();
} catch (error) {
console.error("read file error", src);
return null;
}
}
function hook(fun) {
const db = new sqlite3.Database(dbName);
let r = new Promise((res, rej) => fun(db, res, rej));
db.close();
return r;
}
function serialize() {
try {
fs.rmSync(dbName);
} catch (e) {}
hook((db) => {
db.serialize(() => {
db.exec(read$1(`${dbp}/serialize.sql`));
});
});
}
const evmt = process.env;
if (evmt.serialize) {
serialize();
}
function getWhere(data, not = []) {
let keyList = Object.keys(data);
let result = {};
let where = keyList
.flatMap((k) => {
if (not.includes(k) || (data[k] ?? null) === null) {
return [];
}
if (/Like$/.test(k)) {
result[`\$${k}`] = "%" + data[k] + "%";
return `${k.replace(/Like$/, "")} like \$${k}`;
}
result[`\$${k}`] = data[k];
return `${k} = \$${k}`;
})
.join(" and ");
if (where) {
where = `where ${where}`;
}
return { where, data: result };
}
function getSet(data, not = []) {
let keyList = Object.keys(data);
let result = {};
let str = keyList
.flatMap((k) => {
if (not.includes(k) || (data[k] ?? null) === null) {
return [];
}
result[`\$${k}`] = data[k];
return `${k} = \$${k}`;
})
.join(" , ");
if (str) {
str = `SET ${str}`;
}
return { set: str, data: result };
}
function all(query, data) {
return hook((db, res, rej) => {
db.all(query.trim(), data, function (error, rows) {
if (error) rej(error);
else res(rows);
});
});
}
function tableList(name, match) {
if ((match.isUsing ?? null) !== null) match.isUsing = match.isUsing ? 1 : 0;
let { where, data } = getWhere(match);
let query = `SELECT * FROM ${name} ${where}`;
return all(query, data);
}
function updateItem(name, id, info) {
if (info.isUsing ?? null === null) info.isUsing = 1;
info.isUsing = info.isUsing ? 1 : 0;
let { set, data } = getSet({ ...info });
let query = `UPDATE ${name} ${set} where id = $id`;
return hook((db, res, rej) => {
db.run(query, { ...data, $id: id }, function (error) {
if (error) rej(error);
res();
});
});
}
function getInsert(info) {
if (info.isUsing ?? null === null) info.isUsing = 1;
info.isUsing = info.isUsing ? 1 : 0;
let key = Object.keys(info);
let key2 = key.map((k) => `\$${k}`);
let data = Object.fromEntries(key2.map((k, i) => [k, info[key[i]]]));
return { key, key1: key, key2, data };
}
function insertItem(name, info) {
let { key1, key2, data } = getInsert({ ...info });
let query = `INSERT INTO ${name} (${key1}) VALUES (${key2})`;
return hook((db, res, rej) => {
db.run(query, { ...data }, function (error, data) {
if (error) rej(error);
db.get(`select last_insert_rowid() as id from ${name} LIMIT 1;`, function (error, data) {
if (error) rej(error);
res(data.id);
});
});
});
}
function deleteItem(name, id) {
let query = `DELETE FROM ${name} WHERE id = $id`;
return hook((db, res, rej) => {
db.run(query, { $id: id }, function (error, data) {
if (error) rej(error);
res(data);
});
});
}
var db = /*#__PURE__*/Object.freeze({
__proto__: null,
deleteItem: deleteItem,
insertItem: insertItem,
tableList: tableList,
updateItem: updateItem
});
function isIP(str) {
return node_net.isIP(str) > 0;
}
const MATCH = { END: "#", IP: ".", BAN: "!", REG: "/", FUZZY: "*", FUN: "$" };
let domainTree = {};
let domainAction = {
ban: () => MATCH.BAN,
i192(domain, ban, msg) {
let n = Number(domain[0]) || 0;
return `192.168.12.${n}`;
},
};
function never(msg) {
throw new Error(msg);
}
function toA(matchData, site, t1) {
matchData = Array.isArray(matchData) ? matchData : [matchData];
matchData = matchData.flatMap((ip) => {
if (!ip) return [];
try {
return dns.A({
name: site,
address: ip || never("[ip] cannot be empty, [dns.A] must have an [address]"),
ttl: Date.now() - t1 + 10,
});
} catch (e) {
console.error(e);
return [];
}
});
return matchData;
}
function next(target, arr, index) {
if (!target) return null;
return nodeMatch(target, arr, index + 1);
}
function nodeMatch(target, arr, index = 0) {
if (!target) return null;
if (index == arr.length) {
let end = target[MATCH.END];
if (end) {
if (end[MATCH.BAN]) return MATCH.BAN;
if (end[MATCH.IP]?.length > 0) return end[MATCH.IP];
if (end[MATCH.FUN]?.length > 0) return end[MATCH.FUN];
}
return null;
}
let key = arr[index];
let regMatch = (target, arr, index) => {
if (!target) return null;
let key = target[MATCH.REG] || [];
key = key.filter((str) => {
str = str.replace(/^\//, "").replace(/\/$/, "");
return new RegExp(str).test(arr[index]);
});
return key.reduce((a, b) => a || (b && next(target[b], arr, index)), null);
};
return next(target[key], arr, index) || regMatch(target, arr, index) || next(target[MATCH.FUZZY], arr, index);
}
function localMatch(site) {
let t1 = Date.now();
let target = domainTree;
let arr = site.split(".");
let result = nodeMatch(target, [...arr].reverse(), 0);
if (!Array.isArray(result)) {
result = [result];
}
let shield = false;
result = result.flatMap((v) => {
if (isIP(v)) return v;
if (v === MATCH.BAN) {
shield = true;
}
let fun = domainAction[v];
if (fun) {
let r = fun(arr, MATCH.BAN, "// todo path");
if (r === MATCH.BAN) {
shield = true;
}
return r;
}
return [];
});
if (shield) result = [];
result = toA(result, site, t1);
return { data: result ?? [], shield: shield };
}
function creatMap(data, clear = true) {
let sm = clear ? {} : domainTree;
data.forEach((item) => {
let nameList = nameSplit(item.domain);
let node = sm;
[...nameList.reverse(), MATCH.END].forEach((nameStr) => {
if (nameStr.startsWith("/")) {
node[MATCH.REG] = [...(node[MATCH.REG] || []), nameStr];
}
if (!node[nameStr]) {
node[nameStr] = {};
}
node = node[nameStr];
});
if (isIP(item.action)) {
let v = node[MATCH.IP] || [];
node[MATCH.IP] = [...v, item.action];
} else if (item.action == "ban") {
node[MATCH.BAN] = true;
} else {
let v = node[MATCH.FUN] || [];
node[MATCH.FUN] = [...v, item.action];
}
});
domainTree = sm;
}
function nameSplit(name) {
let arr = name.split(".");
let subIndex = arr.map((item, i) => {
let s = false,
e = false;
if (item.startsWith("/")) {
s = true;
}
if (item.endsWith("/")) {
e = true;
}
if (s || e) return { i: i, s, e };
return null;
});
let result = [];
let startArr = subIndex.filter((item) => !!item && item.s);
if (startArr.length == 0) {
return arr;
}
result = arr.slice(0, startArr[0]?.i || 0);
let end = null;
startArr.forEach((start) => {
end = subIndex.find((v, i) => v && v.e == true && i >= start.i);
let r = arr.slice(start.i, end.i + 1);
if (r.length > 0) {
r = r.join(".");
result.push(r);
}
});
result = [...result, ...arr.slice(end?.i + 1)];
return result;
}
function ask(siteName, address = "8.8.8.8", port = 53, timeout = 1000) {
let server = { address: address || "8.8.8.8", port: port || 53, type: "udp" };
return new Promise((resolve, reject) => {
var question = dns.Question({
name: siteName,
type: "A",
});
try {
var req = dns.Request({
question: question,
server: server,
timeout: timeout || 1000,
});
req.on("timeout", function () {
reject({
type: "timeout",
siteName: siteName,
msg: "Timeout in making request",
server: server,
});
});
req.on("message", function (err, answer) {
resolve(answer.answer);
});
req.on("end", function () {
resolve(null);
});
req.send();
} catch (e) {
reject({
type: "run error",
siteName: siteName,
server: server,
error: e,
});
}
});
}
async function askList(siteName, log = null) {
let list = dnsList();
let data = null;
let address = null;
for (let i = 0; i < list.length; i++) {
address = list[i];
data = await ask(siteName, address).catch((e) => {
if (!log) console.error(e);
if (log) log(e);
return null;
});
if (data && Array.isArray(data) && data.length > 0) break;
}
return { data, dns: address };
}
let flag = false;
function resultLog(title, siteName, result, log = null) {
let data = (result || [])
.map((item) => {
item = JSON.parse(JSON.stringify(item));
if (item.class != null) {
item.class = [null, "IN"][item.class] || item.class;
item.type =
{
1: "A",
2: "NS",
5: "CNAME",
6: "SOA",
12: "PTR",
15: "MX",
16: "TXT",
28: "AAAA",
33: "SRV",
35: "NAPTR",
52: "TLSA",
}[item.type] || item.type;
}
return JSON.stringify(item);
})
.join("\n ");
let msg = [
[title, "=>", siteName, "", "(count:" + (result?.length || 0) + ")"],
[" ", data],
["\n"],
];
msg.forEach((line) => {
if (log) {
log.push(line);
}
console.log(...line);
});
}
function domainLookup(request, response, log = null) {
let siteName = request.question[0].name;
let replyAnswer = (answer) => {
answer = answer || [];
answer.forEach((item) => {
response.answer.push({ ...item });
});
response.send();
};
let result = localMatch(siteName);
let isEmpty = (result.data?.length || 0) == 0;
let shield = result.shield;
let needLocal = !isEmpty || shield;
let needRemote = isEmpty && !shield;
if (needLocal) {
resultLog("DNS [local]", siteName, result.data, log);
replyAnswer(result.data);
}
if (needRemote) {
askList(siteName, (e) => {
console.log(e);
resultLog("DNS [" + e.server.address + "]", siteName, [e], log);
})
.then(({ data, dns }) => {
resultLog("DNS [" + dns + "]", siteName, data, log);
replyAnswer(data);
})
.catch((e) => {
console.error(e);
console.log("error", e);
resultLog("DNS Error [" + e.name + "] " + e.message, siteName, log);
response.send();
});
}
}
function healthCheck(server) {
flag = true;
const address = server.address();
console.log(`dns server listening ${address.address}:${address.port}`);
console.log("成功", `dig home.abc.loc @127.0.0.1 -p ${address.port}`, "\n\n");
}
function errorLog(err, buff, req, res) {
console.log(err.stack);
}
function dnsServer(address, port) {
flag = false;
var server = dns.createServer();
server.once("listening", () => healthCheck(server));
server.on("request", domainLookup);
server.on("error", errorLog);
server.serve(port, address);
setTimeout(() => {
if (!flag) {
console.log(
"DNS启动失败",
"\n\t",
`1.请检查[${address}:${port}]端口是否被占用;`,
"\n\t",
"2.端口小于1024时,需要管理员或root权限;"
);
}
}, 50);
return server;
}
function read(src) {
try {
return fs.readFileSync(src).toString();
} catch (error) {
console.error("read file error", src);
return null;
}
}
const httpServe = express();
httpServe.use(express.static("html"));
httpServe.use(express.json());
httpServe.use(express.urlencoded({ extended: true }));
loadApi();
httpServe.get("**", (req, res, next) => {
next();
res.send("--404--");
});
function loadApi(src = "./serve/api") {
fs.readdirSync(`${src}`)
.filter((v) => {
return /\.js$/.test(v);
})
.forEach((file) => {
let code = read(`./${src}/${file}`);
let fun = new Function("arg", `${code}; init(arg);`);
fun({ httpServe, db, domainLookup });
console.log(" load", file);
});
}
function loadDnsData() {
tableList("domain", { isUsing: true }).then((list) => {
return creatMap(list);
});
tableList("dns", { isUsing: true }).then((list) => {
dnsList(list.map((v) => v.ip));
});
}
function main() {
loadDnsData();
setTimeout(() => {
dnsServer(address, port);
}, 100);
setTimeout(() => {
if (webPage) {
httpServe.listen(httpPort, () => {
console.log("DNS Web Management");
console.log(`http://${address}:${httpPort}/`);
});
}
}, 800);
}
main();