diff --git a/.gitignore b/.gitignore index e7bc7d07..2435a43f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ Thumbs.db /tools/buildtools/linux64/clang-format /tools/buildtools/mac/clang-format /tools/buildtools/win/clang-format.exe +/java-cef.iml diff --git a/java/org/cef/CefClient.java b/java/org/cef/CefClient.java index 687be93d..b3247154 100644 --- a/java/org/cef/CefClient.java +++ b/java/org/cef/CefClient.java @@ -42,6 +42,7 @@ import org.cef.network.CefRequest.TransitionType; import org.cef.network.CefResponse; import org.cef.network.CefURLRequest; +import org.cef.security.CefSSLInfo; import java.awt.Component; import java.awt.Container; @@ -101,7 +102,7 @@ public void propertyChange(PropertyChangeEvent evt) { * The CTOR is only accessible within this package. * Use CefApp.createClient() to create an instance of * this class. - * @see org.cef.CefApp.createClient() + * @see org.cef.CefApp#createClient() */ CefClient() throws UnsatisfiedLinkError { super(); @@ -554,7 +555,7 @@ public void onAfterCreated(CefBrowser browser) { if (browser == null) return; // keep browser reference - Integer identifier = browser.getIdentifier(); + Integer identifier = Integer.valueOf(browser.getIdentifier()); synchronized (browser_) { browser_.put(identifier, browser); } @@ -588,7 +589,7 @@ private void cleanupBrowser(int identifier) { synchronized (browser_) { if (identifier >= 0) { // Remove the specific browser that closed. - browser_.remove(identifier); + browser_.remove(Integer.valueOf(identifier)); } else if (!browser_.isEmpty()) { // Close all browsers. Collection browserList = browser_.values(); @@ -845,9 +846,9 @@ public boolean getAuthCredentials(CefBrowser browser, String origin_url, boolean @Override public boolean onCertificateError( - CefBrowser browser, ErrorCode cert_error, String request_url, CefCallback callback) { + CefBrowser browser, ErrorCode cert_error, String request_url, CefSSLInfo sslInfo, CefCallback callback) { if (requestHandler_ != null) - return requestHandler_.onCertificateError(browser, cert_error, request_url, callback); + return requestHandler_.onCertificateError(browser, cert_error, request_url, sslInfo, callback); return false; } diff --git a/java/org/cef/handler/CefRequestHandler.java b/java/org/cef/handler/CefRequestHandler.java index 22ba60dc..e30d715f 100644 --- a/java/org/cef/handler/CefRequestHandler.java +++ b/java/org/cef/handler/CefRequestHandler.java @@ -11,6 +11,7 @@ import org.cef.misc.BoolRef; import org.cef.network.CefRequest; import org.cef.network.CefURLRequest; +import org.cef.security.CefSSLInfo; /** * Implement this interface to handle events related to browser requests. The methods of this class @@ -114,13 +115,15 @@ boolean getAuthCredentials(CefBrowser browser, String origin_url, boolean isProx * @param browser The corresponding browser. * @param cert_error Error code describing the error. * @param request_url The requesting URL. + * @param sslInfo The certificate with the status * @param callback Call CefCallback.Continue() either in this method or at a later time * to continue or cancel the request. If null the error cannot be recovered from and the * request will be canceled automatically. - * @return True to handle the request (callback must be executed) or false to reject it. + * @return True to handle the request later(callback must be executed) or false to reject it immediately. */ - boolean onCertificateError(CefBrowser browser, CefLoadHandler.ErrorCode cert_error, - String request_url, CefCallback callback); + boolean onCertificateError( + CefBrowser browser, CefLoadHandler.ErrorCode cert_error, String request_url, CefSSLInfo sslInfo, + CefCallback callback); /** * Called on the browser process UI thread when the render process terminates unexpectedly. diff --git a/java/org/cef/handler/CefRequestHandlerAdapter.java b/java/org/cef/handler/CefRequestHandlerAdapter.java index a6c04ef6..6dc8b052 100644 --- a/java/org/cef/handler/CefRequestHandlerAdapter.java +++ b/java/org/cef/handler/CefRequestHandlerAdapter.java @@ -12,6 +12,7 @@ import org.cef.misc.BoolRef; import org.cef.network.CefRequest; import org.cef.network.CefURLRequest; +import org.cef.security.CefSSLInfo; /** * An abstract adapter class for receiving browser request events. @@ -46,7 +47,8 @@ public boolean getAuthCredentials(CefBrowser browser, String origin_url, boolean @Override public boolean onCertificateError( - CefBrowser browser, ErrorCode cert_error, String request_url, CefCallback callback) { + CefBrowser browser, ErrorCode cert_error, String request_url, CefSSLInfo sslInfo, + CefCallback callback) { return false; } diff --git a/java/org/cef/network/CefRequest.java b/java/org/cef/network/CefRequest.java index 5ce64451..a2d91f83 100644 --- a/java/org/cef/network/CefRequest.java +++ b/java/org/cef/network/CefRequest.java @@ -143,7 +143,7 @@ public int getQualifiers() { /** * Removes a qualifier from the enum. - * @param The qualifier to be removed. + * @param flag The qualifier to be removed. */ public void removeQualifier(TransitionFlags flag) { value &= ~flag.getValue(); diff --git a/java/org/cef/security/CefCertStatus.java b/java/org/cef/security/CefCertStatus.java new file mode 100644 index 00000000..38dd91db --- /dev/null +++ b/java/org/cef/security/CefCertStatus.java @@ -0,0 +1,41 @@ +// Copyright (c) 2022 The Chromium Embedded Framework 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 org.cef.security; + +public enum CefCertStatus { + CERT_STATUS_NONE(0), + CERT_STATUS_COMMON_NAME_INVALID(1), + CERT_STATUS_DATE_INVALID(1 << 1), + CERT_STATUS_AUTHORITY_INVALID(1 << 2), + // 1 << 3 is reserved for ERR_CERT_CONTAINS_ERRORS (not useful with WinHTTP). + CERT_STATUS_NO_REVOCATION_MECHANISM(1 << 4), + CERT_STATUS_UNABLE_TO_CHECK_REVOCATION(1 << 5), + CERT_STATUS_REVOKED(1 << 6), + CERT_STATUS_INVALID(1 << 7), + CERT_STATUS_WEAK_SIGNATURE_ALGORITHM(1 << 8), + // 1 << 9 was used for CERT_STATUS_NOT_IN_DNS + CERT_STATUS_NON_UNIQUE_NAME(1 << 10), + CERT_STATUS_WEAK_KEY(1 << 11), + // 1 << 12 was used for CERT_STATUS_WEAK_DH_KEY + CERT_STATUS_PINNED_KEY_MISSING(1 << 13), + CERT_STATUS_NAME_CONSTRAINT_VIOLATION(1 << 14), + CERT_STATUS_VALIDITY_TOO_LONG(1 << 15), + // Bits 16 to 31 are for non-error statuses. + CERT_STATUS_IS_EV(1 << 16), + CERT_STATUS_REV_CHECKING_ENABLED(1 << 17), + // Bit 18 was CERT_STATUS_IS_DNSSEC + CERT_STATUS_SHA1_SIGNATURE_PRESENT(1 << 19), + CERT_STATUS_CT_COMPLIANCE_FAILED(1 << 20); + + private final int statusBitmask; + + CefCertStatus(int statusBitmask) { + this.statusBitmask = statusBitmask; + } + + public boolean hasStatus(int bitset) { + return (bitset & statusBitmask) == statusBitmask; + } +} diff --git a/java/org/cef/security/CefSSLInfo.java b/java/org/cef/security/CefSSLInfo.java new file mode 100644 index 00000000..b0625bcb --- /dev/null +++ b/java/org/cef/security/CefSSLInfo.java @@ -0,0 +1,19 @@ +// Copyright (c) 2022 The Chromium Embedded Framework 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 org.cef.security; + +/** + * The class aggregates {@link CefX509Certificate} with its status bitset(see {@link org.cef.security.CefCertStatus}). + */ + +public class CefSSLInfo { + public CefSSLInfo(int statusBitset, CefX509Certificate certificate) { + this.statusBiset = statusBitset; + this.certificate = certificate; + } + + public final int statusBiset; + public final CefX509Certificate certificate; +} diff --git a/java/org/cef/security/CefX509Certificate.java b/java/org/cef/security/CefX509Certificate.java new file mode 100644 index 00000000..0313d5c9 --- /dev/null +++ b/java/org/cef/security/CefX509Certificate.java @@ -0,0 +1,49 @@ +// Copyright (c) 2022 The Chromium Embedded Framework 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 org.cef.security; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + + +/** + * The class represents a {@link X509Certificate} chain including the subject certificate. + */ +public final class CefX509Certificate { + private X509Certificate[] chain_; + + public CefX509Certificate(byte[][] chainDERData) { + chain_ = new X509Certificate[chainDERData.length]; + try { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + for (int i = 0; i < chainDERData.length; ++i) { + InputStream in = new ByteArrayInputStream(chainDERData[i]); + chain_[i] = (X509Certificate) factory.generateCertificate(in); + } + } catch (Exception e) { + e.printStackTrace(); + this.chain_ = null; + } + } + + /** + * @return The subject certificate + */ + public X509Certificate getSubjectCertificate() { + if (chain_ == null || chain_.length == 0) { + return null; + } + return chain_[0]; + } + + /** + * @return The certificates chain including the subject certificate. Ordered from subject to the issuers. + */ + public X509Certificate[] getCertificatesChain() { + return chain_; + } +} diff --git a/java/tests/detailed/handler/RequestHandler.java b/java/tests/detailed/handler/RequestHandler.java index 44d102a5..f6addb18 100644 --- a/java/tests/detailed/handler/RequestHandler.java +++ b/java/tests/detailed/handler/RequestHandler.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.SwingUtilities; +import org.cef.security.CefSSLInfo; import tests.detailed.dialog.CertErrorDialog; import tests.detailed.dialog.PasswordDialog; @@ -155,7 +156,8 @@ public boolean getAuthCredentials(CefBrowser browser, String origin_url, boolean @Override public boolean onCertificateError( - CefBrowser browser, ErrorCode cert_error, String request_url, CefCallback callback) { + CefBrowser browser, ErrorCode cert_error, String request_url, CefSSLInfo sslInfo, + CefCallback callback) { SwingUtilities.invokeLater(new CertErrorDialog(owner_, cert_error, request_url, callback)); return true; } diff --git a/java/tests/junittests/SelfSignedSSLTest.java b/java/tests/junittests/SelfSignedSSLTest.java new file mode 100644 index 00000000..ebf6ce03 --- /dev/null +++ b/java/tests/junittests/SelfSignedSSLTest.java @@ -0,0 +1,180 @@ +// Copyright (c) 2019 The Chromium Embedded Framework 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 tests.junittests; + +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; +import org.cef.browser.CefBrowser; +import org.cef.browser.CefFrame; +import org.cef.callback.CefCallback; +import org.cef.security.CefCertStatus; +import org.cef.security.CefSSLInfo; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import javax.net.ssl.*; +import java.io.ByteArrayInputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.Certificate; +import java.util.Base64; + +@ExtendWith(TestSetupExtension.class) +class SelfSignedSSLTest { + /* + Base64 encoded Java Keystore Data. + Generate a self-signed certificate: + $ keytool -genkeypair -keyalg RSA -alias selfsigned -keystore testkey.jks -storepass password -validity 7200 -keysize 2048 + $ cat testkey.jks | base64 + */ + final static String JKS_BASE64 = "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"; + final static String STORAGE_PASSWORD = "password"; + + static KeyStore makeKeyStore() { + KeyStore ks = null; + try { + ks = KeyStore.getInstance("jks"); + ks.load(new ByteArrayInputStream(Base64.getDecoder().decode(SelfSignedSSLTest.JKS_BASE64)), STORAGE_PASSWORD.toCharArray()); + } catch (Exception e) { + Assertions.fail("Failed to load the keystore"); + } + return ks; + } + + static HttpsServer makeHttpsServer(KeyStore keyStore) { + try { + // initialise the HTTPS server + HttpsServer server = HttpsServer.create(new InetSocketAddress(0), 0); + SSLContext sslContext = SSLContext.getInstance("TLS"); + + // setup the key manager factory + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, STORAGE_PASSWORD.toCharArray()); + + // setup the trust manager factory + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + + // setup the HTTPS context and parameters + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + server.setHttpsConfigurator(new HttpsConfigurator(sslContext) { + public void configure(HttpsParameters params) { + try { + SSLContext context = getSSLContext(); + SSLEngine engine = context.createSSLEngine(); + params.setNeedClientAuth(false); + params.setCipherSuites(engine.getEnabledCipherSuites()); + params.setProtocols(engine.getEnabledProtocols()); + SSLParameters sslParameters = context.getSupportedSSLParameters(); + params.setSSLParameters(sslParameters); + } catch (Exception ex) { + System.out.println("Failed to create HTTPS port"); + throw new RuntimeException(ex); + } + } + }); + server.createContext("/test", t -> { + String response = "This is the response"; + t.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); + t.sendResponseHeaders(200, response.getBytes().length); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + }); + return server; + } catch (Exception e) { + Assertions.fail("Failed to start HTTPS server. " + e); + } + return null; + } + + @Test + void certificateAccepted() { + KeyStore keyStore = makeKeyStore(); + Certificate[] certificateChainExpected = null; + try { + certificateChainExpected = keyStore.getCertificateChain("selfsigned"); + } catch (KeyStoreException e) { + Assertions.fail("Failed to get certificate chain from the key store"); + } + + HttpsServer server = makeHttpsServer(keyStore); + server.start(); + + var frame = new TestFrame() { + public CefSSLInfo sslInfo = null; + + @Override + protected void setupTest() { + createBrowser("https:/" + server.getAddress()); + super.setupTest(); + } + + @Override + public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { + super.onLoadEnd(browser, frame, httpStatusCode); + terminateTest(); + } + + @Override + public boolean onCertificateError(CefBrowser browser, ErrorCode cert_error, String request_url, CefSSLInfo sslInfo, CefCallback callback) { + this.sslInfo = sslInfo; + callback.Continue(); + return true; + } + }; + + frame.awaitCompletion(); + + Assertions.assertArrayEquals(certificateChainExpected, frame.sslInfo.certificate.getCertificatesChain()); + Assertions.assertTrue(CefCertStatus.CERT_STATUS_AUTHORITY_INVALID.hasStatus(frame.sslInfo.statusBiset)); + server.stop(0); + } + + @Test + void certificateRejected() { + KeyStore keyStore = makeKeyStore(); + HttpsServer server = makeHttpsServer(keyStore); + server.start(); + + var frame = new TestFrame() { + boolean isOnCertificateErrorCalled = false; + boolean isOnLoadErrorCalled = false; + + @Override + protected void setupTest() { + createBrowser("https:/" + server.getAddress()); + super.setupTest(); + } + + @Override + public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { + super.onLoadEnd(browser, frame, httpStatusCode); + terminateTest(); + } + + @Override + public void onLoadError(CefBrowser browser, CefFrame frame, ErrorCode errorCode, String errorText, String failedUrl) { + isOnLoadErrorCalled = true; + super.onLoadError(browser, frame, errorCode, errorText, failedUrl); + } + + @Override + public boolean onCertificateError(CefBrowser browser, ErrorCode cert_error, String request_url, CefSSLInfo sslInfo, CefCallback callback) { + isOnCertificateErrorCalled = true; + return false; + } + }; + + frame.awaitCompletion(); + Assertions.assertTrue(frame.isOnCertificateErrorCalled); + Assertions.assertTrue(frame.isOnLoadErrorCalled); + + server.stop(0); + } +} diff --git a/java/tests/junittests/TestFrame.java b/java/tests/junittests/TestFrame.java index eee36305..e5247731 100644 --- a/java/tests/junittests/TestFrame.java +++ b/java/tests/junittests/TestFrame.java @@ -27,6 +27,7 @@ import org.cef.network.CefRequest.TransitionType; import org.cef.network.CefResponse; import org.cef.network.CefURLRequest; +import org.cef.security.CefSSLInfo; import java.awt.BorderLayout; import java.awt.Component; @@ -237,8 +238,8 @@ public boolean getAuthCredentials(CefBrowser browser, String origin_url, boolean } @Override - public boolean onCertificateError(CefBrowser browser, CefLoadHandler.ErrorCode cert_error, - String request_url, CefCallback callback) { + public boolean onCertificateError(CefBrowser browser, CefLoadHandler.ErrorCode cert_error, String request_url, + CefSSLInfo sslInfo, CefCallback callback) { return false; } diff --git a/java/tests/simple/MainFrame.java b/java/tests/simple/MainFrame.java index 44035d0d..64f7d4df 100644 --- a/java/tests/simple/MainFrame.java +++ b/java/tests/simple/MainFrame.java @@ -14,6 +14,12 @@ import org.cef.handler.CefAppHandlerAdapter; import org.cef.handler.CefDisplayHandlerAdapter; import org.cef.handler.CefFocusHandlerAdapter; +import org.cef.security.CefCertStatus; +import org.cef.callback.CefCallback; +import org.cef.handler.*; +import org.cef.security.CefSSLInfo; +import tests.detailed.dialog.CertErrorDialog; +import tests.detailed.util.DataUri; import java.awt.BorderLayout; import java.awt.Component; @@ -25,6 +31,13 @@ import java.awt.event.FocusEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.List; +import java.util.stream.Collectors; import javax.swing.JFrame; import javax.swing.JPanel; @@ -138,6 +151,27 @@ public void onFullscreenModeChange(CefBrowser browser, boolean fullscreen) { } }); + client_.addRequestHandler(new CefRequestHandlerAdapter() { + @Override + public boolean onCertificateError(CefBrowser browser, CefLoadHandler.ErrorCode cert_error, + String request_url, CefSSLInfo sslInfo, CefCallback callback) { + CertErrorDialog dialog = new CertErrorDialog(MainFrame.this, cert_error, request_url, new CefCallback() { + @Override + public void Continue() { + callback.Continue(); + } + + @Override + public void cancel() { + callback.cancel(); + browser_.loadURL(DataUri.create("text/html", MakeErrorPage(request_url, cert_error, sslInfo))); + } + }); + SwingUtilities.invokeLater(dialog); + return true; + } + }); + // Clear focus from the browser when the address field gains focus. address_.addFocusListener(new FocusAdapter() { @Override @@ -213,6 +247,60 @@ public void run() { }); } + private static String normalize(String path) { + try { + return new File(path).getCanonicalPath(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static String DumpCertData(X509Certificate certificate) { + StringBuilder builder = new StringBuilder(); + builder.append("Subject: "); + builder.append(certificate.getSubjectX500Principal()); + builder.append("
Issuer: "); + builder.append(certificate.getIssuerX500Principal()); + builder.append("
Validity: "); + builder.append(certificate.getNotBefore()); + builder.append(" - "); + builder.append(certificate.getNotAfter()); + builder.append("
DER Encoded: "); + try { + builder.append(Base64.getEncoder().encodeToString(certificate.getEncoded())); + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + return builder.toString(); + } + + private static String MakeErrorPage(String request_url, CefLoadHandler.ErrorCode cert_error, CefSSLInfo info) { + StringBuilder page = new StringBuilder(); + page.append("Page failed to load"); + page.append("

Page failed to load.

URL: "); + page.append(request_url); + page.append("
Error: "); + page.append(cert_error); + page.append("("); + page.append(cert_error.getCode()); + page.append(")

X.509 Certificate Information:

Certificate status: "); + page.append(Arrays.stream(CefCertStatus.values()) + .skip(1) // skip CERT_STATUS_NONE + .filter(status -> status.hasStatus(info.statusBiset)) + .map(Enum::toString) + .collect(Collectors.joining(", "))); + page.append("

Certificated chain(from subject to issuers):

"); + page.append(""); + page.append("
"); + page.append(Arrays.stream(info.certificate.getCertificatesChain()) + .map(MainFrame::DumpCertData) + .collect(Collectors.joining("
"))); + page.append("
"); + return page.toString(); + } + public static void main(String[] args) { // Perform startup initialization on platforms that require it. if (!CefApp.startup(args)) { diff --git a/native/request_handler.cpp b/native/request_handler.cpp index 24434451..86513f2f 100644 --- a/native/request_handler.cpp +++ b/native/request_handler.cpp @@ -10,6 +10,58 @@ #include "resource_request_handler.h" #include "util.h" +#include "include/base/cef_logging.h" + +namespace { + +jobject NewCefX509Certificate(JNIEnv_* env, + CefRefPtr ssl_info) { + ScopedJNIClass byteArrayCls(env, env->FindClass("[B")); + if (!byteArrayCls) { + if (env->ExceptionOccurred()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + return nullptr; + } + + CefX509Certificate::IssuerChainBinaryList der_chain; + ssl_info->GetDEREncodedIssuerChain(der_chain); + der_chain.insert(der_chain.begin(), ssl_info->GetDEREncoded()); + + ScopedJNIObjectLocal certificatesChain( + env, env->NewObjectArray(static_cast(der_chain.size()), + byteArrayCls, nullptr)); + + for (size_t i = 0; i < der_chain.size(); ++i) { + const auto& der_cert = der_chain[i]; + ScopedJNIObjectLocal derArray( + env, env->NewByteArray((jsize)der_cert->GetSize())); + { + void* buf = env->GetPrimitiveArrayCritical((jarray)derArray.get(), 0); + der_cert->GetData(buf, der_cert->GetSize(), 0); + env->ReleasePrimitiveArrayCritical((jarray)derArray.get(), buf, 0); + } + + env->SetObjectArrayElement((jobjectArray)certificatesChain.get(), (jsize)i, + derArray); + } + + return NewJNIObject(env, "org/cef/security/CefX509Certificate", "([[B)V", + certificatesChain.get()); +} + +jobject NewCefSSLInfo(JNIEnv_* env, CefRefPtr ssl_info) { + ScopedJNIObjectLocal certificate( + env, NewCefX509Certificate(env, ssl_info->GetX509Certificate())); + + return NewJNIObject(env, "org/cef/security/CefSSLInfo", + "(ILorg/cef/security/CefX509Certificate;)V", + ssl_info.get()->GetCertStatus(), certificate.get()); +} + +} // namespace + RequestHandler::RequestHandler(JNIEnv* env, jobject handler) : handle_(env, handler) {} @@ -155,15 +207,17 @@ bool RequestHandler::OnCertificateError(CefRefPtr browser, ScopedJNIBrowser jbrowser(env, browser); ScopedJNIObjectLocal jcertError(env, NewJNIErrorCode(env, cert_error)); ScopedJNIString jrequestUrl(env, request_url); + ScopedJNIObjectLocal jSSLInfo(env, NewCefSSLInfo(env, ssl_info)); ScopedJNICallback jcallback(env, callback); jboolean jresult = JNI_FALSE; JNI_CALL_METHOD( env, handle_, "onCertificateError", "(Lorg/cef/browser/CefBrowser;Lorg/cef/handler/CefLoadHandler$ErrorCode;" - "Ljava/lang/String;Lorg/cef/callback/CefCallback;)Z", + "Ljava/lang/String;Lorg/cef/security/CefSSLInfo;Lorg/cef/callback/" + "CefCallback;)Z", Boolean, jresult, jbrowser.get(), jcertError.get(), jrequestUrl.get(), - jcallback.get()); + jSSLInfo.get(), jcallback.get()); if (jresult == JNI_FALSE) { // If the Java method returns "false" the callback won't be used and