krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v1.6.0: DomainDig/PortScanService.swift · raw
1import Foundation
2import Network
3
4struct PortScanService {
5 struct PortInfo: Sendable {
6 let port: UInt16
7 let service: String
8 }
9
10 static let ports: [PortInfo] = [
11 PortInfo(port: 21, service: "FTP"),
12 PortInfo(port: 22, service: "SSH"),
13 PortInfo(port: 25, service: "SMTP"),
14 PortInfo(port: 80, service: "HTTP"),
15 PortInfo(port: 443, service: "HTTPS"),
16 PortInfo(port: 587, service: "SMTP (TLS)"),
17 PortInfo(port: 3306, service: "MySQL"),
18 PortInfo(port: 5432, service: "PostgreSQL"),
19 PortInfo(port: 8080, service: "HTTP Alt"),
20 PortInfo(port: 8443, service: "HTTPS Alt"),
21 ]
22
23 static func scanAll(domain: String) async -> [PortScanResult] {
24 await withTaskGroup(of: PortScanResult.self, returning: [PortScanResult].self) { group in
25 for info in ports {
26 group.addTask {
27 let open = await probe(domain: domain, port: info.port)
28 return PortScanResult(port: info.port, service: info.service, open: open)
29 }
30 }
31
32 var results: [PortScanResult] = []
33 for await result in group {
34 results.append(result)
35 }
36
37 // Sort by port number
38 return results.sorted { $0.port < $1.port }
39 }
40 }
41
42 private static func probe(domain: String, port: UInt16) async -> Bool {
43 await withCheckedContinuation { continuation in
44 let host = NWEndpoint.Host(domain)
45 let nwPort = NWEndpoint.Port(rawValue: port)!
46 let connection = NWConnection(host: host, port: nwPort, using: .tcp)
47 let context = ProbeContext(connection: connection, continuation: continuation)
48
49 connection.stateUpdateHandler = { state in
50 switch state {
51 case .ready:
52 context.finish(open: true)
53 case .failed, .cancelled:
54 context.finish(open: false)
55 default:
56 break
57 }
58 }
59
60 let queue = DispatchQueue(label: "portscan.\(port)")
61 connection.start(queue: queue)
62
63 queue.asyncAfter(deadline: .now() + 3) {
64 context.finish(open: false)
65 }
66 }
67 }
68}
69
70private final class ProbeContext: @unchecked Sendable {
71 private let connection: NWConnection
72 private let continuation: CheckedContinuation<Bool, Never>
73 private let lock = NSLock()
74 private nonisolated(unsafe) var resumed = false
75
76 init(connection: NWConnection, continuation: CheckedContinuation<Bool, Never>) {
77 self.connection = connection
78 self.continuation = continuation
79 }
80
81 nonisolated func finish(open: Bool) {
82 lock.lock()
83 guard !resumed else {
84 lock.unlock()
85 return
86 }
87 resumed = true
88 lock.unlock()
89
90 connection.cancel()
91 continuation.resume(returning: open)
92 }
93}