UNPKG

hostparty

Version:

Programmatic and CLI editing for OS hosts file

543 lines (411 loc) 17.7 kB
var api = ((extend)=>{ "use strict"; const protectedEntries = { ips: ['::1', 'fe80::1%lo0'], hosts: ['localhost'] }; var _ = require('lodash'), os = require('os'), util = require('util'), path = require('path'), promise = require('bluebird'), utils = require('./utils'), pkg = require('../package.json'), fs = promise.promisifyAll(require('fs')), options = { // group hosts by ip group: true, // override validation force: false }, /** * setup * * default options */ setup = (setup)=>{ options = _.extend(options, setup || {}); // return api; }, /** * [add description] * * @method add * * @param {[type]} ip [description] * @param {[type]} hostNames [description] */ add = (ip, hostNames)=>{ ip = (ip || '').trim(); hostNames = _.compact(_.isArray(hostNames) ? hostNames : [hostNames]); return new promise((resolve, reject)=>{ if (!ip.length && !hostNames.length) { return reject('Neither a hostname or IP was supplied.'); } if (!utils.validateIP(ip)) { return reject(util.format('Invalid IP address [%s] was supplied.', ip)); } hostNames.forEach((host)=>{ if (!utils.validateHost(host)) { return reject(util.format('Invalid hostname [%s] was supplied.', host)); } }); loadHosts() .then((hosts)=>{ let row = hosts[ip]; if (!row) { row = hostNames; } else { row = row.concat(hostNames); } hosts[ip] = _.uniq(row); return hosts; }) .then((hosts)=>{ return saveToFile(hosts).then(resolve); }) .catch(reject); }); }, /** * remove * * removes a line by ip address * * @return {[type]} [description] */ remove = (ips)=>{ ips = _.isArray(ips) ? ips : [ips]; return new promise((resolve, reject)=>{ if (!ips.length) { return reject('No IP addresses provided'); } ips.forEach((ip)=>{ if (!utils.validateIP(ip)) { return reject(util.format('Invalid IP address [%s] was supplied.', ip)); } }); ips.forEach((ip)=>{ if (protectedEntries.ips.indexOf(ip) > -1) { if (!options.force) { return reject(util.format('%s is protected by the OS and cannot be removed. Use --force to override this.', ip)); } } }); loadHosts() .then((hosts)=>{ ips.forEach((ip)=>{ if (hosts[ip]) { delete hosts[ip]; } else { return reject(util.format('IP %s not found in hosts file.', ip)); } }); return hosts; }) .then((hosts)=>{ return saveToFile(hosts).then(resolve); }) .catch(reject); }); }, /** * disable * * disables a line by ip address * * @return {[type]} [description] */ disable = (ips)=>{ ips = _.isArray(ips) ? ips : [ips]; return new promise((resolve, reject)=>{ if (!ips.length) { return reject('No IP address provided'); } ips.forEach((ip)=>{ if (!utils.validateIP(ip)) { return reject(util.format('Invalid IP address [%s] was supplied.', ip)); } }); ips.forEach((ip)=>{ if (protectedEntries.ips.indexOf(ip) > -1) { if (!options.force) { return reject(util.format('%s is protected by the OS and cannot be removed. Use --force to override this.', ip)); } } }); loadHosts() .then((hosts)=>{ // console.log(hosts); ips.forEach((ip)=>{ // console.log(hosts[ip], ip); var commentBlock = util.format('# %s', ip); if (hosts[ip]) { hosts[commentBlock] = hosts[ip]; delete hosts[ip]; } else { return reject(util.format('IP %s not in hosts file.', ip)); } }); return hosts; }) .then((hosts)=>{ return saveToFile(hosts).then(resolve); }) .catch(reject); }); }, /** * enable * * renables an previously muted entry * * @return {[type]} [description] */ enable = (ips)=>{ ips = _.isArray(ips) ? ips : [ips]; return new promise((resolve, reject)=>{ if (!ips.length) { return reject('No IP addresses provided'); } ips.forEach((ip)=>{ if (!utils.validateIP(ip)) { return reject(util.format('Invalid IP address [%s] was supplied.', ip)); } }); loadHosts() .then((hosts)=>{ ips.forEach((ip)=>{ var commentBlock = util.format('# %s', ip); if (hosts[commentBlock]) { hosts[ip] = _.clone(hosts[commentBlock]); // delete hosts[commentBlock]; } else { return reject(util.format('IP %s not in file.', ip)); } // console.log(commentBlock, hosts[commentBlock]); }); return hosts; }) .then((hosts)=>{ return saveToFile(hosts).then(resolve); }) .catch(reject); }); }, /** * purge * * removes hosts from an ip mapping * * @return {[type]} [description] */ purge = (hostNames)=>{ hostNames = _.isArray(hostNames) ? hostNames : [hostNames]; return new promise((resolve, reject)=>{ hostNames.forEach((host)=>{ if (!utils.validateHost(host)) { return reject(util.format("Invalid host [%s] supplied", host)); } }); loadHosts() .then((hosts)=>{ // loop supplied hostnames _.each(hostNames, (hostName)=>{ // loop the hosts against the ip _.each(hosts, (hostList, ip)=>{ // purge the matching _.remove(hostList, (host)=>{ return host.toLowerCase().trim() === hostName.toLowerCase().trim(); }); // if the purge leaves the ip hostnbame bindings empty, bin it off if (!hostList.length) { try { delete hosts[ip]; } catch (e) {} } }); }); return hosts; }) .then((hosts)=>{ return saveToFile(hosts).then(resolve); }) .catch(reject); }); }, /** * [list description] * * @method list * * @return {[type]} [description] */ list = (filter)=>{ return loadHosts(filter); }, /** * [getHostFilePath description] * * Operating System Version(s) Location * Unix, Unix-like, POSIX /etc/hosts * Microsoft Windows 3.1 %WinDir%\HOSTS * 95, 98, ME %WinDir%\hosts * NT, 2000, XP,[5] 2003, Vista, * 2008, 7, 2012, 8, 10 %SystemRoot%\System32\drivers\etc\hosts * Mac OS X 10.0–10.1.5 (Added through NetInfo or niload) * Mac OS X 10.2 and newer /etc/hosts (a symbolic link to /private/etc/hosts) * * 'arm', 'arm64', 'ia32', 'mips', 'mipsel', 'ppc', 'ppc64', 's390', 's390x', 'x32', 'x64', and 'x86' * * @method getHostFilePath * * @return {[type]} [description] */ getHostFilePath = ()=>{ var filePath; // path was set by user via options if (options.path) { filePath = options.path; } else { // calculate the path switch (os.platform()) { case 'linux': case 'darwin': case 'freebsd': case 'openbsd': // filePath = '/etc/hosts'; break; case 'win32': // filePath = process.env.windir || process.env.WINDIR; if (filePath) { filePath = path.normalize(util.format('%s\\%s', filePath, '\\System32\\drivers\\etc\\hosts')); } break; default: } } // console.log('Resolved OS "%s" target path to %s', os.platform(), path); // now resolve it return fs.realpathAsync(filePath); }, /** * loadHosts * * @method loadHosts * * @return object of entries in the host file */ loadHosts = (filter)=>{ return getHostFilePath() .then((path)=>{ return fs .readFileAsync(path) .then((file)=>{ let hosts = { // comments: [] }, expression = /((^\#\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/; file.toString().split("\n").map((line)=>{ // comments are ignored // this needs to be better if (line.indexOf('#') === 0) { // console.log("got comment", line.replace(/\#\ /ig, '')); // if (utils.validateHost(line.replace(/\#\ /ig, ''))) { // // hosts.comments.push(line); // console.log('got disabled', line); // } return; } // split the string to get the ip and hosts line = line .replace(/(\s)+/g, ' ') .split(' '); if (!line.length || line.length < 2) { } else { // duplicate ip, so merge them if (line[0].length) { let ip = hosts[line[0]]; if (ip) { ip = ip.concat(_.compact(line.slice(1))); } else { ip = _.compact(line.slice(1)); } // only return matching hosts if (filter) { var matching = _.filter(ip, (ip)=>{ return ip.indexOf(filter) > -1; }); if (matching.length) { ip = matching; } else { return; } } hosts[line[0]] = _.uniq(ip); } } }); return hosts; }) // format the ips .then((hosts)=>{ return utils.sortEntries(hosts); }); }) .catch((e)=>{ console.error(e); }); }, /** * saveToFile * * */ saveToFile = (hosts)=>{ // sort the dictionary of entries hosts = utils.sortEntries(hosts); return new promise((resolve, reject)=>{ getHostFilePath() .then((filePath)=>{ let contents = []; _.each(hosts, (hosts, ip)=>{ // group the hosts by IP if (options.group) { contents.push(util.format('%s %s', ip, hosts.join(' '))); } else { // don't group - 1 line per IP hosts.forEach((host)=>{ contents.push(util.format('%s %s', ip, host)); }); } }); // return fs .writeFileAsync(filePath, util.format("%s%s", contents.join("\n"), "\n")) .then(resolve) .catch((e)=>{ var message = 'Error writing file', elevated = (os.platform().match(/^win/i) ? 'Administrator' : 'root'); // access denied? switch (e.code) { case 'EACCES': message = util.format('Write permission denied on %s. Try running as %s.', filePath, elevated); break; } reject(message); }); }).catch(reject); }); }; return { setup: setup, add: add, remove: remove, list: list, purge: purge }; })(); module.exports = api;