mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-01-21 10:14:40 +00:00
CW-860 Fix status for http auth nodes (#1943)
This commit is contained in:
parent
2fb07dd5d4
commit
aef71d16f1
1 changed files with 174 additions and 12 deletions
|
@ -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';
|
||||
|
||||
|
@ -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
|
||||
||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue