From 7904d8b162e39dd292130d1339353c26391671ad Mon Sep 17 00:00:00 2001 From: Vladimir Kharitonov Date: Fri, 14 Oct 2022 10:59:15 +0200 Subject: [PATCH 1/7] Introducing CefSSLInfo in CefRequestHandler.onCertificateError() --- .gitignore | 1 + java/org/cef/CefClient.java | 11 +- java/org/cef/handler/CefRequestHandler.java | 9 +- .../cef/handler/CefRequestHandlerAdapter.java | 4 +- java/org/cef/network/CefRequest.java | 2 +- java/org/cef/security/CefCertStatus.java | 41 ++++ java/org/cef/security/CefSSLInfo.java | 19 ++ java/org/cef/security/CefX509Certificate.java | 49 +++++ .../detailed/handler/RequestHandler.java | 4 +- java/tests/junittests/SelfSignedSSLTest.java | 180 ++++++++++++++++++ java/tests/junittests/TestFrame.java | 5 +- java/tests/simple/MainFrame.java | 88 +++++++++ native/request_handler.cpp | 58 +++++- 13 files changed, 456 insertions(+), 15 deletions(-) create mode 100644 java/org/cef/security/CefCertStatus.java create mode 100644 java/org/cef/security/CefSSLInfo.java create mode 100644 java/org/cef/security/CefX509Certificate.java create mode 100644 java/tests/junittests/SelfSignedSSLTest.java 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 93cbe95b..8774ca2f 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 @@ -113,13 +114,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 47e38e92..4826e1c0 100644 --- a/java/org/cef/network/CefRequest.java +++ b/java/org/cef/network/CefRequest.java @@ -142,7 +142,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 9dee9799..38b23181 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 From b3ad0c14021177d6319b06330609d55e0b4d26db Mon Sep 17 00:00:00 2001 From: Tim Lappe Date: Fri, 3 Apr 2026 11:59:49 +0200 Subject: [PATCH 2/7] Add CefSSLInfo support, SSL test refactoring, and native binary auto-download for tests - Fix CefSSLInfo.statusBiset typo to statusBitset - Refactor SelfSignedSSLTest to use named inner classes instead of anonymous classes, fixing field access issues with the TestFrame template method pattern - Add TestFrame(String startURL) constructor for direct browser URL setup - Add automatic native binary download from tlappe/jcefbuild GitHub releases via NativesInstaller, integrated into TestSetupExtension using SystemBootstrap.setLoader() for absolute path loading - Add jcef-natives/ to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + java/org/cef/security/CefSSLInfo.java | 4 +- java/tests/junittests/SelfSignedSSLTest.java | 105 ++++---- java/tests/junittests/TestFrame.java | 13 +- java/tests/junittests/TestSetupExtension.java | 60 +++++ java/tests/junittests/setup/ContentRange.java | 63 +++++ .../junittests/setup/NativesDownloader.java | 146 +++++++++++ .../junittests/setup/NativesInstaller.java | 228 ++++++++++++++++++ .../junittests/setup/TarGzExtractor.java | 153 ++++++++++++ java/tests/simple/MainFrame.java | 5 +- 10 files changed, 718 insertions(+), 60 deletions(-) create mode 100644 java/tests/junittests/setup/ContentRange.java create mode 100644 java/tests/junittests/setup/NativesDownloader.java create mode 100644 java/tests/junittests/setup/NativesInstaller.java create mode 100644 java/tests/junittests/setup/TarGzExtractor.java diff --git a/.gitignore b/.gitignore index 2435a43f..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 diff --git a/java/org/cef/security/CefSSLInfo.java b/java/org/cef/security/CefSSLInfo.java index b0625bcb..b310de73 100644 --- a/java/org/cef/security/CefSSLInfo.java +++ b/java/org/cef/security/CefSSLInfo.java @@ -10,10 +10,10 @@ public class CefSSLInfo { public CefSSLInfo(int statusBitset, CefX509Certificate certificate) { - this.statusBiset = statusBitset; + this.statusBitset = statusBitset; this.certificate = certificate; } - public final int statusBiset; + public final int statusBitset; public final CefX509Certificate certificate; } diff --git a/java/tests/junittests/SelfSignedSSLTest.java b/java/tests/junittests/SelfSignedSSLTest.java index ebf6ce03..2793fadc 100644 --- a/java/tests/junittests/SelfSignedSSLTest.java +++ b/java/tests/junittests/SelfSignedSSLTest.java @@ -26,6 +26,53 @@ @ExtendWith(TestSetupExtension.class) class SelfSignedSSLTest { + private static class SSLAcceptTestFrame extends TestFrame { + 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 { + boolean isOnCertificateErrorCalled = false; + 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); + } + + @Override + public boolean onCertificateError(CefBrowser browser, ErrorCode cert_error, String request_url, CefSSLInfo sslInfo, CefCallback callback) { + isOnCertificateErrorCalled = true; + return false; + } + } /* Base64 encoded Java Keystore Data. Generate a self-signed certificate: @@ -106,33 +153,12 @@ void certificateAccepted() { 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; - } - }; + SSLAcceptTestFrame myFrame = new SSLAcceptTestFrame("https:/" + server.getAddress()); - frame.awaitCompletion(); + myFrame.awaitCompletion(); - Assertions.assertArrayEquals(certificateChainExpected, frame.sslInfo.certificate.getCertificatesChain()); - Assertions.assertTrue(CefCertStatus.CERT_STATUS_AUTHORITY_INVALID.hasStatus(frame.sslInfo.statusBiset)); + Assertions.assertArrayEquals(certificateChainExpected, myFrame.sslInfo.certificate.getCertificatesChain()); + Assertions.assertTrue(CefCertStatus.CERT_STATUS_AUTHORITY_INVALID.hasStatus(myFrame.sslInfo.statusBitset)); server.stop(0); } @@ -142,34 +168,7 @@ void certificateRejected() { 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; - } - }; + SSLRejectTestFrame frame = new SSLRejectTestFrame("https:/" + server.getAddress()); frame.awaitCompletion(); Assertions.assertTrue(frame.isOnCertificateErrorCalled); diff --git a/java/tests/junittests/TestFrame.java b/java/tests/junittests/TestFrame.java index e5247731..ebc01b7d 100644 --- a/java/tests/junittests/TestFrame.java +++ b/java/tests/junittests/TestFrame.java @@ -56,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_); @@ -116,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() { diff --git a/java/tests/junittests/TestSetupExtension.java b/java/tests/junittests/TestSetupExtension.java index 66887719..50d8ce60 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,41 @@ 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 { + final File nativesDir = NativesInstaller.ensureInstalled(); + + // Register a custom loader that loads native libraries from the + // installed directory using absolute paths, bypassing java.library.path. + SystemBootstrap.setLoader(new SystemBootstrap.Loader() { + @Override + public void loadLibrary(String libname) { + if ("jawt".equals(libname)) { + // jawt is part of the JDK, load normally + System.loadLibrary(libname); + } else { + String fileName = NativesInstaller.mapLibraryName(libname); + File libFile = new File(nativesDir, fileName); + if (libFile.exists()) { + System.load(libFile.getAbsolutePath()); + } else { + // Fallback: try subdirectories (binary_distrib layout) + File found = findLibrary(nativesDir, fileName); + if (found != null) { + System.load(found.getAbsolutePath()); + } else { + // Last resort: system default + 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 +111,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 64f7d4df..22715b6d 100644 --- a/java/tests/simple/MainFrame.java +++ b/java/tests/simple/MainFrame.java @@ -8,7 +8,6 @@ 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; @@ -36,11 +35,9 @@ 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; import javax.swing.JTextField; import javax.swing.SwingUtilities; @@ -288,7 +285,7 @@ private static String MakeErrorPage(String request_url, CefLoadHandler.ErrorCode 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)) + .filter(status -> status.hasStatus(info.statusBitset)) .map(Enum::toString) .collect(Collectors.joining(", "))); page.append("

Certificated chain(from subject to issuers):

"); From 4f1f661c09446e8e5bb184d61c947a1f424f4987 Mon Sep 17 00:00:00 2001 From: Tim Lappe Date: Fri, 3 Apr 2026 19:29:20 +0200 Subject: [PATCH 3/7] Fix DisplayHandlerTest double callback assertion failure onTitleChange and onAddressChange can be called multiple times (e.g. during browser close). Use an early return guard instead of assertFalse to prevent assertion failures on subsequent calls. Also fix assertEquals argument order in onAddressChange. Co-Authored-By: Claude Opus 4.6 (1M context) --- java/tests/junittests/DisplayHandlerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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(); } }); From da6b86ec52b3629f4211f74abfd1706f39130f9a Mon Sep 17 00:00:00 2001 From: Tim Lappe Date: Fri, 3 Apr 2026 19:30:24 +0200 Subject: [PATCH 4/7] Fix DragDataTest to use getFilePaths instead of getFileNames getFileNames returns display names ("File 1"), not paths. The test was verifying paths, so it should use getFilePaths which was added in a prior commit (CefDragData.getFilePaths). Co-Authored-By: Claude Opus 4.6 (1M context) --- java/tests/junittests/DragDataTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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()); From 50ce587ab7d897c803cac0426b71e426d17a5e83 Mon Sep 17 00:00:00 2001 From: Tim Lappe Date: Fri, 3 Apr 2026 19:31:03 +0200 Subject: [PATCH 5/7] Fix SelfSignedSSLTest for CEF 143 (Chromium 143) compatibility Chromium 143 treats localhost/loopback and private IPs as "potentially trustworthy origins" and silently accepts self-signed certificates without calling onCertificateError. Additionally, valid self-signed certs (not expired) are accepted on mapped domains. Changes: - Use --host-resolver-rules to map a fictitious domain (jcef-test.invalid) to 127.0.0.1, forcing Chromium to treat the connection as external - Generate an expired self-signed certificate programmatically to ensure Chromium triggers onCertificateError - Add terminateTest() call in onLoadError for certificateRejected - Make sslInfo and boolean flags volatile for cross-thread visibility - Bind HTTPS server explicitly to 127.0.0.1 with TLSv1.2 Co-Authored-By: Claude Opus 4.6 (1M context) --- java/tests/junittests/SelfSignedSSLTest.java | 91 +++++++++++++++++--- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/java/tests/junittests/SelfSignedSSLTest.java b/java/tests/junittests/SelfSignedSSLTest.java index 2793fadc..a35e51bc 100644 --- a/java/tests/junittests/SelfSignedSSLTest.java +++ b/java/tests/junittests/SelfSignedSSLTest.java @@ -18,16 +18,21 @@ import javax.net.ssl.*; import java.io.ByteArrayInputStream; 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.security.cert.X509Certificate; import java.util.Base64; +import java.util.Date; @ExtendWith(TestSetupExtension.class) class SelfSignedSSLTest { private static class SSLAcceptTestFrame extends TestFrame { - CefSSLInfo sslInfo; + volatile CefSSLInfo sslInfo; SSLAcceptTestFrame(String url) { super(url); @@ -48,8 +53,8 @@ public boolean onCertificateError(CefBrowser browser, ErrorCode cert_error, Stri } private static class SSLRejectTestFrame extends TestFrame { - boolean isOnCertificateErrorCalled = false; - boolean isOnLoadErrorCalled = false; + volatile boolean isOnCertificateErrorCalled = false; + volatile boolean isOnLoadErrorCalled = false; SSLRejectTestFrame(String url) { super(url); @@ -65,6 +70,7 @@ public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { public void onLoadError(CefBrowser browser, CefFrame frame, ErrorCode errorCode, String errorText, String failedUrl) { isOnLoadErrorCalled = true; super.onLoadError(browser, frame, errorCode, errorText, failedUrl); + terminateTest(); } @Override @@ -93,11 +99,65 @@ static KeyStore makeKeyStore() { return ks; } + // Fictitious domain used for SSL tests. Chromium treats localhost and + // private IPs as "potentially trustworthy" and skips certificate error + // callbacks. By using a fake external domain mapped to 127.0.0.1 via + // --host-resolver-rules (set in TestSetupExtension), we force Chromium + // to validate the certificate and trigger onCertificateError. + static final String SSL_TEST_HOST = "jcef-test.invalid"; + + /** + * 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 { - // initialise the HTTPS server - HttpsServer server = HttpsServer.create(new InetSocketAddress(0), 0); - SSLContext sslContext = SSLContext.getInstance("TLS"); + 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()); @@ -116,11 +176,10 @@ public void configure(HttpsParameters params) { SSLEngine engine = context.createSSLEngine(); params.setNeedClientAuth(false); params.setCipherSuites(engine.getEnabledCipherSuites()); - params.setProtocols(engine.getEnabledProtocols()); + params.setProtocols(new String[]{"TLSv1.2"}); SSLParameters sslParameters = context.getSupportedSSLParameters(); params.setSSLParameters(sslParameters); } catch (Exception ex) { - System.out.println("Failed to create HTTPS port"); throw new RuntimeException(ex); } } @@ -142,10 +201,10 @@ public void configure(HttpsParameters params) { @Test void certificateAccepted() { - KeyStore keyStore = makeKeyStore(); + KeyStore keyStore = makeExpiredKeyStore(); Certificate[] certificateChainExpected = null; try { - certificateChainExpected = keyStore.getCertificateChain("selfsigned"); + certificateChainExpected = keyStore.getCertificateChain("expired"); } catch (KeyStoreException e) { Assertions.fail("Failed to get certificate chain from the key store"); } @@ -153,22 +212,26 @@ void certificateAccepted() { HttpsServer server = makeHttpsServer(keyStore); server.start(); - SSLAcceptTestFrame myFrame = new SSLAcceptTestFrame("https:/" + server.getAddress()); + SSLAcceptTestFrame myFrame = new SSLAcceptTestFrame( + "https://" + SSL_TEST_HOST + ":" + 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_AUTHORITY_INVALID.hasStatus(myFrame.sslInfo.statusBitset)); + 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 = makeKeyStore(); + KeyStore keyStore = makeExpiredKeyStore(); HttpsServer server = makeHttpsServer(keyStore); server.start(); - SSLRejectTestFrame frame = new SSLRejectTestFrame("https:/" + server.getAddress()); + SSLRejectTestFrame frame = new SSLRejectTestFrame( + "https://" + SSL_TEST_HOST + ":" + server.getAddress().getPort() + "/test"); frame.awaitCompletion(); Assertions.assertTrue(frame.isOnCertificateErrorCalled); From 61a913f2dd78935919016a4e84beb17488abe594 Mon Sep 17 00:00:00 2001 From: Tim Lappe Date: Fri, 3 Apr 2026 19:31:26 +0200 Subject: [PATCH 6/7] Clean up TestSetupExtension and add CEF host-resolver-rules - Remove ProcessEnvironment reflection hack (not needed with JDK in PATH) - Keep SystemBootstrap.setLoader for CEF natives (absolute path loading) - Add --host-resolver-rules flag to map jcef-test.invalid to 127.0.0.1 for SSL certificate error testing Co-Authored-By: Claude Opus 4.6 (1M context) --- java/tests/junittests/TestSetupExtension.java | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/java/tests/junittests/TestSetupExtension.java b/java/tests/junittests/TestSetupExtension.java index 50d8ce60..ab130b72 100644 --- a/java/tests/junittests/TestSetupExtension.java +++ b/java/tests/junittests/TestSetupExtension.java @@ -57,30 +57,38 @@ private void initialize(ExtensionContext context) { // Ensure native binaries are available (download if necessary). try { - final File nativesDir = NativesInstaller.ensureInstalled(); - - // Register a custom loader that loads native libraries from the - // installed directory using absolute paths, bypassing java.library.path. + 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)) { - // jawt is part of the JDK, load normally System.loadLibrary(libname); } else { String fileName = NativesInstaller.mapLibraryName(libname); - File libFile = new File(nativesDir, fileName); + File libFile = new File(finalLibDir, fileName); if (libFile.exists()) { System.load(libFile.getAbsolutePath()); } else { - // Fallback: try subdirectories (binary_distrib layout) - File found = findLibrary(nativesDir, fileName); - if (found != null) { - System.load(found.getAbsolutePath()); - } else { - // Last resort: system default - System.loadLibrary(libname); - } + System.loadLibrary(libname); } } } @@ -96,7 +104,15 @@ public void loadLibrary(String libname) { return; } - CefApp.addAppHandler(new CefAppHandlerAdapter(null) { + // Map the fictitious SSL test domain to 127.0.0.1 so that Chromium + // treats it as an external host and fires onCertificateError for + // self-signed certificates. Without this, Chromium skips cert checks + // for localhost and private IPs ("potentially trustworthy origins"). + String[] cefArgs = { + "--host-resolver-rules=MAP jcef-test.invalid 127.0.0.1" + }; + + CefApp.addAppHandler(new CefAppHandlerAdapter(cefArgs) { @Override public void stateHasChanged(org.cef.CefApp.CefAppState state) { if (state == CefAppState.TERMINATED) { From 3a8c05a089b05675fedf81a2dd1bd60db3045a36 Mon Sep 17 00:00:00 2001 From: Tim Lappe Date: Fri, 3 Apr 2026 19:36:39 +0200 Subject: [PATCH 7/7] Simplify SSL tests: remove host-resolver-rules, use localhost The root cause of onCertificateError not firing was the old JetBrains-generated JKS certificate (valid until 2042) being silently accepted by Chromium 143. The programmatically generated expired certificate is sufficient to trigger the callback, even on localhost. No host-resolver-rules hack needed. Also removes the unused JKS_BASE64 constant and makeKeyStore method. Co-Authored-By: Claude Opus 4.6 (1M context) --- java/tests/junittests/SelfSignedSSLTest.java | 32 ++----------------- java/tests/junittests/TestSetupExtension.java | 10 +----- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/java/tests/junittests/SelfSignedSSLTest.java b/java/tests/junittests/SelfSignedSSLTest.java index a35e51bc..654f0e31 100644 --- a/java/tests/junittests/SelfSignedSSLTest.java +++ b/java/tests/junittests/SelfSignedSSLTest.java @@ -16,7 +16,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import javax.net.ssl.*; -import java.io.ByteArrayInputStream; import java.io.OutputStream; import java.math.BigInteger; import java.net.InetSocketAddress; @@ -25,8 +24,6 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.util.Base64; import java.util.Date; @ExtendWith(TestSetupExtension.class) @@ -79,33 +76,8 @@ public boolean onCertificateError(CefBrowser browser, ErrorCode cert_error, Stri return false; } } - /* - 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; - } - - // Fictitious domain used for SSL tests. Chromium treats localhost and - // private IPs as "potentially trustworthy" and skips certificate error - // callbacks. By using a fake external domain mapped to 127.0.0.1 via - // --host-resolver-rules (set in TestSetupExtension), we force Chromium - // to validate the certificate and trigger onCertificateError. - static final String SSL_TEST_HOST = "jcef-test.invalid"; - /** * Creates a KeyStore with an expired self-signed certificate. * The certificate expired in the past, which forces Chromium to @@ -213,7 +185,7 @@ void certificateAccepted() { server.start(); SSLAcceptTestFrame myFrame = new SSLAcceptTestFrame( - "https://" + SSL_TEST_HOST + ":" + server.getAddress().getPort() + "/test"); + "https://localhost:" + server.getAddress().getPort() + "/test"); myFrame.awaitCompletion(); @@ -231,7 +203,7 @@ void certificateRejected() { server.start(); SSLRejectTestFrame frame = new SSLRejectTestFrame( - "https://" + SSL_TEST_HOST + ":" + server.getAddress().getPort() + "/test"); + "https://localhost:" + server.getAddress().getPort() + "/test"); frame.awaitCompletion(); Assertions.assertTrue(frame.isOnCertificateErrorCalled); diff --git a/java/tests/junittests/TestSetupExtension.java b/java/tests/junittests/TestSetupExtension.java index ab130b72..bb1c3458 100644 --- a/java/tests/junittests/TestSetupExtension.java +++ b/java/tests/junittests/TestSetupExtension.java @@ -104,15 +104,7 @@ public void loadLibrary(String libname) { return; } - // Map the fictitious SSL test domain to 127.0.0.1 so that Chromium - // treats it as an external host and fires onCertificateError for - // self-signed certificates. Without this, Chromium skips cert checks - // for localhost and private IPs ("potentially trustworthy origins"). - String[] cefArgs = { - "--host-resolver-rules=MAP jcef-test.invalid 127.0.0.1" - }; - - CefApp.addAppHandler(new CefAppHandlerAdapter(cefArgs) { + CefApp.addAppHandler(new CefAppHandlerAdapter(null) { @Override public void stateHasChanged(org.cef.CefApp.CefAppState state) { if (state == CefAppState.TERMINATED) {