这是我花了一整天的时间写的node.js命令行扫描器,500多行代码,功能完整,附带英文注解和使用说明,可以拿来直接用。
const validOptions = [ { "name": "ports", "cmd": ["-p", "--ports"], "value": "21-23,25,80,443,445,1433,3306,3389", "min": 1, "max": 65535, "type": "range", "description": "The port or range of ports to scan (e.g. '1-1024,1433,3389').", "description_zh": "要扫描的端口或端口范围(例如:'1-1024,1433,3389')" }, { "name": "timeout", "cmd": ["-t", "--timeout"], "value": 1000, "type": "number", "description": "The timeout for each port scan, in milliseconds.", "description_zh": "每个端口扫描的超时时间(毫秒)" }, { "name": "maxQueueSize", "cmd": ["-q", "--max-queue-size"], "value": 100, "type": "number", "description": "The maximum size of the scan queue.", "description_zh": "扫描队列的最大大小" }, { "name": "showAll", "cmd": ["-a", "--show-all"], "value": false, "type": "bool", "description": "Show all ports, including closed and timed out ports.", "description_zh": "显示全部端口,包含关闭和超时的端口。" }, { "name": "help", "cmd": ["-h", "--help"], "value": false, "type": "bool", "description": "Show usage information.", "description_zh": "显示使用说明。" } ]; /** * This function takes an option object and an argument and returns the value of the option based on its type and constraints. * @param {Object} option - An option object with properties "name", "type", "min", and "max". * @param {string} arg - The argument to be parsed and validated. * @returns {*} - The parsed and validated value of the option based on its type and constraints. */ function getOptionValue(option, arg) { if (option.type === "bool") { return arg; } if (option.type === "number") { const value = parseInt(arg, 10); if (isNaN(value)) { throw new Error(`Invalid value for option '${option.name}': '${arg}'`); } if (option.min && value < option.min) { throw new Error(`Value for option '${option.name}' must be greater than or equal to ${option.min}`); } if (option.max && value > option.max) { throw new Error(`Value for option '${option.name}' must be less than or equal to ${option.max}`); } return value; } if (option.type === "range") { const result = []; const ranges = arg.split(","); for (let range of ranges) { range = range.trim(); const parts = range.split("-"); if (parts.length === 1) { // Single port const value = parseInt(parts[0], 10); if (isNaN(value)) { throw new Error(`Invalid value for option '${option.name}': '${range}'`); } if (option.min && value < option.min) { throw new Error(`Value for option '${option.name}' must be greater than or equal to ${option.min}`); } if (option.max && value > option.max) { throw new Error(`Value for option '${option.name}' must be less than or equal to ${option.max}`); } result.push(value); } else if (parts.length === 2) { // Port range const start = parseInt(parts[0], 10); const end = parseInt(parts[1], 10); if (isNaN(start) || isNaN(end)) { throw new Error(`Invalid value for option '${option.name}': '${range}'`); } if (option.min && start < option.min) { throw new Error(`Value for option '${option.name}' must be greater than or equal to ${option.min}`); } if (option.max && end > option.max) { throw new Error(`Value for option '${option.name}' must be less than or equal to ${option.max}`); } if (start > end) { throw new Error(`Invalid range for option '${option.name}': '${range}'`); } for (let i = start; i <= end; i++) { result.push(i); } } else { throw new Error(`Invalid value for option '${option.name}': '${range}'`); } } return result; } } /** * Async function that gets a list of IP addresses from a given host name, domain name or CIDR subnet. * @param {string} host - The host to be checked, which can be an IPv4 address, host name, domain name or CIDR subnet. * @returns {Promise<array>} - returns an array of IP addresses. Otherwise, throws an error. * @throws {Error} - Throws an error if the input is invalid or DNS resolution fails. */ async function getIpList(host) { const dns = require('dns'); /** * Check if the provided string is a valid host name. * @param {string} host - The host name to be checked. * @returns {boolean} Returns true if the string is a valid host name, otherwise false. */ function isHostName(host) { const p = /^([a-z0-9\-]+)[\.]?$/i; return p.test(host); } /** * Check if the given string is a valid domain name format. * @param {string} host The domain name string to be checked. * @returns {boolean} Whether the given string is a valid domain name format. */ function isDomainName(host) { const p = /^([a-z0-9\-]+\.)+([a-z]{2,})+[\.]?$/i; return p.test(host); } /** * Check if the given string is a valid IPv4 address * @param {string} host - the string to be checked * @returns {boolean} - true if the string is a valid IPv4 address, false otherwise */ function isIPv4Addr(host) { const p = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; return p.test(host); } /** * Check if the given host is a valid CIDR notation of an IPv4 address. * @param {string} host - The host to check in CIDR notation (e.g. '192.168.0.1/24'). * @returns {false|array} - If valid, returns an array with two elements: the IPv4 address and the CIDR number. Otherwise, returns false. * @throws {Error} - Throws an error if the CIDR mask is less than 10. */ function isCIDR(host) { const aHost = host.split('/'); const cidr = Number(aHost[1]); if (isNaN(cidr)) { return false; } if (cidr > 32 || cidr < 0) { return false; } if (!isIPv4Addr(aHost[0])) { return false; } if (cidr < 10) { throw new Error("The CIDR mask cannot be less than 10"); } return [aHost[0], cidr]; } /** * Generate a list of IPs within a CIDR range. * @param {String} ip The IP address. * @param {Number} cidr The CIDR prefix. * @returns {Array} The list of IP addresses within the CIDR range. */ function generateIpRangeByCIDR(ip, cidr) { const mask = (0xFFFFFFFF << (32 - cidr)) >>> 0; const start = ip & mask; const end = start + (1 << (32 - cidr)); const ips = []; for (let i = start; i < end; i++) { ips.push(intToIP(i)); } return ips; } function ipToInt(ip) { return ip.split(".").reduce((acc, cur) => (acc << 8) + parseInt(cur), 0) >>> 0; } function intToIP(n) { return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join("."); } // step 1: If the host is an IPv4 address, return it directly if (isIPv4Addr(host)) { return [host]; } // step 2: If it's a name name, return it directly if (isHostName(host)) { return [host]; } // step 3: If it's a domain name, resolve DNS try { if (isDomainName(host)) { const ipList = await dns.promises.resolve4(host); return ipList; } } catch (err) { err.message = `DNS resolution failed: ${err.message}`; throw err; } // step 4: If it's a CIDR, convert it to IP range. const cidr = isCIDR(host); if (cidr) { return generateIpRangeByCIDR(ipToInt(cidr[0]), cidr[1]); } // step 5: for else, throw error. throw new Error("Invalid host name. Please make sure you enter a valid IPv4 address, host name, domain name, or CIDR subnet."); return [host]; } /** * Get command-line arguments. * @param {Array} validOptions Array of valid options. * @returns {Object} An object containing the specified options. */ async function getCommandLineArgs(validOptions) { // Get parameters from command line const args = process.argv.slice(2); // Build a map to facilitate finding command-line options const cmdOptionMap = {}; for (const option of validOptions) { for (const cmd of option.cmd) { cmdOptionMap[cmd] = option; } } const param = {}; const args2 = []; for (const arg of args) { const argi = arg.indexOf('='); var arg0 = arg; var arg1 = true; if (argi > 0) { arg0 = arg.substr(0, argi); arg1 = arg.substr(argi + 1); } if (!cmdOptionMap.hasOwnProperty(arg0)) { args2.push(arg); continue; } const mOption = cmdOptionMap[arg0]; if (argi === -1 && mOption.type !== 'bool') { throw new Error(`The argument ${arg} must be provided with a value in the format of ${arg}=value, for example ${arg}=${mOption.value}`); } param[arg0] = arg1; } // Host name is a required field const host = args2.shift(); for (const arg of args2) { throw new Error(`The parameter ${arg} does not exist.`); } if (!host) { throw new Error("Please provide a IPv4 address, host name, domain name, or CIDR subnet."); } const result = {}; result.ips = await getIpList(host); for (const option of validOptions) { var value = option.value; for (const arg0 of option.cmd) { // Find the matching option cmd if (param.hasOwnProperty(arg0)) { value = param[arg0]; break; } } result[option.name] = getOptionValue(option, value); } return result; } /** * Generate a help text based on the valid options. * * @param {Array} validOptions - An array of valid options, each option being an object * with properties: name, cmd, value and description. * @returns {string} A formatted help text with valid options and descriptions. */ function generateHelpText(validOptions) { const helpText = []; let maxCmdLength = 0; validOptions.forEach(option => { // Find the longest command string length for formatting const cmdLength = option.cmd.join(', ').length; if (cmdLength > maxCmdLength) { maxCmdLength = cmdLength; } // Add the option to the help text helpText.push({ name: option.name, cmd: option.cmd.join(', '), value: option.value, description: option.description }); }); // Generate the formatted help text const formattedText = []; helpText.forEach(option => { const cmdString = option.cmd.padEnd(maxCmdLength); const description = option.description.replace(/\n/g, '\n' + ' '.repeat(maxCmdLength + 3)); formattedText.push(` ${cmdString} ${description} Default: ${option.value}.\n`); }); // Return the complete help text return `\nUsage: node tcp-scanner.js <host> [options]\n\nOptions:\n${formattedText.join('')}`; } /** * This function generates an array of objects, where each object contains an IP address and a port number to scan. * The function calculates the required scanning time based on the input parameters, and prompts the user for confirmation if the scanning time exceeds one minute. * @param {object} config - An object containing the following properties: * ips: An array of IP addresses to scan. * ports: An array of port numbers to scan. * timeout: The timeout value for each scan in milliseconds. * maxQueueSize: The maximum number of simultaneous scans. * @returns {Promise<Array>} - An array of objects containing an IP address and a port number to scan. */ async function generateScanList(config) { const { ips, ports, timeout, maxQueueSize } = config; const totalTargets = ips.length * ports.length; console.log(`Scanning ${ips.length} hosts and ${ports.length} ports (Total targets: ${totalTargets})`); var scanTime = totalTargets * timeout / maxQueueSize; scanTime += totalTargets * 5; if (scanTime > 1000) { console.log(`May take up to ${formatDuration(scanTime)}.`); } const msg = `Are you sure Continue (y/N)? `; const scanTimeInMinutes = scanTime / 1000 / 60; if (scanTimeInMinutes > 1 && !(await promptForContinuation(msg))) { return []; } const scanList = []; for (const host of ips) { for (const port of ports) { scanList.push({ host, port }); } } return scanList; } /** * This function formats a duration in milliseconds to a human-readable string. * @param {number} duration - The duration in milliseconds. * @returns {string} - The formatted duration. */ function formatDuration(duration) { const seconds = Math.floor(duration / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const parts = []; if (days > 0) { parts.push(`${days} day${days === 1 ? '' : 's'}`); } if (hours % 24 > 0) { parts.push(`${hours % 24} hour${hours % 24 === 1 ? '' : 's'}`); } if (minutes % 60 > 0) { parts.push(`${minutes % 60} minute${minutes % 60 === 1 ? '' : 's'}`); } if (seconds % 60 > 0) { parts.push(`${seconds % 60} second${seconds % 60 === 1 ? '' : 's'}`); } return parts.join(' '); } /** * Asks the user if they want to continue scanning. * @param {String} message - The message to prompt the user. * @returns {Boolean} Whether or not the user wants to continue scanning. */ async function promptForContinuation(message) { while (true) { const confirmation = await getUserConfirmation(message); if (!confirmation) { continue; } return confirmation.ret; } } /** * This function prompts the user for confirmation and returns a boolean value indicating whether the user confirmed or not. * @param {string} message - The confirmation message to display to the user. * @returns {Promise<boolean>} - A boolean value indicating whether the user confirmed or not. */ function getUserConfirmation(message) { return new Promise((resolve) => { const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question(message, (answer) => { rl.close(); const a = answer.toLowerCase(); if (a === '') { return resolve({ret: false}); } if (a === 'y') { return resolve({ret: true}); } if (a === 'n') { return resolve({ret: false}); } return resolve(); }); }); } /** * Scan for open ports on the given targets. * * @param {Object} options - The scan options. * @param {Array} options.targets - An array of targets to scan. * @param {number} options.timeout - The timeout for each connection attempt, in milliseconds. * @param {number} options.maxQueueSize - The maximum number of targets that can be in the scan queue at once. * @param {boolean} options.showAll - Whether to show all errors, including timeouts and connection errors. * @param {Function} onResult - A callback function to handle scan results. */ function scanPorts({ targets, timeout, maxQueueSize, showAll }, onResult = () => {}) { const net = require('net'); const queue = [...targets]; // Make a copy of the targets array as the scan queue. const openPorts = {}; // Record open ports. const startTime = Date.now(); // Record start time /** * Returns a promise that resolves when the connection is successful or rejects when it times out or encounters an error. * * @param {Object} target - The target to connect to. * @param {string} target.host - The target's hostname or IP address. * @param {number} target.port - The target's port number. * @returns {Promise<Object>} A promise that resolves to the target object if the connection is successful, or rejects with an error message if it times out or encounters an error. */ function connect({ host, port }) { return new Promise((resolve, reject) => { const socket = net.createConnection({ host, port }, () => { // Close the socket after a successful connection. socket.end(); // Resolve with the target object. resolve({ host, port }); }); socket.setTimeout(timeout, () => { // Destroy the socket after a timeout. socket.destroy(); // Reject with a timeout error message. reject(`[timeout] ${host}:${port}`); }); socket.on('error', (err) => { // Reject with a connection error message. reject(`[${err.code}] ${host}:${port}`); }); }); } let queueLength = 0; /** * Dequeues a target from the scan queue and attempts to connect to it. */ function dequeue() { if (queueLength === 0 && queue.length === 0) { // If the scan queue is empty, output the scan results. const endTime = Date.now(); // Calculate scan time in seconds const scanTime = (endTime - startTime) / 1000; onResult(`Scan complete! Open ports:`); onResult(JSON.stringify(openPorts)); onResult(`Scan time: ${scanTime} seconds.`); return; } if (queueLength >= maxQueueSize) { // If the scan queue is full, wait and try again. setTimeout(dequeue, 100); return; } // Dequeue a target. const target = queue.shift(); if (!target) { // If there are no targets left, wait and try again. setTimeout(dequeue, 1000); return; } // Schedule the next dequeue operation. setTimeout(dequeue, 1); queueLength++; connect(target) .then((mTarget) => { queueLength--; if (!openPorts.hasOwnProperty(mTarget.host)) { openPorts[mTarget.host] = []; } openPorts[mTarget.host].push(mTarget.port); onResult(`[open] ${mTarget.host}:${mTarget.port}`); }) .catch((err) => { queueLength--; if (showAll) { onResult(`${err}`); } }); } dequeue(); } async function main() { var config; try { config = await getCommandLineArgs(validOptions); } catch(e) { console.log(e.toString()); console.log(generateHelpText(validOptions)); return; } config.targets = await generateScanList(config); scanPorts(config, console.log); } main();
保存成tcp-scanner.js即可使用,工具内置使用帮助和指令说明。