diff --git a/src/main/java/org/htmlunit/WebClientOptions.java b/src/main/java/org/htmlunit/WebClientOptions.java index 772907227c..030d13981f 100644 --- a/src/main/java/org/htmlunit/WebClientOptions.java +++ b/src/main/java/org/htmlunit/WebClientOptions.java @@ -25,6 +25,8 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import javax.net.ssl.SSLContext; + import org.apache.commons.io.FileUtils; /** @@ -57,15 +59,16 @@ public class WebClientOptions implements Serializable { private String[] sslClientProtocols_; private String[] sslClientCipherSuites_; + private transient SSLContext sslContext_; + private boolean useInsecureSSL_; // default is secure SSL + private String sslInsecureProtocol_; + private boolean doNotTrackEnabled_; private String homePage_ = "https://www.htmlunit.org/"; private ProxyConfig proxyConfig_; private int timeout_ = 90_000; // like Firefox 16 default's value for network.http.connection-timeout private long connectionTimeToLive_ = -1; // HttpClient default - private boolean useInsecureSSL_; // default is secure SSL - private String sslInsecureProtocol_; - private boolean fileProtocolForXMLHttpRequestsAllowed_; private int maxInMemory_ = 500 * 1024; @@ -87,6 +90,26 @@ public class WebClientOptions implements Serializable { private boolean isFetchPolyfillEnabled_; + /** + * Sets the SSLContext; if this is set it is used and some other settings are ignored + * (protocol, keyStore, keyStorePassword, trustStore, sslClientCertificateStore, sslClientCertificatePassword). + *

This property is transient (because SSLContext is not serializable) + * @param sslContext the SSLContext, {@code null} to use for default value + */ + public void setSSLContext(final SSLContext sslContext) { + sslContext_ = sslContext; + } + + /** + * Gets the SSLContext; if this is set this is used and some other settings are ignored + * (protocol, keyStore, keyStorePassword, trustStore, sslClientCertificateStore, sslClientCertificatePassword). + *

This property is transient (because SSLContext is not serializable) + * @return the SSLContext + */ + public SSLContext getSSLContext() { + return sslContext_; + } + /** * If set to {@code true}, the client will accept connections to any host, regardless of * whether they have valid certificates or not. This is especially useful when you are trying to diff --git a/src/main/java/org/htmlunit/httpclient/HtmlUnitSSLConnectionSocketFactory.java b/src/main/java/org/htmlunit/httpclient/HtmlUnitSSLConnectionSocketFactory.java index 6057914cff..e4dd2e705a 100644 --- a/src/main/java/org/htmlunit/httpclient/HtmlUnitSSLConnectionSocketFactory.java +++ b/src/main/java/org/htmlunit/httpclient/HtmlUnitSSLConnectionSocketFactory.java @@ -84,29 +84,35 @@ public static SSLConnectionSocketFactory buildSSLSocketFactory(final WebClientOp final String[] sslClientProtocols = options.getSSLClientProtocols(); final String[] sslClientCipherSuites = options.getSSLClientCipherSuites(); + SSLContext sslContext = options.getSSLContext(); final boolean useInsecureSSL = options.isUseInsecureSSL(); - if (!useInsecureSSL) { - final KeyStore keyStore = options.getSSLClientCertificateStore(); - final char[] keyStorePassword = keyStore == null ? null : options.getSSLClientCertificatePassword(); - final KeyStore trustStore = options.getSSLTrustStore(); + if (useInsecureSSL) { + // we need insecure SSL + SOCKS awareness + String protocol = options.getSSLInsecureProtocol(); + if (protocol == null) { + protocol = "SSL"; + } + if (sslContext == null) { + sslContext = SSLContext.getInstance(protocol); + } + sslContext.init(getKeyManagers(options), + new X509ExtendedTrustManager[] {new InsecureTrustManager()}, null); - final SSLContext sslContext = SSLContexts.custom() - .loadKeyMaterial(keyStore, keyStorePassword).loadTrustMaterial(trustStore, null).build(); - return new HtmlUnitSSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier(), - useInsecureSSL, sslClientProtocols, sslClientCipherSuites); + return new HtmlUnitSSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE, + true, sslClientProtocols, sslClientCipherSuites); } - // we need insecure SSL + SOCKS awareness - String protocol = options.getSSLInsecureProtocol(); - if (protocol == null) { - protocol = "SSL"; - } - final SSLContext sslContext = SSLContext.getInstance(protocol); - sslContext.init(getKeyManagers(options), new X509ExtendedTrustManager[] {new InsecureTrustManager()}, null); + final KeyStore keyStore = options.getSSLClientCertificateStore(); + final char[] keyStorePassword = keyStore == null ? null : options.getSSLClientCertificatePassword(); + final KeyStore trustStore = options.getSSLTrustStore(); - return new HtmlUnitSSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE, - useInsecureSSL, sslClientProtocols, sslClientCipherSuites); + if (sslContext == null) { + sslContext = SSLContexts.custom() + .loadKeyMaterial(keyStore, keyStorePassword).loadTrustMaterial(trustStore, null).build(); + } + return new HtmlUnitSSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier(), + false, sslClientProtocols, sslClientCipherSuites); } catch (final GeneralSecurityException e) { throw new RuntimeException(e); @@ -207,6 +213,7 @@ private static KeyManager[] getKeyManagers(final WebClientOptions options) { if (options.getSSLClientCertificateStore() == null) { return null; } + try { final KeyStore keyStore = options.getSSLClientCertificateStore(); final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( diff --git a/src/test/java/org/htmlunit/WebClientOptionsTest.java b/src/test/java/org/htmlunit/WebClientOptionsTest.java new file mode 100644 index 0000000000..ce12272e63 --- /dev/null +++ b/src/test/java/org/htmlunit/WebClientOptionsTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2002-2024 Gargoyle Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.htmlunit; + +import javax.net.ssl.SSLContext; + +import org.apache.commons.lang3.SerializationUtils; +import org.htmlunit.junit.BrowserRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link WebClientOptions}. + * + * @author Ronald Brill + */ +@RunWith(BrowserRunner.class) +public class WebClientOptionsTest extends SimpleWebTestCase { + + /** + * @throws Exception if an error occurs + */ + @Test + public void serialization() throws Exception { + final WebClientOptions original = new WebClientOptions(); + + final byte[] bytes = SerializationUtils.serialize(original); + final WebClientOptions deserialized = (WebClientOptions) SerializationUtils.deserialize(bytes); + + assertEquals(original.isJavaScriptEnabled(), deserialized.isJavaScriptEnabled()); + assertEquals(original.isCssEnabled(), deserialized.isCssEnabled()); + + assertEquals(original.isPrintContentOnFailingStatusCode(), deserialized.isPrintContentOnFailingStatusCode()); + assertEquals(original.isThrowExceptionOnFailingStatusCode(), + deserialized.isThrowExceptionOnFailingStatusCode()); + assertEquals(original.isThrowExceptionOnScriptError(), deserialized.isThrowExceptionOnScriptError()); + assertEquals(original.isPopupBlockerEnabled(), deserialized.isPopupBlockerEnabled()); + assertEquals(original.isRedirectEnabled(), deserialized.isRedirectEnabled()); + } + + /** + * @throws Exception if an error occurs + */ + @Test + public void serializationChanged() throws Exception { + final WebClientOptions original = new WebClientOptions(); + original.setJavaScriptEnabled(false); + original.setCssEnabled(false); + + original.setPrintContentOnFailingStatusCode(false); + original.setThrowExceptionOnFailingStatusCode(false); + original.setThrowExceptionOnScriptError(false); + original.setPopupBlockerEnabled(true); + original.setRedirectEnabled(false); + + final byte[] bytes = SerializationUtils.serialize(original); + final WebClientOptions deserialized = (WebClientOptions) SerializationUtils.deserialize(bytes); + + assertEquals(original.isJavaScriptEnabled(), deserialized.isJavaScriptEnabled()); + assertEquals(original.isCssEnabled(), deserialized.isCssEnabled()); + + assertEquals(original.isPrintContentOnFailingStatusCode(), deserialized.isPrintContentOnFailingStatusCode()); + assertEquals(original.isThrowExceptionOnFailingStatusCode(), + deserialized.isThrowExceptionOnFailingStatusCode()); + assertEquals(original.isThrowExceptionOnScriptError(), deserialized.isThrowExceptionOnScriptError()); + assertEquals(original.isPopupBlockerEnabled(), deserialized.isPopupBlockerEnabled()); + assertEquals(original.isRedirectEnabled(), deserialized.isRedirectEnabled()); + } + + /** + * @throws Exception if an error occurs + */ + @Test + public void serializationSSLContextProvider() throws Exception { + final WebClientOptions original = new WebClientOptions(); + original.setSSLContext(SSLContext.getDefault()); + + final byte[] bytes = SerializationUtils.serialize(original); + final WebClientOptions deserialized = (WebClientOptions) SerializationUtils.deserialize(bytes); + + assertNull(deserialized.getSSLContext()); + } +}