Skip to content

Commit

Permalink
CW-860 Fix status for http auth nodes (#1943)
Browse files Browse the repository at this point in the history
  • Loading branch information
MrCyjaneK authored Jan 20, 2025
1 parent 2fb07dd commit aef71d1
Showing 1 changed file with 174 additions and 12 deletions.
186 changes: 174 additions & 12 deletions cw_core/lib/node.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import 'dart:io';
import 'dart:math';
import 'package:cw_core/keyable.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:hive/hive.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/io_client.dart' as ioc;
import 'dart:math' as math;
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart' as crypto;

// import 'package:tor/tor.dart';
import 'package:crypto/crypto.dart';

part 'node.g.dart';

Expand Down Expand Up @@ -170,34 +175,43 @@ class Node extends HiveObject with Keyable {
}

Future<bool> requestMoneroNode() async {
if (uri.toString().contains(".onion") || useSocksProxy) {
if (useSocksProxy) {
return await requestNodeWithProxy();
}


final path = '/json_rpc';
final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path);
final realm = 'monero-rpc';
final body = {'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'};


try {
final authenticatingClient = HttpClient();

authenticatingClient.badCertificateCallback =
((X509Certificate cert, String host, int port) => true);

authenticatingClient.addCredentials(
rpcUri,
realm,
HttpClientDigestCredentials(login ?? '', password ?? ''),
);

final http.Client client = ioc.IOClient(authenticatingClient);

final jsonBody = json.encode(body);

final response = await client.post(
rpcUri,
headers: {'Content-Type': 'application/json'},
body: json.encode(body),
body: jsonBody,
);
client.close();
// Check if we received a 401 Unauthorized response
if (response.statusCode == 401) {
final daemonRpc = DaemonRpc(
rpcUri.toString(),
username: login??'',
password: password??'',
);
final response = await daemonRpc.call('get_info', {});
return !(response['offline'] as bool);
}

printV("node check response: ${response.body}");

if ((response.body.contains("400 Bad Request") // Some other generic error
||
Expand Down Expand Up @@ -225,7 +239,8 @@ class Node extends HiveObject with Keyable {

final resBody = json.decode(response.body) as Map<String, dynamic>;
return !(resBody['result']['offline'] as bool);
} catch (_) {
} catch (e) {
printV("error: $e");
return false;
}
}
Expand Down Expand Up @@ -316,3 +331,150 @@ class Node extends HiveObject with Keyable {
}
}
}

/// https://github.com/ManyMath/digest_auth/
/// HTTP Digest authentication.
///
/// Adapted from https://github.com/dart-lang/http/issues/605#issue-963962341.
///
/// Created because http_auth was not working for Monero daemon RPC responses.
class DigestAuth {
final String username;
final String password;
String? realm;
String? nonce;
String? uri;
String? qop = "auth";
int _nonceCount = 0;

DigestAuth(this.username, this.password);

/// Initialize Digest parameters from the `WWW-Authenticate` header.
void initFromAuthorizationHeader(String authInfo) {
final Map<String, String>? values = _splitAuthenticateHeader(authInfo);
if (values != null) {
realm = values['realm'];
// Check if the nonce has changed.
if (nonce != values['nonce']) {
nonce = values['nonce'];
_nonceCount = 0; // Reset nonce count when nonce changes.
}
}
}

/// Generate the Digest Authorization header.
String getAuthString(String method, String uri) {
this.uri = uri;
_nonceCount++;
String cnonce = _computeCnonce();
String nc = _formatNonceCount(_nonceCount);

String ha1 = md5Hash("$username:$realm:$password");
String ha2 = md5Hash("$method:$uri");
String response = md5Hash("$ha1:$nonce:$nc:$cnonce:$qop:$ha2");

return 'Digest username="$username", realm="$realm", nonce="$nonce", uri="$uri", qop=$qop, nc=$nc, cnonce="$cnonce", response="$response"';
}

/// Helper to parse the `WWW-Authenticate` header.
Map<String, String>? _splitAuthenticateHeader(String? header) {
if (header == null || !header.startsWith('Digest ')) {
return null;
}
String token = header.substring(7); // Remove 'Digest '.
final Map<String, String> result = {};

final components = token.split(',').map((token) => token.trim());
for (final component in components) {
final kv = component.split('=');
final key = kv[0];
final value = kv.sublist(1).join('=').replaceAll('"', '');
result[key] = value;
}
return result;
}

/// Helper to compute a random cnonce.
String _computeCnonce() {
final math.Random rnd = math.Random();
final List<int> values = List<int>.generate(16, (i) => rnd.nextInt(256));
return hex.encode(values);
}

/// Helper to format the nonce count.
String _formatNonceCount(int count) =>
count.toRadixString(16).padLeft(8, '0');

/// Compute the MD5 hash of a string.
String md5Hash(String input) {
return md5.convert(utf8.encode(input)).toString();
}
}

class DaemonRpc {
final String rpcUrl;
final String username;
final String password;

DaemonRpc(this.rpcUrl, {required this.username, required this.password});

/// Perform a JSON-RPC call with Digest Authentication.
Future<Map<String, dynamic>> call(
String method, Map<String, dynamic> params) async {
final http.Client client = http.Client();
final DigestAuth digestAuth = DigestAuth(username, password);

// Initial request to get the `WWW-Authenticate` header.
final initialResponse = await client.post(
Uri.parse(rpcUrl),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'jsonrpc': '2.0',
'id': '0',
'method': method,
'params': params,
}),
);

if (initialResponse.statusCode != 401 ||
!initialResponse.headers.containsKey('www-authenticate')) {
throw Exception('Unexpected response: ${initialResponse.body}');
}

// Extract Digest details from `WWW-Authenticate` header.
final String authInfo = initialResponse.headers['www-authenticate']!;
digestAuth.initFromAuthorizationHeader(authInfo);

// Create Authorization header for the second request.
String uri = Uri.parse(rpcUrl).path;
String authHeader = digestAuth.getAuthString('POST', uri);

// Make the authenticated request.
final authenticatedResponse = await client.post(
Uri.parse(rpcUrl),
headers: {
'Content-Type': 'application/json',
'Authorization': authHeader,
},
body: jsonEncode({
'jsonrpc': '2.0',
'id': '0',
'method': method,
'params': params,
}),
);

if (authenticatedResponse.statusCode != 200) {
throw Exception('RPC call failed: ${authenticatedResponse.body}');
}

final Map<String, dynamic> result = jsonDecode(authenticatedResponse.body) as Map<String, dynamic>;
if (result['error'] != null) {
throw Exception('RPC Error: ${result['error']}');
}

return result['result'] as Map<String, dynamic>;
}
}

0 comments on commit aef71d1

Please sign in to comment.