hosts-etc
Version:
Manages your system host file in a more productive way!
273 lines (236 loc) • 8.66 kB
JavaScript
const fs = require('fs');
const HOSTS_FILE = process.platform === "win32" ? "C:/Windows/System32/drivers/etc/hosts" : "/etc/hosts";
let hostsCache = null;
let cache = true;
class Host {
constructor(host, address, opts) {
opts = opts || {};
this.host = host;
this.address = address;
this.region = opts.region || "";
this.comment = opts.comment || "";
}
makeHostLine() {
return this.address + "\t" + this.host + (this.comment !== "" ? "\t# " + this.comment : "");
}
makeRegionLine() {
if(this.region == "") return "";
return "# region " + this.region;
}
}
module.exports.useCache = function (c) {
if(typeof c === "undefined") c = true;
cache = c;
return module.exports;
};
module.exports.HOSTS = HOSTS_FILE;
module.exports.Host = Host;
module.exports.hostifyData = function (hosts) {
let out = "# Host file generated by TheBrenny/hosts-etc\n\n";
for(let r of Object.keys(hosts).sort()) {
if(hosts[r].length === 0) continue;
out += hosts[r][0].makeRegionLine() + "\n";
for(let h of hosts[r]) out += h.makeHostLine() + "\n";
out += (hosts[r][0].region != "" ? "# end region" : "") + "\n";
}
return out;
};
module.exports.get = function (query) {
if(typeof query === 'undefined') query = "";
query = (query || "").toString();
let lines = getFileContents().replace(/\r\n/g, "\n").split("\n");
let regions = getAllHosts(lines);
let out = {};
let rx = /((\d{1,3}|x)\.(\d{1,3}|x)\.(\d{1,3}|x)\.(\d{1,3}|x)|::1)/;
if(query == "") { // get all
out = regions;
} else if(query.startsWith("#")) { // get region only
let region = query.slice(1).trim();
out[region] = regions[region] || [];
} else if(query.startsWith("c#")) { // get comment equals
let comment = query.slice(2).trim();
for(let r in regions) {
// regions[r] = region object == array of hosts
for(let h of regions[r]) {
// h = host object
if(h.comment === comment) {
out[r] = out[r] || [];
out[r].push(h);
}
}
}
} else if(rx.test(query)) { // get IPs
let ips = query.match(rx)[0];
rx = new RegExp(ips.replace(/x/g, "\\d{1,3}"));
for(let r in regions) {
// r = region object == array of hosts
for(let h of regions[r]) {
// h = host object
if(rx.test(h.address)) {
out[r] = out[r] || [];
out[r].push(h);
}
}
}
} else { // Matching hostname
for(let r in regions) {
// regions[r] = region object == array of hosts
for(let h of regions[r]) {
// h = host object
if(h.host.includes(query)) {
out[r] = out[r] || [];
out[r].push(h);
}
}
}
}
// {"region": [Host, ...]}
return out;
// Gets all hosts into a JSON *object*!
function getAllHosts(lines) {
if(hostsCache && cache) return hostsCache;
let regionStart = /^[ \t]*#[ \t]*region +(.+?)[ \t]*$/gm;
let regionEnd = /^[ \t]*#[ \t]*end region[ \t]*$/gm;
let hostsRx = /^[ \t]*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|::1)[ \t]+?(\S+?)[ \t]*(#.*)?$/gm;
let region = [];
let out = {};
for(let line of lines) {
if(line === "") continue;
// Region End
if(regionEnd.test(line)) {
region.pop();
continue;
}
// Region Start
let r = [...line.matchAll(regionStart)];
if(r.length > 0) {
region.push(r[0][1]);
continue;
}
// Host
r = [...line.matchAll(hostsRx)];
if(r.length > 0) {
r = r[0];
let h = new Host(r[2], r[1], {
region: region.join("."),
comment: (r[3] && r[3].substr(1).trim()) || ""
});
out[region] = out[region] || [];
out[region].push(h);
continue;
}
}
hostsCache = out;
return out;
}
};
module.exports.set = function (host) {
if(host.constructor !== "Host") host = new Host(host.host, host.address, host);
let updated = 0;
let hosts = this.get();
// find if host exists, if yes delete it
let hostsAsArray = [];
for(let r in hosts) hostsAsArray = hostsAsArray.concat(hosts[r]);
let hostIndex = hostsAsArray.findIndex((el) => hostsMatch(el, host));
if(hostIndex !== -1) {
let r = hostsAsArray[hostIndex].region;
hostIndex = hosts[r].findIndex((el) => hostsMatch(el, host)); // reuse to save space
hosts[r].splice(hostIndex, 1);
}
// push new host
hosts[host.region] = hosts[host.region] || [];
hosts[host.region].push(host);
updated++;
// overwrite system hosts file
setFileContents(null, this.hostifyData(hosts));
// return updated
return updated;
function hostsMatch(a, b) {
return a.host === b.host && a.address === b.address;
}
};
const filters = {
comment: (comment, host) => host.comment !== comment,
ip: (rx, host) => !rx.test(host.address),
host: (query, host) => host.host !== query
};
module.exports.remove = function (query) {
let updated = 0;
let hosts = this.get();
let out = Object.assign({}, hosts);
let rx = /((\d{1,3}|x)\.(\d{1,3}|x)\.(\d{1,3}|x)\.(\d{1,3}|x)|::1)/;
if(query.startsWith("#")) { // remove region
let region = query.slice(1).trim();
updated += out[region].length;
delete out[region];
} else if(query.startsWith("c#")) { // remove comment equals
const comment = query.slice(2).trim();
const filter = filters.comment.bind(null, comment);
for(let r in hosts) {
out[r] = out[r].filter(filter);
updated += (hosts[r].length - out[r].length);
if(out[r].length === 0) delete out[r];
}
} else if(rx.test(query)) { // remove IPs
let ips = query.match(rx)[0];
rx = new RegExp(ips.replace(/x/g, "\\d{1,3}"));
const filter = filters.ip.bind(null, rx);
for(let r in hosts) {
out[r] = out[r].filter(filter);
updated += (hosts[r].length - out[r].length);
if(out[r].length === 0) delete out[r];
}
} else { // Matching hostname
const filter = filters.host.bind(null, query);
for(let r in hosts) {
out[r] = out[r].filter(filter);
updated += (hosts[r].length - out[r].length);
if(out[r].length === 0) delete out[r];
}
}
setFileContents(null, this.hostifyData(out));
return updated;
};
function getFileContents(file) {
return fs.readFileSync(file || HOSTS_FILE).toString();
}
function setFileContents(file, data) {
file = file || HOSTS_FILE;
if(!fs.existsSync(file + ".hosts-etc.bkp")) fs.copyFileSync(file, file + ".hosts-etc.bkp");
return fs.writeFileSync(file, data);
}
module.exports.promise = {};
module.exports.promise.HOSTS = HOSTS_FILE; // Deprecated! Use HOSTS_FILE
module.exports.promise.HOSTS_FILE = HOSTS_FILE;
module.exports.promise.Host = Host;
module.exports.promise.hostifyData = async function (hosts) {
return Promise.resolve(module.exports.hostifyData(hosts));
};
// TODO: Actually convert these to use promises, not just a promise wrapper around sync methods!
module.exports.promise.get = async function (query) {
return new Promise((res, rej) => {
try {
res(module.exports.get(query));
} catch(e) {
rej(e);
}
});
};
module.exports.promise.set = async function (hostData) {
return new Promise((res, rej) => {
try {
res(module.exports.set(hostData));
} catch(e) {
rej(e);
}
});
};
module.exports.promise.remove = async function (query) {
return new Promise((res, rej) => {
try {
res(module.exports.remove(query));
} catch(e) {
rej(e);
}
});
};