@capgo/capacitor-network-diagnostics 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapgoCapacitorNetworkDiagnostics.podspec +17 -0
- package/LICENSE +373 -0
- package/Package.swift +28 -0
- package/README.md +467 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/app/capgo/networkdiagnostics/NetworkDiagnostics.java +681 -0
- package/android/src/main/java/app/capgo/networkdiagnostics/NetworkDiagnosticsPlugin.java +141 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +961 -0
- package/dist/esm/definitions.d.ts +276 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +24 -0
- package/dist/esm/web.js +388 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +402 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +405 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Download.swift +71 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+PacketLoss.swift +91 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Run.swift +163 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Utils.swift +202 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics.swift +151 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnosticsPlugin.swift +139 -0
- package/ios/Tests/NetworkDiagnosticsPluginTests/NetworkDiagnosticsTests.swift +11 -0
- package/package.json +92 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
extension NetworkDiagnostics {
|
|
4
|
+
public func runDiagnostics(
|
|
5
|
+
urls: [[String: Any]],
|
|
6
|
+
ports: [[String: Any]],
|
|
7
|
+
websockets: [[String: Any]],
|
|
8
|
+
download: [String: Any]?,
|
|
9
|
+
packetLoss: [String: Any]?
|
|
10
|
+
) async -> [String: Any] {
|
|
11
|
+
let status = await getNetworkStatus()
|
|
12
|
+
var issues = statusIssues(status)
|
|
13
|
+
let urlDiagnostics = await runUrlDiagnostics(urls)
|
|
14
|
+
let portDiagnostics = await runPortDiagnostics(ports)
|
|
15
|
+
let websocketDiagnostics = await runWebSocketDiagnostics(websockets)
|
|
16
|
+
var output: [String: Any] = ["status": status]
|
|
17
|
+
|
|
18
|
+
output["urls"] = urlDiagnostics.results
|
|
19
|
+
output["ports"] = portDiagnostics.results
|
|
20
|
+
output["websockets"] = websocketDiagnostics.results
|
|
21
|
+
issues += urlDiagnostics.issues + portDiagnostics.issues + websocketDiagnostics.issues
|
|
22
|
+
|
|
23
|
+
if let downloadResult = await downloadDiagnostic(download) {
|
|
24
|
+
output["download"] = downloadResult.result
|
|
25
|
+
issues += downloadResult.issues
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if let packetLossResult = await packetLossDiagnostic(packetLoss) {
|
|
29
|
+
output["packetLoss"] = packetLossResult.result
|
|
30
|
+
issues += packetLossResult.issues
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
output["issues"] = issues
|
|
34
|
+
return output
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func runUrlDiagnostics(_ urls: [[String: Any]]) async -> DiagnosticGroupResult {
|
|
38
|
+
var results: [[String: Any]] = []
|
|
39
|
+
var issues: [String] = []
|
|
40
|
+
for options in urls {
|
|
41
|
+
let result = await testUrl(
|
|
42
|
+
url: options["url"] as? String ?? "",
|
|
43
|
+
method: options["method"] as? String ?? "HEAD",
|
|
44
|
+
timeoutMs: options["timeoutMs"] as? Int ?? 10_000,
|
|
45
|
+
followRedirects: options["followRedirects"] as? Bool ?? true
|
|
46
|
+
)
|
|
47
|
+
results.append(result)
|
|
48
|
+
appendUrlIssue(result, to: &issues)
|
|
49
|
+
}
|
|
50
|
+
return DiagnosticGroupResult(results: results, issues: issues)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func runPortDiagnostics(_ ports: [[String: Any]]) async -> DiagnosticGroupResult {
|
|
54
|
+
var results: [[String: Any]] = []
|
|
55
|
+
var issues: [String] = []
|
|
56
|
+
for options in ports {
|
|
57
|
+
let result = await testPort(
|
|
58
|
+
host: options["host"] as? String ?? "",
|
|
59
|
+
port: options["port"] as? Int ?? 0,
|
|
60
|
+
timeoutMs: options["timeoutMs"] as? Int ?? 5_000
|
|
61
|
+
)
|
|
62
|
+
results.append(result)
|
|
63
|
+
appendPortIssue(result, to: &issues)
|
|
64
|
+
}
|
|
65
|
+
return DiagnosticGroupResult(results: results, issues: issues)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func runWebSocketDiagnostics(_ websockets: [[String: Any]]) async -> DiagnosticGroupResult {
|
|
69
|
+
var results: [[String: Any]] = []
|
|
70
|
+
var issues: [String] = []
|
|
71
|
+
for options in websockets {
|
|
72
|
+
let result = await testWebSocket(
|
|
73
|
+
url: options["url"] as? String ?? "",
|
|
74
|
+
timeoutMs: options["timeoutMs"] as? Int ?? 10_000
|
|
75
|
+
)
|
|
76
|
+
results.append(result)
|
|
77
|
+
appendWebSocketIssue(result, to: &issues)
|
|
78
|
+
}
|
|
79
|
+
return DiagnosticGroupResult(results: results, issues: issues)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func downloadDiagnostic(_ download: [String: Any]?) async -> OptionalDiagnosticResult? {
|
|
83
|
+
guard let download = download else {
|
|
84
|
+
return nil
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let result = await testDownloadSpeed(
|
|
88
|
+
url: download["url"] as? String ?? "",
|
|
89
|
+
maxBytes: download["maxBytes"] as? Int ?? 5 * 1024 * 1024,
|
|
90
|
+
timeoutMs: download["timeoutMs"] as? Int ?? 30_000
|
|
91
|
+
)
|
|
92
|
+
var issues: [String] = []
|
|
93
|
+
if !(result["ok"] as? Bool ?? false) {
|
|
94
|
+
issues.append("Download speed test failed: \(result["url"] as? String ?? "")")
|
|
95
|
+
}
|
|
96
|
+
return OptionalDiagnosticResult(result: result, issues: issues)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func packetLossDiagnostic(_ packetLoss: [String: Any]?) async -> OptionalDiagnosticResult? {
|
|
100
|
+
guard let packetLoss = packetLoss else {
|
|
101
|
+
return nil
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let result = await testPacketLoss(packetLossOptions(from: packetLoss))
|
|
105
|
+
var issues: [String] = []
|
|
106
|
+
if let lossPercent = result["lossPercent"] as? Double, lossPercent > 0 {
|
|
107
|
+
issues.append("Packet loss detected: \(lossPercent)% to \(result["target"] as? String ?? "")")
|
|
108
|
+
}
|
|
109
|
+
return OptionalDiagnosticResult(result: result, issues: issues)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func packetLossOptions(from options: [String: Any]) -> PacketLossOptions {
|
|
113
|
+
PacketLossOptions(
|
|
114
|
+
mode: options["mode"] as? String ?? "",
|
|
115
|
+
host: options["host"] as? String ?? "",
|
|
116
|
+
port: options["port"] as? Int ?? 0,
|
|
117
|
+
url: options["url"] as? String ?? "",
|
|
118
|
+
count: options["count"] as? Int ?? 10,
|
|
119
|
+
timeoutMs: options["timeoutMs"] as? Int ?? 3_000,
|
|
120
|
+
intervalMs: options["intervalMs"] as? Int ?? 250
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func statusIssues(_ status: [String: Any]) -> [String] {
|
|
125
|
+
if !(status["connected"] as? Bool ?? false) {
|
|
126
|
+
return ["No active network connection"]
|
|
127
|
+
}
|
|
128
|
+
if !(status["internetReachable"] as? Bool ?? false) {
|
|
129
|
+
return ["Network is connected but internet reachability is not confirmed"]
|
|
130
|
+
}
|
|
131
|
+
return []
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func appendUrlIssue(_ result: [String: Any], to issues: inout [String]) {
|
|
135
|
+
if !(result["reachable"] as? Bool ?? false) {
|
|
136
|
+
issues.append("URL unreachable: \(result["url"] as? String ?? "")")
|
|
137
|
+
} else if !(result["ok"] as? Bool ?? false) {
|
|
138
|
+
issues.append("URL returned non-success status: \(result["url"] as? String ?? "")")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func appendPortIssue(_ result: [String: Any], to issues: inout [String]) {
|
|
143
|
+
if !(result["open"] as? Bool ?? false) {
|
|
144
|
+
issues.append("TCP port blocked or unreachable: \(result["host"] as? String ?? ""):\(result["port"] as? Int ?? 0)")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func appendWebSocketIssue(_ result: [String: Any], to issues: inout [String]) {
|
|
149
|
+
if !(result["open"] as? Bool ?? false) {
|
|
150
|
+
issues.append("WebSocket failed: \(result["url"] as? String ?? "")")
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
struct DiagnosticGroupResult {
|
|
156
|
+
let results: [[String: Any]]
|
|
157
|
+
let issues: [String]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
struct OptionalDiagnosticResult {
|
|
161
|
+
let result: [String: Any]
|
|
162
|
+
let issues: [String]
|
|
163
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Network
|
|
3
|
+
|
|
4
|
+
extension NetworkDiagnostics {
|
|
5
|
+
func currentPath() async -> NWPath {
|
|
6
|
+
await withCheckedContinuation { continuation in
|
|
7
|
+
let monitor = NWPathMonitor()
|
|
8
|
+
let queue = DispatchQueue(label: "app.capgo.networkdiagnostics.path")
|
|
9
|
+
var resumed = false
|
|
10
|
+
|
|
11
|
+
func resume(with path: NWPath) {
|
|
12
|
+
if resumed {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
resumed = true
|
|
16
|
+
monitor.cancel()
|
|
17
|
+
continuation.resume(returning: path)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
monitor.pathUpdateHandler = { path in
|
|
21
|
+
resume(with: path)
|
|
22
|
+
}
|
|
23
|
+
monitor.start(queue: queue)
|
|
24
|
+
queue.asyncAfter(deadline: .now() + 1) {
|
|
25
|
+
resume(with: monitor.currentPath)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func probeTcp(host: String, port: Int, timeoutMs: Int) async -> ProbeResult {
|
|
31
|
+
let started = Date()
|
|
32
|
+
guard let nwPort = NWEndpoint.Port(rawValue: UInt16(port)) else {
|
|
33
|
+
return ProbeResult(success: false, durationMs: elapsedMs(started), errorCode: "INVALID_PORT", errorMessage: "Invalid port")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let connection = NWConnection(host: NWEndpoint.Host(host), port: nwPort, using: .tcp)
|
|
37
|
+
|
|
38
|
+
return await withCheckedContinuation { continuation in
|
|
39
|
+
let queue = DispatchQueue(label: "app.capgo.networkdiagnostics.tcp")
|
|
40
|
+
var finished = false
|
|
41
|
+
|
|
42
|
+
func finish(success: Bool, error: Error?) {
|
|
43
|
+
if finished {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
finished = true
|
|
47
|
+
connection.cancel()
|
|
48
|
+
continuation.resume(
|
|
49
|
+
returning: ProbeResult(
|
|
50
|
+
success: success,
|
|
51
|
+
durationMs: elapsedMs(started),
|
|
52
|
+
errorCode: error.map { errorCode($0) },
|
|
53
|
+
errorMessage: error?.localizedDescription
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
connection.stateUpdateHandler = { state in
|
|
59
|
+
switch state {
|
|
60
|
+
case .ready:
|
|
61
|
+
finish(success: true, error: nil)
|
|
62
|
+
case .failed(let error):
|
|
63
|
+
finish(success: false, error: error)
|
|
64
|
+
case .cancelled:
|
|
65
|
+
if !finished {
|
|
66
|
+
finish(success: false, error: NetworkDiagnosticsError.cancelled)
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
queue.asyncAfter(deadline: .now() + timeout(timeoutMs, fallback: 5)) {
|
|
74
|
+
finish(success: false, error: NetworkDiagnosticsError.timeout)
|
|
75
|
+
}
|
|
76
|
+
connection.start(queue: queue)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func probeHttp(url: String, timeoutMs: Int) async -> ProbeResult {
|
|
81
|
+
let result = await testUrl(url: url, method: "HEAD", timeoutMs: timeoutMs, followRedirects: true)
|
|
82
|
+
let reachable = result["reachable"] as? Bool ?? false
|
|
83
|
+
return ProbeResult(
|
|
84
|
+
success: reachable,
|
|
85
|
+
durationMs: result["durationMs"] as? Int ?? 0,
|
|
86
|
+
errorCode: result["errorCode"] as? String,
|
|
87
|
+
errorMessage: result["errorMessage"] as? String
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func websocketResult(_ base: [String: Any], started: Date, open: Bool, error: Error?) -> [String: Any] {
|
|
92
|
+
var output = base
|
|
93
|
+
output["open"] = open
|
|
94
|
+
output["durationMs"] = elapsedMs(started)
|
|
95
|
+
if let error = error {
|
|
96
|
+
output["errorCode"] = errorCode(error)
|
|
97
|
+
output["errorMessage"] = error.localizedDescription
|
|
98
|
+
}
|
|
99
|
+
return output
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func connectionType(_ path: NWPath) -> String {
|
|
103
|
+
if path.usesInterfaceType(.wifi) {
|
|
104
|
+
return "wifi"
|
|
105
|
+
}
|
|
106
|
+
if path.usesInterfaceType(.cellular) {
|
|
107
|
+
return "cellular"
|
|
108
|
+
}
|
|
109
|
+
if path.usesInterfaceType(.wiredEthernet) {
|
|
110
|
+
return "ethernet"
|
|
111
|
+
}
|
|
112
|
+
if path.usesInterfaceType(.loopback) || path.usesInterfaceType(.other) {
|
|
113
|
+
return "other"
|
|
114
|
+
}
|
|
115
|
+
return "unknown"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func statusLabel(_ status: NWPath.Status) -> String {
|
|
119
|
+
switch status {
|
|
120
|
+
case .satisfied:
|
|
121
|
+
return "satisfied"
|
|
122
|
+
case .unsatisfied:
|
|
123
|
+
return "unsatisfied"
|
|
124
|
+
case .requiresConnection:
|
|
125
|
+
return "requiresConnection"
|
|
126
|
+
@unknown default:
|
|
127
|
+
return "unknown"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func normalizeMethod(_ method: String) -> String {
|
|
132
|
+
method.uppercased() == "GET" ? "GET" : "HEAD"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func normalizePacketLossMode(mode: String, host: String, port: Int, url: String) -> String {
|
|
136
|
+
if mode.lowercased() == "http" {
|
|
137
|
+
return "http"
|
|
138
|
+
}
|
|
139
|
+
if mode.lowercased() == "tcp" {
|
|
140
|
+
return "tcp"
|
|
141
|
+
}
|
|
142
|
+
if !host.isEmpty && port > 0 {
|
|
143
|
+
return "tcp"
|
|
144
|
+
}
|
|
145
|
+
if !url.isEmpty {
|
|
146
|
+
return "http"
|
|
147
|
+
}
|
|
148
|
+
return "tcp"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func positive(_ value: Int, fallback: Int) -> Int {
|
|
152
|
+
value > 0 ? value : fallback
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func timeout(_ value: Int, fallback: TimeInterval) -> TimeInterval {
|
|
156
|
+
TimeInterval(positive(value, fallback: Int(fallback * 1000))) / 1000.0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func elapsedMs(_ started: Date) -> Int {
|
|
160
|
+
max(Int(Date().timeIntervalSince(started) * 1000), 0)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func errorCode(_ error: Error) -> String {
|
|
164
|
+
let nsError = error as NSError
|
|
165
|
+
if nsError.domain == NSURLErrorDomain {
|
|
166
|
+
return urlErrorCode(nsError.code)
|
|
167
|
+
}
|
|
168
|
+
if error is NetworkDiagnosticsError {
|
|
169
|
+
return "\(error)".uppercased()
|
|
170
|
+
}
|
|
171
|
+
return String(describing: type(of: error))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func urlErrorCode(_ code: Int) -> String {
|
|
175
|
+
switch code {
|
|
176
|
+
case NSURLErrorTimedOut:
|
|
177
|
+
return "TIMEOUT"
|
|
178
|
+
case NSURLErrorCannotFindHost, NSURLErrorDNSLookupFailed:
|
|
179
|
+
return "DNS_ERROR"
|
|
180
|
+
case NSURLErrorCannotConnectToHost, NSURLErrorNetworkConnectionLost:
|
|
181
|
+
return "CONNECTION_FAILED"
|
|
182
|
+
case NSURLErrorSecureConnectionFailed, NSURLErrorServerCertificateUntrusted:
|
|
183
|
+
return "TLS_ERROR"
|
|
184
|
+
default:
|
|
185
|
+
return "URL_ERROR_\(code)"
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
enum NetworkDiagnosticsError: Error, LocalizedError {
|
|
191
|
+
case cancelled
|
|
192
|
+
case timeout
|
|
193
|
+
|
|
194
|
+
var errorDescription: String? {
|
|
195
|
+
switch self {
|
|
196
|
+
case .cancelled:
|
|
197
|
+
return "Connection was cancelled"
|
|
198
|
+
case .timeout:
|
|
199
|
+
return "Connection timed out"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Network
|
|
3
|
+
|
|
4
|
+
@objc public class NetworkDiagnostics: NSObject {
|
|
5
|
+
@objc public func getPluginVersion() -> String {
|
|
6
|
+
return "native"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
public func getNetworkStatus() async -> [String: Any] {
|
|
10
|
+
let path = await currentPath()
|
|
11
|
+
let connected = path.status == .satisfied
|
|
12
|
+
var details: [String: Any] = [
|
|
13
|
+
"status": statusLabel(path.status),
|
|
14
|
+
"interfaces": path.availableInterfaces.map { $0.name }.joined(separator: ",")
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
if let gateway = path.gateways.first {
|
|
18
|
+
details["gateway"] = "\(gateway)"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
"connected": connected,
|
|
23
|
+
"connectionType": connected ? connectionType(path) : "none",
|
|
24
|
+
"internetReachable": connected,
|
|
25
|
+
"expensive": path.isExpensive,
|
|
26
|
+
"constrained": path.isConstrained,
|
|
27
|
+
"details": details
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public func testUrl(url: String, method: String, timeoutMs: Int, followRedirects: Bool) async -> [String: Any] {
|
|
32
|
+
let started = Date()
|
|
33
|
+
let normalizedMethod = normalizeMethod(method)
|
|
34
|
+
var result: [String: Any] = [
|
|
35
|
+
"url": url,
|
|
36
|
+
"method": normalizedMethod
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
guard let parsedUrl = URL(string: url) else {
|
|
40
|
+
result["ok"] = false
|
|
41
|
+
result["reachable"] = false
|
|
42
|
+
result["durationMs"] = elapsedMs(started)
|
|
43
|
+
result["errorCode"] = "INVALID_URL"
|
|
44
|
+
result["errorMessage"] = "Invalid URL"
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var request = URLRequest(url: parsedUrl)
|
|
49
|
+
request.httpMethod = normalizedMethod
|
|
50
|
+
request.timeoutInterval = timeout(timeoutMs, fallback: 10)
|
|
51
|
+
|
|
52
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
53
|
+
configuration.timeoutIntervalForRequest = request.timeoutInterval
|
|
54
|
+
configuration.timeoutIntervalForResource = request.timeoutInterval
|
|
55
|
+
let delegate = followRedirects ? nil : NoRedirectDelegate()
|
|
56
|
+
let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
|
|
57
|
+
defer { session.finishTasksAndInvalidate() }
|
|
58
|
+
|
|
59
|
+
do {
|
|
60
|
+
let (_, response) = try await session.data(for: request)
|
|
61
|
+
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
|
|
62
|
+
result["ok"] = statusCode >= 200 && statusCode < 400
|
|
63
|
+
result["reachable"] = true
|
|
64
|
+
result["durationMs"] = elapsedMs(started)
|
|
65
|
+
result["statusCode"] = statusCode
|
|
66
|
+
result["finalUrl"] = response.url?.absoluteString ?? url
|
|
67
|
+
} catch {
|
|
68
|
+
result["ok"] = false
|
|
69
|
+
result["reachable"] = false
|
|
70
|
+
result["durationMs"] = elapsedMs(started)
|
|
71
|
+
result["errorCode"] = errorCode(error)
|
|
72
|
+
result["errorMessage"] = error.localizedDescription
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public func testPort(host: String, port: Int, timeoutMs: Int) async -> [String: Any] {
|
|
79
|
+
let probe = await probeTcp(host: host, port: port, timeoutMs: timeoutMs)
|
|
80
|
+
var result: [String: Any] = [
|
|
81
|
+
"host": host,
|
|
82
|
+
"port": port,
|
|
83
|
+
"open": probe.success,
|
|
84
|
+
"durationMs": probe.durationMs
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
if !probe.success {
|
|
88
|
+
result["errorCode"] = probe.errorCode
|
|
89
|
+
result["errorMessage"] = probe.errorMessage
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public func testWebSocket(url: String, timeoutMs: Int) async -> [String: Any] {
|
|
96
|
+
let started = Date()
|
|
97
|
+
var result: [String: Any] = ["url": url]
|
|
98
|
+
|
|
99
|
+
guard let parsedUrl = URL(string: url), let scheme = parsedUrl.scheme?.lowercased(), scheme == "ws" || scheme == "wss" else {
|
|
100
|
+
result["open"] = false
|
|
101
|
+
result["durationMs"] = elapsedMs(started)
|
|
102
|
+
result["errorCode"] = "INVALID_URL"
|
|
103
|
+
result["errorMessage"] = "WebSocket URL must use ws:// or wss://"
|
|
104
|
+
return result
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
108
|
+
configuration.timeoutIntervalForRequest = timeout(timeoutMs, fallback: 10)
|
|
109
|
+
configuration.timeoutIntervalForResource = timeout(timeoutMs, fallback: 10)
|
|
110
|
+
let session = URLSession(configuration: configuration)
|
|
111
|
+
let task = session.webSocketTask(with: parsedUrl)
|
|
112
|
+
task.resume()
|
|
113
|
+
|
|
114
|
+
return await withCheckedContinuation { continuation in
|
|
115
|
+
let queue = DispatchQueue(label: "app.capgo.networkdiagnostics.websocket")
|
|
116
|
+
var finished = false
|
|
117
|
+
|
|
118
|
+
func finish(open: Bool, error: Error?) {
|
|
119
|
+
if finished {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
finished = true
|
|
123
|
+
task.cancel(with: .goingAway, reason: nil)
|
|
124
|
+
session.invalidateAndCancel()
|
|
125
|
+
continuation.resume(returning: websocketResult(result, started: started, open: open, error: error))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
queue.asyncAfter(deadline: .now() + timeout(timeoutMs, fallback: 10)) {
|
|
129
|
+
finish(open: false, error: NetworkDiagnosticsError.timeout)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
task.sendPing { error in
|
|
133
|
+
queue.async {
|
|
134
|
+
finish(open: error == nil, error: error)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private final class NoRedirectDelegate: NSObject, URLSessionTaskDelegate {
|
|
142
|
+
func urlSession(
|
|
143
|
+
_ session: URLSession,
|
|
144
|
+
task: URLSessionTask,
|
|
145
|
+
willPerformHTTPRedirection response: HTTPURLResponse,
|
|
146
|
+
newRequest request: URLRequest,
|
|
147
|
+
completionHandler: @escaping (URLRequest?) -> Void
|
|
148
|
+
) {
|
|
149
|
+
completionHandler(nil)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
@objc(NetworkDiagnosticsPlugin)
|
|
5
|
+
public class NetworkDiagnosticsPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
6
|
+
public let identifier = "NetworkDiagnosticsPlugin"
|
|
7
|
+
public let jsName = "NetworkDiagnostics"
|
|
8
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
9
|
+
CAPPluginMethod(name: "getNetworkStatus", returnType: CAPPluginReturnPromise),
|
|
10
|
+
CAPPluginMethod(name: "testUrl", returnType: CAPPluginReturnPromise),
|
|
11
|
+
CAPPluginMethod(name: "testPort", returnType: CAPPluginReturnPromise),
|
|
12
|
+
CAPPluginMethod(name: "testWebSocket", returnType: CAPPluginReturnPromise),
|
|
13
|
+
CAPPluginMethod(name: "testDownloadSpeed", returnType: CAPPluginReturnPromise),
|
|
14
|
+
CAPPluginMethod(name: "testPacketLoss", returnType: CAPPluginReturnPromise),
|
|
15
|
+
CAPPluginMethod(name: "runDiagnostics", returnType: CAPPluginReturnPromise),
|
|
16
|
+
CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
private let implementation = NetworkDiagnostics()
|
|
20
|
+
|
|
21
|
+
@objc func getNetworkStatus(_ call: CAPPluginCall) {
|
|
22
|
+
Task {
|
|
23
|
+
call.resolve(await implementation.getNetworkStatus())
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@objc func testUrl(_ call: CAPPluginCall) {
|
|
28
|
+
guard let url = call.getString("url"), !url.isEmpty else {
|
|
29
|
+
call.reject("URL is required")
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Task {
|
|
34
|
+
call.resolve(await implementation.testUrl(
|
|
35
|
+
url: url,
|
|
36
|
+
method: call.getString("method") ?? "HEAD",
|
|
37
|
+
timeoutMs: call.getInt("timeoutMs") ?? 10_000,
|
|
38
|
+
followRedirects: call.getBool("followRedirects") ?? true
|
|
39
|
+
))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@objc func testPort(_ call: CAPPluginCall) {
|
|
44
|
+
guard let host = call.getString("host"), !host.isEmpty else {
|
|
45
|
+
call.reject("Host is required")
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
let port = call.getInt("port") ?? 0
|
|
49
|
+
guard port > 0 && port <= 65_535 else {
|
|
50
|
+
call.reject("Port must be between 1 and 65535")
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Task {
|
|
55
|
+
call.resolve(await implementation.testPort(
|
|
56
|
+
host: host,
|
|
57
|
+
port: port,
|
|
58
|
+
timeoutMs: call.getInt("timeoutMs") ?? 5_000
|
|
59
|
+
))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@objc func testWebSocket(_ call: CAPPluginCall) {
|
|
64
|
+
guard let url = call.getString("url"), !url.isEmpty else {
|
|
65
|
+
call.reject("URL is required")
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Task {
|
|
70
|
+
call.resolve(await implementation.testWebSocket(
|
|
71
|
+
url: url,
|
|
72
|
+
timeoutMs: call.getInt("timeoutMs") ?? 10_000
|
|
73
|
+
))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@objc func testDownloadSpeed(_ call: CAPPluginCall) {
|
|
78
|
+
guard let url = call.getString("url"), !url.isEmpty else {
|
|
79
|
+
call.reject("URL is required")
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Task {
|
|
84
|
+
call.resolve(await implementation.testDownloadSpeed(
|
|
85
|
+
url: url,
|
|
86
|
+
maxBytes: call.getInt("maxBytes") ?? 5 * 1024 * 1024,
|
|
87
|
+
timeoutMs: call.getInt("timeoutMs") ?? 30_000
|
|
88
|
+
))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@objc func testPacketLoss(_ call: CAPPluginCall) {
|
|
93
|
+
let host = call.getString("host") ?? ""
|
|
94
|
+
let port = call.getInt("port") ?? 0
|
|
95
|
+
let url = call.getString("url") ?? ""
|
|
96
|
+
|
|
97
|
+
guard (!host.isEmpty && port > 0) || !url.isEmpty else {
|
|
98
|
+
call.reject("Either host and port, or url is required")
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Task {
|
|
103
|
+
let options = PacketLossOptions(
|
|
104
|
+
mode: call.getString("mode") ?? "",
|
|
105
|
+
host: host,
|
|
106
|
+
port: port,
|
|
107
|
+
url: url,
|
|
108
|
+
count: call.getInt("count") ?? 10,
|
|
109
|
+
timeoutMs: call.getInt("timeoutMs") ?? 3_000,
|
|
110
|
+
intervalMs: call.getInt("intervalMs") ?? 250
|
|
111
|
+
)
|
|
112
|
+
call.resolve(await implementation.testPacketLoss(options))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@objc func runDiagnostics(_ call: CAPPluginCall) {
|
|
117
|
+
let urls = call.getArray("urls", []) as? [[String: Any]] ?? []
|
|
118
|
+
let ports = call.getArray("ports", []) as? [[String: Any]] ?? []
|
|
119
|
+
let websockets = call.getArray("websockets", []) as? [[String: Any]] ?? []
|
|
120
|
+
let download = call.getObject("download")
|
|
121
|
+
let packetLoss = call.getObject("packetLoss")
|
|
122
|
+
|
|
123
|
+
Task {
|
|
124
|
+
call.resolve(await implementation.runDiagnostics(
|
|
125
|
+
urls: urls,
|
|
126
|
+
ports: ports,
|
|
127
|
+
websockets: websockets,
|
|
128
|
+
download: download,
|
|
129
|
+
packetLoss: packetLoss
|
|
130
|
+
))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
135
|
+
call.resolve([
|
|
136
|
+
"version": implementation.getPluginVersion()
|
|
137
|
+
])
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import NetworkDiagnosticsPlugin
|
|
3
|
+
|
|
4
|
+
class NetworkDiagnosticsTests: XCTestCase {
|
|
5
|
+
func testGetPluginVersion() {
|
|
6
|
+
let implementation = NetworkDiagnostics()
|
|
7
|
+
let result = implementation.getPluginVersion()
|
|
8
|
+
|
|
9
|
+
XCTAssertEqual("native", result)
|
|
10
|
+
}
|
|
11
|
+
}
|