@@ -2,6 +2,7 @@ import 'dart:async';
22import 'dart:io' ;
33
44import 'package:flutter/foundation.dart' ;
5+ import 'package:flutter_curl/flutter_curl.dart' as curl;
56
67import 'xray_binary_service.dart' ;
78import 'xray_process_manager.dart' ;
@@ -70,7 +71,7 @@ class CdnScanConfig {
7071 this .concurrentInstances = 5 ,
7172 this .timeout = const Duration (seconds: 10 ),
7273 this .startupDelay = const Duration (seconds: 2 ),
73- this .testUrl = 'https://www.gstatic .com/generate_204' ,
74+ this .testUrl = 'https://www.google .com/generate_204' ,
7475 this .basePort = 10808 ,
7576 this .enableXrayLogs = kDebugMode,
7677 });
@@ -100,6 +101,7 @@ class CdnConfigScanner {
100101 final CdnScanConfig config;
101102
102103 XrayProcessManager ? _processManager;
104+ curl.Client ? _curlClient;
103105 bool _isScanning = false ;
104106 bool _shouldStop = false ;
105107
@@ -194,9 +196,17 @@ class CdnConfigScanner {
194196 return true ;
195197 }
196198
197- /// Test connection through SOCKS5 proxy using curl command
198- /// This feature is only available on desktop (Linux/Windows)
199+ /// Test connection through SOCKS5 proxy.
200+ /// Uses shell curl on desktop (Linux/Windows) and libcurl via flutter_curl on Android.
199201 Future <CdnScanResult > _testWithSocks5Proxy (String ip, int proxyPort) async {
202+ if (Platform .isAndroid) {
203+ return _testWithSocks5ProxyDart (ip, proxyPort);
204+ }
205+ return _testWithSocks5ProxyCurl (ip, proxyPort);
206+ }
207+
208+ /// Test connection through SOCKS5 proxy using curl command (Linux/Windows)
209+ Future <CdnScanResult > _testWithSocks5ProxyCurl (String ip, int proxyPort) async {
200210 try {
201211 // Use Process.run() instead of Process.start() to avoid file descriptor leaks
202212 // Process.run() automatically handles stdout/stderr cleanup
@@ -267,6 +277,68 @@ class CdnConfigScanner {
267277 }
268278 }
269279
280+ /// Test connection through SOCKS5 proxy using flutter_curl / libcurl (Android).
281+ /// Follows the exact same logic as the desktop curl command version.
282+ Future <CdnScanResult > _testWithSocks5ProxyDart (String ip, int proxyPort) async {
283+ try {
284+ assert (_curlClient != null , 'curl client must be initialized before testing' );
285+
286+ final stopwatch = Stopwatch ()..start ();
287+
288+ final response = await _curlClient! .send (curl.Request (
289+ method: 'GET' ,
290+ url: config.testUrl,
291+ proxy: 'socks5h://127.0.0.1:$proxyPort ' ,
292+ verifySSL: false ,
293+ connectTimeout: config.timeout,
294+ timeout: config.timeout + const Duration (seconds: 2 ),
295+ )).timeout (config.timeout + const Duration (seconds: 5 ));
296+
297+ stopwatch.stop ();
298+ final timeMs = stopwatch.elapsedMilliseconds.toDouble ();
299+
300+ // Check for curl-level errors (equivalent to curl exit code != 0)
301+ if (response.errorCode != null && response.errorCode != 0 ) {
302+ final errorMsg = (response.errorMessage? .isNotEmpty == true )
303+ ? response.errorMessage!
304+ : 'curl error code ${response .errorCode }' ;
305+ return CdnScanResult (
306+ ip: ip,
307+ success: false ,
308+ errorMessage: errorMsg,
309+ );
310+ }
311+
312+ // Parse status code – same logic as desktop curl version
313+ final statusCode = response.statusCode;
314+ if (statusCode == 204 || (statusCode >= 200 && statusCode < 300 )) {
315+ return CdnScanResult (
316+ ip: ip,
317+ success: true ,
318+ latencyMs: timeMs,
319+ );
320+ } else {
321+ return CdnScanResult (
322+ ip: ip,
323+ success: false ,
324+ errorMessage: 'HTTP $statusCode ' ,
325+ );
326+ }
327+ } on TimeoutException {
328+ return CdnScanResult (
329+ ip: ip,
330+ success: false ,
331+ errorMessage: 'Connection timed out' ,
332+ );
333+ } catch (e) {
334+ return CdnScanResult (
335+ ip: ip,
336+ success: false ,
337+ errorMessage: e.toString (),
338+ );
339+ }
340+ }
341+
270342 /// Start scanning IPs
271343 Stream <CdnScanProgress > scanIps (
272344 List <String > ips,
@@ -302,6 +374,15 @@ class CdnConfigScanner {
302374 enableProcessLogs: config.enableXrayLogs,
303375 );
304376
377+ // Initialize curl client for Android (uses libcurl instead of shell curl)
378+ if (Platform .isAndroid) {
379+ _curlClient = curl.Client (
380+ verifySSL: false ,
381+ verbose: kDebugMode,
382+ );
383+ await _curlClient! .init ();
384+ }
385+
305386 final ipQueue = List <String >.from (ips);
306387 int ipIndex = 0 ;
307388 final maxConcurrency = _effectiveConcurrency ();
@@ -368,7 +449,12 @@ class CdnConfigScanner {
368449 ) async {
369450 XrayInstance ? instance;
370451 try {
452+ if (kDebugMode) debugPrint ('[CdnScan] Starting xray instance for IP=$ip ' );
371453 instance = await _processManager! .startInstanceForIp (baseConfig, ip);
454+ if (kDebugMode) {
455+ debugPrint ('[CdnScan] Xray started for IP=$ip , port=${instance .port }, pid=${instance .process .pid }' );
456+ debugPrint ('[CdnScan] Waiting ${config .startupDelay .inMilliseconds }ms for xray startup...' );
457+ }
372458 await Future .delayed (config.startupDelay);
373459
374460 if (_shouldStop) {
@@ -379,14 +465,40 @@ class CdnConfigScanner {
379465 );
380466 }
381467
382- return await _testWithSocks5Proxy (ip, instance.port);
468+ // On Android, check if xray process is still alive before testing
469+ if (Platform .isAndroid) {
470+ final exitCodeFuture = instance.process.exitCode;
471+ final aliveCheck = await Future .any ([
472+ exitCodeFuture.then ((code) => 'exited:$code ' ),
473+ Future .delayed (const Duration (milliseconds: 100 ), () => 'alive' ),
474+ ]);
475+ if (kDebugMode) debugPrint ('[CdnScan] Xray process status before test: $aliveCheck ' );
476+ if (aliveCheck.startsWith ('exited:' )) {
477+ final code = aliveCheck.split (':' )[1 ];
478+ if (kDebugMode) debugPrint ('[CdnScan] ERROR: xray died before proxy test! exit=$code , errors: ${instance .errorLog }' );
479+ return CdnScanResult (
480+ ip: ip,
481+ success: false ,
482+ errorMessage: 'Xray process died (exit=$code )' ,
483+ );
484+ }
485+ }
486+
487+ final result = await _testWithSocks5Proxy (ip, instance.port);
488+ if (kDebugMode) {
489+ debugPrint ('[CdnScan] Test result for IP=$ip : success=${result .success }, '
490+ 'latency=${result .latencyMs }ms, error=${result .errorMessage }' );
491+ }
492+ return result;
383493 } on XrayStartupException catch (e) {
494+ if (kDebugMode) debugPrint ('[CdnScan] XrayStartupException for IP=$ip : ${e .message }' );
384495 return CdnScanResult (
385496 ip: ip,
386497 success: false ,
387498 errorMessage: 'Xray start failed: ${e .message }' ,
388499 );
389500 } catch (e) {
501+ if (kDebugMode) debugPrint ('[CdnScan] Exception for IP=$ip : $e ' );
390502 return CdnScanResult (
391503 ip: ip,
392504 success: false ,
@@ -407,6 +519,13 @@ class CdnConfigScanner {
407519 await _processManager! .stopAll ();
408520 _processManager = null ;
409521 }
522+ // Dispose curl client (Android)
523+ if (_curlClient != null ) {
524+ try {
525+ _curlClient! .dispose ();
526+ } catch (_) {}
527+ _curlClient = null ;
528+ }
410529 }
411530
412531 /// Stop the current scan
0 commit comments