View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.httpclient;
16  
17  import java.io.IOException;
18  import java.lang.reflect.Field;
19  import java.net.InetSocketAddress;
20  import java.net.Socket;
21  import java.net.SocketTimeoutException;
22  import java.security.GeneralSecurityException;
23  import java.security.KeyStore;
24  import java.security.cert.CertificateException;
25  import java.security.cert.X509Certificate;
26  import java.util.Arrays;
27  import java.util.HashSet;
28  import java.util.Set;
29  
30  import javax.net.ssl.HostnameVerifier;
31  import javax.net.ssl.KeyManager;
32  import javax.net.ssl.KeyManagerFactory;
33  import javax.net.ssl.SSLContext;
34  import javax.net.ssl.SSLEngine;
35  import javax.net.ssl.SSLSocket;
36  import javax.net.ssl.X509ExtendedTrustManager;
37  
38  import org.apache.http.HttpHost;
39  import org.apache.http.conn.ConnectTimeoutException;
40  import org.apache.http.conn.ssl.DefaultHostnameVerifier;
41  import org.apache.http.conn.ssl.NoopHostnameVerifier;
42  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
43  import org.apache.http.protocol.HttpContext;
44  import org.apache.http.ssl.SSLContexts;
45  import org.htmlunit.WebClientOptions;
46  
47  /**
48   * Socket factory offering facilities for insecure SSL and for SOCKS proxy support.
49   * This looks rather like a hack than like clean code but at the time of the writing it seems to
50   * be the easiest way to provide SOCKS proxy support for HTTPS.
51   *
52   * @author Daniel Gredler
53   * @author Nicolas Belisle
54   * @author Ahmed Ashour
55   * @author Martin Huber
56   * @author Marc Guillemot
57   * @author Ronald Brill
58   * @author Vadzim Miliantsei
59   */
60  public final class HtmlUnitSSLConnectionSocketFactory extends SSLConnectionSocketFactory {
61      private static final String SSL3ONLY = "htmlunit.SSL3Only";
62  
63      private final boolean useInsecureSSL_;
64  
65      /**
66       * Enables/Disables the exclusive usage of SSL3.
67       * @param httpContext the http context
68       * @param ssl3Only true or false
69       */
70      public static void setUseSSL3Only(final HttpContext httpContext, final boolean ssl3Only) {
71          httpContext.setAttribute(SSL3ONLY, ssl3Only);
72      }
73  
74      static boolean isUseSSL3Only(final HttpContext context) {
75          return "TRUE".equalsIgnoreCase((String) context.getAttribute(SSL3ONLY));
76      }
77  
78      /**
79       * Factory method that builds a new SSLConnectionSocketFactory.
80       * @param options the current WebClientOptions
81       * @return the SSLConnectionSocketFactory
82       */
83      public static SSLConnectionSocketFactory buildSSLSocketFactory(final WebClientOptions options) {
84          try {
85              final String[] sslClientProtocols = options.getSSLClientProtocols();
86              final String[] sslClientCipherSuites = options.getSSLClientCipherSuites();
87  
88              SSLContext sslContext = options.getSSLContext();
89              final boolean useInsecureSSL = options.isUseInsecureSSL();
90  
91              if (useInsecureSSL) {
92                  // we need insecure SSL + SOCKS awareness
93                  String protocol = options.getSSLInsecureProtocol();
94                  if (protocol == null) {
95                      protocol = "SSL";
96                  }
97                  if (sslContext == null) {
98                      sslContext = SSLContext.getInstance(protocol);
99                      sslContext.init(getKeyManagers(options),
100                             new X509ExtendedTrustManager[] {new InsecureTrustManager()}, null);
101                 }
102 
103                 return new HtmlUnitSSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE,
104                                 true, sslClientProtocols, sslClientCipherSuites);
105             }
106 
107             final KeyStore keyStore = options.getSSLClientCertificateStore();
108             final char[] keyStorePassword = keyStore == null ? null : options.getSSLClientCertificatePassword();
109             final KeyStore trustStore = options.getSSLTrustStore();
110 
111             if (sslContext == null) {
112                 sslContext = SSLContexts.custom()
113                         .loadKeyMaterial(keyStore, keyStorePassword).loadTrustMaterial(trustStore, null).build();
114             }
115             return new HtmlUnitSSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier(),
116                             false, sslClientProtocols, sslClientCipherSuites);
117         }
118         catch (final GeneralSecurityException e) {
119             throw new RuntimeException(e);
120         }
121     }
122 
123     private HtmlUnitSSLConnectionSocketFactory(final SSLContext sslContext,
124             final HostnameVerifier hostnameVerifier, final boolean useInsecureSSL,
125             final String[] supportedProtocols, final String[] supportedCipherSuites) {
126         super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
127         useInsecureSSL_ = useInsecureSSL;
128     }
129 
130     private static void configureSocket(final SSLSocket sslSocket, final HttpContext context) {
131         if (isUseSSL3Only(context)) {
132             sslSocket.setEnabledProtocols(new String[]{"SSLv3"});
133         }
134     }
135 
136     /**
137      * Connect via socket.
138      * @param connectTimeout the timeout
139      * @param socket the socket
140      * @param host the host
141      * @param remoteAddress the remote address
142      * @param localAddress the local address
143      * @param context the context
144      * @return the created/connected socket
145      * @throws IOException in case of problems
146      */
147     @Override
148     public Socket connectSocket(
149             final int connectTimeout,
150             final Socket socket,
151             final HttpHost host,
152             final InetSocketAddress remoteAddress,
153             final InetSocketAddress localAddress,
154             final HttpContext context) throws IOException {
155         final HttpHost socksProxy = SocksConnectionSocketFactory.getSocksProxy(context);
156         if (socksProxy != null) {
157             final Socket underlying = SocksConnectionSocketFactory.createSocketWithSocksProxy(socksProxy);
158             underlying.setReuseAddress(true);
159 
160             try {
161                 //underlying.setSoTimeout(soTimeout);
162                 underlying.connect(remoteAddress, connectTimeout);
163             }
164             catch (final SocketTimeoutException ex) {
165                 final ConnectTimeoutException cex =
166                         new ConnectTimeoutException("Connect to " + socksProxy.toURI() + " timed out");
167                 cex.initCause(ex);
168                 throw cex;
169             }
170 
171             final Socket sslSocket = getSSLSocketFactory().createSocket(underlying, remoteAddress.getHostName(),
172                     remoteAddress.getPort(), true);
173             configureSocket((SSLSocket) sslSocket, context);
174             return sslSocket;
175         }
176         try {
177             return super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context);
178         }
179         catch (final IOException e) {
180             if (useInsecureSSL_ && "handshake alert:  unrecognized_name".equals(e.getMessage())) {
181                 setEmptyHostname(host);
182 
183                 return super.connectSocket(connectTimeout,
184                         createSocket(context),
185                         host, remoteAddress, localAddress, context);
186             }
187             throw e;
188         }
189     }
190 
191     private static void setEmptyHostname(final HttpHost host) {
192         try {
193             final Field field = HttpHost.class.getDeclaredField("hostname");
194             field.setAccessible(true);
195             field.set(host, "");
196         }
197         catch (final Exception e) {
198             throw new RuntimeException(e);
199         }
200     }
201 
202     private javax.net.ssl.SSLSocketFactory getSSLSocketFactory() {
203         try {
204             final Field field = SSLConnectionSocketFactory.class.getDeclaredField("socketfactory");
205             field.setAccessible(true);
206             return (javax.net.ssl.SSLSocketFactory) field.get(this);
207         }
208         catch (final Exception e) {
209             throw new RuntimeException(e);
210         }
211     }
212 
213     private static KeyManager[] getKeyManagers(final WebClientOptions options) {
214         if (options.getSSLClientCertificateStore() == null) {
215             return null;
216         }
217 
218         try {
219             final KeyStore keyStore = options.getSSLClientCertificateStore();
220             final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
221                     KeyManagerFactory.getDefaultAlgorithm());
222             keyManagerFactory.init(keyStore, options.getSSLClientCertificatePassword());
223             return keyManagerFactory.getKeyManagers();
224         }
225         catch (final Exception e) {
226             throw new RuntimeException(e);
227         }
228     }
229 }
230 
231 /**
232  * A completely insecure (yet very easy to use) x509 trust manager. This manager trusts all servers
233  * and all clients, regardless of credentials or lack thereof.
234  */
235 class InsecureTrustManager extends X509ExtendedTrustManager {
236     private final Set<X509Certificate> acceptedIssuers_ = new HashSet<>();
237 
238     /**
239      * {@inheritDoc}
240      */
241     @Override
242     public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
243         // Everyone is trusted!
244         acceptedIssuers_.addAll(Arrays.asList(chain));
245     }
246 
247     /**
248      * {@inheritDoc}
249      */
250     @Override
251     public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
252         // Everyone is trusted!
253         acceptedIssuers_.addAll(Arrays.asList(chain));
254     }
255 
256     @Override
257     public void checkClientTrusted(final X509Certificate[] chain,
258                     final String authType, final Socket socket) throws CertificateException {
259         // Everyone is trusted!
260         acceptedIssuers_.addAll(Arrays.asList(chain));
261     }
262 
263     @Override
264     public void checkClientTrusted(final X509Certificate[] chain,
265                     final String authType, final SSLEngine sslEngine) throws CertificateException {
266         // Everyone is trusted!
267         acceptedIssuers_.addAll(Arrays.asList(chain));
268     }
269 
270     @Override
271     public void checkServerTrusted(final X509Certificate[] chain,
272                     final String authType, final Socket socket) throws CertificateException {
273         // Everyone is trusted!
274         acceptedIssuers_.addAll(Arrays.asList(chain));
275     }
276 
277     @Override
278     public void checkServerTrusted(final X509Certificate[] chain,
279                     final String authType, final SSLEngine sslEngine) throws CertificateException {
280         // Everyone is trusted!
281         acceptedIssuers_.addAll(Arrays.asList(chain));
282     }
283 
284     /**
285      * {@inheritDoc}
286      */
287     @Override
288     public X509Certificate[] getAcceptedIssuers() {
289         // it seems to be OK for Java <= 6 to return an empty array but not for Java 7 (at least 1.7.0_04-b20):
290         // requesting a URL with a valid certificate (working without WebClient.setUseInsecureSSL(true)) throws a
291         //  javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
292         // when the array returned here is empty
293         if (acceptedIssuers_.isEmpty()) {
294             return new X509Certificate[0];
295         }
296         return acceptedIssuers_.toArray(new X509Certificate[0]);
297     }
298 }