Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 16 additions & 17 deletions lib/electrumx_rpc/client_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ClientManager {

final Map<String, ElectrumClient> _map = {};
final Map<String, TorPlainNetworkOption> _mapNet = {};
final Map<String, String?> _certificates = {};
final Map<String, int> _heights = {};
final Map<String, StreamSubscription<BlockHeader>> _subscriptions = {};
final Map<String, Completer<int>> _heightCompleters = {};
Expand Down Expand Up @@ -42,6 +43,7 @@ class ClientManager {
ElectrumClient client, {
required CryptoCurrency cryptoCurrency,
required TorPlainNetworkOption netType,
String? trustedCertificatePem,
}) async {
final key = _keyHelper(cryptoCurrency);
if (_map[key] != null) {
Expand All @@ -60,6 +62,7 @@ class ClientManager {
_mapNet[key] = netType;
}

_certificates[key] = trustedCertificatePem;
_heightCompleters[key] = Completer<int>();
_subscriptions[key] = client.subscribeHeaders().listen(
(event) {
Expand All @@ -69,45 +72,39 @@ class ClientManager {
_heightCompleters[key]!.complete(event.height);
}
},
onError: (Object err, StackTrace s) => Logging.instance.e(
"ClientManager listen",
error: err,
stackTrace: s,
),
onError: (Object err, StackTrace s) =>
Logging.instance.e("ClientManager listen", error: err, stackTrace: s),
);
}

Future<int> getChainHeightFor(CryptoCurrency cryptoCurrency) async {
final key = _keyHelper(cryptoCurrency);

if (_map[key] == null) {
throw Exception(
"No managed ElectrumClient for $key found.",
);
throw Exception("No managed ElectrumClient for $key found.");
}
if (_heightCompleters[key] == null) {
throw Exception(
"No managed _heightCompleters for $key found.",
);
throw Exception("No managed _heightCompleters for $key found.");
}

if (Prefs.instance.useTor) {
if (_mapNet[key]! == TorPlainNetworkOption.clear) {
throw Exception(
"Non-TOR only client for $key found.",
);
throw Exception("Non-TOR only client for $key found.");
}
} else {
if (_mapNet[key]! == TorPlainNetworkOption.tor) {
throw Exception(
"TOR only client for $key found.",
);
throw Exception("TOR only client for $key found.");
}
}

return _heights[key] ?? await _heightCompleters[key]!.future;
}

/// The certificate the managed connection for [cryptoCurrency] was opened
/// with, or null when it relies on the platform's trusted roots.
String? trustedCertificateFor(CryptoCurrency cryptoCurrency) =>
_certificates[_keyHelper(cryptoCurrency)];

Future<(ElectrumClient?, TorPlainNetworkOption?)> remove({
required CryptoCurrency cryptoCurrency,
}) async {
Expand All @@ -116,6 +113,7 @@ class ClientManager {
_subscriptions.remove(key);
_heights.remove(key);
_heightCompleters.remove(key);
_certificates.remove(key);

return (_map.remove(key), _mapNet.remove(key));
}
Expand All @@ -138,5 +136,6 @@ class ClientManager {
_subscriptions.clear();
_mapNet.clear();
_map.clear();
_certificates.clear();
}
}
43 changes: 42 additions & 1 deletion lib/electrumx_rpc/electrumx_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import '../utilities/amount/amount.dart';
import '../utilities/extensions/impl/string.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../utilities/tls_certificate.dart';
import '../utilities/tor_plain_net_option_enum.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart';
Expand All @@ -51,6 +52,7 @@ class ElectrumXNode {
required this.useSSL,
required this.torEnabled,
required this.clearnetEnabled,
this.trustedCertificatePem,
});
final String address;
final int port;
Expand All @@ -59,6 +61,7 @@ class ElectrumXNode {
final bool useSSL;
final bool torEnabled;
final bool clearnetEnabled;
final String? trustedCertificatePem;

factory ElectrumXNode.from(ElectrumXNode node) {
return ElectrumXNode(
Expand All @@ -69,6 +72,7 @@ class ElectrumXNode {
useSSL: node.useSSL,
torEnabled: node.torEnabled,
clearnetEnabled: node.clearnetEnabled,
trustedCertificatePem: node.trustedCertificatePem,
);
}

Expand All @@ -90,6 +94,7 @@ class ElectrumXClient {

bool get useSSL => _useSSL;
late bool _useSSL;
final String? trustedCertificatePem;

// StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
StreamChannel<dynamic>? _electrumAdapterChannel;
Expand Down Expand Up @@ -132,6 +137,7 @@ class ElectrumXClient {
),
TorService? torService,
EventBus? globalEventBusForTesting,
this.trustedCertificatePem,
}) {
_prefs = prefs;
_torService = torService ?? TorService.sharedInstance;
Expand Down Expand Up @@ -196,6 +202,7 @@ class ElectrumXClient {
host: node.address,
port: node.port,
useSSL: node.useSSL,
trustedCertificatePem: node.trustedCertificatePem,
prefs: prefs,
torService: torService,
failovers: failovers,
Expand Down Expand Up @@ -277,11 +284,13 @@ class ElectrumXClient {
final String useHost;
final int usePort;
final bool useUseSSL;
final String? useCertificatePem;

if (currentFailoverIndex == -1) {
useHost = host;
usePort = port;
useUseSSL = useSSL;
useCertificatePem = trustedCertificatePem;
} else {
_electrumAdapterChannel = null;
await ClientManager.sharedInstance.remove(
Expand All @@ -290,16 +299,47 @@ class ElectrumXClient {
useHost = _failovers[currentFailoverIndex].address;
usePort = _failovers[currentFailoverIndex].port;
useUseSSL = _failovers[currentFailoverIndex].useSSL;
useCertificatePem =
_failovers[currentFailoverIndex].trustedCertificatePem;
}

_electrumAdapterChannel ??= await electrum_adapter.connect(
// Only an SSL connection has a certificate decision.
final String? useTrust = useUseSSL ? useCertificatePem : null;

// A connection another client for this coin already made is shared,
// including one to a failover, as before certificates could be
// trusted. The exception is a connection to this very endpoint that
// was opened under a different certificate decision: a revoked or
// changed approval must reach it here, because no wallet may be loaded
// to close it through updateNode().
final existing = getElectrumAdapter();
if (existing != null) {
final sameEndpoint =
existing.host == useHost &&
existing.port == usePort &&
existing.useSSL == useUseSSL;
final sameTrust =
ClientManager.sharedInstance.trustedCertificateFor(
cryptoCurrency,
) ==
useTrust;
if (!sameEndpoint || sameTrust) {
return;
}
await (await ClientManager.sharedInstance.remove(
cryptoCurrency: cryptoCurrency,
)).$1?.close();
}

_electrumAdapterChannel = await electrum_adapter.connect(
useHost,
port: usePort,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
acceptUnverified: false,
useSSL: useUseSSL,
proxyInfo: proxyInfo,
onBadCertificate: trustedCertificateCheck(useTrust),
);

if (getElectrumAdapter() == null) {
Expand Down Expand Up @@ -328,6 +368,7 @@ class ElectrumXClient {
newClient,
cryptoCurrency: cryptoCurrency,
netType: netType,
trustedCertificatePem: useTrust,
);
}
});
Expand Down
63 changes: 63 additions & 0 deletions lib/models/node_form_data.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import '../utilities/tor_plain_net_option_enum.dart';
import 'node_model.dart';

class NodeFormData {
NodeFormData();

// The certificate is assigned last so it is bound to the node's endpoint.
factory NodeFormData.fromNode(NodeModel node) => NodeFormData()
..name = node.name
..host = node.host
..port = node.port
..useSSL = node.useSSL
..login = node.loginName
..trusted = node.trusted
..isFailover = node.isFailover
..netOption = TorPlainNetworkOption.fromNodeData(
node.torEnabled,
node.clearnetEnabled,
)
..forceNoTor = node.forceNoTor
..isPrimary = node.isPrimary
..apiSecret = node.nodeApiSecret
..trustedCertificatePem = node.trustedCertificatePem;

String? name, host, login, password, apiSecret;
int? port;
bool? useSSL, isFailover, trusted, forceNoTor, isPrimary;
TorPlainNetworkOption? netOption;

// A certificate approval belongs to the endpoint it was reviewed for. It
// stays with that endpoint: editing the host and restoring it keeps the
// approval, while any other host, port or SSL setting reads as unapproved.
String? _approvedPem;
String? _approvedHost;
int? _approvedPort;
bool? _approvedUseSSL;

String? get trustedCertificatePem =>
_approvedPem != null &&
host == _approvedHost &&
port == _approvedPort &&
useSSL == _approvedUseSSL
? _approvedPem
: null;

set trustedCertificatePem(String? value) {
_approvedPem = value;
_approvedHost = value == null ? null : host;
_approvedPort = value == null ? null : port;
_approvedUseSSL = value == null ? null : useSSL;
}

/// Whether the form currently names [node]'s endpoint, the identity a
/// trusted certificate is bound to.
bool matchesEndpointOf(NodeModel node) =>
host == node.host && port == node.port && useSSL == node.useSSL;

@override
String toString() {
return "{ name: $name, host: $host, port: $port, useSSL: $useSSL, "
"trusted: $trusted, netOption: $netOption, isPrimary: $isPrimary }";
}
}
40 changes: 40 additions & 0 deletions lib/models/node_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:hive_ce/hive.dart';

import '../utilities/default_nodes.dart';
import '../utilities/flutter_secure_storage_interface.dart';
import '../utilities/tls_certificate.dart';

part 'type_adaptors/node_model.g.dart';

Expand Down Expand Up @@ -49,6 +50,8 @@ class NodeModel {
final bool isPrimary;
// @HiveField(15)
final String? nodeApiSecret;
// @HiveField(16)
final String? trustedCertificatePem;

NodeModel({
required this.host,
Expand All @@ -67,6 +70,7 @@ class NodeModel {
this.loginName,
this.trusted,
this.nodeApiSecret,
this.trustedCertificatePem,
});

factory NodeModel.fromStackBackup(
Expand Down Expand Up @@ -97,9 +101,25 @@ class NodeModel {
fallback: legacyPrimaryNodeIds?.contains(id) ?? false,
),
nodeApiSecret: map['nodeApiSecret'] as String?,
trustedCertificatePem: _backupCertificatePem(
map['trustedCertificatePem'],
),
);
}

/// A PEM that cannot be parsed would make every connection to this node
/// throw, so a damaged backup value is dropped. The node then uses the
/// platform roots until its certificate is reviewed again.
static String? _backupCertificatePem(Object? value) {
if (value is! String) return null;
try {
certificateDer(value);
return value;
} catch (_) {
return null;
}
}

static bool _backupBool(Object? value, {required bool fallback}) =>
_nullableBackupBool(value) ?? fallback;

Expand All @@ -117,6 +137,14 @@ class NodeModel {
return null;
}

/// Whether [host], [port] and [useSSL] name this node's endpoint, the
/// identity a trusted certificate is bound to.
bool sameEndpoint({
required String host,
required int port,
required bool useSSL,
}) => this.host == host && this.port == port && this.useSSL == useSSL;

NodeModel copyWith({
String? host,
int? port,
Expand All @@ -133,7 +161,14 @@ class NodeModel {
bool? clearnetEnabled,
bool? isPrimary,
String? nodeApiSecret,
String? trustedCertificatePem,
bool clearTrustedCertificate = false,
}) {
final endpointChanged = !sameEndpoint(
host: host ?? this.host,
port: port ?? this.port,
useSSL: useSSL ?? this.useSSL,
);
return NodeModel(
host: host ?? this.host,
port: port ?? this.port,
Expand All @@ -151,6 +186,10 @@ class NodeModel {
forceNoTor: forceNoTor ?? this.forceNoTor,
isPrimary: isPrimary ?? this.isPrimary,
nodeApiSecret: nodeApiSecret ?? this.nodeApiSecret,
trustedCertificatePem: clearTrustedCertificate
? null
: trustedCertificatePem ??
(endpointChanged ? null : this.trustedCertificatePem),
);
}

Expand All @@ -177,6 +216,7 @@ class NodeModel {
map['forceNoTor'] = forceNoTor;
map['isPrimary'] = isPrimary;
map['nodeApiSecret'] = nodeApiSecret;
map['trustedCertificatePem'] = trustedCertificatePem;
return map;
}

Expand Down
Loading
Loading