diff --git a/.gitignore b/.gitignore index e7bc7d07..5c9ebd00 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ Thumbs.db # JCEF generated directories /binary_distrib /jcef_build +/jcef-natives /out # JCEF generated files /native/jcef_version.h @@ -55,3 +56,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..b310de73 --- /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.statusBitset = statusBitset; + this.certificate = certificate; + } + + public final int statusBitset; + 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/DisplayHandlerTest.java b/java/tests/junittests/DisplayHandlerTest.java index 81b873af..5f4a290e 100644 --- a/java/tests/junittests/DisplayHandlerTest.java +++ b/java/tests/junittests/DisplayHandlerTest.java @@ -31,7 +31,7 @@ protected void setupTest() { client_.addDisplayHandler(new CefDisplayHandlerAdapter() { @Override public void onTitleChange(CefBrowser browser, String title) { - assertFalse(gotCallback_); + if (gotCallback_) return; gotCallback_ = true; assertEquals("Test Title", title); terminateTest(); @@ -59,9 +59,9 @@ protected void setupTest() { client_.addDisplayHandler(new CefDisplayHandlerAdapter() { @Override public void onAddressChange(CefBrowser browser, CefFrame frame, String url) { - assertFalse(gotCallback_); + if (gotCallback_) return; gotCallback_ = true; - assertEquals(url, testUrl_); + assertEquals(testUrl_, url); terminateTest(); } }); diff --git a/java/tests/junittests/DragDataTest.java b/java/tests/junittests/DragDataTest.java index bd904309..21d56357 100644 --- a/java/tests/junittests/DragDataTest.java +++ b/java/tests/junittests/DragDataTest.java @@ -80,12 +80,12 @@ void createFile() { dragData.addFile(path1, "File 1"); dragData.addFile(path2, "File 2"); - Vector fileNames = new Vector<>(); - assertTrue(dragData.getFileNames(fileNames)); + Vector filePaths = new Vector<>(); + assertTrue(dragData.getFilePaths(filePaths)); - assertEquals(2, fileNames.size()); - assertEquals(path1, fileNames.get(0)); - assertEquals(path2, fileNames.get(1)); + assertEquals(2, filePaths.size()); + assertEquals(path1, filePaths.get(0)); + assertEquals(path2, filePaths.get(1)); assertFalse(dragData.isLink()); assertTrue(dragData.isFile()); diff --git a/java/tests/junittests/SelfSignedSSLTest.java b/java/tests/junittests/SelfSignedSSLTest.java new file mode 100644 index 00000000..654f0e31 --- /dev/null +++ b/java/tests/junittests/SelfSignedSSLTest.java @@ -0,0 +1,214 @@ +// 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.OutputStream; +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.Certificate; +import java.util.Date; + +@ExtendWith(TestSetupExtension.class) +class SelfSignedSSLTest { + private static class SSLAcceptTestFrame extends TestFrame { + volatile CefSSLInfo sslInfo; + + SSLAcceptTestFrame(String url) { + super(url); + } + + @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; + } + } + + private static class SSLRejectTestFrame extends TestFrame { + volatile boolean isOnCertificateErrorCalled = false; + volatile boolean isOnLoadErrorCalled = false; + + SSLRejectTestFrame(String url) { + super(url); + } + + @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); + terminateTest(); + } + + @Override + public boolean onCertificateError(CefBrowser browser, ErrorCode cert_error, String request_url, CefSSLInfo sslInfo, CefCallback callback) { + isOnCertificateErrorCalled = true; + return false; + } + } + final static String STORAGE_PASSWORD = "password"; + + /** + * Creates a KeyStore with an expired self-signed certificate. + * The certificate expired in the past, which forces Chromium to + * trigger onCertificateError with ERR_CERT_DATE_INVALID. + */ + static KeyStore makeExpiredKeyStore() { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair keyPair = kpg.generateKeyPair(); + + // Use sun.security.x509 to create a self-signed cert with past dates + sun.security.x509.X500Name owner = new sun.security.x509.X500Name( + "CN=jcef-test-expired, O=JCEF Test, L=Test, C=DE"); + Date notBefore = new Date(System.currentTimeMillis() - 365L * 24 * 60 * 60 * 1000); // 1 year ago + Date notAfter = new Date(System.currentTimeMillis() - 1L * 24 * 60 * 60 * 1000); // 1 day ago + + sun.security.x509.CertificateValidity validity = + new sun.security.x509.CertificateValidity(notBefore, notAfter); + + sun.security.x509.X509CertInfo info = new sun.security.x509.X509CertInfo(); + info.set(sun.security.x509.X509CertInfo.VERSION, + new sun.security.x509.CertificateVersion(sun.security.x509.CertificateVersion.V3)); + info.set(sun.security.x509.X509CertInfo.SERIAL_NUMBER, + new sun.security.x509.CertificateSerialNumber(BigInteger.valueOf(System.currentTimeMillis()))); + info.set(sun.security.x509.X509CertInfo.SUBJECT, owner); + info.set(sun.security.x509.X509CertInfo.ISSUER, owner); + info.set(sun.security.x509.X509CertInfo.VALIDITY, validity); + info.set(sun.security.x509.X509CertInfo.KEY, + new sun.security.x509.CertificateX509Key(keyPair.getPublic())); + info.set(sun.security.x509.X509CertInfo.ALGORITHM_ID, + new sun.security.x509.CertificateAlgorithmId( + sun.security.x509.AlgorithmId.get("SHA256withRSA"))); + + sun.security.x509.X509CertImpl cert = new sun.security.x509.X509CertImpl(info); + cert.sign(keyPair.getPrivate(), "SHA256withRSA"); + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, STORAGE_PASSWORD.toCharArray()); + ks.setKeyEntry("expired", keyPair.getPrivate(), STORAGE_PASSWORD.toCharArray(), + new Certificate[]{cert}); + return ks; + } catch (Exception e) { + Assertions.fail("Failed to create expired certificate: " + e.getMessage()); + return null; + } + } + + static HttpsServer makeHttpsServer(KeyStore keyStore) { + try { + HttpsServer server = HttpsServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + + // 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(new String[]{"TLSv1.2"}); + SSLParameters sslParameters = context.getSupportedSSLParameters(); + params.setSSLParameters(sslParameters); + } catch (Exception ex) { + 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 = makeExpiredKeyStore(); + Certificate[] certificateChainExpected = null; + try { + certificateChainExpected = keyStore.getCertificateChain("expired"); + } catch (KeyStoreException e) { + Assertions.fail("Failed to get certificate chain from the key store"); + } + + HttpsServer server = makeHttpsServer(keyStore); + server.start(); + + SSLAcceptTestFrame myFrame = new SSLAcceptTestFrame( + "https://localhost:" + server.getAddress().getPort() + "/test"); + + myFrame.awaitCompletion(); + + Assertions.assertNotNull(myFrame.sslInfo, "onCertificateError was not called"); + Assertions.assertArrayEquals(certificateChainExpected, myFrame.sslInfo.certificate.getCertificatesChain()); + Assertions.assertTrue(CefCertStatus.CERT_STATUS_DATE_INVALID.hasStatus(myFrame.sslInfo.statusBitset) + || CefCertStatus.CERT_STATUS_AUTHORITY_INVALID.hasStatus(myFrame.sslInfo.statusBitset)); + server.stop(0); + } + + @Test + void certificateRejected() { + KeyStore keyStore = makeExpiredKeyStore(); + HttpsServer server = makeHttpsServer(keyStore); + server.start(); + + SSLRejectTestFrame frame = new SSLRejectTestFrame( + "https://localhost:" + server.getAddress().getPort() + "/test"); + + 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..ebc01b7d 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; @@ -55,7 +56,14 @@ private class ResourceContent { protected final CefClient client_; protected CefBrowser browser_ = null; + private final String startURL_; + TestFrame() { + this(null); + } + + TestFrame(String startURL) { + startURL_ = startURL; client_ = CefApp.getInstance().createClient(); assertNotNull(client_); @@ -115,7 +123,11 @@ protected void createBrowser(String startURL) { } // Override this method to perform test setup. - protected void setupTest() {} + protected void setupTest() { + if (startURL_ != null) { + createBrowser(startURL_); + } + } // Override this method to perform test cleanup. protected void cleanupTest() { @@ -237,8 +249,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/junittests/TestSetupExtension.java b/java/tests/junittests/TestSetupExtension.java index 66887719..bb1c3458 100644 --- a/java/tests/junittests/TestSetupExtension.java +++ b/java/tests/junittests/TestSetupExtension.java @@ -9,10 +9,14 @@ import org.cef.CefApp; import org.cef.CefApp.CefAppState; import org.cef.CefSettings; +import org.cef.SystemBootstrap; import org.cef.handler.CefAppHandlerAdapter; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import tests.junittests.setup.NativesInstaller; + +import java.io.File; import java.util.concurrent.CountDownLatch; // All test cases must install this extension for CEF to be properly initialized @@ -51,6 +55,49 @@ private void initialize(ExtensionContext context) { // Register a callback hook for when the root test context is shut down. context.getRoot().getStore(GLOBAL).put("jcef_test_setup", this); + // Ensure native binaries are available (download if necessary). + try { + File nativesDir = NativesInstaller.ensureInstalled(); + + // Find the actual directory containing the native libraries. + // The binary_distrib tar.gz may nest them (e.g. bin/lib/win64/). + File jcefDll = findLibrary(nativesDir, NativesInstaller.mapLibraryName("jcef")); + File libDir = jcefDll != null ? jcefDll.getParentFile() : nativesDir; + + // Add the CEF natives directory to java.library.path so that + // CefApp.getJcefLibPath() can locate jcef.dll and resolve + // browser_subprocess_path correctly. + String currentPath = System.getProperty("java.library.path", ""); + String separator = System.getProperty("path.separator"); + System.setProperty("java.library.path", + libDir.getAbsolutePath() + separator + currentPath); + + // Register a custom loader that loads CEF native libraries from + // the installed directory using absolute paths. This is necessary + // because java.library.path changes at runtime are not picked up + // by the default System.loadLibrary(). + final File finalLibDir = libDir; + SystemBootstrap.setLoader(new SystemBootstrap.Loader() { + @Override + public void loadLibrary(String libname) { + if ("jawt".equals(libname)) { + System.loadLibrary(libname); + } else { + String fileName = NativesInstaller.mapLibraryName(libname); + File libFile = new File(finalLibDir, fileName); + if (libFile.exists()) { + System.load(libFile.getAbsolutePath()); + } else { + System.loadLibrary(libname); + } + } + } + }); + } catch (Exception e) { + System.out.println("WARNING: Could not install native binaries: " + e.getMessage()); + System.out.println("Tests will attempt to use java.library.path instead."); + } + // Perform startup initialization on platforms that require it. if (!CefApp.startup(null)) { System.out.println("Startup initialization failed!"); @@ -72,6 +119,27 @@ public void stateHasChanged(org.cef.CefApp.CefAppState state) { CefApp.getInstance(settings); } + /** + * Recursively searches for a library file in a directory tree. + */ + private static File findLibrary(File dir, String fileName) { + File[] files = dir.listFiles(); + if (files == null) return null; + for (File f : files) { + if (f.isFile() && f.getName().equals(fileName)) { + return f; + } + } + // Search subdirectories + for (File f : files) { + if (f.isDirectory()) { + File found = findLibrary(f, fileName); + if (found != null) return found; + } + } + return null; + } + // Executed after all tests have completed. @Override public void close() { diff --git a/java/tests/junittests/setup/ContentRange.java b/java/tests/junittests/setup/ContentRange.java new file mode 100644 index 00000000..3e23be5b --- /dev/null +++ b/java/tests/junittests/setup/ContentRange.java @@ -0,0 +1,63 @@ +// Copyright (c) 2024 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.setup; + +/** + * Represents an HTTP Content-Range header value. + */ +class ContentRange { + private final long start; + private final long end; + private final long total; + + ContentRange(long start, long end, long total) { + this.start = start; + this.end = end; + this.total = total; + } + + /** + * Parses a Content-Range header value. + * + * @param contentRange standard format "bytes 5000-9999/10000" + * @return the parsed ContentRange + */ + static ContentRange parse(String contentRange) { + if (contentRange == null || !contentRange.startsWith("bytes ")) { + throw new IllegalArgumentException("Invalid Content-Range header: " + contentRange); + } + + String[] parts = contentRange.substring(6).trim().split("/"); + if (parts.length != 2) { + throw new IllegalArgumentException("Invalid Content-Range format: " + contentRange); + } + + String[] range = parts[0].split("-"); + if (range.length != 2) { + throw new IllegalArgumentException("Invalid range in Content-Range: " + contentRange); + } + + try { + long start = Long.parseLong(range[0]); + long end = Long.parseLong(range[1]); + long total = Long.parseLong(parts[1]); + return new ContentRange(start, end, total); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Failed to parse Content-Range values: " + contentRange, e); + } + } + + long getStart() { + return start; + } + + long getEnd() { + return end; + } + + long getTotal() { + return total; + } +} diff --git a/java/tests/junittests/setup/NativesDownloader.java b/java/tests/junittests/setup/NativesDownloader.java new file mode 100644 index 00000000..ab17d4d5 --- /dev/null +++ b/java/tests/junittests/setup/NativesDownloader.java @@ -0,0 +1,146 @@ +// Copyright (c) 2024 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.setup; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Downloads a file via HTTP with resume support for interrupted downloads. + * Uses HTTP Range headers and RandomAccessFile to continue partial downloads. + */ +class NativesDownloader { + private static final Logger LOG = Logger.getLogger(NativesDownloader.class.getName()); + private static final int BUFFER_SIZE = 16 * 1024; + private static final int READ_TIMEOUT = 30000; + private static final int CONNECT_TIMEOUT = 10000; + + static void download(String downloadUrl, File destination) throws IOException { + if (!destination.exists() && !destination.createNewFile()) { + throw new IOException("Could not create target file: " + destination); + } + + long existingSize = destination.length(); + + // Resolve redirects first (GitHub releases redirect to S3) + URL resolvedUrl = resolveRedirects(new URL(downloadUrl)); + long totalSize = getContentLength(resolvedUrl); + + if (totalSize != -1 && existingSize == totalSize) { + LOG.info("File already fully downloaded."); + return; + } + + if (totalSize != -1 && existingSize > totalSize) { + // Existing file is larger than expected — re-download + if (!destination.delete() || !destination.createNewFile()) { + throw new IOException("Could not recreate target file: " + destination); + } + existingSize = 0; + } + + HttpURLConnection connection = (HttpURLConnection) resolvedUrl.openConnection(); + connection.setReadTimeout(READ_TIMEOUT); + connection.setConnectTimeout(CONNECT_TIMEOUT); + connection.setInstanceFollowRedirects(true); + + long seekPosition = 0; + if (existingSize > 0) { + LOG.info("Resuming download at byte " + existingSize); + connection.setRequestProperty("Range", "bytes=" + existingSize + "-"); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_PARTIAL) { + String contentRange = connection.getHeaderField("Content-Range"); + LOG.info("Content-Range: " + contentRange); + ContentRange range = ContentRange.parse(contentRange); + seekPosition = Math.min(range.getStart(), existingSize); + } + // If server doesn't support Range, seekPosition stays 0 and we re-download + } + + try (InputStream in = connection.getInputStream()) { + int responseCode = connection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK + && responseCode != HttpURLConnection.HTTP_PARTIAL) { + throw new IOException("Download failed with HTTP " + responseCode); + } + + try (RandomAccessFile outputFile = new RandomAccessFile(destination, "rw")) { + outputFile.seek(seekPosition); + + byte[] buffer = new byte[BUFFER_SIZE]; + long transferred = seekPosition; + int read; + long lastLog = System.currentTimeMillis(); + + while ((read = in.read(buffer)) > 0) { + outputFile.write(buffer, 0, read); + transferred += read; + + // Log progress every 2 seconds + long now = System.currentTimeMillis(); + if (now - lastLog > 2000) { + lastLog = now; + if (totalSize > 0) { + LOG.info(String.format("Downloaded %.1f MB / %.1f MB (%.0f%%)", + transferred / 1048576.0, totalSize / 1048576.0, + transferred * 100.0 / totalSize)); + } else { + LOG.info(String.format("Downloaded %.1f MB", transferred / 1048576.0)); + } + } + } + + LOG.info(String.format("Download complete: %.1f MB", transferred / 1048576.0)); + } + } finally { + connection.disconnect(); + } + } + + private static URL resolveRedirects(URL url) throws IOException { + int maxRedirects = 5; + for (int i = 0; i < maxRedirects; i++) { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setInstanceFollowRedirects(false); + conn.setConnectTimeout(CONNECT_TIMEOUT); + conn.setRequestMethod("HEAD"); + + int code = conn.getResponseCode(); + conn.disconnect(); + + if (code == HttpURLConnection.HTTP_MOVED_TEMP + || code == HttpURLConnection.HTTP_MOVED_PERM + || code == 307 || code == 308) { + String location = conn.getHeaderField("Location"); + if (location != null) { + url = new URL(location); + continue; + } + } + return url; + } + return url; + } + + private static long getContentLength(URL url) throws IOException { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + try { + connection.setReadTimeout(READ_TIMEOUT); + connection.setConnectTimeout(CONNECT_TIMEOUT); + connection.setRequestMethod("HEAD"); + return connection.getContentLengthLong(); + } finally { + connection.disconnect(); + } + } +} diff --git a/java/tests/junittests/setup/NativesInstaller.java b/java/tests/junittests/setup/NativesInstaller.java new file mode 100644 index 00000000..257a960e --- /dev/null +++ b/java/tests/junittests/setup/NativesInstaller.java @@ -0,0 +1,228 @@ +// Copyright (c) 2024 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.setup; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.logging.Logger; + +/** + * Downloads and installs JCEF native binaries from a GitHub release. + * Designed for local test execution — downloads binaries built by GitHub Actions + * from the tlappe/jcefbuild repository. + * + *

