diff --git a/kit/package-lock.json b/kit/package-lock.json index fb2b06e..929c74e 100644 --- a/kit/package-lock.json +++ b/kit/package-lock.json @@ -10,12 +10,14 @@ "dependencies": { "auto-launch": "^5.0.6", "cross-port-killer": "^1.4.0", + "diskusage-ng": "^1.0.4", "extract-zip": "^2.0.1", "get-port": "^7.0.0", "json5": "^2.2.3", "log4js": "^6.9.1", "physical-cpu-count": "^2.0.0", "sm-crypto": "^0.3.13", + "systeminformation": "^5.25.11", "webdav": "^5.3.1" }, "bin": { @@ -156,6 +158,15 @@ } } }, + "node_modules/diskusage-ng": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/diskusage-ng/-/diskusage-ng-1.0.4.tgz", + "integrity": "sha512-30qT0Bn2dNGii6dAXllGgYRaIt+7gPhwCJks+byhutkCOCgsqfqvXmAV2zTDOvU1ylnGHUjZMMaKhM95FyQQrg==", + "license": "MIT", + "engines": { + "node": ">=8.8.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -528,6 +539,32 @@ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, + "node_modules/systeminformation": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.6.tgz", + "integrity": "sha512-LEIyK1aEv5P3BhAPW3swdlIyCihxwEq/Gki+kcONieU4PIeRCSLDuGkk0Va/56PSBgjVgEksOM88dmY6YqOyfQ==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/kit/package.json b/kit/package.json index 398a571..3e84942 100644 --- a/kit/package.json +++ b/kit/package.json @@ -3,6 +3,7 @@ "version": "0.1", "bin": "src/index.js", "scripts": { + "test": "bun src/index.js", "linux": "bun build ./src/index.js --compile --bytecode --minify --target=bun-linux-x64-baseline --outfile=./dist/linux/kit", "windows": "bun build ./src/index.js --compile --bytecode --minify --target=bun-windows-x64-baseline --outfile=./dist/windows/kit.exe", "pkg-alpine": "pkg --compress GZip . -t node14-alpine-arm64 -o ./dist/alpine/kit", diff --git a/kit/src/index.js b/kit/src/index.js index 109f3aa..ca7827c 100644 --- a/kit/src/index.js +++ b/kit/src/index.js @@ -183,12 +183,18 @@ parser.addCmdLine("checkip", "获取自己的公网 ip;", async function (cli) { if( "ok" === ret.ret ) { $logger.info( "ip : " + ret.data.ip ) $logger.info( "location : " + ret.data.location.join(" ") ) - $logger.info("\n" + ret.data.ip) } else { $logger.error("获取失败") } }); +parser.addCmdLine("scan [ports]", "扫描局域网;", async function (cli) { + let ipAndMask = cli.getParamValue("ip-and-mask") + let ports = cli.getParamValue("ports") + let ret = await net.scan(ipAndMask, ports) + $logger.info( JSON.stringify(ret, null, 4) ) +}); + parser.addCmdLine("telnet [--timeout] [port]", "验证远程 TCP 端口是否可用;", async function (cli) { let ip = cli.getParamValue("ip-port") diff --git a/kit/src/net.js b/kit/src/net.js index a9929a5..a679752 100644 --- a/kit/src/net.js +++ b/kit/src/net.js @@ -2,7 +2,7 @@ require("./init") const osutil = require("./util/osutil") const killer = require('cross-port-killer'); const net = require('net'); - +const dns = require('dns').promises; /** * 模拟 telnet ,检测端口是否开放 @@ -79,6 +79,299 @@ async function checkip(){ }); } + +/** + * 内网IP端口扫描函数 + * @param {string} ipAndMask - IP地址或IP段,如:"192.168.1.1" 或 "192.168.1.0/24" + * @param {string|null} ports - 端口,可以是:null(使用默认端口)、单个端口、多个端口逗号分隔 + * @returns {Promise} - 返回扫描结果数组 + */ +async function scan(ipAndMask, ports) { + const defaultPorts = [22, 80, 443, 3389, 8889]; + let targetIps = []; + let targetPorts = []; + + // 1. 解析端口参数 + if (!ports) { + targetPorts = defaultPorts; + } else if (ports.includes(',')) { + targetPorts = ports.split(',').map(p => parseInt(p.trim())).filter(p => p > 0 && p <= 65535); + } else { + targetPorts = [parseInt(ports)]; + } + + // 2. 解析IP地址/网段 + if (ipAndMask.includes('/')) { + // CIDR表示法,如 192.168.1.0/24 + targetIps = await parseCIDR(ipAndMask); + } else if (net.isIP(ipAndMask)) { + // 单个IP地址 + targetIps = [ipAndMask]; + } else { + // 可能是主机名,尝试解析 + try { + const addresses = await dns.resolve4(ipAndMask); + targetIps = addresses; + } catch (error) { + throw new Error(`无法解析主机名: ${ipAndMask}`); + } + } + + // 3. 扫描所有IP和端口 + const scanPromises = targetIps.map(ip => + scanPortsForIP(ip, targetPorts) + ); + + const results = await Promise.all(scanPromises); + + // 4. 过滤出有开放端口的结果 + return results.filter(result => result.openPorts.length > 0); +} + +/** + * 解析CIDR格式的IP段 + * @param {string} cidr - CIDR格式,如 "192.168.1.0/24" + * @returns {Array} - IP地址数组 + */ +function parseCIDR(cidr) { + const [ipStr, maskStr] = cidr.split('/'); + const mask = parseInt(maskStr); + + if (mask < 0 || mask > 32) { + throw new Error('子网掩码必须在0-32之间'); + } + + // 将IP转换为32位整数 + const ipToInt = (ip) => { + const parts = ip.split('.').map(Number); + return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3]; + }; + + // 将32位整数转换为IP字符串 + const intToIp = (int) => { + return [ + (int >>> 24) & 255, + (int >>> 16) & 255, + (int >>> 8) & 255, + int & 255 + ].join('.'); + }; + + const ipInt = ipToInt(ipStr); + const hostBits = 32 - mask; + const networkMask = (0xFFFFFFFF << hostBits) >>> 0; // 无符号右移 + const networkAddress = ipInt & networkMask; + const broadcastAddress = networkAddress | ((1 << hostBits) - 1); + + // 排除网络地址和广播地址 + const start = networkAddress + 1; + const end = broadcastAddress - 1; + + const ips = []; + for (let i = start; i <= end; i++) { + ips.push(intToIp(i)); + } + + return ips; +} + +/** + * 扫描单个IP的所有指定端口 + * @param {string} ip - IP地址 + * @param {Array} ports - 端口数组 + * @param {number} timeout - 连接超时时间(毫秒) + * @returns {Promise} - 扫描结果 + */ +async function scanPortsForIP(ip, ports, timeout = 2000) { + const results = { + ip: ip, + openPorts: [], + scannedPorts: ports.length, + startTime: Date.now() + }; + + // 并行扫描所有端口 + const portPromises = ports.map(port => + scanPort(ip, port, timeout) + ); + + const portResults = await Promise.allSettled(portPromises); + + // 收集开放的端口 + portResults.forEach((result, index) => { + if (result.status === 'fulfilled' && result.value.open) { + results.openPorts.push({ + port: ports[index], + service: result.value.service + }); + } + }); + + results.endTime = Date.now(); + results.scanDuration = results.endTime - results.startTime; + + return results; +} + +/** + * 扫描单个端口 + * @param {string} ip - IP地址 + * @param {number} port - 端口号 + * @param {number} timeout - 超时时间 + * @returns {Promise} - 端口扫描结果 + */ +function scanPort(ip, port, timeout) { + return new Promise((resolve) => { + const socket = new net.Socket(); + const result = { + ip: ip, + port: port, + open: false, + service: getServiceName(port) + }; + + // 设置超时 + socket.setTimeout(timeout); + + socket.on('connect', () => { + result.open = true; + socket.destroy(); + resolve(result); + }); + + socket.on('timeout', () => { + socket.destroy(); + resolve(result); + }); + + socket.on('error', () => { + socket.destroy(); + resolve(result); + }); + + socket.on('close', () => { + // 连接已关闭 + }); + + // 开始连接 + socket.connect(port, ip); + }); +} + +/** + * 根据端口号获取常见服务名称 + * @param {number} port - 端口号 + * @returns {string} - 服务名称 + */ +function getServiceName(port) { + const commonServices = { + 20: 'FTP Data', + 21: 'FTP Control', + 22: 'SSH', + 23: 'Telnet', + 25: 'SMTP', + 53: 'DNS', + 67: 'DHCP Server', + 68: 'DHCP Client', + 69: 'TFTP', + 80: 'HTTP', + 88: 'Kerberos', + 110: 'POP3', + 123: 'NTP', + 135: 'MSRPC', + 137: 'NetBIOS Name', + 138: 'NetBIOS Datagram', + 139: 'NetBIOS Session', + 143: 'IMAP', + 161: 'SNMP', + 162: 'SNMP Trap', + 389: 'LDAP', + 443: 'HTTPS', + 445: 'SMB', + 465: 'SMTPS', + 514: 'Syslog', + 515: 'LPD/LPR', + 587: 'SMTP Submission', + 636: 'LDAPS', + 993: 'IMAPS', + 995: 'POP3S', + 1433: 'MSSQL', + 1521: 'Oracle', + 1723: 'PPTP', + 1883: 'MQTT', + 1900: 'UPnP', + 2082: 'cPanel', + 2083: 'cPanel SSL', + 2086: 'WHM', + 2087: 'WHM SSL', + 2095: 'Webmail', + 2096: 'Webmail SSL', + 2181: 'ZooKeeper', + 2375: 'Docker', + 2376: 'Docker TLS', + 3000: 'Node.js', + 3306: 'MySQL', + 3389: 'RDP', + 4333: 'mSQL', + 4443: 'HTTPS Alt', + 4567: 'Sinatra', + 4848: 'GlassFish', + 5432: 'PostgreSQL', + 5500: 'VNC', + 5601: 'Kibana', + 5672: 'RabbitMQ', + 5900: 'VNC', + 5984: 'CouchDB', + 6379: 'Redis', + 6443: 'Kubernetes API', + 6666: 'IRC', + 6667: 'IRC', + 7000: 'Cassandra', + 7001: 'WebLogic', + 7077: 'Spark', + 7200: 'Cassandra Native', + 7474: 'Neo4j', + 7687: 'Neo4j Bolt', + 7777: 'Game Server', + 8000: 'HTTP Alt', + 8001: 'Tor', + 8080: 'HTTP Proxy', + 8081: 'HTTP Proxy Alt', + 8088: 'HTTP Alt', + 8089: 'HTTP Alt', + 8090: 'HTTP Alt', + 8091: 'HTTP Alt', + 8181: 'HTTP Alt', + 8443: 'HTTPS', + 8880: 'HTTP Alt', + 8888: 'HTTP Alt', + 8889: 'Sunyard HSM', + 9000: 'PHP-FPM', + 9001: 'Tor', + 9090: 'HTTP Alt', + 9091: 'HTTP Alt', + 9100: 'Print Server', + 9200: 'Elasticsearch', + 9300: 'Elasticsearch', + 9418: 'Git', + 9999: 'HTTP Alt', + 10000: 'Webmin', + 11211: 'Memcached', + 15672: 'RabbitMQ Management', + 27017: 'MongoDB', + 27018: 'MongoDB', + 28017: 'MongoDB HTTP', + 50000: 'SAP', + 50030: 'Hadoop JobTracker', + 50070: 'Hadoop NameNode', + 61616: 'ActiveMQ', + // 添加更多端口... + }; + + return commonServices[port] || `Unknown (${port})`; +} + + module.exports = { - killport, getport, telnet, checkip + killport, getport, telnet, checkip, scan } \ No newline at end of file