@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.
Files changed (31) hide show
  1. package/CapgoCapacitorNetworkDiagnostics.podspec +17 -0
  2. package/LICENSE +373 -0
  3. package/Package.swift +28 -0
  4. package/README.md +467 -0
  5. package/android/build.gradle +59 -0
  6. package/android/src/main/AndroidManifest.xml +4 -0
  7. package/android/src/main/java/app/capgo/networkdiagnostics/NetworkDiagnostics.java +681 -0
  8. package/android/src/main/java/app/capgo/networkdiagnostics/NetworkDiagnosticsPlugin.java +141 -0
  9. package/android/src/main/res/.gitkeep +0 -0
  10. package/dist/docs.json +961 -0
  11. package/dist/esm/definitions.d.ts +276 -0
  12. package/dist/esm/definitions.js +2 -0
  13. package/dist/esm/definitions.js.map +1 -0
  14. package/dist/esm/index.d.ts +4 -0
  15. package/dist/esm/index.js +7 -0
  16. package/dist/esm/index.js.map +1 -0
  17. package/dist/esm/web.d.ts +24 -0
  18. package/dist/esm/web.js +388 -0
  19. package/dist/esm/web.js.map +1 -0
  20. package/dist/plugin.cjs.js +402 -0
  21. package/dist/plugin.cjs.js.map +1 -0
  22. package/dist/plugin.js +405 -0
  23. package/dist/plugin.js.map +1 -0
  24. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Download.swift +71 -0
  25. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+PacketLoss.swift +91 -0
  26. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Run.swift +163 -0
  27. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Utils.swift +202 -0
  28. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics.swift +151 -0
  29. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnosticsPlugin.swift +139 -0
  30. package/ios/Tests/NetworkDiagnosticsPluginTests/NetworkDiagnosticsTests.swift +11 -0
  31. package/package.json +92 -0