Can be used in two ways: + *

    + *
  • Automatically via {@link #ensureInstalled()} from test setup
  • + *
  • Standalone via {@link #main(String[])} as an IntelliJ Run Configuration
  • + *
+ */ +public class NativesInstaller { + private static final Logger LOG = Logger.getLogger(NativesInstaller.class.getName()); + + private static final String GITHUB_REPO = "tlappe/jcefbuild"; + private static final String LATEST_RELEASE_API = + "https://api.github.com/repos/" + GITHUB_REPO + "/releases/latest"; + private static final String INSTALL_LOCK = "install.lock"; + private static final String TEMP_FILE = "download.tar.gz.tmp"; + + private static final File DEFAULT_INSTALL_DIR = new File("jcef-natives"); + + /** + * Ensures native binaries are installed. Downloads from the latest GitHub + * release if not already present. Safe to call multiple times. + * + * @return the directory containing the native binaries + */ + public static File ensureInstalled() throws IOException { + return ensureInstalled(DEFAULT_INSTALL_DIR); + } + + /** + * Ensures native binaries are installed in the given directory. + * + * @param installDir the target directory for native binaries + * @return the directory containing the native binaries + */ + public static File ensureInstalled(File installDir) throws IOException { + File lockFile = new File(installDir, INSTALL_LOCK); + if (lockFile.exists()) { + LOG.info("Native binaries already installed in: " + installDir.getAbsolutePath()); + return installDir; + } + + LOG.info("Native binaries not found. Starting download..."); + + if (!installDir.exists() && !installDir.mkdirs()) { + throw new IOException("Could not create install directory: " + installDir); + } + + String platform = detectPlatform(); + LOG.info("Detected platform: " + platform); + + String downloadUrl = findDownloadUrl(platform); + LOG.info("Download URL: " + downloadUrl); + + File tempFile = new File(installDir, TEMP_FILE); + NativesDownloader.download(downloadUrl, tempFile); + + LOG.info("Extracting..."); + TarGzExtractor.extract(tempFile, installDir); + + if (!tempFile.delete()) { + LOG.warning("Could not delete temp file: " + tempFile); + } + + if (!lockFile.createNewFile()) { + throw new IOException("Could not create install lock: " + lockFile); + } + + LOG.info("Native binaries installed successfully in: " + installDir.getAbsolutePath()); + return installDir; + } + + /** + * Detects the current platform in the format used by jcefbuild releases. + * + * @return platform identifier like "windows-amd64", "linux-amd64", "macosx-amd64" + */ + static String detectPlatform() { + String osName = System.getProperty("os.name", "").toLowerCase(); + String osArch = System.getProperty("os.arch", "").toLowerCase(); + + String os; + if (osName.contains("win")) { + os = "windows"; + } else if (osName.contains("mac") || osName.contains("darwin")) { + os = "macosx"; + } else { + os = "linux"; + } + + String arch; + if (osArch.contains("amd64") || osArch.contains("x86_64")) { + arch = "amd64"; + } else if (osArch.contains("aarch64") || osArch.contains("arm64")) { + arch = "arm64"; + } else if (osArch.contains("386") || osArch.equals("x86") || osArch.equals("i386") || osArch.equals("i686")) { + arch = "i386"; + } else { + arch = osArch; + } + + return os + "-" + arch; + } + + /** + * Queries the GitHub API for the latest release and finds the download URL + * for the given platform's tar.gz asset. + */ + private static String findDownloadUrl(String platform) throws IOException { + String expectedAsset = platform + ".tar.gz"; + + URL url = new URL(LATEST_RELEASE_API); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("Accept", "application/vnd.github.v3+json"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + + try { + int responseCode = conn.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + throw new IOException("GitHub API returned HTTP " + responseCode + + ". Is there a release in " + GITHUB_REPO + "?"); + } + + // Read entire response + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } + + String json = sb.toString(); + return extractDownloadUrl(json, expectedAsset); + } finally { + conn.disconnect(); + } + } + + /** + * Extracts the browser_download_url for the given asset name from + * a GitHub releases JSON response. Simple string-based parsing to + * avoid any JSON library dependency. + */ + static String extractDownloadUrl(String json, String assetName) throws IOException { + // Find the asset by name, then extract its browser_download_url. + // The JSON structure has: "assets": [..., {"name": "X", ..., "browser_download_url": "Y"}, ...] + int searchFrom = 0; + while (true) { + int nameIdx = json.indexOf("\"name\"", searchFrom); + if (nameIdx < 0) { + break; + } + + // Find the value after "name": + int colonIdx = json.indexOf(':', nameIdx + 6); + if (colonIdx < 0) break; + int valueStart = json.indexOf('"', colonIdx + 1); + if (valueStart < 0) break; + int valueEnd = json.indexOf('"', valueStart + 1); + if (valueEnd < 0) break; + + String name = json.substring(valueStart + 1, valueEnd); + + if (name.equals(assetName)) { + // Found the asset — now find browser_download_url nearby + int downloadUrlIdx = json.indexOf("\"browser_download_url\"", valueEnd); + if (downloadUrlIdx < 0) break; + + int dColonIdx = json.indexOf(':', downloadUrlIdx + 22); + if (dColonIdx < 0) break; + int dValueStart = json.indexOf('"', dColonIdx + 1); + if (dValueStart < 0) break; + int dValueEnd = json.indexOf('"', dValueStart + 1); + if (dValueEnd < 0) break; + + return json.substring(dValueStart + 1, dValueEnd); + } + + searchFrom = valueEnd + 1; + } + + throw new IOException("Asset '" + assetName + "' not found in the latest release of " + + GITHUB_REPO + ". Available assets may not include your platform."); + } + + /** + * Maps a library name to the platform-specific file name. + * E.g. "libcef" → "libcef.dll" on Windows, "cef" → "libcef.so" on Linux. + */ + public static String mapLibraryName(String libName) { + String osName = System.getProperty("os.name", "").toLowerCase(); + if (osName.contains("win")) { + return libName + ".dll"; + } else if (osName.contains("mac") || osName.contains("darwin")) { + return "lib" + libName + ".dylib"; + } else { + return "lib" + libName + ".so"; + } + } + + /** + * Standalone entry point. Downloads and installs native binaries. + */ + public static void main(String[] args) throws IOException { + File installDir = args.length > 0 ? new File(args[0]) : DEFAULT_INSTALL_DIR; + ensureInstalled(installDir); + } +} diff --git a/java/tests/junittests/setup/TarGzExtractor.java b/java/tests/junittests/setup/TarGzExtractor.java new file mode 100644 index 00000000..54c58218 --- /dev/null +++ b/java/tests/junittests/setup/TarGzExtractor.java @@ -0,0 +1,153 @@ +// Copyright (c) 2024 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.setup; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.GZIPInputStream; + +/** + * Extracts .tar.gz archives using only Java standard library classes. + * Parses the tar format manually (512-byte header blocks, POSIX/UStar). + */ +class TarGzExtractor { + private static final int BLOCK_SIZE = 512; + private static final int BUFFER_SIZE = 16 * 1024; + + static void extract(File tarGzFile, File targetDir) throws IOException { + if (!targetDir.exists() && !targetDir.mkdirs()) { + throw new IOException("Could not create target directory: " + targetDir); + } + + try (InputStream fileIn = new BufferedInputStream(new FileInputStream(tarGzFile)); + InputStream gzipIn = new GZIPInputStream(fileIn)) { + extractTar(gzipIn, targetDir); + } + } + + private static void extractTar(InputStream tarIn, File targetDir) throws IOException { + byte[] header = new byte[BLOCK_SIZE]; + byte[] buffer = new byte[BUFFER_SIZE]; + + while (true) { + int bytesRead = readFully(tarIn, header); + if (bytesRead < BLOCK_SIZE || isEndOfArchive(header)) { + break; + } + + String name = parseString(header, 0, 100); + if (name.isEmpty()) { + break; + } + + // Check for UStar prefix (offset 345, 155 bytes) + String prefix = parseString(header, 345, 155); + if (!prefix.isEmpty()) { + name = prefix + "/" + name; + } + + long size = parseOctal(header, 124, 12); + byte typeFlag = header[156]; + + File outputFile = new File(targetDir, name); + + // Security: prevent path traversal + if (!outputFile.getCanonicalPath().startsWith(targetDir.getCanonicalPath())) { + throw new IOException("Tar entry outside target directory: " + name); + } + + if (typeFlag == '5' || name.endsWith("/")) { + // Directory + if (!outputFile.exists() && !outputFile.mkdirs()) { + throw new IOException("Could not create directory: " + outputFile); + } + } else if (typeFlag == '0' || typeFlag == 0) { + // Regular file + File parent = outputFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw new IOException("Could not create parent directory: " + parent); + } + + try (OutputStream out = new FileOutputStream(outputFile)) { + long remaining = size; + while (remaining > 0) { + int toRead = (int) Math.min(buffer.length, remaining); + int read = tarIn.read(buffer, 0, toRead); + if (read < 0) { + throw new IOException("Unexpected end of tar stream"); + } + out.write(buffer, 0, read); + remaining -= read; + } + } + + // Skip padding to next 512-byte boundary + long remainder = size % BLOCK_SIZE; + if (remainder != 0) { + long skip = BLOCK_SIZE - remainder; + skipFully(tarIn, skip); + } + } else { + // Symlink or other type — skip + long blocks = (size + BLOCK_SIZE - 1) / BLOCK_SIZE; + skipFully(tarIn, blocks * BLOCK_SIZE); + } + } + } + + private static String parseString(byte[] buf, int offset, int length) { + int end = offset; + int max = Math.min(offset + length, buf.length); + while (end < max && buf[end] != 0) { + end++; + } + return new String(buf, offset, end - offset).trim(); + } + + private static long parseOctal(byte[] buf, int offset, int length) { + String s = parseString(buf, offset, length); + if (s.isEmpty()) { + return 0; + } + return Long.parseLong(s, 8); + } + + private static boolean isEndOfArchive(byte[] block) { + for (byte b : block) { + if (b != 0) return false; + } + return true; + } + + private static int readFully(InputStream in, byte[] buf) throws IOException { + int total = 0; + while (total < buf.length) { + int read = in.read(buf, total, buf.length - total); + if (read < 0) { + return total; + } + total += read; + } + return total; + } + + private static void skipFully(InputStream in, long bytes) throws IOException { + long remaining = bytes; + byte[] skipBuf = new byte[BUFFER_SIZE]; + while (remaining > 0) { + int toRead = (int) Math.min(skipBuf.length, remaining); + int read = in.read(skipBuf, 0, toRead); + if (read < 0) { + throw new IOException("Unexpected end of stream while skipping"); + } + remaining -= read; + } + } +} diff --git a/java/tests/simple/MainFrame.java b/java/tests/simple/MainFrame.java index 44035d0d..22715b6d 100644 --- a/java/tests/simple/MainFrame.java +++ b/java/tests/simple/MainFrame.java @@ -8,12 +8,17 @@ import org.cef.CefApp.CefAppState; import org.cef.CefClient; import org.cef.CefSettings; -import org.cef.OS; import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; 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,9 +30,14 @@ 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.stream.Collectors; import javax.swing.JFrame; -import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingUtilities; @@ -138,6 +148,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 +244,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.statusBitset)) + .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