Skip to content

[webview_flutter_android][webview_flutter_wkwebview] Adds support to respond to recoverable SSL certificate errors #9281

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

Merged
merged 4 commits into from
May 19, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 4.7.0

* Adds support to respond to recoverable SSL certificate errors. See `AndroidNavigationDelegate.setOnSSlAuthError`.

## 4.6.0

* Adds support to set using wide view port. See `AndroidWebViewController.setUseWideViewPort`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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.
// Autogenerated from Pigeon (v25.3.1), do not edit directly.
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

Expand Down 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 @@ -5584,6 +5594,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 @@ -6153,3 +6169,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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 static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import org.junit.Test;

public class CertificateTest {
@Test
public void getEncoded() throws CertificateEncodingException {
final PigeonApiCertificate api = new TestProxyApiRegistrar().getPigeonApiCertificate();

final Certificate instance = mock(Certificate.class);
final byte[] value = new byte[] {(byte) 0xA1};
when(instance.getEncoded()).thenReturn(value);

assertEquals(value, api.getEncoded(instance));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ tasks.register("clean", Delete) {
gradle.projectsEvaluated {
project(":webview_flutter_android") {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:all" << "-Werror"
// Ignore classfile due to https://issuetracker.google.com/issues/342067844
options.compilerArgs << "-Xlint:all" << "-Werror" << "-Xlint:-classfile"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ Page resource error:
})
..setOnHttpAuthRequest((HttpAuthRequest request) {
openDialog(request);
})
..setOnSSlAuthError((PlatformSslAuthError error) {
debugPrint('SSL error from ${(error as AndroidSslAuthError).url}');
error.cancel();
}),
)
..addJavaScriptChannel(JavaScriptChannelParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
webview_flutter_platform_interface: ^2.12.0
webview_flutter_platform_interface: ^2.13.0

dev_dependencies:
espresso: ^0.4.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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.

import 'package:meta/meta.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
import 'android_webkit.g.dart' as android;

/// Implementation of the [PlatformSslAuthError] with the Android WebView API.
class AndroidSslAuthError extends PlatformSslAuthError {
/// Creates an [AndroidSslAuthError].
AndroidSslAuthError._({
required super.certificate,
required super.description,
required android.SslErrorHandler handler,
required this.url,
}) : _handler = handler;

final android.SslErrorHandler _handler;

/// The URL associated with the error.
final String url;

/// Creates an [AndroidSslAuthError] from the parameters from the native
/// `WebViewClient.onReceivedSslError`.
@internal
static Future<AndroidSslAuthError> fromNativeCallback({
required android.SslError error,
required android.SslErrorHandler handler,
}) async {
final android.SslCertificate certificate = error.certificate;
final android.X509Certificate? x509Certificate =
await certificate.getX509Certificate();

final android.SslErrorType errorType = await error.getPrimaryError();
final String errorDescription = switch (errorType) {
android.SslErrorType.dateInvalid =>
'The date of the certificate is invalid.',
android.SslErrorType.expired => 'The certificate has expired.',
android.SslErrorType.idMismatch => 'Hostname mismatch.',
android.SslErrorType.invalid => 'A generic error occurred.',
android.SslErrorType.notYetValid => 'The certificate is not yet valid.',
android.SslErrorType.untrusted =>
'The certificate authority is not trusted.',
android.SslErrorType.unknown => 'The certificate has an unknown error.',
};

return AndroidSslAuthError._(
certificate: X509Certificate(
data:
x509Certificate != null ? await x509Certificate.getEncoded() : null,
),
handler: handler,
description: errorDescription,
url: error.url,
);
}

@override
Future<void> cancel() => _handler.cancel();

@override
Future<void> proceed() => _handler.proceed();
}
Loading