View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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 Nicolas Belisle
53   * @author Ahmed Ashour
54   * @author Martin Huber
55   * @author Marc Guillemot
56   * @author Ronald Brill
57   * @author Vadzim Miliantsei
58   */
59  public final class HtmlUnitSSLConnectionSocketFactory extends SSLConnectionSocketFactory {
60      private static final String SSL3ONLY = "htmlunit.SSL3Only";
61  
62      private final boolean useInsecureSSL_;
63  
64      /**
65       * Enables/Disables the exclusive usage of SSL3.
66       * @param httpContext the http context
67       * @param ssl3Only true or false
68       */
69      public static void setUseSSL3Only(final HttpContext httpContext, final boolean ssl3Only) {
70          httpContext.setAttribute(SSL3ONLY, ssl3Only);
71      }
72  
73      static boolean isUseSSL3Only(final HttpContext context) {
74          return "TRUE".equalsIgnoreCase((String) context.getAttribute(SSL3ONLY));
75      }
76  
77      /**
78       * Factory method that builds a new SSLConnectionSocketFactory.
79       * @param options the current WebClientOptions
80       * @return the SSLConnectionSocketFactory
81       */
82      public static SSLConnectionSocketFactory buildSSLSocketFactory(final WebClientOptions options) {
83          try {
84              final String[] sslClientProtocols = options.getSSLClientProtocols();
85              final String[] sslClientCipherSuites = options.getSSLClientCipherSuites();
86  
87              SSLContext sslContext = options.getSSLContext();
88              final boolean useInsecureSSL = options.isUseInsecureSSL();
89  
90              if (useInsecureSSL) {
91                  // we need insecure SSL + SOCKS awareness
92                  String protocol = options.getSSLInsecureProtocol();
93                  if (protocol == null) {
94                      protocol = "SSL";
95                  }
96                  if (sslContext == null) {
97                      sslContext = SSLContext.getInstance(protocol);
98                      sslContext.init(getKeyManagers(options),
99                              new X509ExtendedTrustManager[] {new InsecureTrustManager()}, null);
100                 }
101 
102                 return new HtmlUnitSSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE,
103                                 true, sslClientProtocols, sslClientCipherSuites);
104             }
105 
106             final KeyStore keyStore = options.getSSLClientCertificateStore();
107             final char[] keyStorePassword = keyStore == null ? null : options.getSSLClientCertificatePassword();
108             final KeyStore trustStore = options.getSSLTrustStore();
109 
110             if (sslContext == null) {
111                 sslContext = SSLContexts.custom()
112                         .loadKeyMaterial(keyStore, keyStorePassword).loadTrustMaterial(trustStore, null).build();
113             }
114             return new HtmlUnitSSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier(),
115                             false, sslClientProtocols, sslClientCipherSuites);
116         }
117         catch (final GeneralSecurityException e) {
118             throw new RuntimeException(e);
119         }
120     }
121 
122     private HtmlUnitSSLConnectionSocketFactory(final SSLContext sslContext,
123             final HostnameVerifier hostnameVerifier, final boolean useInsecureSSL,
124             final String[] supportedProtocols, final String[] supportedCipherSuites) {
125         super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
126         useInsecureSSL_ = useInsecureSSL;
127     }
128 
129     private static void configureSocket(final SSLSocket sslSocket, final HttpContext context) {
130         if (isUseSSL3Only(context)) {
131             sslSocket.setEnabledProtocols(new String[]{"SSLv3"});
132         }
133     }
134 
135     /**
136      * Connect via socket.
137      * @param connectTimeout the timeout
138      * @param socket the socket
139      * @param host the host
140      * @param remoteAddress the remote address
141      * @param localAddress the local address
142      * @param context the context
143      * @return the created/connected socket
144      * @throws IOException in case of problems
145      */
146     @Override
147     public Socket connectSocket(
148             final int connectTimeout,
149             final Socket socket,
150             final HttpHost host,
151             final InetSocketAddress remoteAddress,
152             final InetSocketAddress localAddress,
153             final HttpContext context) throws IOException {
154         final HttpHost socksProxy = SocksConnectionSocketFactory.getSocksProxy(context);
155         if (socksProxy != null) {
156             final Socket underlying = SocksConnectionSocketFactory.createSocketWithSocksProxy(socksProxy);
157             underlying.setReuseAddress(true);
158 
159             try {
160                 //underlying.setSoTimeout(soTimeout);
161                 underlying.connect(remoteAddress, connectTimeout);
162             }
163             catch (final SocketTimeoutException ex) {
164                 final ConnectTimeoutException cex =
165                         new ConnectTimeoutException("Connect to " + socksProxy.toURI() + " timed out");
166                 cex.initCause(ex);
167                 throw cex;
168             }
169 
170             final Socket sslSocket = getSSLSocketFactory().createSocket(underlying, remoteAddress.getHostName(),
171                     remoteAddress.getPort(), true);
172             configureSocket((SSLSocket) sslSocket, context);
173             return sslSocket;
174         }
175         try {
176             return super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context);
177         }
178         catch (final IOException e) {
179             if (useInsecureSSL_ && "handshake alert:  unrecognized_name".equals(e.getMessage())) {
180                 setEmptyHostname(host);
181 
182                 return super.connectSocket(connectTimeout,
183                         createSocket(context),
184                         host, remoteAddress, localAddress, context);
185             }
186             throw e;
187         }
188     }
189 
190     private static void setEmptyHostname(final HttpHost host) {
191         try {
192             final Field field = HttpHost.class.getDeclaredField("hostname");
193             field.setAccessible(true);
194             field.set(host, "");
195         }
196         catch (final Exception e) {
197             throw new RuntimeException(e);
198         }
199     }
200 
201     private javax.net.ssl.SSLSocketFactory getSSLSocketFactory() {
202         try {
203             final Field field = SSLConnectionSocketFactory.class.getDeclaredField("socketfactory");
204             field.setAccessible(true);
205             return (javax.net.ssl.SSLSocketFactory) field.get(this);
206         }
207         catch (final Exception e) {
208             throw new RuntimeException(e);
209         }
210     }
211 
212     private static KeyManager[] getKeyManagers(final WebClientOptions options) {
213         if (options.getSSLClientCertificateStore() == null) {
214             return null;
215         }
216 
217         try {
218             final KeyStore keyStore = options.getSSLClientCertificateStore();
219             final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
220                     KeyManagerFactory.getDefaultAlgorithm());
221             keyManagerFactory.init(keyStore, options.getSSLClientCertificatePassword());
222             return keyManagerFactory.getKeyManagers();
223         }
224         catch (final Exception e) {
225             throw new RuntimeException(e);
226         }
227     }
228 }
229 
230 /**
231  * A completely insecure (yet very easy to use) x509 trust manager. This manager trusts all servers
232  * and all clients, regardless of credentials or lack thereof.
233  *
234  * @author Daniel Gredler
235  * @author Marc Guillemot
236  * @author Ronald Brill
237  */
238 class InsecureTrustManager extends X509ExtendedTrustManager {
239     private final Set<X509Certificate> acceptedIssuers_ = new HashSet<>();
240 
241     /**
242      * {@inheritDoc}
243      */
244     @Override
245     public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
246         // Everyone is trusted!
247         acceptedIssuers_.addAll(Arrays.asList(chain));
248     }
249 
250     /**
251      * {@inheritDoc}
252      */
253     @Override
254     public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
255         // Everyone is trusted!
256         acceptedIssuers_.addAll(Arrays.asList(chain));
257     }
258 
259     @Override
260     public void checkClientTrusted(final X509Certificate[] chain,
261                     final String authType, final Socket socket) throws CertificateException {
262         // Everyone is trusted!
263         acceptedIssuers_.addAll(Arrays.asList(chain));
264     }
265 
266     @Override
267     public void checkClientTrusted(final X509Certificate[] chain,
268                     final String authType, final SSLEngine sslEngine) throws CertificateException {
269         // Everyone is trusted!
270         acceptedIssuers_.addAll(Arrays.asList(chain));
271     }
272 
273     @Override
274     public void checkServerTrusted(final X509Certificate[] chain,
275                     final String authType, final Socket socket) throws CertificateException {
276         // Everyone is trusted!
277         acceptedIssuers_.addAll(Arrays.asList(chain));
278     }
279 
280     @Override
281     public void checkServerTrusted(final X509Certificate[] chain,
282                     final String authType, final SSLEngine sslEngine) throws CertificateException {
283         // Everyone is trusted!
284         acceptedIssuers_.addAll(Arrays.asList(chain));
285     }
286 
287     /**
288      * {@inheritDoc}
289      */
290     @Override
291     public X509Certificate[] getAcceptedIssuers() {
292         // 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):
293         // requesting a URL with a valid certificate (working without WebClient.setUseInsecureSSL(true)) throws a
294         //  javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
295         // when the array returned here is empty
296         if (acceptedIssuers_.isEmpty()) {
297             return new X509Certificate[0];
298         }
299         return acceptedIssuers_.toArray(new X509Certificate[0]);
300     }
301 }