summaryrefslogtreecommitdiff
path: root/DomainDig/PortScanService.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-18 11:59:38 -0500
committerChristian Cleberg <[email protected]>2026-03-18 11:59:38 -0500
commit376e7b55b4a0c4f54faa0c2828dea4fd17dff288 (patch)
tree540d11ad423f14975553f2490617e05aea61ae2c /DomainDig/PortScanService.swift
parente11b7d29a6aa9e52f63fcb35ee7ed9127850fffe (diff)
downloaddomain-dig-376e7b55b4a0c4f54faa0c2828dea4fd17dff288.tar.gz
domain-dig-376e7b55b4a0c4f54faa0c2828dea4fd17dff288.tar.bz2
domain-dig-376e7b55b4a0c4f54faa0c2828dea4fd17dff288.zip
realigning commits
Diffstat (limited to 'DomainDig/PortScanService.swift')
-rw-r--r--DomainDig/PortScanService.swift93
1 files changed, 93 insertions, 0 deletions
diff --git a/DomainDig/PortScanService.swift b/DomainDig/PortScanService.swift
new file mode 100644
index 0000000..63d0571
--- /dev/null
+++ b/DomainDig/PortScanService.swift
@@ -0,0 +1,93 @@
+import Foundation
+import Network
+
+struct PortScanService {
+ struct PortInfo: Sendable {
+ let port: UInt16
+ let service: String
+ }
+
+ static let ports: [PortInfo] = [
+ PortInfo(port: 21, service: "FTP"),
+ PortInfo(port: 22, service: "SSH"),
+ PortInfo(port: 25, service: "SMTP"),
+ PortInfo(port: 80, service: "HTTP"),
+ PortInfo(port: 443, service: "HTTPS"),
+ PortInfo(port: 587, service: "SMTP (TLS)"),
+ PortInfo(port: 3306, service: "MySQL"),
+ PortInfo(port: 5432, service: "PostgreSQL"),
+ PortInfo(port: 8080, service: "HTTP Alt"),
+ PortInfo(port: 8443, service: "HTTPS Alt"),
+ ]
+
+ static func scanAll(domain: String) async -> [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)
+ }
+ }
+
+ var results: [PortScanResult] = []
+ for await result in group {
+ results.append(result)
+ }
+
+ // Sort by port number
+ return results.sorted { $0.port < $1.port }
+ }
+ }
+
+ private static func probe(domain: String, port: UInt16) async -> Bool {
+ await withCheckedContinuation { continuation in
+ let host = NWEndpoint.Host(domain)
+ let nwPort = NWEndpoint.Port(rawValue: port)!
+ let connection = NWConnection(host: host, port: nwPort, using: .tcp)
+ let context = ProbeContext(connection: connection, continuation: continuation)
+
+ connection.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ context.finish(open: true)
+ case .failed, .cancelled:
+ context.finish(open: false)
+ default:
+ break
+ }
+ }
+
+ let queue = DispatchQueue(label: "portscan.\(port)")
+ connection.start(queue: queue)
+
+ queue.asyncAfter(deadline: .now() + 3) {
+ context.finish(open: false)
+ }
+ }
+ }
+}
+
+private final class ProbeContext: @unchecked Sendable {
+ private let connection: NWConnection
+ private let continuation: CheckedContinuation<Bool, Never>
+ private let lock = NSLock()
+ private nonisolated(unsafe) var resumed = false
+
+ init(connection: NWConnection, continuation: CheckedContinuation<Bool, Never>) {
+ self.connection = connection
+ self.continuation = continuation
+ }
+
+ nonisolated func finish(open: Bool) {
+ lock.lock()
+ guard !resumed else {
+ lock.unlock()
+ return
+ }
+ resumed = true
+ lock.unlock()
+
+ connection.cancel()
+ continuation.resume(returning: open)
+ }
+}