Skip to content

[webview_flutter] Adds support to respond to recoverable SSL certificate errors #9150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
84edf6f
add interface portion
bparrishMines Apr 22, 2025
a986f6e
Merge branch 'main' of github.com:flutter/packages into webview_ssl
bparrishMines Apr 22, 2025
7735439
update dependencies
bparrishMines Apr 22, 2025
2a8cf7c
android impl without tests
bparrishMines Apr 23, 2025
f3a1122
fix test names
bparrishMines Apr 23, 2025
887104a
new pigeon generate
bparrishMines Apr 23, 2025
c2aa4a4
add certificate impl
bparrishMines Apr 23, 2025
f952665
update pigeon
bparrishMines Apr 23, 2025
095e9dc
implement auth request
bparrishMines Apr 23, 2025
1fbe36a
improve code a bit
bparrishMines Apr 23, 2025
945763d
start of front facing but need to restart
bparrishMines Apr 24, 2025
16c5e25
update platform interface
bparrishMines Apr 24, 2025
92c6078
android impl
bparrishMines Apr 24, 2025
7c145c4
ios impl
bparrishMines Apr 24, 2025
d3e43a4
webview_flutter impl
bparrishMines Apr 24, 2025
70f77e3
platform intrface error
bparrishMines Apr 24, 2025
0a55007
android docs
bparrishMines Apr 24, 2025
c195d52
navigation delegate tests with new mocks
bparrishMines Apr 24, 2025
ea1c23f
fix test mocks i guess
bparrishMines Apr 24, 2025
e46a9bf
finish android tests
bparrishMines Apr 24, 2025
a221ece
update proxy
bparrishMines Apr 24, 2025
6a7a4db
add webkit unit test
bparrishMines Apr 25, 2025
efd2ad9
add internal and error
bparrishMines Apr 25, 2025
2c6eb01
formatting
bparrishMines Apr 25, 2025
32b690a
Merge branch 'main' of github.com:flutter/packages into webview_ssl
bparrishMines Apr 25, 2025
6f24861
app facing tests
bparrishMines Apr 25, 2025
d5e3879
lint and docs
bparrishMines Apr 25, 2025
1f3ec87
add ssl handler to android
bparrishMines Apr 25, 2025
1d72722
update ios dart implementation
bparrishMines Apr 25, 2025
fa0aa28
update dart unit tests
bparrishMines Apr 25, 2025
c0ea481
fix swift side
bparrishMines Apr 25, 2025
28e7bb7
formatting
bparrishMines Apr 25, 2025
2981d00
fix ios tests
bparrishMines Apr 25, 2025
1f0aa84
formatting and any remove
bparrishMines Apr 25, 2025
f4d1e59
remove any again
bparrishMines Apr 25, 2025
364b3c1
add test to verify perform is default
bparrishMines Apr 25, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ flutter:
- assets/sample_video.mp4
- assets/www/index.html
- assets/www/styles/style.css
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
webview_flutter_android: {path: ../../../../packages/webview_flutter/webview_flutter_android}
webview_flutter_platform_interface: {path: ../../../../packages/webview_flutter/webview_flutter_platform_interface}
webview_flutter_wkwebview: {path: ../../../../packages/webview_flutter/webview_flutter_wkwebview}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ class NavigationDelegate {
/// Constructs a [NavigationDelegate].
///
/// {@template webview_fluttter.NavigationDelegate.constructor}
/// `onUrlChange`: invoked when the underlying web view changes to a new url.
/// `onHttpAuthRequest`: invoked when the web view is requesting authentication.
/// **`onUrlChange`:** invoked when the underlying web view changes to a new url.
/// **`onHttpAuthRequest`:** invoked when the web view is requesting authentication.
/// **`onSslAuthError`:** Invoked when the web view receives a recoverable SSL
/// error for a certificate. The host application must call either
/// [SslAuthError.cancel] or [SslAuthError.proceed].
/// {@endtemplate}
NavigationDelegate({
FutureOr<NavigationDecision> Function(NavigationRequest request)?
Expand All @@ -51,6 +54,7 @@ class NavigationDelegate {
void Function(UrlChange change)? onUrlChange,
void Function(HttpAuthRequest request)? onHttpAuthRequest,
void Function(HttpResponseError error)? onHttpError,
void Function(SslAuthError request)? onSslAuthError,
}) : this.fromPlatformCreationParams(
const PlatformNavigationDelegateCreationParams(),
onNavigationRequest: onNavigationRequest,
Expand All @@ -61,6 +65,7 @@ class NavigationDelegate {
onUrlChange: onUrlChange,
onHttpAuthRequest: onHttpAuthRequest,
onHttpError: onHttpError,
onSslAuthError: onSslAuthError,
);

/// Constructs a [NavigationDelegate] from creation params for a specific
Expand Down Expand Up @@ -105,6 +110,7 @@ class NavigationDelegate {
void Function(UrlChange change)? onUrlChange,
void Function(HttpAuthRequest request)? onHttpAuthRequest,
void Function(HttpResponseError error)? onHttpError,
void Function(SslAuthError request)? onSslAuthError,
}) : this.fromPlatform(
PlatformNavigationDelegate(params),
onNavigationRequest: onNavigationRequest,
Expand All @@ -115,6 +121,7 @@ class NavigationDelegate {
onUrlChange: onUrlChange,
onHttpAuthRequest: onHttpAuthRequest,
onHttpError: onHttpError,
onSslAuthError: onSslAuthError,
);

/// Constructs a [NavigationDelegate] from a specific platform implementation.
Expand All @@ -130,6 +137,7 @@ class NavigationDelegate {
void Function(UrlChange change)? onUrlChange,
HttpAuthRequestCallback? onHttpAuthRequest,
void Function(HttpResponseError error)? onHttpError,
void Function(SslAuthError request)? onSslAuthError,
}) {
if (onNavigationRequest != null) {
platform.setOnNavigationRequest(onNavigationRequest!);
Expand All @@ -155,6 +163,13 @@ class NavigationDelegate {
if (onHttpError != null) {
platform.setOnHttpError(onHttpError);
}
if (onSslAuthError != null) {
platform.setOnSSlAuthError(
(PlatformSslAuthError error) {
onSslAuthError(SslAuthError._fromPlatform(error));
},
);
}
}

/// Implementation of [PlatformNavigationDelegate] for the current platform.
Expand Down Expand Up @@ -184,3 +199,53 @@ class NavigationDelegate {
/// Invoked when a resource loading error occurred.
final WebResourceErrorCallback? onWebResourceError;
}

/// Represents an SSL error with the associated certificate.
///
/// The host application must call [cancel] or, contrary to secure web
/// communication standards, [proceed] to provide the web view's response to the
/// error.
///
/// ## Platform-Specific Features
/// This class contains an underlying implementation provided by the current
/// platform. Once a platform implementation is imported, the examples below
/// can be followed to use features provided by a platform's implementation.
///
/// Below is an example of accessing the platform-specific implementation for
/// iOS and Android:
///
/// ```dart
/// final SslAuthError error = ...;
///
/// if (WebViewPlatform.instance is WebKitWebViewPlatform) {
/// final WebKitSslAuthError webKitError =
/// error.platform as WebKitSslAuthError;
/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) {
/// final AndroidSslAuthError androidError =
/// error.platform as AndroidSslAuthError;
/// }
/// ```
class SslAuthError {
SslAuthError._fromPlatform(this.platform);

/// An implementation of [PlatformSslAuthError] for the current platform.
final PlatformSslAuthError platform;

/// The certificate associated with this error.
X509Certificate? get certificate => platform.certificate;

/// Instructs the WebView that encountered the SSL certificate error to
/// terminate communication with the server.
///
/// The host application must call this method to prevent a resource from
/// loading when an SSL certificate is invalid.
Future<void> cancel() => platform.cancel();

/// Instructs the WebView that encountered the SSL certificate error to ignore
/// the error and continue communicating with the server.
///
/// **Warning:** When an SSL error occurs, the host application should always
/// call [cancel] rather than [proceed] because an invalid SSL certificate
/// means the connection is not secure.
Future<void> proceed() => platform.proceed();
}
6 changes: 6 additions & 0 deletions packages/webview_flutter/webview_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ topics:
- html
- webview
- webview-flutter
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
webview_flutter_android: {path: ../../../packages/webview_flutter/webview_flutter_android}
webview_flutter_platform_interface: {path: ../../../packages/webview_flutter/webview_flutter_platform_interface}
webview_flutter_wkwebview: {path: ../../../packages/webview_flutter/webview_flutter_wkwebview}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ void main() {

verify(delegate.platform.setOnHttpError(onHttpError));
});

test('onSslAuthError', () async {
WebViewPlatform.instance = TestWebViewPlatform();

final NavigationDelegate delegate = NavigationDelegate(
onSslAuthError: expectAsync1((_) {}),
);

final void Function(PlatformSslAuthError) callback = verify(
(delegate.platform as MockPlatformNavigationDelegate)
.setOnSSlAuthError(captureAny))
.captured
.single as void Function(PlatformSslAuthError);

callback(TestPlatformSslAuthError());
});
});
}

