├── CHANGELOG.md ├── lib ├── spinner.mjs ├── utils.mjs ├── host.mjs ├── stdout.mjs ├── ping.mjs ├── server.mjs ├── constant.mjs ├── ip.mjs └── painter.mjs ├── package.json ├── LICENSE ├── bin └── cmd.js ├── README.md ├── .gitignore └── index.mjs /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.0 (2023-02-20) 2 | 3 | ### feature 4 | 5 | - system host setting 6 | 7 | ## 1.1.0 (2023-02-13) 8 | 9 | ### feature 10 | 11 | - support DNS resolve time 12 | - support node19.x 13 | - more elegant IP ping latency style 14 | 15 | ### fix 16 | 17 | - IO stream cache flush 18 | -------------------------------------------------------------------------------- /lib/spinner.mjs: -------------------------------------------------------------------------------- 1 | import { SPINNER_FRAME } from "./constant.mjs" 2 | 3 | export class Spinner { 4 | constructor(type) { 5 | this.tick = 0 6 | this.frames = SPINNER_FRAME[type] 7 | } 8 | 9 | text() { 10 | if (this.tick >= this.frames.length) { 11 | this.tick = 0 12 | } 13 | return this.frames[this.tick++] 14 | } 15 | } -------------------------------------------------------------------------------- /lib/utils.mjs: -------------------------------------------------------------------------------- 1 | export function getRealTime(time) { 2 | return time === 'timeout' ? Infinity : time 3 | } 4 | 5 | export function interval(callback, time) { 6 | return new Promise(async r => { 7 | await callback() 8 | await sleep(time) 9 | r() 10 | }).then(() => interval(callback, time)) 11 | } 12 | 13 | export async function sleep(time) { 14 | return new Promise(r => { 15 | setTimeout(r, time) 16 | }) 17 | } -------------------------------------------------------------------------------- /lib/host.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "node:fs/promises"; 2 | 3 | 4 | export class HostSetting { 5 | constructor(host) { 6 | this.host = host 7 | this.hostFile = '/etc/hosts' 8 | } 9 | 10 | async getContent() { 11 | const content = await readFile(this.hostFile); 12 | 13 | this.content = content.toString().split('\n') 14 | } 15 | 16 | setHostIP(ip) { 17 | this.ip = ip; 18 | this.line = this.content.findIndex(str => !/^\s*#/.test(str) && str.includes(this.host)) 19 | const settingStr = `${ip.addr} ${this.host}` 20 | if (this.line > -1) { 21 | this.content[this.line] = settingStr 22 | } else { 23 | this.content.push(settingStr) 24 | } 25 | 26 | return writeFile(this.hostFile, this.content.join('\n'), { mode: 0o777 }) 27 | } 28 | } -------------------------------------------------------------------------------- /lib/stdout.mjs: -------------------------------------------------------------------------------- 1 | import { stdout } from "node:process" 2 | import { Readline } from 'node:readline/promises' 3 | import { emitKeypressEvents } from 'node:readline' 4 | import { COLORS } from "./constant.mjs" 5 | 6 | const rl = new Readline(stdout) 7 | 8 | stdout.clearAll = () => stdout.write('\x1Bc') 9 | stdout.showCursor = () => stdout.write('\u001B[?25h') 10 | stdout.hideCursor = () => stdout.write('\u001B[?25l') 11 | stdout.clearLines = (dy) => { 12 | rl.cursorTo(0) 13 | rl.moveCursor(0, -dy) 14 | // rl.clearScreenDown(); 15 | return rl.commit() 16 | } 17 | 18 | stdout.error = (err) => stdout.write('dns-detector: ' + COLORS.red + err) 19 | 20 | export const watchKeypress = (callback) => { 21 | emitKeypressEvents(process.stdin) 22 | process.stdin.setRawMode(true) 23 | process.stdin.on('keypress', callback) 24 | } 25 | 26 | export { stdout } -------------------------------------------------------------------------------- /lib/ping.mjs: -------------------------------------------------------------------------------- 1 | import { platform } from 'node:os' 2 | import { spawn } from 'node:child_process' 3 | 4 | export class Ping { 5 | constructor(ip) { 6 | this.ping = spawn('ping', [ip]) 7 | 8 | this.ping.stderr.on('data', data => { 9 | this.exit() 10 | }) 11 | } 12 | 13 | onResponse(callback) { 14 | this.ping.stdout.on('data', data => { 15 | callback(this.parse(data.toString())) 16 | }) 17 | } 18 | 19 | parse(out) { 20 | let reg = platform() === 'win32' ? /bytes=(\d+).+time=(\d+)/ : /(\d+)\s*bytes.+time\=(.+)\s*ms/ 21 | const [, received, time] = out.match(reg) || [, '0', 'timeout'] 22 | return { received, time } 23 | } 24 | 25 | exit() { 26 | this.ping.kill('SIGINT') 27 | } 28 | 29 | } 30 | 31 | export class PingQueue extends Map { 32 | exit() { 33 | this.forEach(ping => ping.exit()) 34 | } 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dns-detector", 3 | "version": "1.2.0", 4 | "description": "A nodejs cli tool to resolve host's IPs", 5 | "type": "module", 6 | "module": "index.mjs", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sun0day/dns-detector.git" 13 | }, 14 | "engines": { 15 | "node": ">= 18.0.0" 16 | }, 17 | "files": [ 18 | "bin", 19 | "lib", 20 | "index.mjs" 21 | ], 22 | "keywords": [ 23 | "dns", 24 | "ping", 25 | "cli", 26 | "ip" 27 | ], 28 | "bin": { 29 | "dns": "./bin/cmd.js" 30 | }, 31 | "author": "sun0day", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/sun0day/dns-detector/issues" 35 | }, 36 | "homepage": "https://github.com/sun0day/dns-detector#readme" 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 sun0day 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/server.mjs: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events' 2 | import { Resolver } from 'node:dns/promises' 3 | import { DNS_SERVER, RESOLVE_EVENT } from './constant.mjs' 4 | 5 | export class DnsServer extends EventEmitter { 6 | constructor({ server, timeout = 2000, tries } = {}) { 7 | super(); 8 | this.timeout = timeout 9 | this.tries = tries 10 | this.servers = server ? DNS_SERVER.concat(server) : DNS_SERVER 11 | 12 | this.emit(RESOLVE_EVENT.INIT) 13 | } 14 | 15 | async resolve(host) { 16 | const ips = await Promise.allSettled(this.servers.map(async server => { 17 | const baseParams = { server, host } 18 | 19 | this.emit(RESOLVE_EVENT.PENDING,) 20 | 21 | const resolver = new Resolver({ timeout: this.timeout, tries: this.tries }) 22 | resolver.setServers([server]) 23 | 24 | try { 25 | const ips = await resolver.resolve(host) 26 | const data = { ...baseParams, ips, error: null } 27 | 28 | this.emit(RESOLVE_EVENT.FULFILLED, data) 29 | 30 | return data 31 | } catch (error) { 32 | const data = { ...baseParams, ips: [], error } 33 | 34 | this.emit(RESOLVE_EVENT.REJECT, data) 35 | return data 36 | } 37 | })) 38 | 39 | const data = ips.map(({ value }) => value) 40 | 41 | this.emit(RESOLVE_EVENT.FINISHED, data) 42 | 43 | return data 44 | } 45 | } -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { argv as args } from "node:process"; 4 | import { resolve, stdout } from "../index.mjs"; 5 | 6 | const optionSchema = { 7 | server: { 8 | type: String, 9 | }, 10 | host: { 11 | required: true, 12 | type: String, 13 | }, 14 | timeout: { 15 | default: 2000, 16 | type: Number, 17 | }, 18 | tries: { 19 | default: 4, 20 | type: Number, 21 | }, 22 | }; 23 | 24 | function resolveOptions(args) { 25 | const options = {}; 26 | let optKey = ""; 27 | args.forEach((arg, index) => { 28 | if (index < 2) { 29 | return; 30 | } 31 | 32 | const [key, value] = arg.replace(/^--/, "").split("="); 33 | 34 | if (!optKey && !value && !optionSchema[key]) { 35 | stdout.error(`unknown option ${arg}\n`); 36 | process.exit(1); 37 | } 38 | 39 | if (value) { 40 | options[key] = value; 41 | } else { 42 | if (optKey) { 43 | options[optKey] = key; 44 | optKey = ""; 45 | } else { 46 | optKey = key; 47 | } 48 | } 49 | }); 50 | 51 | Object.keys(optionSchema).forEach((key) => { 52 | const { required, type, default: dv } = optionSchema[key]; 53 | if (!options[key]) { 54 | if (required) { 55 | stdout.error(`${key} option is required\n`); 56 | process.exit(1); 57 | } else { 58 | options[key] = dv; 59 | } 60 | } 61 | options[key] = type(options[key]); 62 | }); 63 | 64 | return options; 65 | } 66 | 67 | resolve(resolveOptions(args)); 68 | -------------------------------------------------------------------------------- /lib/constant.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const DNS_SERVER = [ 3 | '1.1.1.1', 4 | '8.8.8.8', 5 | '199.85.126.10', 6 | '208.67.222.222', 7 | '84.200.69.80', 8 | '8.26.56.26', 9 | '64.6.64.6', 10 | '192.95.54.3', 11 | '81.218.119.11', 12 | '114.114.114.114', 13 | '119.29.29.29', 14 | '223.5.5.5' 15 | ] 16 | 17 | export const RESOLVE_STATUS = { 18 | PENDING: 'pending', 19 | SUCCESS: 'success', 20 | FAIL: 'fail' 21 | } 22 | 23 | export const RESOLVE_EVENT = { 24 | INIT: 'init', 25 | PENDING: 'pending', 26 | FULFILLED: 'fulfilled', 27 | REJECT: 'reject', 28 | FINISHED: 'finished' 29 | } 30 | 31 | export const SPINNER_FRAME = { 32 | bouncingBall: [ 33 | "( ● )", 34 | "( ● )", 35 | "( ●)", 36 | "( ● )", 37 | "( ● )", 38 | "(● )", 39 | ], 40 | bouncingBar: [ 41 | "[ ]", 42 | "[= ]", 43 | "[== ]", 44 | "[=== ]", 45 | "[ ===]", 46 | "[ ==]", 47 | "[ =]", 48 | "[ ]", 49 | "[ =]", 50 | "[ ==]", 51 | "[ ===]", 52 | "[====]", 53 | "[=== ]", 54 | "[== ]", 55 | "[= ]" 56 | ] 57 | } 58 | 59 | export const COLORS = { 60 | reset: '\x1b[0m', 61 | reverse: '\x1b[7m', 62 | 63 | red: '\x1b[31m', 64 | yellow: '\x1b[33m', 65 | green: '\x1b[32m', 66 | cyan: '\x1b[36m', 67 | bright: '\x1b[1m', 68 | magenta: '\x1b[35m', 69 | } 70 | 71 | export const ACTION = { 72 | next: 'next', 73 | prev: 'prev', 74 | setting: 'setting' 75 | } 76 | 77 | export const CHAR = { 78 | check: '✔', 79 | close: '×', 80 | block: '◼', 81 | star: '★' 82 | } -------------------------------------------------------------------------------- /lib/ip.mjs: -------------------------------------------------------------------------------- 1 | import { ACTION } from './constant.mjs' 2 | import { getRealTime } from './utils.mjs' 3 | 4 | export class IP { 5 | constructor({ server, time, addr, received = 0, resolveTime, selected }) { 6 | this.addr = addr 7 | this.server = server 8 | this.time = time 9 | this.received = received 10 | this.resolveTime = resolveTime 11 | this.selected = selected 12 | } 13 | } 14 | 15 | export class IPQueue extends Map { 16 | getAllIPs() { 17 | return Array.from(this.values()) 18 | } 19 | 20 | sortByTime() { 21 | return this.getAllIPs().filter(ip => !!ip.time).sort((next, cur) => getRealTime(next.time) - getRealTime(cur.time)) 22 | } 23 | 24 | sortByResolveTime() { 25 | return this.getAllIPs().sort((next, cur) => getRealTime(next.resolveTime) - getRealTime(cur.resolveTime)) 26 | } 27 | 28 | groupByServer() { 29 | const group = {} 30 | 31 | this.sortByResolveTime().forEach(ip => { 32 | group[ip.server] ??= [] 33 | group[ip.server].push(ip) 34 | }) 35 | 36 | return group 37 | } 38 | 39 | selectIP(action) { 40 | const ips = this.sortByResolveTime() 41 | 42 | let index = ips.findIndex(ip => ip.selected) 43 | 44 | switch (action) { 45 | case ACTION.next: 46 | ips[(index + 1) % ips.length].selected = true 47 | 48 | if (index > -1) { 49 | ips[index].selected = false 50 | } 51 | break 52 | case ACTION.prev: 53 | ips[(index ? index : ips.length) - 1].selected = true 54 | 55 | if (index > -1) { 56 | ips[index].selected = false 57 | } 58 | } 59 | } 60 | 61 | getSelectedIP() { 62 | return this.getAllIPs().find(ip => ip.selected) 63 | } 64 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | A tiny nodejs cli tool to resolve host IPs and find the fastest IP
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |