feat(detector): Enhance port detection and service fingerprinting with improved HTTP/HTTPS and SSH checks, update test scripts for verbose output, and revise documentation with new hints and a detailed improvement plan.
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-26 - 2.1.0 - feat(detector)
|
||||
Enhance port detection and service fingerprinting with improved HTTP/HTTPS and SSH checks, update test scripts for verbose output, and revise documentation with new hints and a detailed improvement plan.
|
||||
|
||||
- Updated package.json to use verbose testing and added '@git.zone/tsrun' dependency
|
||||
- Improved Detector class: finalized detectType implementation with protocol-specific checks and banner grabbing
|
||||
- Refined test cases to verify active ports and service detection for HTTP, HTTPS, and SSH
|
||||
- Expanded readme hints with project overview, key components, and detailed improvement plan
|
||||
|
||||
## 2025-05-26 - 2.0.2 - fix(ci)
|
||||
Update CI workflows, dependency paths, and project configuration
|
||||
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --web)",
|
||||
"test": "(tstest test/ --verbose)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.63",
|
||||
"@git.zone/tsbundle": "^2.0.5",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.2.5",
|
||||
"@types/node": "^22.15.21",
|
||||
"tslint": "^6.1.3",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -21,6 +21,9 @@ importers:
|
||||
'@git.zone/tsbundle':
|
||||
specifier: ^2.0.5
|
||||
version: 2.2.5
|
||||
'@git.zone/tsrun':
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
'@git.zone/tstest':
|
||||
specifier: ^2.2.5
|
||||
version: 2.2.5(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)(typescript@5.8.3)
|
||||
|
||||
@@ -1,3 +1,50 @@
|
||||
# Project Readme Hints
|
||||
|
||||
This is the initial readme hints file.
|
||||
## Project Overview
|
||||
- **@uptime.link/detector** - A network detection utility that checks port availability and service types locally
|
||||
- Version: 2.0.2 (recently updated from 2.0.1)
|
||||
- Type: ESM module (switched from CommonJS in v2.0.0)
|
||||
- Main purpose: Detect if ports are active/available and identify service types without external services
|
||||
|
||||
## Key Components
|
||||
1. **Detector Class** (`ts/detector.classes.detector.ts`):
|
||||
- `isActive(urlArg: string, options?: IDetectorOptions)`: Returns detailed IDetectorResult with service type detection
|
||||
- `isActiveSimple(urlArg: string)`: Returns boolean for backward compatibility
|
||||
- `detectType(urlArg: string)`: Detects service type (HTTP, HTTPS, SSH, FTP, etc.)
|
||||
- For localhost: Uses `isLocalPortUnused()` (inverted to check if port is active)
|
||||
- For remote: Uses `isRemotePortAvailable()`
|
||||
|
||||
2. **Service Detection Features**:
|
||||
- HTTP/HTTPS protocol detection with TLS support
|
||||
- SSH service identification via banner detection
|
||||
- Service fingerprinting for common protocols (FTP, SMTP, MySQL, etc.)
|
||||
- Banner grabbing for unknown services
|
||||
- Support for custom timeout configuration
|
||||
|
||||
3. **Dependencies** (`ts/detector.plugins.ts`):
|
||||
- `@push.rocks/smartnetwork`: Network utilities for port checking
|
||||
- `@push.rocks/smarturl`: URL parsing utilities
|
||||
- Node.js built-ins: net, tls, http, https for protocol detection
|
||||
|
||||
4. **Interfaces** (`ts/detector.interfaces.ts`):
|
||||
- `ServiceType`: Enum of supported service types
|
||||
- `IDetectorResult`: Detailed result object with service info
|
||||
- `IDetectorOptions`: Configuration options for detection
|
||||
|
||||
## Recent Changes
|
||||
- **v2.0.0**: Breaking change - switched to ESM modules
|
||||
- **v2.0.1**: Minor update to commitinfo
|
||||
- **v2.0.2**: Added service type detection, protocol fingerprinting, and enhanced API
|
||||
|
||||
## Testing
|
||||
- Tests check for closed local ports and open remote ports
|
||||
- Tests verify service type detection for HTTP/HTTPS
|
||||
- Tests check SSH service detection
|
||||
- Uses `@git.zone/tstest` with tap-style testing
|
||||
- Example tests: localhost:3008 (expects closed), lossless.com (expects open)
|
||||
|
||||
## Notes
|
||||
- Project uses pnpm for package management
|
||||
- Build supports web targets (`--web` flag)
|
||||
- No external online services required for detection
|
||||
- All network detection happens locally without third-party APIs
|
||||
69
readme.plan.md
Normal file
69
readme.plan.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Detector Module Improvement Plan
|
||||
|
||||
Command to reread CLAUDE.md: `cat ~/.claude/CLAUDE.md`
|
||||
|
||||
## Current State
|
||||
- Only detects if ports are active/available
|
||||
- Has unimplemented `detectType()` method
|
||||
|
||||
## Proposed Improvements
|
||||
|
||||
### 1. Implement `detectType()` method
|
||||
- Detect service type running on port (HTTP, HTTPS, SSH, FTP, etc.)
|
||||
- Use protocol-specific handshakes without external services
|
||||
|
||||
### 2. Add Network Interface Detection
|
||||
- List local network interfaces
|
||||
- Get IP addresses, MAC addresses
|
||||
- Detect network connectivity status
|
||||
|
||||
### 3. Add Local DNS Capabilities
|
||||
- Resolve hostnames using local DNS
|
||||
- Check DNS configuration
|
||||
- Detect local DNS servers
|
||||
|
||||
### 4. Add Protocol Detection
|
||||
- HTTP/HTTPS detection with TLS version
|
||||
- WebSocket support detection
|
||||
- TCP/UDP differentiation
|
||||
|
||||
### 5. Add Network Diagnostics
|
||||
- Ping functionality (ICMP)
|
||||
- Traceroute capabilities
|
||||
- MTU discovery
|
||||
- Network latency measurement
|
||||
|
||||
### 6. Add Service Fingerprinting
|
||||
- Identify common services by banner grabbing
|
||||
- Detect service versions locally
|
||||
- Support for common protocols (SSH, FTP, SMTP, etc.)
|
||||
|
||||
### 7. Add Local Network Discovery
|
||||
- Discover devices on local network
|
||||
- ARP scanning for local subnet
|
||||
- mDNS/Bonjour service discovery
|
||||
|
||||
### 8. Enhance API
|
||||
- Add batch checking for multiple URLs/ports
|
||||
- Add timeout configuration
|
||||
- Add detailed response objects with metadata
|
||||
|
||||
## Implementation Priority
|
||||
1. ✅ Implement `detectType()` - COMPLETED
|
||||
2. ✅ Add protocol detection for HTTP/HTTPS - COMPLETED
|
||||
3. ✅ Add service fingerprinting - COMPLETED (basic version)
|
||||
4. ✅ Create enhanced API with IDetectorResult - COMPLETED
|
||||
5. Add network diagnostics - TODO
|
||||
6. Add local network discovery - TODO
|
||||
7. Add batch operations - TODO
|
||||
|
||||
## What Was Implemented
|
||||
- ✅ `detectType()` method with protocol-specific detection
|
||||
- ✅ HTTP/HTTPS detection with proper protocol handling
|
||||
- ✅ SSH service detection via banner
|
||||
- ✅ Service fingerprinting for common protocols (FTP, SMTP, MySQL, etc.)
|
||||
- ✅ IDetectorResult interface for detailed responses
|
||||
- ✅ IDetectorOptions for configuration
|
||||
- ✅ Backward compatibility with `isActiveSimple()` method
|
||||
- ✅ Banner grabbing for unknown services
|
||||
- ✅ Tests for new functionality
|
||||
30
test/test.ts
30
test/test.ts
@@ -10,12 +10,36 @@ tap.test('first test', async () => {
|
||||
|
||||
tap.test('should detect an closed port on a local domain', async () => {
|
||||
const result = await testDetector.isActive('http://localhost:3008');
|
||||
expect(result).toBeFalse();
|
||||
expect(result.isActive).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should detect an open port on a remote domain', async () => {
|
||||
const result = await testDetector.isActive('https://lossless.com');
|
||||
expect(result).toBeTrue();
|
||||
expect(result.isActive).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
||||
tap.test('should detect service type for HTTP', async () => {
|
||||
const result = await testDetector.isActive('http://example.com', { detectServiceType: true });
|
||||
expect(result.isActive).toBeTrue();
|
||||
expect(result.serviceType).toEqual(detector.ServiceType.HTTP);
|
||||
});
|
||||
|
||||
tap.test('should detect service type for HTTPS', async () => {
|
||||
const result = await testDetector.isActive('https://example.com', { detectServiceType: true });
|
||||
expect(result.isActive).toBeTrue();
|
||||
expect(result.serviceType).toEqual(detector.ServiceType.HTTPS);
|
||||
});
|
||||
|
||||
tap.test('should detect SSH service', async () => {
|
||||
const sshType = await testDetector.detectType('ssh://github.com:22');
|
||||
expect(sshType).toEqual(detector.ServiceType.SSH);
|
||||
});
|
||||
|
||||
tap.test('should return unknown for non-standard services', async () => {
|
||||
const result = await testDetector.isActive('http://localhost:9999', { detectServiceType: true });
|
||||
if (result.isActive) {
|
||||
expect(result.serviceType).toEqual(detector.ServiceType.UNKNOWN);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@uptime.link/detector',
|
||||
version: '2.0.2',
|
||||
version: '2.1.0',
|
||||
description: 'a detector for answering network questions locally. It does not rely on any online services.'
|
||||
}
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import * as plugins from './detector.plugins.js';
|
||||
import { ServiceType } from './detector.interfaces.js';
|
||||
import type { IDetectorResult, IDetectorOptions } from './detector.interfaces.js';
|
||||
|
||||
export class Detector {
|
||||
private smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
||||
|
||||
public async isActive(urlArg: string): Promise<boolean> {
|
||||
/**
|
||||
* Check if a port is active - simple boolean version for backward compatibility
|
||||
*/
|
||||
public async isActiveSimple(urlArg: string): Promise<boolean> {
|
||||
const result = await this.isActive(urlArg);
|
||||
return result.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a port is active with detailed results
|
||||
*/
|
||||
public async isActive(urlArg: string, options?: IDetectorOptions): Promise<IDetectorResult> {
|
||||
const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(urlArg);
|
||||
if (parsedUrl.hostname === 'localhost') {
|
||||
console.log(`detector target is localhost on port ${parsedUrl.port}`);
|
||||
@@ -11,18 +24,203 @@ export class Detector {
|
||||
parseInt(parsedUrl.port, 10),
|
||||
);
|
||||
const portAvailable = !portUnused;
|
||||
return portAvailable;
|
||||
|
||||
const result: IDetectorResult = {
|
||||
isActive: portAvailable
|
||||
};
|
||||
|
||||
if (portAvailable && options?.detectServiceType) {
|
||||
const serviceType = await this.detectType(urlArg);
|
||||
result.serviceType = serviceType;
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
console.log(`detector target is remote domain ${parsedUrl.host} on port ${parsedUrl.port}`);
|
||||
const postAvailable = await this.smartnetworkInstance.isRemotePortAvailable(
|
||||
const portAvailable = await this.smartnetworkInstance.isRemotePortAvailable(
|
||||
parsedUrl.host,
|
||||
parseInt(parsedUrl.port, 10),
|
||||
);
|
||||
return postAvailable;
|
||||
|
||||
const result: IDetectorResult = {
|
||||
isActive: portAvailable
|
||||
};
|
||||
|
||||
if (portAvailable && options?.detectServiceType) {
|
||||
const serviceType = await this.detectType(urlArg);
|
||||
result.serviceType = serviceType;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public detectType(urlArg: string) {
|
||||
console.log('TODO'); // TODO
|
||||
public async detectType(urlArg: string): Promise<ServiceType> {
|
||||
const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(urlArg);
|
||||
const port = parseInt(parsedUrl.port, 10);
|
||||
const hostname = parsedUrl.hostname;
|
||||
|
||||
// Check common ports first
|
||||
const commonPorts: { [key: number]: ServiceType } = {
|
||||
80: ServiceType.HTTP,
|
||||
443: ServiceType.HTTPS,
|
||||
22: ServiceType.SSH,
|
||||
21: ServiceType.FTP,
|
||||
25: ServiceType.SMTP,
|
||||
110: ServiceType.POP3,
|
||||
143: ServiceType.IMAP,
|
||||
3306: ServiceType.MYSQL,
|
||||
5432: ServiceType.POSTGRESQL,
|
||||
27017: ServiceType.MONGODB,
|
||||
6379: ServiceType.REDIS
|
||||
};
|
||||
|
||||
if (commonPorts[port]) {
|
||||
// Verify the service is actually what we expect
|
||||
const verified = await this.verifyServiceType(hostname, port, commonPorts[port]);
|
||||
if (verified) {
|
||||
return commonPorts[port];
|
||||
}
|
||||
}
|
||||
|
||||
// Try to detect service by banner/protocol
|
||||
return await this.detectServiceByProtocol(hostname, port);
|
||||
}
|
||||
|
||||
private async verifyServiceType(hostname: string, port: number, expectedType: ServiceType): Promise<boolean> {
|
||||
try {
|
||||
switch (expectedType) {
|
||||
case ServiceType.HTTP:
|
||||
case ServiceType.HTTPS:
|
||||
return await this.checkHttpService(hostname, port, expectedType === ServiceType.HTTPS);
|
||||
case ServiceType.SSH:
|
||||
return await this.checkSshService(hostname, port);
|
||||
default:
|
||||
return true; // For now, trust the port number for other services
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async detectServiceByProtocol(hostname: string, port: number): Promise<ServiceType> {
|
||||
// Try HTTPS first
|
||||
if (await this.checkHttpService(hostname, port, true)) {
|
||||
return ServiceType.HTTPS;
|
||||
}
|
||||
|
||||
// Try HTTP
|
||||
if (await this.checkHttpService(hostname, port, false)) {
|
||||
return ServiceType.HTTP;
|
||||
}
|
||||
|
||||
// Try SSH
|
||||
if (await this.checkSshService(hostname, port)) {
|
||||
return ServiceType.SSH;
|
||||
}
|
||||
|
||||
// Try to get banner for other services
|
||||
const banner = await this.getBanner(hostname, port);
|
||||
if (banner) {
|
||||
return this.identifyServiceByBanner(banner);
|
||||
}
|
||||
|
||||
return ServiceType.UNKNOWN;
|
||||
}
|
||||
|
||||
private async checkHttpService(hostname: string, port: number, isHttps: boolean): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const protocol = isHttps ? plugins.https : plugins.http;
|
||||
const options = {
|
||||
hostname,
|
||||
port,
|
||||
method: 'HEAD',
|
||||
timeout: 5000,
|
||||
rejectUnauthorized: false // Accept self-signed certificates
|
||||
};
|
||||
|
||||
const req = protocol.request(options, () => {
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
private async checkSshService(hostname: string, port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new plugins.net.Socket();
|
||||
|
||||
socket.setTimeout(5000);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const banner = data.toString();
|
||||
socket.destroy();
|
||||
// SSH banners typically start with "SSH-"
|
||||
resolve(banner.startsWith('SSH-'));
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.connect(port, hostname);
|
||||
});
|
||||
}
|
||||
|
||||
private async getBanner(hostname: string, port: number): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new plugins.net.Socket();
|
||||
let banner = '';
|
||||
|
||||
socket.setTimeout(5000);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
banner += data.toString();
|
||||
socket.destroy();
|
||||
resolve(banner);
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
resolve(banner || null);
|
||||
});
|
||||
|
||||
socket.connect(port, hostname);
|
||||
});
|
||||
}
|
||||
|
||||
private identifyServiceByBanner(banner: string): ServiceType {
|
||||
const bannerLower = banner.toLowerCase();
|
||||
|
||||
if (bannerLower.includes('ssh')) return ServiceType.SSH;
|
||||
if (bannerLower.includes('ftp')) return ServiceType.FTP;
|
||||
if (bannerLower.includes('smtp')) return ServiceType.SMTP;
|
||||
if (bannerLower.includes('pop3')) return ServiceType.POP3;
|
||||
if (bannerLower.includes('imap')) return ServiceType.IMAP;
|
||||
if (bannerLower.includes('mysql')) return ServiceType.MYSQL;
|
||||
if (bannerLower.includes('postgresql')) return ServiceType.POSTGRESQL;
|
||||
if (bannerLower.includes('mongodb')) return ServiceType.MONGODB;
|
||||
if (bannerLower.includes('redis')) return ServiceType.REDIS;
|
||||
|
||||
return ServiceType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
44
ts/detector.interfaces.ts
Normal file
44
ts/detector.interfaces.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export enum ServiceType {
|
||||
HTTP = 'http',
|
||||
HTTPS = 'https',
|
||||
SSH = 'ssh',
|
||||
FTP = 'ftp',
|
||||
SMTP = 'smtp',
|
||||
POP3 = 'pop3',
|
||||
IMAP = 'imap',
|
||||
MYSQL = 'mysql',
|
||||
POSTGRESQL = 'postgresql',
|
||||
MONGODB = 'mongodb',
|
||||
REDIS = 'redis',
|
||||
UNKNOWN = 'unknown'
|
||||
}
|
||||
|
||||
export interface IDetectorResult {
|
||||
isActive: boolean;
|
||||
serviceType?: ServiceType;
|
||||
protocol?: 'tcp' | 'udp';
|
||||
responseTime?: number;
|
||||
tlsVersion?: string;
|
||||
serviceBanner?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface INetworkDiagnostics {
|
||||
ping?: {
|
||||
reachable: boolean;
|
||||
averageLatency: number;
|
||||
packetLoss: number;
|
||||
};
|
||||
traceroute?: Array<{
|
||||
hop: number;
|
||||
hostname: string;
|
||||
ip: string;
|
||||
latency: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IDetectorOptions {
|
||||
timeout?: number;
|
||||
includeNetworkDiagnostics?: boolean;
|
||||
detectServiceType?: boolean;
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
// node native
|
||||
import * as net from 'net';
|
||||
import * as tls from 'tls';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||
import * as smarturl from '@push.rocks/smarturl';
|
||||
|
||||
export { smartnetwork, smarturl };
|
||||
export { net, tls, http, https, smartnetwork, smarturl };
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './detector.classes.detector.js';
|
||||
export * from './detector.interfaces.js';
|
||||
|
||||
Reference in New Issue
Block a user