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:
2025-05-26 09:40:16 +00:00
parent f54a2908ac
commit 7e1b99827c
11 changed files with 414 additions and 13 deletions

View File

@@ -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.'
}

View File

@@ -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
View 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;
}

View File

@@ -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 };

View File

@@ -1 +1,2 @@
export * from './detector.classes.detector.js';
export * from './detector.interfaces.js';