1- import 'dart:async' ;
21import 'dart:convert' ;
3- import 'dart:io' ;
42
3+ import 'package:bb_mobile/core/electrum/data/electrum_socket_connector.dart' ;
54import 'package:bb_mobile/core/electrum/domain/ports/server_status_port.dart' ;
65import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_server_network.dart' ;
76import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_server_status.dart' ;
7+ import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_server_url.dart' ;
88import 'package:bb_mobile/core/utils/logger.dart' ;
9+ import 'package:bull_tor/tor.dart' ;
910
1011class ServerStatusAdapter implements ServerStatusPort {
11- const ServerStatusAdapter ();
12+ final ElectrumSocketConnector _socketConnector;
13+
14+ const ServerStatusAdapter (this ._socketConnector);
1215
1316 /// Bitcoin pizza day — first real-world BTC purchase, May 22 2010.
1417 static const _bitcoinMainnetProbeTxid =
@@ -22,8 +25,7 @@ class ServerStatusAdapter implements ServerStatusPort {
2225 Future <ElectrumServerStatus > checkSocket ({
2326 required String url,
2427 int ? timeout,
25- bool useTorProxy = false ,
26- int torProxyPort = 9050 ,
28+ TorProxyEndpoint ? proxyEndpoint,
2729 }) async {
2830 try {
2931 if (url.isEmpty) return ElectrumServerStatus .unknown;
@@ -33,23 +35,18 @@ class ServerStatusAdapter implements ServerStatusPort {
3335
3436 final effectiveTimeout = _resolveTimeout (uri, timeout);
3537
36- final isConnectable = useTorProxy
37- ? await _checkThroughSocks5 (
38- uri: uri,
39- proxyPort: torProxyPort,
40- timeoutSeconds: effectiveTimeout,
41- )
42- : await _checkRawSocket (uri: uri, timeoutSeconds: effectiveTimeout);
43-
44- return isConnectable
45- ? ElectrumServerStatus .online
46- : ElectrumServerStatus .offline;
47- } catch (e) {
48- log.severe (
49- message: 'Error checking server socket for $url ' ,
50- error: e,
51- trace: StackTrace .current,
38+ final socket = await _socketConnector.connect (
39+ server: uri,
40+ proxy: proxyEndpoint,
41+ timeout: Duration (seconds: effectiveTimeout),
42+ allowBadCertificate: true ,
5243 );
44+ socket.destroy ();
45+ return ElectrumServerStatus .online;
46+ } catch (e) {
47+ // A server we cannot reach is the expected answer of this check, not a
48+ // fault: `severe` would report every offline server to Sentry.
49+ log.warning ('Socket check failed for $url - $e ' );
5350 return ElectrumServerStatus .offline;
5451 }
5552 }
@@ -60,6 +57,7 @@ class ServerStatusAdapter implements ServerStatusPort {
6057 required ElectrumServerNetwork network,
6158 required bool validateDomain,
6259 int ? timeout,
60+ TorProxyEndpoint ? proxyEndpoint,
6361 }) async {
6462 try {
6563 if (url.isEmpty) return ElectrumServerStatus .unknown;
@@ -81,6 +79,7 @@ class ServerStatusAdapter implements ServerStatusPort {
8179 request: request,
8280 timeoutSeconds: effectiveTimeout,
8381 validateDomain: validateDomain,
82+ proxyEndpoint: proxyEndpoint,
8483 );
8584
8685 if (response.isEmpty) return ElectrumServerStatus .offline;
@@ -126,34 +125,30 @@ class ServerStatusAdapter implements ServerStatusPort {
126125 /// Onion addresses need longer timeouts due to Tor circuit building.
127126 /// Default: 5 seconds for clearnet, 30 seconds for .onion addresses.
128127 int _resolveTimeout (Uri uri, int ? timeout) {
129- final isOnion = uri.host. endsWith ( '.onion' );
128+ final isOnion = ElectrumServerUrl . isOnionHost (uri.host );
130129 return timeout ?? (isOnion ? 30 : 5 );
131130 }
132131
133- /// Sends a JSON-RPC request and returns the raw response line.
134- /// [SecureSocket] extends [Socket] , so both branches share the same
135- /// write/read logic after construction .
132+ /// Sends a JSON-RPC request and returns the raw response line. Plain, TLS
133+ /// and proxied sockets all arrive as a [Socket] , so the read/write path
134+ /// below is the same for every transport .
136135 Future <String > _sendRequest ({
137136 required Uri uri,
138137 required String request,
139138 required int timeoutSeconds,
140139 required bool validateDomain,
140+ TorProxyEndpoint ? proxyEndpoint,
141141 }) async {
142- // A null onBadCertificate callback enforces strict CA validation. The
143- // flag comes from the user's electrum settings, so the probe accepts
144- // exactly the certificates the BDK/LWK sync would accept.
145- final Socket socket = uri.scheme == 'ssl'
146- ? await SecureSocket .connect (
147- uri.host,
148- uri.port,
149- timeout: Duration (seconds: timeoutSeconds),
150- onBadCertificate: validateDomain ? null : (_) => true ,
151- )
152- : await Socket .connect (
153- uri.host,
154- uri.port,
155- timeout: Duration (seconds: timeoutSeconds),
156- );
142+ // Certificates follow the user's `validateDomain` setting rather than
143+ // being blanket-accepted: probing laxer than the sync reports a server
144+ // online that the sync will then refuse, and probing stricter hides a
145+ // server that would have worked.
146+ final socket = await _socketConnector.connect (
147+ server: uri,
148+ proxy: proxyEndpoint,
149+ timeout: Duration (seconds: timeoutSeconds),
150+ allowBadCertificate: ! validateDomain,
151+ );
157152
158153 try {
159154 socket.write (request);
@@ -167,127 +162,4 @@ class ServerStatusAdapter implements ServerStatusPort {
167162 socket.destroy ();
168163 }
169164 }
170-
171- Future <bool > _checkRawSocket ({
172- required Uri uri,
173- required int timeoutSeconds,
174- }) async {
175- try {
176- final socket = await Socket .connect (
177- uri.host,
178- uri.port,
179- timeout: Duration (seconds: timeoutSeconds),
180- );
181- socket.destroy ();
182- return true ;
183- } on SocketException catch (e) {
184- log.warning ('Socket connection failed for $uri - $e ' );
185- return false ;
186- } catch (e) {
187- log.severe (
188- message: 'Unexpected error checking socket for $uri ' ,
189- error: e,
190- trace: StackTrace .current,
191- );
192- return false ;
193- }
194- }
195-
196- /// Checks connectivity through a SOCKS5 proxy (used for Tor).
197- Future <bool > _checkThroughSocks5 ({
198- required Uri uri,
199- required int proxyPort,
200- required int timeoutSeconds,
201- }) async {
202- Socket ? socket;
203- StreamSubscription <List <int >>? subscription;
204- try {
205- socket = await Socket .connect (
206- '127.0.0.1' ,
207- proxyPort,
208- timeout: Duration (seconds: timeoutSeconds),
209- );
210-
211- final handshakeCompleter = Completer <List <int >>();
212- final connectCompleter = Completer <List <int >>();
213- var isHandshakeDone = false ;
214-
215- subscription = socket.listen (
216- (List <int > data) {
217- if (! isHandshakeDone) {
218- handshakeCompleter.complete (data);
219- isHandshakeDone = true ;
220- } else {
221- connectCompleter.complete (data);
222- }
223- },
224- onError: (Object error) {
225- if (! handshakeCompleter.isCompleted) {
226- handshakeCompleter.completeError (error);
227- }
228- if (! connectCompleter.isCompleted) {
229- connectCompleter.completeError (error);
230- }
231- },
232- cancelOnError: true ,
233- );
234-
235- // SOCKS5 handshake: version 5, 1 auth method (no auth)
236- socket.add ([0x05 , 0x01 , 0x00 ]);
237-
238- final handshakeResponse = await handshakeCompleter.future.timeout (
239- Duration (seconds: timeoutSeconds),
240- );
241-
242- if (handshakeResponse.length < 2 || handshakeResponse[0 ] != 0x05 ) {
243- log.warning ('Invalid SOCKS5 handshake response' );
244- return false ;
245- }
246-
247- socket.add (_buildSocks5ConnectRequest (uri));
248-
249- final connectResponse = await connectCompleter.future.timeout (
250- Duration (seconds: timeoutSeconds),
251- );
252-
253- if (connectResponse.length < 2 ||
254- connectResponse[0 ] != 0x05 ||
255- connectResponse[1 ] != 0x00 ) {
256- log.severe (
257- error: Exception ('SOCKS5 connection failed' ),
258- trace: StackTrace .current,
259- );
260- return false ;
261- }
262-
263- return true ;
264- } catch (e) {
265- log.severe (
266- message: 'SOCKS5 connection check failed for $uri ' ,
267- error: e,
268- trace: StackTrace .current,
269- );
270- return false ;
271- } finally {
272- await subscription? .cancel ();
273- socket? .destroy ();
274- }
275- }
276-
277- /// Builds a SOCKS5 CONNECT request.
278- /// Format: [version, command, reserved, address_type, address, port]
279- List <int > _buildSocks5ConnectRequest (Uri uri) {
280- final request = < int > [
281- 0x05 , // SOCKS version 5
282- 0x01 , // CONNECT command
283- 0x00 , // Reserved
284- 0x03 , // Address type: domain name
285- ];
286- final hostBytes = uri.host.codeUnits;
287- request.add (hostBytes.length);
288- request.addAll (hostBytes);
289- request.add ((uri.port >> 8 ) & 0xFF );
290- request.add (uri.port & 0xFF );
291- return request;
292- }
293165}
0 commit comments