/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. * Generated by Cypher Stack on 2023-05-26 * */ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:socks5_proxy/socks.dart'; import 'package:tor_ffi_plugin/socks_socket.dart'; import '../widgets/desktop/primary_button.dart'; import '../widgets/desktop/secondary_button.dart'; import '../widgets/stack_dialog.dart'; import 'format.dart'; import 'logger.dart'; class MoneroNodeConnectionResponse { final X509Certificate? cert; final String? url; final int? port; final bool success; MoneroNodeConnectionResponse(this.cert, this.url, this.port, this.success); } Future testMoneroNodeConnection( Uri uri, bool allowBadX509Certificate, { required ({ InternetAddress host, int port, })? proxyInfo, }) async { final httpClient = HttpClient(); MoneroNodeConnectionResponse? badCertResponse; try { if (proxyInfo != null) { SocksTCPClient.assignToHttpClient(httpClient, [ ProxySettings( proxyInfo.host, proxyInfo.port, ), ]); } httpClient.badCertificateCallback = (cert, url, port) { if (allowBadX509Certificate) { return true; } if (badCertResponse == null) { badCertResponse = MoneroNodeConnectionResponse(cert, url, port, false); } else { return false; } return false; }; if (!uri.host.endsWith('.onion')) { final request = await httpClient.postUrl(uri); final body = utf8.encode( jsonEncode({ "jsonrpc": "2.0", "id": "0", "method": "get_info", }), ); request.headers.add( 'Content-Length', body.length.toString(), preserveHeaderCase: true, ); request.headers.set( 'Content-Type', 'application/json', preserveHeaderCase: true, ); request.add(body); final response = await request.close(); final result = await response.transform(utf8.decoder).join(); // TODO: json decoded without error so assume connection exists? // or we can check for certain values in the response to decide return MoneroNodeConnectionResponse(null, null, null, true); } else { // If the URL ends in .onion, we can't use an httpClient to connect to it. // // The SOCKSSocket class from the tor_ffi_plugin package can be used to // connect to .onion addresses. We'll do the same things as above but // with SOCKSSocket instead of httpClient. final socket = await SOCKSSocket.create( proxyHost: proxyInfo!.host.address, proxyPort: proxyInfo.port, sslEnabled: false, ); await socket.connect(); await socket.connectTo(uri.host, uri.port); final body = utf8.encode( jsonEncode({ "jsonrpc": "2.0", "id": "0", "method": "get_info", }), ); // Write the request body to the socket. socket.write(body); // Read the response. final response = await socket.inputStream.first; final result = utf8.decode(response); // Close the socket. await socket.close(); return MoneroNodeConnectionResponse(null, null, null, true); // Parse the response. // // This is commented because any issues should throw. // final Map jsonResponse = jsonDecode(result); // print(jsonResponse); // if (jsonResponse.containsKey('result')) { // return MoneroNodeConnectionResponse(null, null, null, true); // } else { // return MoneroNodeConnectionResponse(null, null, null, false); // } } } catch (e, s) { if (badCertResponse != null) { return badCertResponse!; } else { Logging.instance.log("$e\n$s", level: LogLevel.Warning); return MoneroNodeConnectionResponse(null, null, null, false); } } finally { httpClient.close(force: true); } } Future showBadX509CertificateDialog( X509Certificate cert, String url, int port, BuildContext context, ) async { final chars = Format.uint8listToString(cert.sha1) .toUpperCase() .characters .toList(growable: false); String sha1 = chars.sublist(0, 2).join(); for (int i = 2; i < chars.length; i += 2) { sha1 += ":${chars.sublist(i, i + 2).join()}"; } final result = await showDialog( context: context, barrierDismissible: false, builder: (context) { return StackDialog( title: "Untrusted X509Certificate", message: "SHA1:\n$sha1", leftButton: SecondaryButton( label: "Cancel", onPressed: () { Navigator.of(context).pop(false); }, ), rightButton: PrimaryButton( label: "Trust", onPressed: () { Navigator.of(context).pop(true); }, ), ); }, ); return result ?? false; }