krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v1.7.0: DomainDig/ReachabilityService.swift · raw
1import Foundation
2import Network
3
4struct ReachabilityService {
5 static func check(domain: String, port: UInt16) async -> PortReachability {
6 await withCheckedContinuation { continuation in
7 let host = NWEndpoint.Host(domain)
8 let nwPort = NWEndpoint.Port(rawValue: port)!
9 let connection = NWConnection(host: host, port: nwPort, using: .tcp)
10 let context = ConnectionContext(port: port, connection: connection, continuation: continuation)
11
12 connection.stateUpdateHandler = { state in
13 switch state {
14 case .ready:
15 context.finish(reachable: true)
16 case .failed, .cancelled:
17 context.finish(reachable: false)
18 default:
19 break
20 }
21 }
22
23 let queue = DispatchQueue(label: "reachability.\(port)")
24 connection.start(queue: queue)
25
26 queue.asyncAfter(deadline: .now() + 5) {
27 context.finish(reachable: false)
28 }
29 }
30 }
31
32 static func checkAll(domain: String) async -> [PortReachability] {
33 async let port443 = check(domain: domain, port: 443)
34 async let port80 = check(domain: domain, port: 80)
35 return await [port443, port80]
36 }
37}
38
39private final class ConnectionContext: @unchecked Sendable {
40 private let port: UInt16
41 private let connection: NWConnection
42 private let continuation: CheckedContinuation<PortReachability, Never>
43 private let start = CFAbsoluteTimeGetCurrent()
44 private let lock = NSLock()
45 private nonisolated(unsafe) var resumed = false
46
47 init(port: UInt16, connection: NWConnection, continuation: CheckedContinuation<PortReachability, Never>) {
48 self.port = port
49 self.connection = connection
50 self.continuation = continuation
51 }
52
53 nonisolated func finish(reachable: Bool) {
54 lock.lock()
55 guard !resumed else {
56 lock.unlock()
57 return
58 }
59 resumed = true
60 lock.unlock()
61
62 let elapsed = CFAbsoluteTimeGetCurrent() - start
63 let ms = reachable ? Int(elapsed * 1000) : nil
64 connection.cancel()
65 continuation.resume(returning: PortReachability(port: port, reachable: reachable, latencyMs: ms))
66 }
67}