summaryrefslogtreecommitdiff
path: root/DomainDig/PortScanService.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-20 15:28:12 -0500
committerChristian Cleberg <[email protected]>2026-04-20 15:28:12 -0500
commitee2520f2c0c2d806fd284c3d6b8a81a3822f5270 (patch)
tree0680550a1071e01546ba074c2f20b1bf5a452238 /DomainDig/PortScanService.swift
parent4e94ee723cd19c964b1cf37792931e0c7c4614f4 (diff)
downloaddomain-dig-ee2520f2c0c2d806fd284c3d6b8a81a3822f5270.tar.gz
domain-dig-ee2520f2c0c2d806fd284c3d6b8a81a3822f5270.tar.bz2
domain-dig-ee2520f2c0c2d806fd284c3d6b8a81a3822f5270.zip
feat(v1.8): improve result structure, history, and consistency
- normalize sections and add summary card - fix resolver usage in reverse DNS - include custom port scans in history/export - standardize error handling - decompose ContentView into smaller views
Diffstat (limited to 'DomainDig/PortScanService.swift')
-rw-r--r--DomainDig/PortScanService.swift54
1 files changed, 41 insertions, 13 deletions
diff --git a/DomainDig/PortScanService.swift b/DomainDig/PortScanService.swift
index 4dc9001..36c2827 100644
--- a/DomainDig/PortScanService.swift
+++ b/DomainDig/PortScanService.swift
@@ -20,12 +20,18 @@ struct PortScanService {
PortInfo(port: 8443, service: "HTTPS Alt"),
]
- static func scanAll(domain: String) async -> [PortScanResult] {
+ static func scanAll(domain: String) async -> ServiceResult<[PortScanResult]> {
await withTaskGroup(of: PortScanResult.self, returning: [PortScanResult].self) { group in
for info in ports {
group.addTask {
- let open = await probe(domain: domain, port: info.port)
- return PortScanResult(port: info.port, service: info.service, open: open)
+ let result = await probe(domain: domain, port: info.port)
+ return PortScanResult(
+ port: info.port,
+ service: info.service,
+ open: result.open,
+ kind: .standard,
+ durationMs: result.durationMs
+ )
}
}
@@ -35,20 +41,24 @@ struct PortScanService {
}
// Sort by port number
- return results.sorted { $0.port < $1.port }
+ let sorted = results.sorted { $0.port < $1.port }
+ return sorted.isEmpty ? [] : sorted
}
+ .pipe { $0.isEmpty ? .empty("No port scan results") : .success($0) }
}
- static func scanPorts(domain: String, ports: [UInt16], timeout: TimeInterval) async -> [PortScanResult] {
+ static func scanPorts(domain: String, ports: [UInt16], timeout: TimeInterval) async -> ServiceResult<[PortScanResult]> {
await withTaskGroup(of: PortScanResult.self, returning: [PortScanResult].self) { group in
for port in ports {
let service = self.ports.first(where: { $0.port == port })?.service ?? "Custom"
group.addTask {
- let open = await probe(domain: domain, port: port, timeout: timeout)
+ let result = await probe(domain: domain, port: port, timeout: timeout)
return PortScanResult(
port: port,
service: service,
- open: open
+ open: result.open,
+ kind: .custom,
+ durationMs: result.durationMs
)
}
}
@@ -58,8 +68,10 @@ struct PortScanService {
results.append(result)
}
- return results.sorted { $0.port < $1.port }
+ let sorted = results.sorted { $0.port < $1.port }
+ return sorted.isEmpty ? [] : sorted
}
+ .pipe { $0.isEmpty ? .empty("No custom port scan results") : .success($0) }
}
static func grabBanner(host: String, port: UInt16, timeout: TimeInterval = 3.0) async -> String? {
@@ -111,11 +123,11 @@ struct PortScanService {
}
}
- private static func probe(domain: String, port: UInt16) async -> Bool {
+ private static func probe(domain: String, port: UInt16) async -> PortProbeResult {
await probe(domain: domain, port: port, timeout: 5)
}
- private static func probe(domain: String, port: UInt16, timeout: TimeInterval) async -> Bool {
+ private static func probe(domain: String, port: UInt16, timeout: TimeInterval) async -> PortProbeResult {
await withCheckedContinuation { continuation in
let host = NWEndpoint.Host(domain)
let nwPort = NWEndpoint.Port(rawValue: port)!
@@ -143,13 +155,19 @@ struct PortScanService {
}
}
+private struct PortProbeResult: Sendable {
+ let open: Bool
+ let durationMs: Int?
+}
+
private final class ProbeContext: @unchecked Sendable {
private let connection: NWConnection
- private let continuation: CheckedContinuation<Bool, Never>
+ private let continuation: CheckedContinuation<PortProbeResult, Never>
+ private let start = CFAbsoluteTimeGetCurrent()
private let lock = NSLock()
private nonisolated(unsafe) var resumed = false
- init(connection: NWConnection, continuation: CheckedContinuation<Bool, Never>) {
+ init(connection: NWConnection, continuation: CheckedContinuation<PortProbeResult, Never>) {
self.connection = connection
self.continuation = continuation
}
@@ -164,7 +182,17 @@ private final class ProbeContext: @unchecked Sendable {
lock.unlock()
connection.cancel()
- continuation.resume(returning: open)
+ let elapsedMs = Int((CFAbsoluteTimeGetCurrent() - start) * 1000)
+ continuation.resume(returning: PortProbeResult(
+ open: open,
+ durationMs: elapsedMs >= 0 ? elapsedMs : nil
+ ))
+ }
+}
+
+private extension Array {
+ func pipe<T>(_ transform: (Self) -> T) -> T {
+ transform(self)
}
}