@johannes-berggren/netprobe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/package.json +50 -0
- package/src/commands/scan.ts +107 -0
- package/src/index.ts +52 -0
- package/src/scanners/connection.ts +33 -0
- package/src/scanners/devices.ts +76 -0
- package/src/scanners/ports.ts +66 -0
- package/src/scanners/speed.ts +85 -0
- package/src/ui/output.ts +131 -0
- package/src/ui/spinner.ts +29 -0
- package/src/utils/exec.ts +10 -0
- package/src/utils/format.ts +18 -0
- package/src/utils/vendor.ts +1442 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Johannes Berggren
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# netprobe
|
|
2
|
+
|
|
3
|
+
Network Swiss Army Knife - A single command for comprehensive network diagnostics.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
╭──────────────────────────────────╮
|
|
7
|
+
│ NETPROBE - Network Diagnostics │
|
|
8
|
+
╰──────────────────────────────────╯
|
|
9
|
+
┌─ CONNECTION ────────────────────────────────────────────────┐
|
|
10
|
+
│ Interface: Wi-Fi (en0) │
|
|
11
|
+
│ Local IP: 192.168.0.31 │
|
|
12
|
+
│ Gateway: 192.168.0.1 │
|
|
13
|
+
│ External IP: 85.123.45.67 │
|
|
14
|
+
│ DNS: 1.1.1.1, 8.8.8.8 │
|
|
15
|
+
└──────────────────────────────────────────────────────────────┘
|
|
16
|
+
┌─ SPEED ─────────────────────────────────────────────────────┐
|
|
17
|
+
│ ↓ Download: 245.3 Mbps │
|
|
18
|
+
│ ↑ Upload: 48.7 Mbps │
|
|
19
|
+
│ Latency: 12ms (jitter: 2.3ms) │
|
|
20
|
+
└──────────────────────────────────────────────────────────────┘
|
|
21
|
+
┌─ DEVICES (4 found) ─────────────────────────────────────────┐
|
|
22
|
+
│ IP │ MAC │ Vendor │ Name │
|
|
23
|
+
├─────────────────┼────────────────────┼──────────────┼────────┤
|
|
24
|
+
│ 192.168.0.1 │ F0:81:75:22:32:22 │ Sagemcom │ - │
|
|
25
|
+
│ 192.168.0.7 │ 00:17:88:2E:09:31 │ Philips │ - │
|
|
26
|
+
│ 192.168.0.31 │ 80:A9:97:35:64:71 │ Apple │ This Mac│
|
|
27
|
+
└─────────────────┴────────────────────┴──────────────┴────────┘
|
|
28
|
+
┌─ GATEWAY PORTS (192.168.0.1) ───────────────────────────────┐
|
|
29
|
+
│ 53/tcp DNS OPEN │
|
|
30
|
+
│ 80/tcp HTTP OPEN │
|
|
31
|
+
└──────────────────────────────────────────────────────────────┘
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Connection Info** - Local IP, gateway, external IP, DNS servers
|
|
37
|
+
- **Device Discovery** - Find all devices on your network via ARP with vendor identification
|
|
38
|
+
- **Speed Test** - Download/upload speeds and latency via Cloudflare
|
|
39
|
+
- **Port Scanner** - Scan common ports on any host
|
|
40
|
+
- **Beautiful Output** - Clean terminal UI with colors and tables
|
|
41
|
+
- **JSON Output** - Pipe results to other tools
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
### Using Bun (recommended)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Run directly without installing
|
|
49
|
+
bunx @jberggren/netprobe
|
|
50
|
+
|
|
51
|
+
# Or install globally
|
|
52
|
+
bun install -g @jberggren/netprobe
|
|
53
|
+
netprobe
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Using npm
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Run directly
|
|
60
|
+
npx @jberggren/netprobe
|
|
61
|
+
|
|
62
|
+
# Or install globally
|
|
63
|
+
npm install -g @jberggren/netprobe
|
|
64
|
+
netprobe
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### From source
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/jberggren/netprobe.git
|
|
71
|
+
cd netprobe
|
|
72
|
+
bun install
|
|
73
|
+
bun link
|
|
74
|
+
netprobe
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Full network scan (connection, speed, devices, ports)
|
|
81
|
+
netprobe
|
|
82
|
+
|
|
83
|
+
# Individual scans
|
|
84
|
+
netprobe -d # Devices only
|
|
85
|
+
netprobe -s # Speed test only
|
|
86
|
+
netprobe -p # Gateway ports only
|
|
87
|
+
|
|
88
|
+
# Scan specific host
|
|
89
|
+
netprobe -p -t 192.168.0.7
|
|
90
|
+
|
|
91
|
+
# JSON output for scripting
|
|
92
|
+
netprobe --json
|
|
93
|
+
netprobe -d --json | jq '.devices[] | select(.vendor == "Apple")'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Options
|
|
97
|
+
|
|
98
|
+
| Flag | Short | Description |
|
|
99
|
+
|------|-------|-------------|
|
|
100
|
+
| `--all` | `-a` | Run all scans (default) |
|
|
101
|
+
| `--devices` | `-d` | Scan for network devices |
|
|
102
|
+
| `--speed` | `-s` | Run speed test |
|
|
103
|
+
| `--ports` | `-p` | Scan gateway ports |
|
|
104
|
+
| `--target <ip>` | `-t` | Scan specific IP for ports |
|
|
105
|
+
| `--json` | | Output as JSON |
|
|
106
|
+
| `--help` | `-h` | Show help |
|
|
107
|
+
|
|
108
|
+
## Requirements
|
|
109
|
+
|
|
110
|
+
- **Bun** >= 1.0 or **Node.js** >= 18
|
|
111
|
+
- **macOS** (Linux support coming soon)
|
|
112
|
+
|
|
113
|
+
## How it works
|
|
114
|
+
|
|
115
|
+
- **Connection**: Uses `ipconfig`, `netstat`, and ipify.org API
|
|
116
|
+
- **Devices**: Parses the ARP table with MAC vendor lookup (1000+ vendors)
|
|
117
|
+
- **Speed**: Tests against Cloudflare's speed test endpoints
|
|
118
|
+
- **Ports**: TCP connect scan on common service ports
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@johannes-berggren/netprobe",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Network Swiss Army Knife - CLI tool for comprehensive network diagnostics",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"netprobe": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "bun run src/index.ts",
|
|
11
|
+
"dev": "bun run --watch src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"network",
|
|
15
|
+
"diagnostics",
|
|
16
|
+
"cli",
|
|
17
|
+
"speed-test",
|
|
18
|
+
"port-scanner",
|
|
19
|
+
"arp",
|
|
20
|
+
"devices",
|
|
21
|
+
"terminal"
|
|
22
|
+
],
|
|
23
|
+
"author": "Johannes Berggren",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Johannes-Berggren/netprobe.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/Johannes-Berggren/netprobe/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/Johannes-Berggren/netprobe#readme",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18",
|
|
35
|
+
"bun": ">=1.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^5.3.0",
|
|
43
|
+
"boxen": "^7.1.1",
|
|
44
|
+
"cli-table3": "^0.6.3",
|
|
45
|
+
"ora": "^8.0.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/bun": "latest"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { getConnectionInfo } from '../scanners/connection';
|
|
2
|
+
import { scanDevices } from '../scanners/devices';
|
|
3
|
+
import { runSpeedTest } from '../scanners/speed';
|
|
4
|
+
import { scanPorts } from '../scanners/ports';
|
|
5
|
+
import {
|
|
6
|
+
header,
|
|
7
|
+
connectionSection,
|
|
8
|
+
speedSection,
|
|
9
|
+
devicesSection,
|
|
10
|
+
portsSection,
|
|
11
|
+
outputJson,
|
|
12
|
+
type ScanResults,
|
|
13
|
+
} from '../ui/output';
|
|
14
|
+
import { startSpinner, updateSpinner, stopSpinner } from '../ui/spinner';
|
|
15
|
+
|
|
16
|
+
export interface ScanOptions {
|
|
17
|
+
devices: boolean;
|
|
18
|
+
speed: boolean;
|
|
19
|
+
ports: boolean;
|
|
20
|
+
json: boolean;
|
|
21
|
+
target?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function scan(options: ScanOptions): Promise<void> {
|
|
25
|
+
const results: ScanResults = {};
|
|
26
|
+
|
|
27
|
+
// Always get connection info first (needed for other scans)
|
|
28
|
+
if (!options.json) {
|
|
29
|
+
startSpinner('Getting connection info...');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
results.connection = await getConnectionInfo();
|
|
34
|
+
results.gateway = options.target || results.connection.gateway;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
stopSpinner();
|
|
37
|
+
if (!options.json) {
|
|
38
|
+
console.error('Failed to get connection info');
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Run device scan
|
|
44
|
+
if (options.devices) {
|
|
45
|
+
if (!options.json) {
|
|
46
|
+
updateSpinner('Scanning for devices...');
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
results.devices = await scanDevices(results.connection.localIP);
|
|
50
|
+
} catch {
|
|
51
|
+
// Continue with other scans
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Run speed test
|
|
56
|
+
if (options.speed) {
|
|
57
|
+
if (!options.json) {
|
|
58
|
+
updateSpinner('Running speed test...');
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
results.speed = await runSpeedTest((stage) => {
|
|
62
|
+
if (!options.json) {
|
|
63
|
+
updateSpinner(stage);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
// Continue with other scans
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Run port scan
|
|
72
|
+
if (options.ports && results.gateway) {
|
|
73
|
+
if (!options.json) {
|
|
74
|
+
updateSpinner(`Scanning ports on ${results.gateway}...`);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
results.ports = await scanPorts(results.gateway);
|
|
78
|
+
} catch {
|
|
79
|
+
// Continue
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
stopSpinner();
|
|
84
|
+
|
|
85
|
+
// Output results
|
|
86
|
+
if (options.json) {
|
|
87
|
+
outputJson(results);
|
|
88
|
+
} else {
|
|
89
|
+
header();
|
|
90
|
+
|
|
91
|
+
if (results.connection) {
|
|
92
|
+
connectionSection(results.connection);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (results.speed) {
|
|
96
|
+
speedSection(results.speed);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (results.devices) {
|
|
100
|
+
devicesSection(results.devices);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (results.ports !== undefined && results.gateway) {
|
|
104
|
+
portsSection(results.ports, results.gateway);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { scan } from './commands/scan';
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
7
|
+
console.log(`
|
|
8
|
+
${'\x1b[36m'}netprobe${'\x1b[0m'} - Network Swiss Army Knife
|
|
9
|
+
|
|
10
|
+
${'\x1b[1m'}Usage:${'\x1b[0m'} netprobe [options]
|
|
11
|
+
|
|
12
|
+
${'\x1b[1m'}Options:${'\x1b[0m'}
|
|
13
|
+
--all, -a Run all scans (default)
|
|
14
|
+
--devices, -d Only scan for devices
|
|
15
|
+
--speed, -s Only run speed test
|
|
16
|
+
--ports, -p Only scan gateway ports
|
|
17
|
+
--target, -t <ip> Scan specific IP for ports
|
|
18
|
+
--json Output as JSON
|
|
19
|
+
--help, -h Show help
|
|
20
|
+
|
|
21
|
+
${'\x1b[1m'}Examples:${'\x1b[0m'}
|
|
22
|
+
netprobe Full network scan
|
|
23
|
+
netprobe -d List network devices only
|
|
24
|
+
netprobe -s Speed test only
|
|
25
|
+
netprobe -p Scan gateway ports
|
|
26
|
+
netprobe -p -t 192.168.0.7 Scan ports on specific host
|
|
27
|
+
netprobe --json Output results as JSON
|
|
28
|
+
`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Parse target option
|
|
33
|
+
let target: string | undefined;
|
|
34
|
+
const targetIndex = args.findIndex(a => a === '-t' || a === '--target');
|
|
35
|
+
if (targetIndex !== -1 && args[targetIndex + 1]) {
|
|
36
|
+
target = args[targetIndex + 1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Determine which scans to run
|
|
40
|
+
const hasSpecificFlags = args.includes('-d') || args.includes('--devices') ||
|
|
41
|
+
args.includes('-s') || args.includes('--speed') ||
|
|
42
|
+
args.includes('-p') || args.includes('--ports');
|
|
43
|
+
|
|
44
|
+
const runAll = args.includes('-a') || args.includes('--all') || !hasSpecificFlags;
|
|
45
|
+
|
|
46
|
+
await scan({
|
|
47
|
+
devices: runAll || args.includes('-d') || args.includes('--devices'),
|
|
48
|
+
speed: runAll || args.includes('-s') || args.includes('--speed'),
|
|
49
|
+
ports: runAll || args.includes('-p') || args.includes('--ports'),
|
|
50
|
+
json: args.includes('--json'),
|
|
51
|
+
target,
|
|
52
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { exec } from '../utils/exec';
|
|
2
|
+
|
|
3
|
+
export interface ConnectionInfo {
|
|
4
|
+
interface: string;
|
|
5
|
+
localIP: string;
|
|
6
|
+
gateway: string;
|
|
7
|
+
externalIP: string;
|
|
8
|
+
dns: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function getConnectionInfo(): Promise<ConnectionInfo> {
|
|
12
|
+
// Try to find the active interface
|
|
13
|
+
const [localIPEn0, localIPEn1, gateway, externalIP, dns] = await Promise.all([
|
|
14
|
+
exec('ipconfig getifaddr en0'),
|
|
15
|
+
exec('ipconfig getifaddr en1'),
|
|
16
|
+
exec("netstat -nr | grep default | head -1 | awk '{print $2}'"),
|
|
17
|
+
fetch('https://api.ipify.org', { signal: AbortSignal.timeout(5000) })
|
|
18
|
+
.then(r => r.text())
|
|
19
|
+
.catch(() => 'Unknown'),
|
|
20
|
+
exec("scutil --dns | grep 'nameserver\\[' | head -4 | awk '{print $3}'"),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const localIP = localIPEn0.trim() || localIPEn1.trim() || 'Unknown';
|
|
24
|
+
const interfaceName = localIPEn0.trim() ? 'Wi-Fi (en0)' : localIPEn1.trim() ? 'Ethernet (en1)' : 'Unknown';
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
interface: interfaceName,
|
|
28
|
+
localIP,
|
|
29
|
+
gateway: gateway.trim() || 'Unknown',
|
|
30
|
+
externalIP: externalIP.trim(),
|
|
31
|
+
dns: dns.trim().split('\n').filter(Boolean),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { exec } from '../utils/exec';
|
|
2
|
+
import { lookupVendor } from '../utils/vendor';
|
|
3
|
+
|
|
4
|
+
export interface Device {
|
|
5
|
+
ip: string;
|
|
6
|
+
mac: string;
|
|
7
|
+
vendor: string;
|
|
8
|
+
hostname?: string;
|
|
9
|
+
isCurrentDevice?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Normalize MAC address to have leading zeros (0:17:88 -> 00:17:88)
|
|
13
|
+
function normalizeMac(mac: string): string {
|
|
14
|
+
return mac
|
|
15
|
+
.split(':')
|
|
16
|
+
.map(part => part.padStart(2, '0'))
|
|
17
|
+
.join(':')
|
|
18
|
+
.toUpperCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if IP is a multicast or broadcast address
|
|
22
|
+
function isSpecialAddress(ip: string): boolean {
|
|
23
|
+
const firstOctet = parseInt(ip.split('.')[0]);
|
|
24
|
+
// Filter multicast (224-239), broadcast (.255), and link-local (169.254)
|
|
25
|
+
if (firstOctet >= 224 && firstOctet <= 239) return true;
|
|
26
|
+
if (ip.endsWith('.255')) return true;
|
|
27
|
+
if (ip.startsWith('169.254.')) return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function scanDevices(localIP?: string): Promise<Device[]> {
|
|
32
|
+
// Get current device's MAC
|
|
33
|
+
const ifconfigOutput = await exec('ifconfig en0 | grep ether');
|
|
34
|
+
const currentMacMatch = ifconfigOutput.match(/ether\s+([0-9a-f:]+)/i);
|
|
35
|
+
const currentMac = currentMacMatch ? normalizeMac(currentMacMatch[1]) : '';
|
|
36
|
+
|
|
37
|
+
// Read ARP table
|
|
38
|
+
const arpOutput = await exec('arp -a');
|
|
39
|
+
|
|
40
|
+
// Parse: "? (192.168.0.1) at f0:81:75:22:32:22 on en0 [ethernet]"
|
|
41
|
+
// or: "hostname (192.168.0.1) at f0:81:75:22:32:22 on en0 [ethernet]"
|
|
42
|
+
const devices = arpOutput
|
|
43
|
+
.split('\n')
|
|
44
|
+
.filter(line => line.includes(' at ') && !line.includes('incomplete'))
|
|
45
|
+
.map(line => {
|
|
46
|
+
const ipMatch = line.match(/\(([0-9.]+)\)/);
|
|
47
|
+
const macMatch = line.match(/at ([0-9a-f:]+)/i);
|
|
48
|
+
const hostMatch = line.match(/^(\S+)\s+\(/);
|
|
49
|
+
|
|
50
|
+
if (!ipMatch || !macMatch) return null;
|
|
51
|
+
|
|
52
|
+
const ip = ipMatch[1];
|
|
53
|
+
|
|
54
|
+
// Skip multicast and broadcast addresses
|
|
55
|
+
if (isSpecialAddress(ip)) return null;
|
|
56
|
+
|
|
57
|
+
const mac = normalizeMac(macMatch[1]);
|
|
58
|
+
const isCurrentDevice = localIP === ip || mac === currentMac;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
ip,
|
|
62
|
+
mac,
|
|
63
|
+
vendor: lookupVendor(mac),
|
|
64
|
+
hostname: hostMatch?.[1] !== '?' ? hostMatch[1] : undefined,
|
|
65
|
+
isCurrentDevice,
|
|
66
|
+
};
|
|
67
|
+
})
|
|
68
|
+
.filter(Boolean) as Device[];
|
|
69
|
+
|
|
70
|
+
// Sort by IP numerically
|
|
71
|
+
return devices.sort((a, b) => {
|
|
72
|
+
const aNum = a.ip.split('.').reduce((acc, n, i) => acc + parseInt(n) * Math.pow(256, 3 - i), 0);
|
|
73
|
+
const bNum = b.ip.split('.').reduce((acc, n, i) => acc + parseInt(n) * Math.pow(256, 3 - i), 0);
|
|
74
|
+
return aNum - bNum;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { connect, type Socket } from 'net';
|
|
2
|
+
|
|
3
|
+
const COMMON_PORTS: Record<number, string> = {
|
|
4
|
+
21: 'FTP',
|
|
5
|
+
22: 'SSH',
|
|
6
|
+
23: 'Telnet',
|
|
7
|
+
25: 'SMTP',
|
|
8
|
+
53: 'DNS',
|
|
9
|
+
80: 'HTTP',
|
|
10
|
+
110: 'POP3',
|
|
11
|
+
143: 'IMAP',
|
|
12
|
+
443: 'HTTPS',
|
|
13
|
+
445: 'SMB',
|
|
14
|
+
548: 'AFP',
|
|
15
|
+
3306: 'MySQL',
|
|
16
|
+
3389: 'RDP',
|
|
17
|
+
5432: 'PostgreSQL',
|
|
18
|
+
5900: 'VNC',
|
|
19
|
+
8080: 'HTTP-Alt',
|
|
20
|
+
8443: 'HTTPS-Alt',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface PortResult {
|
|
24
|
+
port: number;
|
|
25
|
+
service: string;
|
|
26
|
+
open: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function scanPorts(
|
|
30
|
+
host: string,
|
|
31
|
+
ports = Object.keys(COMMON_PORTS).map(Number)
|
|
32
|
+
): Promise<PortResult[]> {
|
|
33
|
+
const results = await Promise.all(
|
|
34
|
+
ports.map(port => checkPort(host, port))
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return results
|
|
38
|
+
.filter(r => r.open)
|
|
39
|
+
.map(r => ({ ...r, service: COMMON_PORTS[r.port] || 'Unknown' }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function checkPort(host: string, port: number, timeout = 1000): Promise<PortResult> {
|
|
43
|
+
return new Promise(resolve => {
|
|
44
|
+
const socket: Socket = connect({ host, port, timeout });
|
|
45
|
+
|
|
46
|
+
const cleanup = () => {
|
|
47
|
+
socket.removeAllListeners();
|
|
48
|
+
socket.destroy();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
socket.on('connect', () => {
|
|
52
|
+
cleanup();
|
|
53
|
+
resolve({ port, service: '', open: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
socket.on('error', () => {
|
|
57
|
+
cleanup();
|
|
58
|
+
resolve({ port, service: '', open: false });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
socket.on('timeout', () => {
|
|
62
|
+
cleanup();
|
|
63
|
+
resolve({ port, service: '', open: false });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export interface SpeedResult {
|
|
2
|
+
download: number; // Mbps
|
|
3
|
+
upload: number; // Mbps
|
|
4
|
+
latency: number; // ms
|
|
5
|
+
jitter: number; // ms
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function runSpeedTest(
|
|
9
|
+
onProgress?: (stage: string) => void
|
|
10
|
+
): Promise<SpeedResult> {
|
|
11
|
+
// Latency test
|
|
12
|
+
onProgress?.('Testing latency...');
|
|
13
|
+
const latencies = await Promise.all([
|
|
14
|
+
pingHost('1.1.1.1'),
|
|
15
|
+
pingHost('8.8.8.8'),
|
|
16
|
+
pingHost('208.67.222.222'),
|
|
17
|
+
]);
|
|
18
|
+
const validLatencies = latencies.filter(l => l > 0);
|
|
19
|
+
const latency = validLatencies.length > 0 ? average(validLatencies) : 0;
|
|
20
|
+
const jitter = validLatencies.length > 1 ? standardDeviation(validLatencies) : 0;
|
|
21
|
+
|
|
22
|
+
// Download test using Cloudflare's speed test endpoint
|
|
23
|
+
onProgress?.('Testing download...');
|
|
24
|
+
const downloadBytes = 10_000_000; // 10MB for faster results
|
|
25
|
+
const downloadStart = performance.now();
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(`https://speed.cloudflare.com/__down?bytes=${downloadBytes}`, {
|
|
29
|
+
signal: AbortSignal.timeout(30000),
|
|
30
|
+
});
|
|
31
|
+
await response.arrayBuffer(); // Consume the response
|
|
32
|
+
} catch {
|
|
33
|
+
// If Cloudflare fails, return partial results
|
|
34
|
+
return { download: 0, upload: 0, latency, jitter };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const downloadTime = (performance.now() - downloadStart) / 1000;
|
|
38
|
+
const download = (downloadBytes * 8) / downloadTime / 1_000_000;
|
|
39
|
+
|
|
40
|
+
// Upload test
|
|
41
|
+
onProgress?.('Testing upload...');
|
|
42
|
+
const uploadData = new Uint8Array(5_000_000); // 5MB
|
|
43
|
+
const uploadStart = performance.now();
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await fetch('https://speed.cloudflare.com/__up', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: uploadData,
|
|
49
|
+
signal: AbortSignal.timeout(30000),
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
return { download, upload: 0, latency, jitter };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const uploadTime = (performance.now() - uploadStart) / 1000;
|
|
56
|
+
const upload = (uploadData.length * 8) / uploadTime / 1_000_000;
|
|
57
|
+
|
|
58
|
+
return { download, upload, latency, jitter };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function pingHost(host: string): Promise<number> {
|
|
62
|
+
const start = performance.now();
|
|
63
|
+
try {
|
|
64
|
+
await fetch(`https://${host}`, {
|
|
65
|
+
method: 'HEAD',
|
|
66
|
+
mode: 'no-cors',
|
|
67
|
+
signal: AbortSignal.timeout(5000),
|
|
68
|
+
});
|
|
69
|
+
return performance.now() - start;
|
|
70
|
+
} catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function average(arr: number[]): number {
|
|
76
|
+
if (arr.length === 0) return 0;
|
|
77
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function standardDeviation(arr: number[]): number {
|
|
81
|
+
if (arr.length < 2) return 0;
|
|
82
|
+
const avg = average(arr);
|
|
83
|
+
const squareDiffs = arr.map(value => Math.pow(value - avg, 2));
|
|
84
|
+
return Math.sqrt(average(squareDiffs));
|
|
85
|
+
}
|