1
1
import 'dart:io' ;
2
+ import 'dart:math' ;
2
3
import 'package:cw_core/keyable.dart' ;
4
+ import 'package:cw_core/utils/print_verbose.dart' ;
3
5
import 'dart:convert' ;
4
6
import 'package:http/http.dart' as http;
5
7
import 'package:hive/hive.dart' ;
6
8
import 'package:cw_core/hive_type_ids.dart' ;
7
9
import 'package:cw_core/wallet_type.dart' ;
8
10
import 'package:http/io_client.dart' as ioc;
11
+ import 'dart:math' as math;
12
+ import 'package:convert/convert.dart' ;
13
+ import 'package:crypto/crypto.dart' as crypto;
9
14
10
- // import 'package:tor/tor .dart';
15
+ import 'package:crypto/crypto .dart' ;
11
16
12
17
part 'node.g.dart' ;
13
18
@@ -170,34 +175,43 @@ class Node extends HiveObject with Keyable {
170
175
}
171
176
172
177
Future <bool > requestMoneroNode () async {
173
- if (uri. toString (). contains ( ".onion" ) || useSocksProxy) {
178
+ if (useSocksProxy) {
174
179
return await requestNodeWithProxy ();
175
180
}
181
+
182
+
176
183
final path = '/json_rpc' ;
177
184
final rpcUri = isSSL ? Uri .https (uri.authority, path) : Uri .http (uri.authority, path);
178
- final realm = 'monero-rpc' ;
179
185
final body = {'jsonrpc' : '2.0' , 'id' : '0' , 'method' : 'get_info' };
180
186
187
+
181
188
try {
182
189
final authenticatingClient = HttpClient ();
183
-
184
190
authenticatingClient.badCertificateCallback =
185
191
((X509Certificate cert, String host, int port) => true );
186
192
187
- authenticatingClient.addCredentials (
188
- rpcUri,
189
- realm,
190
- HttpClientDigestCredentials (login ?? '' , password ?? '' ),
191
- );
192
193
193
194
final http.Client client = ioc.IOClient (authenticatingClient);
194
195
196
+ final jsonBody = json.encode (body);
197
+
195
198
final response = await client.post (
196
199
rpcUri,
197
200
headers: {'Content-Type' : 'application/json' },
198
- body: json. encode (body) ,
201
+ body: jsonBody ,
199
202
);
200
- client.close ();
203
+ // Check if we received a 401 Unauthorized response
204
+ if (response.statusCode == 401 ) {
205
+ final daemonRpc = DaemonRpc (
206
+ rpcUri.toString (),
207
+ username: login?? '' ,
208
+ password: password?? '' ,
209
+ );
210
+ final response = await daemonRpc.call ('get_info' , {});
211
+ return ! (response['offline' ] as bool );
212
+ }
213
+
214
+ printV ("node check response: ${response .body }" );
201
215
202
216
if ((response.body.contains ("400 Bad Request" ) // Some other generic error
203
217
||
@@ -225,7 +239,8 @@ class Node extends HiveObject with Keyable {
225
239
226
240
final resBody = json.decode (response.body) as Map <String , dynamic >;
227
241
return ! (resBody['result' ]['offline' ] as bool );
228
- } catch (_) {
242
+ } catch (e) {
243
+ printV ("error: $e " );
229
244
return false ;
230
245
}
231
246
}
@@ -316,3 +331,150 @@ class Node extends HiveObject with Keyable {
316
331
}
317
332
}
318
333
}
334
+
335
+ /// https://github.com/ManyMath/digest_auth/
336
+ /// HTTP Digest authentication.
337
+ ///
338
+ /// Adapted from https://github.com/dart-lang/http/issues/605#issue-963962341.
339
+ ///
340
+ /// Created because http_auth was not working for Monero daemon RPC responses.
341
+ class DigestAuth {
342
+ final String username;
343
+ final String password;
344
+ String ? realm;
345
+ String ? nonce;
346
+ String ? uri;
347
+ String ? qop = "auth" ;
348
+ int _nonceCount = 0 ;
349
+
350
+ DigestAuth (this .username, this .password);
351
+
352
+ /// Initialize Digest parameters from the `WWW-Authenticate` header.
353
+ void initFromAuthorizationHeader (String authInfo) {
354
+ final Map <String , String >? values = _splitAuthenticateHeader (authInfo);
355
+ if (values != null ) {
356
+ realm = values['realm' ];
357
+ // Check if the nonce has changed.
358
+ if (nonce != values['nonce' ]) {
359
+ nonce = values['nonce' ];
360
+ _nonceCount = 0 ; // Reset nonce count when nonce changes.
361
+ }
362
+ }
363
+ }
364
+
365
+ /// Generate the Digest Authorization header.
366
+ String getAuthString (String method, String uri) {
367
+ this .uri = uri;
368
+ _nonceCount++ ;
369
+ String cnonce = _computeCnonce ();
370
+ String nc = _formatNonceCount (_nonceCount);
371
+
372
+ String ha1 = md5Hash ("$username :$realm :$password " );
373
+ String ha2 = md5Hash ("$method :$uri " );
374
+ String response = md5Hash ("$ha1 :$nonce :$nc :$cnonce :$qop :$ha2 " );
375
+
376
+ return 'Digest username="$username ", realm="$realm ", nonce="$nonce ", uri="$uri ", qop=$qop , nc=$nc , cnonce="$cnonce ", response="$response "' ;
377
+ }
378
+
379
+ /// Helper to parse the `WWW-Authenticate` header.
380
+ Map <String , String >? _splitAuthenticateHeader (String ? header) {
381
+ if (header == null || ! header.startsWith ('Digest ' )) {
382
+ return null ;
383
+ }
384
+ String token = header.substring (7 ); // Remove 'Digest '.
385
+ final Map <String , String > result = {};
386
+
387
+ final components = token.split (',' ).map ((token) => token.trim ());
388
+ for (final component in components) {
389
+ final kv = component.split ('=' );
390
+ final key = kv[0 ];
391
+ final value = kv.sublist (1 ).join ('=' ).replaceAll ('"' , '' );
392
+ result[key] = value;
393
+ }
394
+ return result;
395
+ }
396
+
397
+ /// Helper to compute a random cnonce.
398
+ String _computeCnonce () {
399
+ final math.Random rnd = math.Random ();
400
+ final List <int > values = List <int >.generate (16 , (i) => rnd.nextInt (256 ));
401
+ return hex.encode (values);
402
+ }
403
+
404
+ /// Helper to format the nonce count.
405
+ String _formatNonceCount (int count) =>
406
+ count.toRadixString (16 ).padLeft (8 , '0' );
407
+
408
+ /// Compute the MD5 hash of a string.
409
+ String md5Hash (String input) {
410
+ return md5.convert (utf8.encode (input)).toString ();
411
+ }
412
+ }
413
+
414
+ class DaemonRpc {
415
+ final String rpcUrl;
416
+ final String username;
417
+ final String password;
418
+
419
+ DaemonRpc (this .rpcUrl, {required this .username, required this .password});
420
+
421
+ /// Perform a JSON-RPC call with Digest Authentication.
422
+ Future <Map <String , dynamic >> call (
423
+ String method, Map <String , dynamic > params) async {
424
+ final http.Client client = http.Client ();
425
+ final DigestAuth digestAuth = DigestAuth (username, password);
426
+
427
+ // Initial request to get the `WWW-Authenticate` header.
428
+ final initialResponse = await client.post (
429
+ Uri .parse (rpcUrl),
430
+ headers: {
431
+ 'Content-Type' : 'application/json' ,
432
+ },
433
+ body: jsonEncode ({
434
+ 'jsonrpc' : '2.0' ,
435
+ 'id' : '0' ,
436
+ 'method' : method,
437
+ 'params' : params,
438
+ }),
439
+ );
440
+
441
+ if (initialResponse.statusCode != 401 ||
442
+ ! initialResponse.headers.containsKey ('www-authenticate' )) {
443
+ throw Exception ('Unexpected response: ${initialResponse .body }' );
444
+ }
445
+
446
+ // Extract Digest details from `WWW-Authenticate` header.
447
+ final String authInfo = initialResponse.headers['www-authenticate' ]! ;
448
+ digestAuth.initFromAuthorizationHeader (authInfo);
449
+
450
+ // Create Authorization header for the second request.
451
+ String uri = Uri .parse (rpcUrl).path;
452
+ String authHeader = digestAuth.getAuthString ('POST' , uri);
453
+
454
+ // Make the authenticated request.
455
+ final authenticatedResponse = await client.post (
456
+ Uri .parse (rpcUrl),
457
+ headers: {
458
+ 'Content-Type' : 'application/json' ,
459
+ 'Authorization' : authHeader,
460
+ },
461
+ body: jsonEncode ({
462
+ 'jsonrpc' : '2.0' ,
463
+ 'id' : '0' ,
464
+ 'method' : method,
465
+ 'params' : params,
466
+ }),
467
+ );
468
+
469
+ if (authenticatedResponse.statusCode != 200 ) {
470
+ throw Exception ('RPC call failed: ${authenticatedResponse .body }' );
471
+ }
472
+
473
+ final Map <String , dynamic > result = jsonDecode (authenticatedResponse.body) as Map <String , dynamic >;
474
+ if (result['error' ] != null ) {
475
+ throw Exception ('RPC Error: ${result ['error' ]}' );
476
+ }
477
+
478
+ return result['result' ] as Map <String , dynamic >;
479
+ }
480
+ }
0 commit comments