├── .npmignore ├── img.png ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── LICENSE ├── package.json ├── test └── basic.js ├── bin └── cmd.js ├── README.md └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | img.png 2 | test/ 3 | 4 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/hostile/HEAD/img.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: daily 7 | labels: 8 | - dependency 9 | versioning-strategy: increase-if-necessary 10 | - package-ecosystem: github-actions 11 | directory: / 12 | schedule: 13 | interval: daily 14 | labels: 15 | - dependency 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 'on': 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node ${{ matrix.node }} / ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: 13 | - ubuntu-latest 14 | node: 15 | - '14' 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: ${{ matrix.node }} 21 | - run: npm install 22 | - run: npm run build --if-present 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Feross Aboukhadijeh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hostile", 3 | "description": "Simple /etc/hosts manipulation", 4 | "version": "1.4.0", 5 | "author": { 6 | "name": "Feross Aboukhadijeh", 7 | "email": "feross@feross.org", 8 | "url": "https://feross.org" 9 | }, 10 | "bin": "bin/cmd.js", 11 | "bugs": { 12 | "url": "https://github.com/feross/hostile/issues" 13 | }, 14 | "dependencies": { 15 | "@ljharb/through": "^2.3.11", 16 | "chalk": "^4.1.2", 17 | "minimist": "^1.2.8", 18 | "once": "^1.4.0", 19 | "split": "^1.0.1" 20 | }, 21 | "devDependencies": { 22 | "standard": "*", 23 | "tape": "^5.7.2" 24 | }, 25 | "homepage": "https://github.com/feross/hostile", 26 | "keywords": [ 27 | "/etc/hosts", 28 | "change hosts", 29 | "etc hosts", 30 | "hostname", 31 | "hosts file" 32 | ], 33 | "license": "MIT", 34 | "main": "index.js", 35 | "repository": "git://github.com/feross/hostile", 36 | "scripts": { 37 | "test": "standard && tape test/*.js" 38 | }, 39 | "funding": [ 40 | { 41 | "type": "github", 42 | "url": "https://github.com/sponsors/feross" 43 | }, 44 | { 45 | "type": "patreon", 46 | "url": "https://www.patreon.com/feross" 47 | }, 48 | { 49 | "type": "consulting", 50 | "url": "https://feross.org/support" 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var hostile = require('../') 3 | var os = require('os') 4 | var path = require('path') 5 | var test = require('tape') 6 | 7 | var TMP = os.tmpdir() 8 | var TEST_HOSTS = path.join(TMP, 'hostile-hosts') 9 | 10 | test('setup', function (t) { 11 | // copy hosts file to /tmp 12 | fs.createReadStream(hostile.HOSTS) 13 | .pipe(fs.createWriteStream(TEST_HOSTS)) 14 | .on('close', function () { 15 | // monkey patch the `fs` module 16 | var _createReadStream = fs.createReadStream 17 | fs.createReadStream = function (filename) { 18 | var args = Array.prototype.slice.call(arguments, 0) 19 | if (filename === hostile.HOSTS) { 20 | args[0] = TEST_HOSTS 21 | } 22 | return _createReadStream.apply(fs, args) 23 | } 24 | var _createWriteStream = fs.createWriteStream 25 | fs.createWriteStream = function (filename) { 26 | var args = Array.prototype.slice.call(arguments, 0) 27 | if (filename === hostile.HOSTS) { 28 | args[0] = TEST_HOSTS 29 | } 30 | return _createWriteStream.apply(fs, args) 31 | } 32 | 33 | t.pass('setup complete') 34 | t.end() 35 | }) 36 | .on('error', function (err) { 37 | t.fail(err.message) 38 | }) 39 | }) 40 | 41 | test('set', function (t) { 42 | t.plan(3) 43 | hostile.set('127.0.0.1', 'peercdn.com', function (err) { 44 | t.error(err) 45 | hostile.get(false, function (err, lines) { 46 | t.error(err) 47 | lines.forEach(function (line) { 48 | if (line[0] === '127.0.0.1' && line[1] === 'peercdn.com') { 49 | t.pass('set worked') 50 | } 51 | }) 52 | }) 53 | }) 54 | }) 55 | 56 | test('set ipv6', function (t) { 57 | t.plan(4) 58 | hostile.set('::1', 'peercdn.com', function (err) { 59 | t.error(err) 60 | hostile.get(false, function (err, lines) { 61 | t.error(err) 62 | var exists = lines.some(function (line) { 63 | return line[0] === '::1' && line[1] === 'peercdn.com' 64 | }) 65 | t.ok(exists, 'ipv6 line was added') 66 | exists = lines.some(function (line) { 67 | return line[0] === '127.0.0.1' && line[1] === 'peercdn.com' 68 | }) 69 | t.ok(exists, 'ipv4 line still exists & was not replaced') 70 | }) 71 | }) 72 | }) 73 | 74 | test('remove ipv4', function (t) { 75 | t.plan(2) 76 | hostile.remove('127.0.0.1', 'peercdn.com', function (err) { 77 | t.error(err) 78 | hostile.get(false, function (err, lines) { 79 | t.error(err) 80 | lines.forEach(function (line) { 81 | if (line[0] === '127.0.0.1' && line[1] === 'peercdn.com') { 82 | t.fail('remove failed') 83 | } 84 | }) 85 | }) 86 | }) 87 | }) 88 | 89 | test('remove ipv6', function (t) { 90 | t.plan(2) 91 | hostile.remove('::1', 'peercdn.com', function (err) { 92 | t.error(err) 93 | hostile.get(false, function (err, lines) { 94 | t.error(err) 95 | lines.forEach(function (line) { 96 | if (line[0] === '::1' && line[1] === 'peercdn.com') { 97 | t.fail('remove failed') 98 | } 99 | }) 100 | }) 101 | }) 102 | }) 103 | 104 | test('set and get space-separated domains', function (t) { 105 | t.plan(3) 106 | hostile.set('127.0.0.5', 'www.peercdn.com m.peercdn.com', function (err) { 107 | t.error(err) 108 | hostile.get(false, function (err, lines) { 109 | t.error(err) 110 | var exists = lines.some(function (line) { 111 | return line[0] === '127.0.0.5' && line[1] === 'www.peercdn.com m.peercdn.com' 112 | }) 113 | t.ok(exists, 'host line exists') 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var chalk = require('chalk') 4 | var hostile = require('../') 5 | var minimist = require('minimist') 6 | var net = require('net') 7 | 8 | var argv = minimist(process.argv.slice(2)) 9 | 10 | var command = argv._[0] 11 | 12 | if (command === 'list' || command === 'ls') list() 13 | if (command === 'set') set(argv._[1], argv._[2]) 14 | if (command === 'remove') remove(argv._[1]) 15 | if (command === 'load') load(argv._[1]) 16 | if (command === 'unload') unload(argv._[1]) 17 | if (!command) help() 18 | 19 | /** 20 | * Print help message 21 | */ 22 | function help () { 23 | console.log(function () { /* 24 | Usage: hostile [command] 25 | 26 | Commands: 27 | 28 | list List all current domain records in hosts file 29 | set [ip] [host] Set a domain in the hosts file 30 | remove [domain] Remove a domain from the hosts file 31 | load [file] Load a set of host entries from a file 32 | unload [file] Remove a set of host entries from a file 33 | 34 | */ }.toString().split(/\n/).slice(1, -1).join('\n')) 35 | } 36 | 37 | /** 38 | * Display all current ip records 39 | */ 40 | function list () { 41 | var lines 42 | try { 43 | lines = hostile.get(false) 44 | } catch (err) { 45 | return error(err) 46 | } 47 | lines.forEach(function (item) { 48 | if (item.length > 1) { 49 | console.log(item[0], chalk.green(item[1])) 50 | } else { 51 | console.log(item) 52 | } 53 | }) 54 | } 55 | 56 | /** 57 | * Set a new host 58 | * @param {string} ip 59 | * @param {string} host 60 | */ 61 | function set (ip, host) { 62 | if (!ip || !host) { 63 | return error('Invalid syntax: hostile set ') 64 | } 65 | 66 | if (ip === 'local' || ip === 'localhost') { 67 | ip = '127.0.0.1' 68 | } else if (!net.isIP(ip)) { 69 | return error('Invalid IP address') 70 | } 71 | 72 | try { 73 | hostile.set(ip, host) 74 | } catch (err) { 75 | return error('Error: ' + err.message + '. Are you running as root?') 76 | } 77 | console.log(chalk.green('Added ' + host)) 78 | } 79 | 80 | /** 81 | * Remove a host 82 | * @param {string} host 83 | */ 84 | function remove (host) { 85 | var lines 86 | try { 87 | lines = hostile.get(false) 88 | } catch (err) { 89 | return error(err) 90 | } 91 | lines.forEach(function (item) { 92 | if (item[1] === host) { 93 | try { 94 | hostile.remove(item[0], host) 95 | } catch (err) { 96 | return error('Error: ' + err.message + '. Are you running as root?') 97 | } 98 | console.log(chalk.green('Removed ' + host)) 99 | } 100 | }) 101 | } 102 | 103 | /** 104 | * Load hosts given a file 105 | * @param {string} filePath 106 | */ 107 | function load (filePath) { 108 | var lines = parseFile(filePath) 109 | 110 | lines.forEach(function (item) { 111 | set(item[0], item[1]) 112 | }) 113 | console.log(chalk.green('\nAdded %d hosts!'), lines.length) 114 | } 115 | 116 | /** 117 | * Remove hosts given a file 118 | * @param {string} filePath 119 | */ 120 | function unload (filePath) { 121 | var lines = parseFile(filePath) 122 | 123 | lines.forEach(function (item) { 124 | remove(item[1]) 125 | }) 126 | console.log(chalk.green('Removed %d hosts!'), lines.length) 127 | } 128 | 129 | /** 130 | * Get all the lines of the file as array of arrays [[IP, host]] 131 | * @param {string} filePath 132 | */ 133 | function parseFile (filePath) { 134 | var lines 135 | try { 136 | lines = hostile.getFile(filePath, false) 137 | } catch (err) { 138 | return error(err) 139 | } 140 | return lines 141 | } 142 | 143 | /** 144 | * Print an error and exit the program 145 | * @param {string} message 146 | */ 147 | function error (err) { 148 | console.error(chalk.red(err.message || err)) 149 | process.exit(-1) 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hostile [![ci][ci-image]][ci-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [ci-image]: https://img.shields.io/github/workflow/status/feross/hostile/ci/master 4 | [ci-url]: https://github.com/feross/hostile/actions 5 | [npm-image]: https://img.shields.io/npm/v/hostile.svg 6 | [npm-url]: https://npmjs.org/package/hostile 7 | [downloads-image]: https://img.shields.io/npm/dm/hostile.svg 8 | [downloads-url]: https://npmjs.org/package/hostile 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | #### Simple, programmatic `/etc/hosts` manipulation (in node.js) 13 | 14 | ![hostile](https://raw.github.com/feross/hostile/master/img.png) 15 | 16 | ## install 17 | 18 | ```bash 19 | npm install hostile 20 | ``` 21 | 22 | ## usage 23 | 24 | If you use OS X or Linux, this module assumes your hosts file is at `/etc/hosts`. On 25 | Windows, it assumes your hosts file is at `C:/Windows/System32/drivers/etc/hosts`. 26 | 27 | **Commands that modify the hosts file require root privileges.** 28 | 29 | #### list all host file records 30 | 31 | ```bash 32 | hostile list 33 | ``` 34 | 35 | #### set a domain in the hosts file 36 | 37 | ```bash 38 | hostile set [ip] [host] 39 | ``` 40 | 41 | examples: 42 | ```bash 43 | hostile set localhost domain.com 44 | hostile set 192.168.33.10 domain.com 45 | ``` 46 | 47 | #### remove a domain from the hosts file 48 | 49 | ```bash 50 | hostile remove [host] 51 | ``` 52 | 53 | example: 54 | ```bash 55 | hostile remove domain.com 56 | ``` 57 | 58 | #### load a set of hosts from a file 59 | 60 | ```bash 61 | hostile load [file_path] 62 | ``` 63 | hosts.txt 64 | ```bash 65 | # hosts.txt 66 | 127.0.0.1 github.com 67 | 127.0.0.1 twitter.com 68 | ``` 69 | 70 | example: 71 | ```bash 72 | hostile load hosts.txt 73 | ``` 74 | 75 | #### unload [remove] a set of hosts from a file 76 | 77 | ```bash 78 | hostile unload [file_path] 79 | ``` 80 | 81 | ```bash 82 | # hosts.txt 83 | 127.0.0.1 github.com 84 | 127.0.0.1 twitter.com 85 | ``` 86 | 87 | example: 88 | ```bash 89 | hostile unload hosts.txt 90 | ``` 91 | 92 | ## methods 93 | 94 | Commands that modify the hosts file require root privileges. 95 | 96 | I wouldn't recommend running your production node server with admin privileges unless you 97 | downgrade to a normal user with 98 | [`process.setuid(id)`](http://nodejs.org/api/process.html#process_process_setuid_id) 99 | before you start accepting requests. 100 | 101 | **All methods have sync versions. Just omit the callback parameter.** 102 | 103 | #### add a rule to /etc/hosts 104 | 105 | ```js 106 | var hostile = require('hostile') 107 | hostile.set('127.0.0.1', 'peercdn.com', function (err) { 108 | if (err) { 109 | console.error(err) 110 | } else { 111 | console.log('set /etc/hosts successfully!') 112 | } 113 | }) 114 | ``` 115 | 116 | If the rule already exists, then this does nothing. 117 | 118 | #### remove a rule from /etc/hosts 119 | 120 | ```js 121 | hostile.remove('127.0.0.1', 'peercdn.com', function (err) { 122 | if (err) { 123 | console.error(err) 124 | } else { 125 | console.log('set /etc/hosts successfully!') 126 | } 127 | }) 128 | ``` 129 | 130 | If the rule does not exist, then this does nothing. 131 | 132 | #### get all lines in /etc/hosts 133 | 134 | ```js 135 | // If `preserveFormatting` is true, then include comments, blank lines and other 136 | // non-host entries in the result 137 | var preserveFormatting = false 138 | 139 | hostile.get(preserveFormatting, function (err, lines) { 140 | if (err) { 141 | console.error(err.message) 142 | } 143 | lines.forEach(function (line) { 144 | console.log(line) // [IP, Host] 145 | }) 146 | }) 147 | ``` 148 | 149 | #### get all lines in any file 150 | 151 | ```js 152 | // If `preserveFormatting` is true, then include comments, blank lines and other 153 | // non-host entries in the result 154 | var preserveFormatting = false 155 | 156 | hostile.getFile(file_path, preserveFormatting, function (err, lines) { 157 | if (err) { 158 | console.error(err.message) 159 | } 160 | lines.forEach(function (line) { 161 | console.log(line) // [IP, Host] 162 | }) 163 | }) 164 | ``` 165 | 166 | ## contributors 167 | 168 | - [Feross Aboukhadijeh](http://feross.org) (author) 169 | - [Maayan Glikser](https://github.com/morsdyce) 170 | 171 | ## license 172 | 173 | MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). 174 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! hostile. MIT License. Feross Aboukhadijeh */ 2 | var fs = require('fs') 3 | var once = require('once') 4 | var split = require('split') 5 | var through = require('@ljharb/through') 6 | var net = require('net') 7 | 8 | var WINDOWS = process.platform === 'win32' 9 | var EOL = WINDOWS 10 | ? '\r\n' 11 | : '\n' 12 | 13 | exports.HOSTS = WINDOWS 14 | ? 'C:/Windows/System32/drivers/etc/hosts' 15 | : '/etc/hosts' 16 | 17 | /** 18 | * Get a list of the lines that make up the filePath. If the 19 | * `preserveFormatting` parameter is true, then include comments, blank lines 20 | * and other non-host entries in the result. 21 | * 22 | * @param {boolean} preserveFormatting 23 | * @param {function(err, lines)=} cb 24 | */ 25 | 26 | exports.getFile = function (filePath, preserveFormatting, cb) { 27 | var lines = [] 28 | if (typeof cb !== 'function') { 29 | fs.readFileSync(filePath, { encoding: 'utf8' }).split(/\r?\n/).forEach(online) 30 | return lines 31 | } 32 | 33 | cb = once(cb) 34 | fs.createReadStream(filePath, { encoding: 'utf8' }) 35 | .pipe(split()) 36 | .pipe(through(online)) 37 | .on('close', function () { 38 | cb(null, lines) 39 | }) 40 | .on('error', cb) 41 | 42 | function online (line) { 43 | // Remove all comment text from the line 44 | var lineSansComments = line.replace(/#.*/, '') 45 | var matches = /^\s*?(.+?)\s+(.+?)\s*$/.exec(lineSansComments) 46 | if (matches && matches.length === 3) { 47 | // Found a hosts entry 48 | var ip = matches[1] 49 | var host = matches[2] 50 | lines.push([ip, host]) 51 | } else { 52 | // Found a comment, blank line, or something else 53 | if (preserveFormatting) { 54 | lines.push(line) 55 | } 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Wrapper of `getFile` for getting a list of lines in the Host file 62 | * 63 | * @param {boolean} preserveFormatting 64 | * @param {function(err, lines)=} cb 65 | */ 66 | exports.get = function (preserveFormatting, cb) { 67 | return exports.getFile(exports.HOSTS, preserveFormatting, cb) 68 | } 69 | 70 | /** 71 | * Add a rule to /etc/hosts. If the rule already exists, then this does nothing. 72 | * 73 | * @param {string} ip 74 | * @param {string} host 75 | * @param {function(Error)=} cb 76 | */ 77 | exports.set = function (ip, host, cb) { 78 | var didUpdate = false 79 | if (typeof cb !== 'function') { 80 | return _set(exports.get(true)) 81 | } 82 | 83 | exports.get(true, function (err, lines) { 84 | if (err) return cb(err) 85 | _set(lines) 86 | }) 87 | 88 | function _set (lines) { 89 | // Try to update entry, if host already exists in file 90 | lines = lines.map(mapFunc) 91 | 92 | // If entry did not exist, let's add it 93 | if (!didUpdate) { 94 | // If the last line is empty, or just whitespace, then insert the new entry 95 | // right before it 96 | var lastLine = lines[lines.length - 1] 97 | if (typeof lastLine === 'string' && /\s*/.test(lastLine)) { 98 | lines.splice(lines.length - 1, 0, [ip, host]) 99 | } else { 100 | lines.push([ip, host]) 101 | } 102 | } 103 | 104 | exports.writeFile(lines, cb) 105 | } 106 | 107 | function mapFunc (line) { 108 | // replace a line if both hostname and ip version of the address matches 109 | if (Array.isArray(line) && line[1] === host && net.isIP(line[0]) === net.isIP(ip)) { 110 | line[0] = ip 111 | didUpdate = true 112 | } 113 | return line 114 | } 115 | } 116 | 117 | /** 118 | * Remove a rule from /etc/hosts. If the rule does not exist, then this does 119 | * nothing. 120 | * 121 | * @param {string} ip 122 | * @param {string} host 123 | * @param {function(Error)=} cb 124 | */ 125 | exports.remove = function (ip, host, cb) { 126 | if (typeof cb !== 'function') { 127 | return _remove(exports.get(true)) 128 | } 129 | 130 | exports.get(true, function (err, lines) { 131 | if (err) return cb(err) 132 | _remove(lines) 133 | }) 134 | 135 | function _remove (lines) { 136 | // Try to remove entry, if it exists 137 | lines = lines.filter(filterFunc) 138 | return exports.writeFile(lines, cb) 139 | } 140 | 141 | function filterFunc (line) { 142 | return !(Array.isArray(line) && line[0] === ip && line[1] === host) 143 | } 144 | } 145 | 146 | /** 147 | * Write out an array of lines to the host file. Assumes that they're in the 148 | * format that `get` returns. 149 | * 150 | * @param {Array.>} lines 151 | * @param {function(Error)=} cb 152 | */ 153 | exports.writeFile = function (lines, cb) { 154 | lines = lines.map(function (line, lineNum) { 155 | if (Array.isArray(line)) { 156 | line = line[0] + ' ' + line[1] 157 | } 158 | return line + (lineNum === lines.length - 1 ? '' : EOL) 159 | }) 160 | 161 | if (typeof cb !== 'function') { 162 | var stat = fs.statSync(exports.HOSTS) 163 | fs.writeFileSync(exports.HOSTS, lines.join(''), { mode: stat.mode }) 164 | return true 165 | } 166 | 167 | cb = once(cb) 168 | fs.stat(exports.HOSTS, function (err, stat) { 169 | if (err) { 170 | return cb(err) 171 | } 172 | var s = fs.createWriteStream(exports.HOSTS, { mode: stat.mode }) 173 | s.on('close', cb) 174 | s.on('error', cb) 175 | 176 | lines.forEach(function (data) { 177 | s.write(data) 178 | }) 179 | s.end() 180 | }) 181 | } 182 | --------------------------------------------------------------------------------