Skip to content

Commit

Permalink
make the sslcontext configurable (see #684)
Browse files Browse the repository at this point in the history
  • Loading branch information
rbri committed Sep 23, 2024
1 parent af50808 commit aa0116d
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 20 deletions.
29 changes: 26 additions & 3 deletions src/main/java/org/htmlunit/WebClientOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

import javax.net.ssl.SSLContext;

import org.apache.commons.io.FileUtils;

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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).
* <p>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).
* <p>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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
95 changes: 95 additions & 0 deletions src/test/java/org/htmlunit/WebClientOptionsTest.java
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit aa0116d

Please sign in to comment.