Expand All @@ -125,3 +141,17 @@ class TestWebViewPlatform extends WebViewPlatform {

class TestMockPlatformNavigationDelegate extends MockPlatformNavigationDelegate
with MockPlatformInterfaceMixin {}

class TestPlatformSslAuthError extends PlatformSslAuthError {
TestPlatformSslAuthError() : super(certificate: null, description: '');

@override
Future<void> cancel() {
throw UnimplementedError();
}

@override
Future<void> proceed() {
throw UnimplementedError();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,14 @@ class MockPlatformNavigationDelegate extends _i1.Mock
returnValue: _i8.Future<void>.value(),
returnValueForMissingStub: _i8.Future<void>.value(),
) as _i8.Future<void>);

@override
_i8.Future<void> setOnSSlAuthError(
_i3.SslAuthErrorCallback? onSslAuthError,
) =>
(super.noSuchMethod(
Invocation.method(#setOnSSlAuthError, [onSslAuthError]),
returnValue: _i8.Future<void>.value(),
returnValueForMissingStub: _i8.Future<void>.value(),
) as _i8.Future<void>);
}
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,14 @@ class MockPlatformNavigationDelegate extends _i1.Mock
returnValue: _i5.Future<void>.value(),
returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);

@override
_i5.Future<void> setOnSSlAuthError(
_i6.SslAuthErrorCallback? onSslAuthError,
) =>
(super.noSuchMethod(
Invocation.method(#setOnSSlAuthError, [onSslAuthError]),
returnValue: _i5.Future<void>.value(),
returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);
}
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,12 @@ abstract class AndroidWebkitLibraryPigeonProxyApiRegistrar(val binaryMessenger:
*/
abstract fun getPigeonApiSslCertificate(): PigeonApiSslCertificate

/**
* An implementation of [PigeonApiCertificate] used to add a new Dart instance of `Certificate` to
* the Dart `InstanceManager`.
*/
abstract fun getPigeonApiCertificate(): PigeonApiCertificate

fun setUp() {
AndroidWebkitLibraryPigeonInstanceManagerApi.setUpMessageHandlers(
binaryMessenger, instanceManager)
Expand Down Expand Up @@ -591,6 +597,7 @@ abstract class AndroidWebkitLibraryPigeonProxyApiRegistrar(val binaryMessenger:
PigeonApiSslCertificateDName.setUpMessageHandlers(
binaryMessenger, getPigeonApiSslCertificateDName())
PigeonApiSslCertificate.setUpMessageHandlers(binaryMessenger, getPigeonApiSslCertificate())
PigeonApiCertificate.setUpMessageHandlers(binaryMessenger, getPigeonApiCertificate())
}

fun tearDown() {
Expand All @@ -615,6 +622,7 @@ abstract class AndroidWebkitLibraryPigeonProxyApiRegistrar(val binaryMessenger:
PigeonApiSslError.setUpMessageHandlers(binaryMessenger, null)
PigeonApiSslCertificateDName.setUpMessageHandlers(binaryMessenger, null)
PigeonApiSslCertificate.setUpMessageHandlers(binaryMessenger, null)
PigeonApiCertificate.setUpMessageHandlers(binaryMessenger, null)
}
}

Expand Down Expand Up @@ -716,6 +724,8 @@ private class AndroidWebkitLibraryPigeonProxyApiBaseCodec(
registrar.getPigeonApiSslCertificateDName().pigeon_newInstance(value) {}
} else if (value is android.net.http.SslCertificate) {
registrar.getPigeonApiSslCertificate().pigeon_newInstance(value) {}
} else if (value is java.security.cert.Certificate) {
registrar.getPigeonApiCertificate().pigeon_newInstance(value) {}
}

when {
Expand Down Expand Up @@ -5522,6 +5532,12 @@ open class PigeonApiX509Certificate(
}
}
}

@Suppress("FunctionName")
/** An implementation of [PigeonApiCertificate] used to access callback methods */
fun pigeon_getPigeonApiCertificate(): PigeonApiCertificate {
return pigeonRegistrar.getPigeonApiCertificate()
}
}
/**
* Represents a request for handling an SSL error.
Expand Down Expand Up @@ -6091,3 +6107,80 @@ abstract class PigeonApiSslCertificate(
}
}
}
/**
* Abstract class for managing a variety of identity certificates.
*
* See https://developer.android.com/reference/java/security/cert/Certificate.
*/
@Suppress("UNCHECKED_CAST")
abstract class PigeonApiCertificate(
open val pigeonRegistrar: AndroidWebkitLibraryPigeonProxyApiRegistrar
) {
/** The encoded form of this certificate. */
abstract fun getEncoded(pigeon_instance: java.security.cert.Certificate): ByteArray

companion object {
@Suppress("LocalVariableName")
fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiCertificate?) {
val codec = api?.pigeonRegistrar?.codec ?: AndroidWebkitLibraryPigeonCodec()
run {
val channel =
BasicMessageChannel<Any?>(
binaryMessenger,
"dev.flutter.pigeon.webview_flutter_android.Certificate.getEncoded",
codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val pigeon_instanceArg = args[0] as java.security.cert.Certificate
val wrapped: List<Any?> =
try {
listOf(api.getEncoded(pigeon_instanceArg))
} catch (exception: Throwable) {
AndroidWebkitLibraryPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}

@Suppress("LocalVariableName", "FunctionName")
/** Creates a Dart instance of Certificate and attaches it to [pigeon_instanceArg]. */
fun pigeon_newInstance(
pigeon_instanceArg: java.security.cert.Certificate,
callback: (Result<Unit>) -> Unit
) {
if (pigeonRegistrar.ignoreCallsToDart) {
callback(
Result.failure(
AndroidWebKitError("ignore-calls-error", "Calls to Dart are being ignored.", "")))
} else if (pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) {
callback(Result.success(Unit))
} else {
val pigeon_identifierArg =
pigeonRegistrar.instanceManager.addHostCreatedInstance(pigeon_instanceArg)
val binaryMessenger = pigeonRegistrar.binaryMessenger
val codec = pigeonRegistrar.codec
val channelName = "dev.flutter.pigeon.webview_flutter_android.Certificate.pigeon_newInstance"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(pigeon_identifierArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(
Result.failure(
AndroidWebKitError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(
Result.failure(AndroidWebkitLibraryPigeonUtils.createConnectionError(channelName)))
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.webviewflutter;

import androidx.annotation.NonNull;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;

/**
* ProxyApi implementation for {@link Certificate}. This class may handle instantiating native
* object instances that are attached to a Dart instance or handle method calls on the associated
* native class or an instance of that class.
*/
class CertificateProxyApi extends PigeonApiCertificate {
CertificateProxyApi(@NonNull ProxyApiRegistrar pigeonRegistrar) {
super(pigeonRegistrar);
}

@NonNull
@Override
public byte[] getEncoded(@NonNull Certificate pigeon_instance) {
try {
return pigeon_instance.getEncoded();
} catch (CertificateEncodingException exception) {
throw new RuntimeException(exception);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ public PigeonApiAndroidMessage getPigeonApiAndroidMessage() {
return new MessageProxyApi(this);
}

@NonNull
@Override
public PigeonApiCertificate getPigeonApiCertificate() {
return new CertificateProxyApi(this);
}

@NonNull
public Context getContext() {
return context;
Expand Down
Loading