@@ -0,0 +1,681 @@
1
+ package app.capgo.networkdiagnostics;
2
+
3
+ import android.content.Context;
4
+ import android.net.ConnectivityManager;
5
+ import android.net.LinkProperties;
6
+ import android.net.Network;
7
+ import android.net.NetworkCapabilities;
8
+ import android.net.NetworkInfo;
9
+ import android.util.Base64;
10
+ import com.getcapacitor.JSArray;
11
+ import com.getcapacitor.JSObject;
12
+ import java.io.BufferedReader;
13
+ import java.io.BufferedWriter;
14
+ import java.io.InputStream;
15
+ import java.io.InputStreamReader;
16
+ import java.io.OutputStreamWriter;
17
+ import java.net.ConnectException;
18
+ import java.net.HttpURLConnection;
19
+ import java.net.InetSocketAddress;
20
+ import java.net.Socket;
21
+ import java.net.SocketTimeoutException;
22
+ import java.net.URI;
23
+ import java.net.URL;
24
+ import java.net.UnknownHostException;
25
+ import java.nio.charset.StandardCharsets;
26
+ import java.security.SecureRandom;
27
+ import java.util.Locale;
28
+ import javax.net.ssl.SSLException;
29
+ import javax.net.ssl.SSLSocket;
30
+ import javax.net.ssl.SSLSocketFactory;
31
+ import org.json.JSONArray;
32
+ import org.json.JSONObject;
33
+
34
+ public class NetworkDiagnostics {
35
+
36
+ private static final int DEFAULT_URL_TIMEOUT_MS = 10000;
37
+ private static final int DEFAULT_PORT_TIMEOUT_MS = 5000;
38
+ private static final int DEFAULT_DOWNLOAD_TIMEOUT_MS = 30000;
39
+ private static final int DEFAULT_DOWNLOAD_MAX_BYTES = 5 * 1024 * 1024;
40
+ private static final int DEFAULT_PACKET_COUNT = 10;
41
+ private static final int DEFAULT_PACKET_TIMEOUT_MS = 3000;
42
+ private static final int DEFAULT_PACKET_INTERVAL_MS = 250;
43
+ private static final SecureRandom RANDOM = new SecureRandom();
44
+
45
+ public String getPluginVersion() {
46
+ return "native";
47
+ }
48
+
49
+ public JSObject getNetworkStatus(Context context) {
50
+ JSObject ret = new JSObject();
51
+ JSObject details = new JSObject();
52
+ ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
53
+
54
+ if (connectivityManager == null) {
55
+ ret.put("connected", false);
56
+ ret.put("connectionType", "unknown");
57
+ ret.put("internetReachable", false);
58
+ ret.put("details", details);
59
+ return ret;
60
+ }
61
+
62
+ Network network = connectivityManager.getActiveNetwork();
63
+ NetworkCapabilities capabilities = network == null ? null : connectivityManager.getNetworkCapabilities(network);
64
+ LinkProperties linkProperties = network == null ? null : connectivityManager.getLinkProperties(network);
65
+ boolean connected = capabilities != null;
66
+ boolean validated = capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
67
+ boolean captivePortal = capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
68
+
69
+ ret.put("connected", connected);
70
+ ret.put("connectionType", capabilities == null ? fallbackConnectionType(connectivityManager) : connectionType(capabilities));
71
+ ret.put("internetReachable", validated);
72
+ ret.put("expensive", connectivityManager.isActiveNetworkMetered());
73
+ ret.put("constrained", false);
74
+ ret.put("captivePortal", captivePortal);
75
+
76
+ if (capabilities != null) {
77
+ details.put("hasInternetCapability", capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET));
78
+ details.put("validated", validated);
79
+ details.put("captivePortal", captivePortal);
80
+ details.put("transports", transports(capabilities));
81
+ }
82
+ if (linkProperties != null) {
83
+ if (linkProperties.getInterfaceName() != null) {
84
+ details.put("interfaceName", linkProperties.getInterfaceName());
85
+ }
86
+ details.put("dnsServers", linkProperties.getDnsServers().toString());
87
+ }
88
+
89
+ ret.put("details", details);
90
+ return ret;
91
+ }
92
+
93
+ public JSObject testUrl(String url, String method, int timeoutMs, boolean followRedirects) {
94
+ long started = System.nanoTime();
95
+ String normalizedMethod = normalizeMethod(method);
96
+ HttpURLConnection connection = null;
97
+ JSObject ret = new JSObject();
98
+ ret.put("url", url);
99
+ ret.put("method", normalizedMethod);
100
+
101
+ try {
102
+ URL parsedUrl = new URL(url);
103
+ connection = (HttpURLConnection) parsedUrl.openConnection();
104
+ connection.setConnectTimeout(positive(timeoutMs, DEFAULT_URL_TIMEOUT_MS));
105
+ connection.setReadTimeout(positive(timeoutMs, DEFAULT_URL_TIMEOUT_MS));
106
+ connection.setInstanceFollowRedirects(followRedirects);
107
+ connection.setRequestMethod(normalizedMethod);
108
+ connection.setUseCaches(false);
109
+
110
+ int statusCode = connection.getResponseCode();
111
+ ret.put("ok", statusCode >= 200 && statusCode < 400);
112
+ ret.put("reachable", true);
113
+ ret.put("durationMs", elapsedMs(started));
114
+ ret.put("statusCode", statusCode);
115
+ ret.put("finalUrl", connection.getURL().toString());
116
+ } catch (Exception exception) {
117
+ ret.put("ok", false);
118
+ ret.put("reachable", false);
119
+ ret.put("durationMs", elapsedMs(started));
120
+ ret.put("errorCode", errorCode(exception));
121
+ ret.put("errorMessage", exception.getMessage());
122
+ } finally {
123
+ if (connection != null) {
124
+ connection.disconnect();
125
+ }
126
+ }
127
+
128
+ return ret;
129
+ }
130
+
131
+ public JSObject testPort(String host, int port, int timeoutMs) {
132
+ ProbeResult probe = probeTcp(host, port, positive(timeoutMs, DEFAULT_PORT_TIMEOUT_MS));
133
+ JSObject ret = new JSObject();
134
+ ret.put("host", host);
135
+ ret.put("port", port);
136
+ ret.put("open", probe.success);
137
+ ret.put("durationMs", probe.durationMs);
138
+ if (!probe.success) {
139
+ ret.put("errorCode", probe.errorCode);
140
+ ret.put("errorMessage", probe.errorMessage);
141
+ }
142
+ return ret;
143
+ }
144
+
145
+ public JSObject testWebSocket(String url, int timeoutMs) {
146
+ long started = System.nanoTime();
147
+ JSObject ret = new JSObject();
148
+ ret.put("url", url);
149
+
150
+ try {
151
+ URI uri = URI.create(url);
152
+ String scheme = uri.getScheme() == null ? "" : uri.getScheme().toLowerCase(Locale.US);
153
+ if (!scheme.equals("ws") && !scheme.equals("wss")) {
154
+ throw new IllegalArgumentException("WebSocket URL must use ws:// or wss://");
155
+ }
156
+
157
+ String host = uri.getHost();
158
+ if (host == null || host.isEmpty()) {
159
+ throw new IllegalArgumentException("WebSocket host is required");
160
+ }
161
+
162
+ int port = uri.getPort() == -1 ? defaultWebSocketPort(scheme) : uri.getPort();
163
+ int timeout = positive(timeoutMs, DEFAULT_URL_TIMEOUT_MS);
164
+ Socket rawSocket = new Socket();
165
+ rawSocket.connect(new InetSocketAddress(host, port), timeout);
166
+ rawSocket.setSoTimeout(timeout);
167
+
168
+ Socket socket = rawSocket;
169
+ if (scheme.equals("wss")) {
170
+ SSLSocketFactory socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
171
+ SSLSocket sslSocket = (SSLSocket) socketFactory.createSocket(rawSocket, host, port, true);
172
+ sslSocket.setSoTimeout(timeout);
173
+ sslSocket.startHandshake();
174
+ socket = sslSocket;
175
+ }
176
+
177
+ final Socket activeSocket = socket;
178
+ try (
179
+ activeSocket;
180
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(activeSocket.getOutputStream(), StandardCharsets.UTF_8));
181
+ BufferedReader reader = new BufferedReader(new InputStreamReader(activeSocket.getInputStream(), StandardCharsets.UTF_8))
182
+ ) {
183
+ writer.write(webSocketHandshake(uri, host, port, scheme));
184
+ writer.flush();
185
+
186
+ String statusLine = reader.readLine();
187
+ int statusCode = parseStatusCode(statusLine);
188
+ ret.put("open", statusCode == 101);
189
+ ret.put("durationMs", elapsedMs(started));
190
+ ret.put("statusCode", statusCode);
191
+ if (statusCode != 101) {
192
+ ret.put("errorCode", "WEBSOCKET_HANDSHAKE_FAILED");
193
+ ret.put("errorMessage", statusLine == null ? "No handshake response" : statusLine);
194
+ }
195
+ }
196
+ } catch (Exception exception) {
197
+ ret.put("open", false);
198
+ ret.put("durationMs", elapsedMs(started));
199
+ ret.put("errorCode", errorCode(exception));
200
+ ret.put("errorMessage", exception.getMessage());
201
+ }
202
+
203
+ return ret;
204
+ }
205
+
206
+ public JSObject testDownloadSpeed(String url, int maxBytes, int timeoutMs) {
207
+ long started = System.nanoTime();
208
+ int byteLimit = positive(maxBytes, DEFAULT_DOWNLOAD_MAX_BYTES);
209
+ int bytesDownloaded = 0;
210
+ int statusCode = 0;
211
+ HttpURLConnection connection = null;
212
+ JSObject ret = new JSObject();
213
+ ret.put("url", url);
214
+
215
+ try {
216
+ connection = (HttpURLConnection) new URL(url).openConnection();
217
+ connection.setConnectTimeout(positive(timeoutMs, DEFAULT_DOWNLOAD_TIMEOUT_MS));
218
+ connection.setReadTimeout(positive(timeoutMs, DEFAULT_DOWNLOAD_TIMEOUT_MS));
219
+ connection.setRequestMethod("GET");
220
+ connection.setUseCaches(false);
221
+ statusCode = connection.getResponseCode();
222
+
223
+ InputStream stream = statusCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
224
+ if (stream != null) {
225
+ try (InputStream inputStream = stream) {
226
+ byte[] buffer = new byte[16 * 1024];
227
+ while (bytesDownloaded < byteLimit) {
228
+ int maxRead = Math.min(buffer.length, byteLimit - bytesDownloaded);
229
+ int read = inputStream.read(buffer, 0, maxRead);
230
+ if (read == -1) {
231
+ break;
232
+ }
233
+ bytesDownloaded += read;
234
+ }
235
+ }
236
+ }
237
+
238
+ long durationMs = Math.max(elapsedMs(started), 1);
239
+ double bytesPerSecond = bytesDownloaded / (durationMs / 1000.0);
240
+ ret.put("ok", statusCode >= 200 && statusCode < 400);
241
+ ret.put("durationMs", durationMs);
242
+ ret.put("bytesDownloaded", bytesDownloaded);
243
+ ret.put("bytesPerSecond", bytesPerSecond);
244
+ ret.put("mbps", (bytesPerSecond * 8) / 1_000_000);
245
+ ret.put("statusCode", statusCode);
246
+ } catch (Exception exception) {
247
+ long durationMs = Math.max(elapsedMs(started), 1);
248
+ double bytesPerSecond = bytesDownloaded / (durationMs / 1000.0);
249
+ ret.put("ok", false);
250
+ ret.put("durationMs", durationMs);
251
+ ret.put("bytesDownloaded", bytesDownloaded);
252
+ ret.put("bytesPerSecond", bytesPerSecond);
253
+ ret.put("mbps", (bytesPerSecond * 8) / 1_000_000);
254
+ if (statusCode > 0) {
255
+ ret.put("statusCode", statusCode);
256
+ }
257
+ ret.put("errorCode", errorCode(exception));
258
+ ret.put("errorMessage", exception.getMessage());
259
+ } finally {
260
+ if (connection != null) {
261
+ connection.disconnect();
262
+ }
263
+ }
264
+
265
+ return ret;
266
+ }
267
+
268
+ public JSObject testPacketLoss(String mode, String host, int port, String url, int count, int timeoutMs, int intervalMs) {
269
+ String normalizedMode = normalizePacketLossMode(mode, host, port, url);
270
+ int probeCount = positive(count, DEFAULT_PACKET_COUNT);
271
+ int probeTimeout = positive(timeoutMs, DEFAULT_PACKET_TIMEOUT_MS);
272
+ int delay = positive(intervalMs, DEFAULT_PACKET_INTERVAL_MS);
273
+ JSArray latencies = new JSArray();
274
+ int received = 0;
275
+ String lastErrorCode = null;
276
+ String lastErrorMessage = null;
277
+
278
+ for (int index = 0; index < probeCount; index++) {
279
+ ProbeResult probe = normalizedMode.equals("http") ? probeHttp(url, probeTimeout) : probeTcp(host, port, probeTimeout);
280
+
281
+ if (probe.success) {
282
+ received++;
283
+ latencies.put(probe.durationMs);
284
+ } else {
285
+ lastErrorCode = probe.errorCode;
286
+ lastErrorMessage = probe.errorMessage;
287
+ }
288
+
289
+ if (index < probeCount - 1) {
290
+ sleep(delay);
291
+ }
292
+ }
293
+
294
+ return packetLossResult(
295
+ normalizedMode,
296
+ normalizedMode.equals("http") ? url : host + ":" + port,
297
+ probeCount,
298
+ received,
299
+ latencies,
300
+ lastErrorCode,
301
+ lastErrorMessage
302
+ );
303
+ }
304
+
305
+ public JSObject runDiagnostics(
306
+ Context context,
307
+ JSONArray urls,
308
+ JSONArray ports,
309
+ JSONArray websockets,
310
+ JSONObject download,
311
+ JSONObject packetLoss
312
+ ) {
313
+ JSObject ret = new JSObject();
314
+ JSArray urlResults = new JSArray();
315
+ JSArray portResults = new JSArray();
316
+ JSArray websocketResults = new JSArray();
317
+ JSArray issues = new JSArray();
318
+ JSObject status = getNetworkStatus(context);
319
+
320
+ ret.put("status", status);
321
+ addStatusIssues(status, issues);
322
+
323
+ for (int index = 0; index < urls.length(); index++) {
324
+ JSONObject options = urls.optJSONObject(index);
325
+ if (options == null) {
326
+ continue;
327
+ }
328
+ JSObject result = testUrl(
329
+ options.optString("url", ""),
330
+ options.optString("method", "HEAD"),
331
+ options.optInt("timeoutMs", DEFAULT_URL_TIMEOUT_MS),
332
+ options.optBoolean("followRedirects", true)
333
+ );
334
+ urlResults.put(result);
335
+ addUrlIssue(result, issues);
336
+ }
337
+
338
+ for (int index = 0; index < ports.length(); index++) {
339
+ JSONObject options = ports.optJSONObject(index);
340
+ if (options == null) {
341
+ continue;
342
+ }
343
+ JSObject result = testPort(
344
+ options.optString("host", ""),
345
+ options.optInt("port", 0),
346
+ options.optInt("timeoutMs", DEFAULT_PORT_TIMEOUT_MS)
347
+ );
348
+ portResults.put(result);
349
+ addPortIssue(result, issues);
350
+ }
351
+
352
+ for (int index = 0; index < websockets.length(); index++) {
353
+ JSONObject options = websockets.optJSONObject(index);
354
+ if (options == null) {
355
+ continue;
356
+ }
357
+ JSObject result = testWebSocket(options.optString("url", ""), options.optInt("timeoutMs", DEFAULT_URL_TIMEOUT_MS));
358
+ websocketResults.put(result);
359
+ addWebSocketIssue(result, issues);
360
+ }
361
+
362
+ ret.put("urls", urlResults);
363
+ ret.put("ports", portResults);
364
+ ret.put("websockets", websocketResults);
365
+
366
+ if (download != null) {
367
+ JSObject result = testDownloadSpeed(
368
+ download.optString("url", ""),
369
+ download.optInt("maxBytes", DEFAULT_DOWNLOAD_MAX_BYTES),
370
+ download.optInt("timeoutMs", DEFAULT_DOWNLOAD_TIMEOUT_MS)
371
+ );
372
+ ret.put("download", result);
373
+ if (!result.optBoolean("ok", false)) {
374
+ issues.put("Download speed test failed: " + result.optString("url", ""));
375
+ }
376
+ }
377
+
378
+ if (packetLoss != null) {
379
+ JSObject result = testPacketLoss(
380
+ packetLoss.optString("mode", ""),
381
+ packetLoss.optString("host", ""),
382
+ packetLoss.optInt("port", 0),
383
+ packetLoss.optString("url", ""),
384
+ packetLoss.optInt("count", DEFAULT_PACKET_COUNT),
385
+ packetLoss.optInt("timeoutMs", DEFAULT_PACKET_TIMEOUT_MS),
386
+ packetLoss.optInt("intervalMs", DEFAULT_PACKET_INTERVAL_MS)
387
+ );
388
+ ret.put("packetLoss", result);
389
+ if (result.optDouble("lossPercent", 0) > 0) {
390
+ issues.put("Packet loss detected: " + result.optDouble("lossPercent", 0) + "% to " + result.optString("target", ""));
391
+ }
392
+ }
393
+
394
+ ret.put("issues", issues);
395
+ return ret;
396
+ }
397
+
398
+ private void addStatusIssues(JSObject status, JSArray issues) {
399
+ if (!status.optBoolean("connected", false)) {
400
+ issues.put("No active network connection");
401
+ } else if (!status.optBoolean("internetReachable", false)) {
402
+ issues.put("Network is connected but internet reachability is not confirmed");
403
+ }
404
+ if (status.optBoolean("captivePortal", false)) {
405
+ issues.put("Captive portal detected");
406
+ }
407
+ }
408
+
409
+ private void addUrlIssue(JSObject result, JSArray issues) {
410
+ if (!result.optBoolean("reachable", false)) {
411
+ issues.put("URL unreachable: " + result.optString("url", ""));
412
+ } else if (!result.optBoolean("ok", false)) {
413
+ issues.put("URL returned non-success status: " + result.optString("url", ""));
414
+ }
415
+ }
416
+
417
+ private void addPortIssue(JSObject result, JSArray issues) {
418
+ if (!result.optBoolean("open", false)) {
419
+ issues.put("TCP port blocked or unreachable: " + result.optString("host", "") + ":" + result.optInt("port", 0));
420
+ }
421
+ }
422
+
423
+ private void addWebSocketIssue(JSObject result, JSArray issues) {
424
+ if (!result.optBoolean("open", false)) {
425
+ issues.put("WebSocket failed: " + result.optString("url", ""));
426
+ }
427
+ }
428
+
429
+ @SuppressWarnings("deprecation")
430
+ private String fallbackConnectionType(ConnectivityManager connectivityManager) {
431
+ NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
432
+ if (networkInfo == null || !networkInfo.isConnected()) {
433
+ return "none";
434
+ }
435
+ switch (networkInfo.getType()) {
436
+ case ConnectivityManager.TYPE_WIFI:
437
+ return "wifi";
438
+ case ConnectivityManager.TYPE_MOBILE:
439
+ return "cellular";
440
+ case ConnectivityManager.TYPE_ETHERNET:
441
+ return "ethernet";
442
+ case ConnectivityManager.TYPE_VPN:
443
+ return "vpn";
444
+ default:
445
+ return "other";
446
+ }
447
+ }
448
+
449
+ private String connectionType(NetworkCapabilities capabilities) {
450
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
451
+ return "vpn";
452
+ }
453
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
454
+ return "wifi";
455
+ }
456
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
457
+ return "cellular";
458
+ }
459
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
460
+ return "ethernet";
461
+ }
462
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
463
+ return "other";
464
+ }
465
+ return "unknown";
466
+ }
467
+
468
+ private String transports(NetworkCapabilities capabilities) {
469
+ StringBuilder builder = new StringBuilder();
470
+ appendTransport(builder, capabilities, NetworkCapabilities.TRANSPORT_WIFI, "wifi");
471
+ appendTransport(builder, capabilities, NetworkCapabilities.TRANSPORT_CELLULAR, "cellular");
472
+ appendTransport(builder, capabilities, NetworkCapabilities.TRANSPORT_ETHERNET, "ethernet");
473
+ appendTransport(builder, capabilities, NetworkCapabilities.TRANSPORT_VPN, "vpn");
474
+ appendTransport(builder, capabilities, NetworkCapabilities.TRANSPORT_BLUETOOTH, "bluetooth");
475
+ return builder.toString();
476
+ }
477
+
478
+ private void appendTransport(StringBuilder builder, NetworkCapabilities capabilities, int transport, String label) {
479
+ if (!capabilities.hasTransport(transport)) {
480
+ return;
481
+ }
482
+ if (builder.length() > 0) {
483
+ builder.append(",");
484
+ }
485
+ builder.append(label);
486
+ }
487
+
488
+ private ProbeResult probeTcp(String host, int port, int timeoutMs) {
489
+ long started = System.nanoTime();
490
+
491
+ try (Socket socket = new Socket()) {
492
+ socket.connect(new InetSocketAddress(host, port), timeoutMs);
493
+ return ProbeResult.success(elapsedMs(started));
494
+ } catch (Exception exception) {
495
+ return ProbeResult.failure(elapsedMs(started), errorCode(exception), exception.getMessage());
496
+ }
497
+ }
498
+
499
+ private ProbeResult probeHttp(String url, int timeoutMs) {
500
+ JSObject result = testUrl(url, "HEAD", timeoutMs, true);
501
+ boolean reachable = result.optBoolean("reachable", false);
502
+ return reachable
503
+ ? ProbeResult.success(result.optLong("durationMs", 0))
504
+ : ProbeResult.failure(
505
+ result.optLong("durationMs", 0),
506
+ result.optString("errorCode", "ERROR"),
507
+ result.optString("errorMessage", "")
508
+ );
509
+ }
510
+
511
+ private JSObject packetLossResult(
512
+ String mode,
513
+ String target,
514
+ int sent,
515
+ int received,
516
+ JSArray latencies,
517
+ String errorCode,
518
+ String errorMessage
519
+ ) {
520
+ int lost = sent - received;
521
+ JSObject ret = new JSObject();
522
+ ret.put("mode", mode);
523
+ ret.put("target", target);
524
+ ret.put("sent", sent);
525
+ ret.put("received", received);
526
+ ret.put("lost", lost);
527
+ ret.put("lossPercent", sent == 0 ? 0 : (lost * 100.0) / sent);
528
+
529
+ if (latencies.length() > 0) {
530
+ long total = 0;
531
+ long min = Long.MAX_VALUE;
532
+ long max = 0;
533
+ for (int index = 0; index < latencies.length(); index++) {
534
+ long latency = latencies.optLong(index, 0);
535
+ total += latency;
536
+ min = Math.min(min, latency);
537
+ max = Math.max(max, latency);
538
+ }
539
+ ret.put("averageLatencyMs", total / (double) latencies.length());
540
+ ret.put("minLatencyMs", min);
541
+ ret.put("maxLatencyMs", max);
542
+ }
543
+
544
+ if (errorCode != null) {
545
+ ret.put("errorCode", errorCode);
546
+ }
547
+ if (errorMessage != null) {
548
+ ret.put("errorMessage", errorMessage);
549
+ }
550
+
551
+ return ret;
552
+ }
553
+
554
+ private String normalizeMethod(String method) {
555
+ return "GET".equalsIgnoreCase(method) ? "GET" : "HEAD";
556
+ }
557
+
558
+ private String normalizePacketLossMode(String mode, String host, int port, String url) {
559
+ if ("http".equalsIgnoreCase(mode)) {
560
+ return "http";
561
+ }
562
+ if ("tcp".equalsIgnoreCase(mode)) {
563
+ return "tcp";
564
+ }
565
+ if (host != null && !host.isEmpty() && port > 0) {
566
+ return "tcp";
567
+ }
568
+ if (url != null && !url.isEmpty()) {
569
+ return "http";
570
+ }
571
+ return "tcp";
572
+ }
573
+
574
+ private String webSocketHandshake(URI uri, String host, int port, String scheme) {
575
+ String path = uri.getRawPath() == null || uri.getRawPath().isEmpty() ? "/" : uri.getRawPath();
576
+ if (uri.getRawQuery() != null && !uri.getRawQuery().isEmpty()) {
577
+ path += "?" + uri.getRawQuery();
578
+ }
579
+
580
+ byte[] nonce = new byte[16];
581
+ RANDOM.nextBytes(nonce);
582
+ String key = Base64.encodeToString(nonce, Base64.NO_WRAP);
583
+ String hostHeader = isDefaultWebSocketPort(scheme, port) ? host : host + ":" + port;
584
+
585
+ return (
586
+ "GET " +
587
+ path +
588
+ " HTTP/1.1\r\n" +
589
+ "Host: " +
590
+ hostHeader +
591
+ "\r\n" +
592
+ "Upgrade: websocket\r\n" +
593
+ "Connection: Upgrade\r\n" +
594
+ "Sec-WebSocket-Key: " +
595
+ key +
596
+ "\r\n" +
597
+ "Sec-WebSocket-Version: 13\r\n\r\n"
598
+ );
599
+ }
600
+
601
+ private int parseStatusCode(String statusLine) {
602
+ if (statusLine == null) {
603
+ return 0;
604
+ }
605
+ String[] parts = statusLine.split(" ");
606
+ if (parts.length < 2) {
607
+ return 0;
608
+ }
609
+ try {
610
+ return Integer.parseInt(parts[1]);
611
+ } catch (NumberFormatException exception) {
612
+ return 0;
613
+ }
614
+ }
615
+
616
+ private int defaultWebSocketPort(String scheme) {
617
+ return scheme.equals("wss") ? 443 : 80;
618
+ }
619
+
620
+ private boolean isDefaultWebSocketPort(String scheme, int port) {
621
+ return port == defaultWebSocketPort(scheme);
622
+ }
623
+
624
+ private long elapsedMs(long started) {
625
+ return Math.round((System.nanoTime() - started) / 1_000_000.0);
626
+ }
627
+
628
+ private String errorCode(Exception exception) {
629
+ if (exception instanceof SocketTimeoutException) {
630
+ return "TIMEOUT";
631
+ }
632
+ if (exception instanceof UnknownHostException) {
633
+ return "DNS_ERROR";
634
+ }
635
+ if (exception instanceof ConnectException) {
636
+ return "CONNECTION_FAILED";
637
+ }
638
+ if (exception instanceof SSLException) {
639
+ return "TLS_ERROR";
640
+ }
641
+ if (exception instanceof IllegalArgumentException) {
642
+ return "INVALID_ARGUMENT";
643
+ }
644
+ return exception.getClass().getSimpleName();
645
+ }
646
+
647
+ private int positive(int value, int fallback) {
648
+ return value > 0 ? value : fallback;
649
+ }
650
+
651
+ private void sleep(int intervalMs) {
652
+ try {
653
+ Thread.sleep(intervalMs);
654
+ } catch (InterruptedException exception) {
655
+ Thread.currentThread().interrupt();
656
+ }
657
+ }
658
+
659
+ private static class ProbeResult {
660
+
661
+ final boolean success;
662
+ final long durationMs;
663
+ final String errorCode;
664
+ final String errorMessage;
665
+
666
+ private ProbeResult(boolean success, long durationMs, String errorCode, String errorMessage) {
667
+ this.success = success;
668
+ this.durationMs = durationMs;
669
+ this.errorCode = errorCode;
670
+ this.errorMessage = errorMessage;
671
+ }
672
+
673
+ static ProbeResult success(long durationMs) {
674
+ return new ProbeResult(true, durationMs, null, null);
675
+ }
676
+
677
+ static ProbeResult failure(long durationMs, String errorCode, String errorMessage) {
678
+ return new ProbeResult(false, durationMs, errorCode, errorMessage);
679
+ }
680
+ }
681
+ }