From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id A583296225 for ; Mon, 15 Apr 2024 12:30:35 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id D836F799B for ; Mon, 15 Apr 2024 12:30:34 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Mon, 15 Apr 2024 12:30:31 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id EA55D44AB8 for ; Mon, 15 Apr 2024 12:30:28 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Mon, 15 Apr 2024 12:30:23 +0200 Message-Id: <20240415103027.3000412-5-d.csapak@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240415103027.3000412-1-d.csapak@proxmox.com> References: <20240415103027.3000412-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.111 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LOTSOFHASH 0.25 Emails with lots of hash-like gibberish SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH pve-flutter-frontend 1/5] console: use flutter inappwebview as webview X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 15 Apr 2024 10:30:35 -0000 instead of flutter webview. This new dependency does not have the limitations of the "official" flutter webview. We now can ignore https errors and even get some info on the certificate. We use that to now show a 'do you want to trust this cert' dialog with the fingerprint of the server, which the user can then either trust or abort. When pressing yes, we save that fingerprint in the shared preferences so that we don't have to ask the next time. Signed-off-by: Dominik Csapak --- this depends on the login-manager patch to add the 'trustedFingerprints' list lib/widgets/pve_console_menu_widget.dart | 128 ++++++++++++++++++++--- pubspec.lock | 42 ++------ pubspec.yaml | 3 +- 3 files changed, 126 insertions(+), 47 deletions(-) diff --git a/lib/widgets/pve_console_menu_widget.dart b/lib/widgets/pve_console_menu_widget.dart index 767a51c..243baf1 100644 --- a/lib/widgets/pve_console_menu_widget.dart +++ b/lib/widgets/pve_console_menu_widget.dart @@ -6,7 +6,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'; -import 'package:webview_flutter/webview_flutter.dart'; +import 'package:proxmox_login_manager/proxmox_general_settings_model.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:crypto/crypto.dart'; class PveConsoleMenu extends StatelessWidget { static const platform = @@ -104,8 +106,7 @@ class PveConsoleMenu extends StatelessWidget { "noVNC Console", // xterm.js doesn't work that well on mobile style: TextStyle(fontWeight: FontWeight.bold), ), - subtitle: const Text( - "Open console view (requires trusted SSL certificate)"), + subtitle: const Text("Open console view"), onTap: () async { if (Platform.isAndroid) { if (['qemu', 'lxc'].contains(type)) { @@ -209,6 +210,8 @@ class PVEWebConsole extends StatefulWidget { } class PVEWebConsoleState extends State { + InAppWebViewController? webViewController; + @override Widget build(BuildContext context) { final ticket = widget.apiClient.credentials.ticket!; @@ -222,24 +225,123 @@ class PVEWebConsoleState extends State { } else { consoleUrl += "&console=shell"; } - - final controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setBackgroundColor(Theme.of(context).colorScheme.background); + WidgetsFlutterBinding.ensureInitialized(); return FutureBuilder( - future: WebViewCookieManager().setCookie(WebViewCookie( + future: CookieManager.instance().setCookie( + url: Uri.parse(consoleUrl), name: 'PVEAuthCookie', value: ticket, - domain: baseUrl.origin, - )), + ), builder: (context, snapshot) { - controller.loadRequest(Uri.parse(consoleUrl)); return SafeArea( - child: WebViewWidget( - controller: controller, + child: InAppWebView( + onReceivedServerTrustAuthRequest: (controller, challenge) async { + final cert = challenge.protectionSpace.sslCertificate; + final certBytes = cert?.x509Certificate?.encoded; + final sslError = challenge.protectionSpace.sslError?.message; + + String? issuedTo = cert?.issuedTo?.CName.toString(); + String? hash = certBytes != null + ? sha256.convert(certBytes).toString() + : null; + + final settings = + await ProxmoxGeneralSettingsModel.fromLocalStorage(); + + bool trust = false; + if (hash != null && settings.trustedFingerprints != null) { + trust = settings.trustedFingerprints!.contains(hash); + } + + if (!trust) { + // format hash to '01:23:...' format + String? formattedHash = hash?.toUpperCase().replaceAllMapped( + RegExp(r"[a-zA-Z0-9]{2}"), + (match) => "${match.group(0)}:"); + formattedHash = formattedHash?.substring( + 0, formattedHash.length - 1); // remove last ':' + + if (context.mounted) { + trust = await showTLSWarning( + context, + sslError ?? 'An unknown TLS error has occurred', + issuedTo ?? 'unknown', + formattedHash ?? 'unknown'); + } + } + + // save Fingerprint + if (trust && hash != null) { + await settings + .rebuild((b) => b..trustedFingerprints.add(hash)) + .toLocalStorage(); + print(settings.toJson()); + } + + final action = trust + ? ServerTrustAuthResponseAction.PROCEED + : ServerTrustAuthResponseAction.CANCEL; + return ServerTrustAuthResponse(action: action); + }, + onWebViewCreated: (controller) { + webViewController = controller; + controller.loadUrl( + urlRequest: URLRequest(url: Uri.parse(consoleUrl))); + }, ), ); }); } + + Future showTLSWarning(BuildContext context, String sslError, + String issuedTo, String hash) async { + return await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return PopScope( + // prevent back button from canceling this callback + canPop: false, + child: AlertDialog( + title: const Text('An TLS error has occurred:'), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: const Text('Error Message:'), + subtitle: Text(sslError), + ), + const ListTile( + title: Text('Certificate Information:'), + ), + ListTile( + title: const Text('Issued to:'), + subtitle: Text(issuedTo), + ), + ListTile( + title: const Text('Fingerprint:'), + subtitle: Text(hash), + ), + const Text(''), // spacer + const Text('Do you want to continue?'), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + Navigator.of(context).pop(); + }, + child: const Text('No')), + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('Yes')) + ]), + ); + }); + } } diff --git a/pubspec.lock b/pubspec.lock index 7e39fba..94c0656 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -170,7 +170,7 @@ packages: source: hosted version: "3.1.1" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab @@ -230,6 +230,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350 + url: "https://pub.dev" + source: hosted + version: "5.8.0" flutter_lints: dependency: "direct dev" description: @@ -819,38 +827,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.5" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932" - url: "https://pub.dev" - source: hosted - version: "4.7.0" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934 - url: "https://pub.dev" - source: hosted - version: "3.16.0" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d - url: "https://pub.dev" - source: hosted - version: "2.10.0" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7 - url: "https://pub.dev" - source: hosted - version: "3.13.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 065749c..976dbb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,6 @@ dependencies: url_launcher: ^6.0.17 intl: ^0.19.0 path_provider: ^2.0.8 - webview_flutter: ^4.2.0 proxmox_dart_api_client: path: ../proxmox_dart_api_client proxmox_login_manager: @@ -42,6 +41,8 @@ dependencies: collection: ^1.15.0-nullsafety.4 shared_preferences: any + flutter_inappwebview: ^5.8.0 + crypto: ^3.0.3 dev_dependencies: flutter_test: sdk: flutter -- 2.39.2