From d518fa9115b1b20972ebab575d3fbd4b7503cff9 Mon Sep 17 00:00:00 2001 From: Shahrad Elahi Date: Sun, 21 Apr 2024 03:33:08 +0330 Subject: [PATCH] feat: add `netstat` utils (#18) --- .github/workflows/ci.yml | 2 ++ package.json | 10 ++++++++ src/index.ts | 1 + src/netstat/index.ts | 51 +++++++++++++++++++++++++++++++++++++ src/utils/array.ts | 9 +++++++ tests/netstat/index.test.ts | 21 +++++++++++++++ tsconfig.json | 4 ++- tsup.config.ts | 2 +- 8 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/netstat/index.ts create mode 100644 src/utils/array.ts create mode 100644 tests/netstat/index.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fb8e7c..8ee46e8 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,7 @@ jobs: strategy: matrix: node-version: [18, 20] + name: node ${{ matrix.node-version }} steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 @@ -55,4 +56,5 @@ jobs: cache: 'pnpm' - run: pnpm install --frozen-lockfile + - run: pnpm build - run: pnpm test diff --git a/package.json b/package.json index dba71be..2a75230 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,16 @@ "types": "./dist/ip/index.d.cts", "default": "./dist/ip/index.cjs" } + }, + "./netstat": { + "import": { + "types": "./dist/netstat/index.d.ts", + "default": "./dist/netstat/index.js" + }, + "require": { + "types": "./dist/netstat/index.d.cts", + "default": "./dist/netstat/index.cjs" + } } }, "author": "Shahrad Elahi (https://github.com/shahradelahi)", diff --git a/src/index.ts b/src/index.ts index 904880a..f09e195 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export * as ip from './ip'; +export * as netstat from './netstat'; diff --git a/src/netstat/index.ts b/src/netstat/index.ts new file mode 100644 index 0000000..ffb3d38 --- /dev/null +++ b/src/netstat/index.ts @@ -0,0 +1,51 @@ +import { execShell } from '@/utils/exec-shell'; +import { removeDuplicate } from '@/utils/array'; + +// ------------------------ + +export interface ActiveConnection { + protocol: string; + program?: string; + recvQ: number; + sendQ: number; + address: string; + port: number; + state?: string; +} + +export async function activeConnections(): Promise { + const { error, data } = await execShell(['netstat', '-tuapn']); + if (error) { + throw error; + } + + const lines = data.output.split('\n'); + const ports: ActiveConnection[] = []; + for (const line of lines) { + const match = + /^(\w+)\s+(\d+)\s+(\d+)\s+((?:::)?(?:(?:(?:\d{1,3}\.){3}(?:\d{1,3}){1})?[0-9a-f]{0,4}:{0,2}){1,8}(?:::)?)\s+((?:::)?(?:(?:(?:\d{1,3}\.){3}(?:\d{1,3}){1})?[0-9a-f]{0,4}:{0,2}){1,8}(?:::)?\*?)\s+(\w+)?\s+(.*)$/.exec( + line + ); + if (match) { + const port = match[4].split(':').at(-1); + const address = match[4].replace(`:${port}`, ''); + const connection: ActiveConnection = { + protocol: match[1], + recvQ: Number(match[2]), + sendQ: Number(match[3]), + address: address, + port: Number(port), + state: match[6] ? match[6].trim() : undefined, + program: match[7].trim(), + }; + ports.push(connection); + } + } + + return ports; +} + +export async function allocatedPorts(): Promise { + const cns = await activeConnections(); + return removeDuplicate(cns.map((cn) => cn.port)); +} diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 0000000..e2c08b7 --- /dev/null +++ b/src/utils/array.ts @@ -0,0 +1,9 @@ +export function removeDuplicate(d: T[]): T[] { + const final: T[] = []; + for (const item of d) { + if (!final.includes(item)) { + final.push(item); + } + } + return final; +} diff --git a/tests/netstat/index.test.ts b/tests/netstat/index.test.ts new file mode 100644 index 0000000..71563ca --- /dev/null +++ b/tests/netstat/index.test.ts @@ -0,0 +1,21 @@ +import { expect } from 'chai'; +import { IPV4_REGEX } from '@/ip'; +import { activeConnections, allocatedPorts } from 'node-netkit/netstat'; + +describe('Active Connections', () => { + it('should get active connections', async () => { + const connections = await activeConnections(); + expect(connections).to.be.an('array'); + expect(connections.length).to.have.greaterThan(0); + + expect(Object.keys(connections[0])).to.have.lengthOf(7); + expect(connections[0].address).to.match(IPV4_REGEX); + }); + + it('should get allocated ports', async () => { + for (const p of await allocatedPorts()) { + expect(p).to.be.greaterThan(0); + expect(p).to.be.lessThan(65535); + } + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 8b68bc7..c4a6bff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,9 @@ "allowJs": true, "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": ["src/*"], + "node-netkit/*": ["dist/*"], + "node-netkit": ["dist/index.js"] }, "outDir": "build" }, diff --git a/tsup.config.ts b/tsup.config.ts index 04ef366..c73ac16 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ clean: true, dts: true, - entry: ['src/index.ts', 'src/ip/index.ts'], + entry: ['src/index.ts', 'src/ip/index.ts', 'src/netstat/index.ts'], format: ['cjs', 'esm'], target: 'esnext', outDir: 'dist',