diff --git a/wpeview/src/main/cpp/Runtime/WKWebView.cpp b/wpeview/src/main/cpp/Runtime/WKWebView.cpp index 6c0f01b85..e866c3a6b 100644 --- a/wpeview/src/main/cpp/Runtime/WKWebView.cpp +++ b/wpeview/src/main/cpp/Runtime/WKWebView.cpp @@ -43,6 +43,93 @@ void handleCommitBuffer(void* context, WPEAndroidBuffer* buffer, int fenceID) const int httpErrorsStart = 400; +class SslErrorHandler final { +public: + static SslErrorHandler* createHandler( + WebKitWebView* webView, GTlsCertificate* certificate, const char* failingURI, gchar** certificatePEM) noexcept + { + if (!webView || !certificate || !failingURI || !*failingURI || !certificatePEM) { + return nullptr; + } + + gchar* host = nullptr; + // NOLINTBEGIN(clang-analyzer-optin.core.EnumCastOutOfRange) + GUri* uri + = g_uri_parse(failingURI, static_cast(G_URI_FLAGS_PARSE_RELAXED | G_URI_FLAGS_ENCODED), nullptr); + // NOLINTEND(clang-analyzer-optin.core.EnumCastOutOfRange) + if (uri) { + const char* str = g_uri_get_host(uri); + if (str) { + host = g_strdup(str); + } + g_uri_unref(uri); + } + if (!host) { + return nullptr; + } + + *certificatePEM = nullptr; + g_object_get(certificate, "certificate-pem", certificatePEM, nullptr); + if (!*certificatePEM) { + g_free(host); + return nullptr; + } + + return new SslErrorHandler(webView, certificate, g_strdup(failingURI), host); + } + + ~SslErrorHandler() + { + handlingFinished(); + g_weak_ref_clear(&m_webViewWeakRef); + } + + SslErrorHandler(SslErrorHandler&&) = delete; + SslErrorHandler& operator=(SslErrorHandler&&) = delete; + SslErrorHandler(const SslErrorHandler&) = delete; + SslErrorHandler& operator=(const SslErrorHandler&) = delete; + + void acceptCertificate() noexcept + { + auto* webView = static_cast(g_weak_ref_get(&m_webViewWeakRef)); + if (webView) { + auto* networkSession = webkit_web_view_get_network_session(webView); + if (networkSession) { + webkit_network_session_allow_tls_certificate_for_host(networkSession, m_certificate, m_host); + g_object_unref(networkSession); + + webkit_web_view_load_uri(webView, m_failingURI); + } + g_object_unref(webView); + } + handlingFinished(); + } + + void rejectCertificate() noexcept { handlingFinished(); } + +private: + GWeakRef m_webViewWeakRef = {}; + GTlsCertificate* m_certificate = nullptr; + gchar* m_failingURI = nullptr; + gchar* m_host = nullptr; + + SslErrorHandler(WebKitWebView* webView, GTlsCertificate* certificate, gchar* failingURI, gchar* host) noexcept + : m_failingURI(failingURI) + , m_host(host) + { + g_weak_ref_init(&m_webViewWeakRef, webView); + m_certificate = g_object_ref(certificate); + } + + void handlingFinished() noexcept + { + g_weak_ref_set(&m_webViewWeakRef, nullptr); + g_clear_object(&m_certificate); + g_clear_pointer(&m_failingURI, g_free); + g_clear_pointer(&m_host, g_free); + } +}; + } // namespace /*********************************************************************************************************************** @@ -92,7 +179,7 @@ class JNIWKWebViewCache final : public JNI::TypedClass { static_cast(webkit_web_view_can_go_forward(webView))); } - static gboolean onScriptDialog(WKWebView* wkWebView, WebKitScriptDialog* dialog, WebKitWebView* webView) + static gboolean onScriptDialog(WKWebView* wkWebView, WebKitScriptDialog* dialog, WebKitWebView* webView) noexcept { auto dialogPtr = reinterpret_cast(webkit_script_dialog_ref(dialog)); auto jActiveURL = JNI::String(webkit_web_view_get_uri(webView)); @@ -108,7 +195,7 @@ class JNIWKWebViewCache final : public JNI::TypedClass { } static gboolean onDecidePolicy(WKWebView* wkWebView, WebKitPolicyDecision* decision, - WebKitPolicyDecisionType decisionType, WebKitWebView* /*webView*/) + WebKitPolicyDecisionType decisionType, WebKitWebView* /*webView*/) noexcept { if (decisionType != WEBKIT_POLICY_DECISION_TYPE_RESPONSE) return FALSE; @@ -164,6 +251,72 @@ class JNIWKWebViewCache final : public JNI::TypedClass { return FALSE; } + static gboolean onReceivedSslError(WKWebView* wkWebView, char* failingURI, GTlsCertificate* certificate, + GTlsCertificateFlags errorFlags, WebKitWebView* webView) noexcept + { + if (wkWebView->m_webView != webView) { + return FALSE; + } + + gchar* certificatePEM = nullptr; + SslErrorHandler* handler = SslErrorHandler::createHandler(webView, certificate, failingURI, &certificatePEM); + if (!handler) { + return FALSE; + } + + auto jFailingURI = JNI::String(failingURI); + auto jCertificatePEM = JNI::String(certificatePEM); + g_free(certificatePEM); + + // Android SslError values are: + // (https://developer.android.com/reference/android/net/http/SslError#constants_1) + // + // SSL_NOTYETVALID = 0x00 + // SSL_EXPIRED = 0x01 + // SSL_IDMISMATCH = 0x02 + // SSL_UNTRUSTED = 0x03 + // SSL_DATE_INVALID = 0x04 + // SSL_INVALID = 0x05 + int nbErrors = 0; + int errors[5] = {0}; + if (errorFlags & (G_TLS_CERTIFICATE_UNKNOWN_CA | G_TLS_CERTIFICATE_REVOKED)) { + errors[nbErrors++] = 0x03; // SSL_UNTRUSTED + } + if (errorFlags & G_TLS_CERTIFICATE_EXPIRED) { + errors[nbErrors++] = 0x01; // SSL_EXPIRED + } + if (errorFlags & G_TLS_CERTIFICATE_NOT_ACTIVATED) { + errors[nbErrors++] = 0x00; // SSL_NOTYETVALID + } + if (errorFlags & G_TLS_CERTIFICATE_BAD_IDENTITY) { + errors[nbErrors++] = 0x02; // SSL_IDMISMATCH + } + if ((errorFlags & (G_TLS_CERTIFICATE_INSECURE | G_TLS_CERTIFICATE_GENERIC_ERROR)) || (nbErrors == 0)) { + errors[nbErrors++] = 0x05; // SSL_INVALID + } + + auto jErrorsArray = JNI::ScalarArray(nbErrors); + nbErrors = 0; + for (auto& arrayValue : jErrorsArray.getContent()) { + arrayValue = errors[nbErrors++]; + } + + try { + if (!getJNIPageCache().m_onReceivedSslError.invoke(wkWebView->m_webViewJavaInstance.get(), + static_cast(jFailingURI), static_cast(jCertificatePEM), + static_cast(jErrorsArray), reinterpret_cast(handler))) { + delete handler; + return FALSE; + } + + return TRUE; + } catch (const std::exception& ex) { + Logging::logError("Cannot send the [received SSL error] event to Java runtime (%s)", ex.what()); + delete handler; + return FALSE; + } + } + static bool onFullscreenRequest(WKWebView* wkWebView, bool fullscreen) noexcept { if (wkWebView->m_viewBackend != nullptr) { @@ -184,7 +337,7 @@ class JNIWKWebViewCache final : public JNI::TypedClass { void onInputMethodContextOut(jobject obj) const noexcept { callJavaMethod(m_onInputMethodContextOut, obj); } - static void onEvaluateJavascriptReady(WebKitWebView* webView, GAsyncResult* result, JNIWKCallback callback) + static void onEvaluateJavascriptReady(WebKitWebView* webView, GAsyncResult* result, JNIWKCallback callback) noexcept { GError* error = nullptr; JSCValue* value = webkit_web_view_evaluate_javascript_finish(webView, result, &error); @@ -232,6 +385,7 @@ class JNIWKWebViewCache final : public JNI::TypedClass { const JNI::Method m_onScriptDialog; const JNI::Method m_onInputMethodContextIn; const JNI::Method m_onInputMethodContextOut; + const JNI::Method m_onReceivedSslError; const JNI::Method m_onEnterFullscreenMode; const JNI::Method m_onExitFullscreenMode; const JNI::Method m_onReceivedHttpError; @@ -265,6 +419,9 @@ class JNIWKWebViewCache final : public JNI::TypedClass { static void nativeScriptDialogConfirm( JNIEnv* env, jobject obj, jlong dialogPtr, jboolean confirm, jstring text) noexcept; static void nativeSetTLSErrorsPolicy(JNIEnv* env, jobject obj, jlong wkWebViewPtr, jint policy) noexcept; + + static void nativeTriggerSslErrorHandler( + JNIEnv* env, jclass klass, jlong handlerPtr, jboolean acceptCertificate) noexcept; }; const JNIWKWebViewCache& getJNIPageCache() @@ -283,6 +440,7 @@ JNIWKWebViewCache::JNIWKWebViewCache() , m_onScriptDialog(getMethod("onScriptDialog")) , m_onInputMethodContextIn(getMethod("onInputMethodContextIn")) , m_onInputMethodContextOut(getMethod("onInputMethodContextOut")) + , m_onReceivedSslError(getMethod("onReceivedSslError")) , m_onEnterFullscreenMode(getMethod("onEnterFullscreenMode")) , m_onExitFullscreenMode(getMethod("onExitFullscreenMode")) , m_onReceivedHttpError( @@ -319,7 +477,9 @@ JNIWKWebViewCache::JNIWKWebViewCache() JNI::NativeMethod("nativeScriptDialogClose", JNIWKWebViewCache::nativeScriptDialogClose), JNI::NativeMethod( "nativeScriptDialogConfirm", JNIWKWebViewCache::nativeScriptDialogConfirm), - JNI::NativeMethod("nativeSetTLSErrorsPolicy", JNIWKWebViewCache::nativeSetTLSErrorsPolicy)); + JNI::NativeMethod("nativeSetTLSErrorsPolicy", JNIWKWebViewCache::nativeSetTLSErrorsPolicy), + JNI::StaticNativeMethod( + "nativeTriggerSslErrorHandler", JNIWKWebViewCache::nativeTriggerSslErrorHandler)); } jlong JNIWKWebViewCache::nativeInit( @@ -595,6 +755,22 @@ void JNIWKWebViewCache::nativeSetTLSErrorsPolicy( } } +void JNIWKWebViewCache::nativeTriggerSslErrorHandler( + JNIEnv* /*env*/, jclass /*klass*/, jlong handlerPtr, jboolean acceptCertificate) noexcept +{ + Logging::logDebug( + "WKWebView::nativeTriggerSslErrorHandler(%s) [tid %d]", acceptCertificate ? "true" : "false", gettid()); + auto* handler = reinterpret_cast(handlerPtr); // NOLINT(performance-no-int-to-ptr) + if (handler != nullptr) { + if (acceptCertificate) { + handler->acceptCertificate(); + } else { + handler->rejectCertificate(); + } + delete handler; + } +} + /*********************************************************************************************************************** * Native WKWebView class implementation **********************************************************************************************************************/ @@ -640,6 +816,8 @@ WKWebView::WKWebView(JNIEnv* env, JNIWKWebView jniWKWebView, WKWebContext* wkWeb g_signal_connect_swapped(m_webView, "script-dialog", G_CALLBACK(JNIWKWebViewCache::onScriptDialog), this)); m_signalHandlers.push_back( g_signal_connect_swapped(m_webView, "decide-policy", G_CALLBACK(JNIWKWebViewCache::onDecidePolicy), this)); + m_signalHandlers.push_back(g_signal_connect_swapped( + m_webView, "load-failed-with-tls-errors", G_CALLBACK(JNIWKWebViewCache::onReceivedSslError), this)); wpe_view_backend_set_fullscreen_handler(wpeBackend, reinterpret_cast(JNIWKWebViewCache::onFullscreenRequest), this); diff --git a/wpeview/src/main/java/org/wpewebkit/wpe/WKWebView.java b/wpeview/src/main/java/org/wpewebkit/wpe/WKWebView.java index 4899409d4..30211aeec 100644 --- a/wpeview/src/main/java/org/wpewebkit/wpe/WKWebView.java +++ b/wpeview/src/main/java/org/wpewebkit/wpe/WKWebView.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.DialogInterface; import android.net.Uri; +import android.net.http.SslError; import android.os.Handler; import android.os.Looper; import android.util.DisplayMetrics; @@ -58,9 +59,12 @@ import org.wpewebkit.wpeview.WPEView; import org.wpewebkit.wpeview.WPEViewClient; +import java.io.ByteArrayInputStream; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; @@ -584,6 +588,88 @@ private void onInputMethodContextOut() { }); } + private static final class SslErrorHandlerImpl implements WPEViewClient.SslErrorHandler { + private final Handler m_handler; + private long m_nativeHandlerPtr = 0; + + protected SslErrorHandlerImpl(long nativeHandlerPtr) { + m_nativeHandlerPtr = nativeHandlerPtr; + + Looper looper = Looper.myLooper(); + if (looper == null) { + looper = Looper.getMainLooper(); + } + m_handler = new Handler(looper); + } + + private void triggerSslErrorHandler(boolean acceptCertificate) { + if (m_nativeHandlerPtr != 0) { + nativeTriggerSslErrorHandler(m_nativeHandlerPtr, acceptCertificate); + m_nativeHandlerPtr = 0; + } + } + + private void executeOrPostTask(Runnable task) { + if (Looper.myLooper() == m_handler.getLooper()) { + task.run(); + } else { + m_handler.post(task); + } + } + + @Override + public void proceed() { + executeOrPostTask(() -> triggerSslErrorHandler(true)); + } + + @Override + public void cancel() { + executeOrPostTask(() -> triggerSslErrorHandler(false)); + } + + @Override + protected void finalize() throws Throwable { + cancel(); + } + } + + @Keep + private boolean onReceivedSslError(@NonNull String failingURI, @NonNull String certificatePEM, + @NonNull int[] sslErrors, long nativeHandlerPtr) { + Log.d(LOGTAG, "onReceivedSslError()"); + if (wpeViewClient == null) { + return false; + } + + SslError sslError; + try { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + X509Certificate certificate = + (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certificatePEM.getBytes())); + + sslError = new SslError(sslErrors[0], certificate, failingURI); + for (int i = 1; i < sslErrors.length; ++i) { + sslError.addError(sslErrors[i]); + } + } catch (Exception e) { + Log.e(LOGTAG, "Error while wrapping the SSL certificate in onReceivedSslError()", e); + return false; + } + + SslErrorHandlerImpl handler = new SslErrorHandlerImpl(nativeHandlerPtr); + try { + wpeViewClient.onReceivedSslError(wpeView, handler, sslError); + } catch (Exception e) { + Log.e(LOGTAG, + "Exception thrown while calling WPEViewClient.onReceivedSslError(), certificate is " + + "automatically rejected", + e); + handler.cancel(); + } + + return true; + } + @Keep private void onEnterFullscreenMode() { Log.d(LOGTAG, "onEnterFullscreenMode()"); @@ -658,4 +744,6 @@ private native void nativeOnTouchEvent(long nativePtr, long time, int type, int private native void nativeScriptDialogClose(long nativeDialogPtr); private native void nativeScriptDialogConfirm(long nativeDialogPtr, boolean confirm, @Nullable String text); private native void nativeSetTLSErrorsPolicy(long nativePtr, int policy); + + protected static native void nativeTriggerSslErrorHandler(long nativeHandlerPtr, boolean acceptCertificate); } diff --git a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEViewClient.java b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEViewClient.java index 663fa86c2..b09944415 100644 --- a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEViewClient.java +++ b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEViewClient.java @@ -21,6 +21,8 @@ package org.wpewebkit.wpeview; +import android.net.http.SslError; + import androidx.annotation.NonNull; public class WPEViewClient { @@ -61,4 +63,38 @@ public void onViewReady(@NonNull WPEView view) {} */ public void onReceivedHttpError(@NonNull WPEView view, @NonNull WPEResourceRequest request, @NonNull WPEResourceResponse errorResponse) {} + + /** + * The interface used to accept or reject an invalid SSL certificate. + * + * @see #onReceivedSslError + */ + public interface SslErrorHandler { + /** + * Call this method to accept an invalid SSL certificate. + * + * @see #onReceivedSslError + */ + void proceed(); + + /** + * Call this method to reject an invalid SSL certificate. + * + * @see #onReceivedSslError + */ + void cancel(); + } + + /** + * Notify the host application that the loading of an HTTPS resource has failed because of error(s) with the SSL + * certificate. + * + * @param view The WPEView that is initiating the callback. + * @param handler The host application must call either {@code SslErrorHandler.cancel()} or {@code SslErrorHandler + * .proceed()} to reject the invalid SSL certificate (default behavior) or to accept it (in this case it will be + * accepted for all further calls to the same host with the same certificate). Accepting an invalid certificate + * will automatically reload the web page right after registering the certificate. + * @param error The SSL error(s) which happened. + */ + public void onReceivedSslError(@NonNull WPEView view, @NonNull SslErrorHandler handler, @NonNull SslError error) {} }