84 lines
2.8 KiB
Swift
Raw Normal View History

2026-07-19 22:46:52 +02:00
// retoor <retoor@molodetz.nl>
import Foundation
import NIO
enum PortScanner {
struct PortResult {
let host: String
let port: Int
let open: Bool
let banner: String?
let latencyMs: Double
let tls: Bool
let error: String?
}
static func scan(host: String, port: Int, timeoutSeconds: Double = 5.0, useTLS: Bool = false) async -> PortResult {
let start = Date()
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { try? group.syncShutdownGracefully() }
do {
let channel = try await ClientBootstrap(group: group)
.connectTimeout(.seconds(Int64(timeoutSeconds)))
.connect(host: host, port: port)
.get()
let latency = Date().timeIntervalSince(start) * 1000
var banner: String?
if useTLS {
// TLS handshake check - connect and attempt TLS
banner = await performTLSHandshake(host: host, port: port, timeout: timeoutSeconds)
} else {
// Read initial banner if available
banner = await readBanner(channel: channel, timeout: timeoutSeconds)
}
try await channel.close(mode: .all).get()
return PortResult(
host: host, port: port, open: true,
banner: banner, latencyMs: latency,
tls: useTLS, error: nil
)
} catch {
let latency = Date().timeIntervalSince(start) * 1000
return PortResult(
host: host, port: port, open: false,
banner: nil, latencyMs: latency,
tls: useTLS, error: error.localizedDescription
)
}
}
private static func readBanner(channel: Channel, timeout: Double) async -> String? {
// Simple banner read - up to 4KB
var buffer = ByteBufferAllocator().buffer(capacity: 4096)
do {
// Wait briefly for banner data
try await Task.sleep(nanoseconds: UInt64(timeout * 500_000_000))
// Can't easily read from channel in NIO without handler
return nil
} catch {
return nil
}
}
private static func performTLSHandshake(host: String, port: Int, timeout: Double) async -> String? {
// Simplified: attempt TCP connect to the TLS port
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { try? group.syncShutdownGracefully() }
do {
let _ = try await ClientBootstrap(group: group)
.connectTimeout(.seconds(Int64(timeout)))
.connect(host: host, port: port)
.get()
return "TLS reachable"
} catch {
return nil
}
}
}