package org.jboss.resteasy.client.jaxrs; import org.apache.http.HttpHost; import org.apache.http.client.params.HttpClientParams; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.params.ConnRoutePNames; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.AllowAllHostnameVerifier; import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.apache.http.conn.ssl.X509HostnameVerifier; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.BasicClientConnectionManager; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine; import org.jboss.resteasy.client.jaxrs.engines.PassthroughTrustManager; import org.jboss.resteasy.client.jaxrs.engines.factory.ApacheHttpClient4EngineFactory; import org.jboss.resteasy.client.jaxrs.i18n.Messages; import org.jboss.resteasy.client.jaxrs.internal.ClientConfiguration; import org.jboss.resteasy.client.jaxrs.internal.LocalResteasyProviderFactory; import org.jboss.resteasy.plugins.providers.RegisterBuiltin; import org.jboss.resteasy.spi.ResteasyProviderFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.core.Configuration; import java.io.IOException; import java.security.KeyStore; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * Abstraction for creating Clients. Allows SSL configuration. Uses Apache Http Client under * the covers. If used with other ClientHttpEngines though, all configuration options are ignored. * * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 1 $ */ public class ResteasyClientBuilder extends ClientBuilder { public static enum HostnameVerificationPolicy { /** * Hostname verification is not done on the server's certificate */ ANY, /** * Allows wildcards in subdomain names i.e. *.foo.com */ WILDCARD, /** * CN must match hostname connecting to */ STRICT } private static final boolean useOldHTTPClient = Boolean.getBoolean("org.jboss.resteasy.client.useOldHTTPClient"); private static final boolean newHTTPClientAvailable; static { boolean res = true; try { Class.forName(HttpClientBuilder43.class.getName()); } catch (Throwable t) { res = false; } newHTTPClientAvailable = res; } protected KeyStore truststore; protected KeyStore clientKeyStore; protected String clientPrivateKeyPassword; protected boolean disableTrustManager; protected HostnameVerificationPolicy policy = HostnameVerificationPolicy.WILDCARD; protected ResteasyProviderFactory providerFactory; protected ExecutorService asyncExecutor; protected boolean cleanupExecutor; protected SSLContext sslContext; protected Map<String, Object> properties = new HashMap<String, Object>(); protected ClientHttpEngine httpEngine; protected int connectionPoolSize; protected int maxPooledPerRoute = 0; protected long connectionTTL = -1; protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS; protected long socketTimeout = -1; protected TimeUnit socketTimeoutUnits = TimeUnit.MILLISECONDS; protected long establishConnectionTimeout = -1; protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS; protected int connectionCheckoutTimeoutMs = -1; protected HostnameVerifier verifier = null; protected HttpHost defaultProxy; protected int responseBufferSize; protected List<String> sniHostNames = new ArrayList<>(); /** * Changing the providerFactory will wipe clean any registered components or properties. * * @param providerFactory * @return */ public ResteasyClientBuilder providerFactory(ResteasyProviderFactory providerFactory) { this.providerFactory = providerFactory; return this; } /** * Executor to use to run AsyncInvoker invocations * * @param asyncExecutor * @return */ public ResteasyClientBuilder asyncExecutor(ExecutorService asyncExecutor) { return asyncExecutor(asyncExecutor, false); } /** * Executor to use to run AsyncInvoker invocations * * @param asyncExecutor * @param cleanupExecutor true if the Client should close the executor when it is closed * @return */ public ResteasyClientBuilder asyncExecutor(ExecutorService asyncExecutor, boolean cleanupExecutor) { this.asyncExecutor = asyncExecutor; this.cleanupExecutor = cleanupExecutor; return this; } /** * If there is a connection pool, set the time to live in the pool. * * @param ttl * @param unit * @return */ public ResteasyClientBuilder connectionTTL(long ttl, TimeUnit unit) { this.connectionTTL = ttl; this.connectionTTLUnit = unit; return this; } /** * The timeout for waiting for data. A timeout value of zero is interpreted as an infinite timeout * * @param timeout * @param unit * @return */ public ResteasyClientBuilder socketTimeout(long timeout, TimeUnit unit) { this.socketTimeout = timeout; this.socketTimeoutUnits = unit; return this; } /** * When trying to make an initial socket connection, what is the timeout? * * @param timeout * @param unit * @return */ public ResteasyClientBuilder establishConnectionTimeout(long timeout, TimeUnit unit) { this.establishConnectionTimeout = timeout; this.establishConnectionTimeoutUnits = unit; return this; } /** * If connection pooling enabled, how many connections to pool per url? * * @param maxPooledPerRoute * @return */ public ResteasyClientBuilder maxPooledPerRoute(int maxPooledPerRoute) { this.maxPooledPerRoute = maxPooledPerRoute; return this; } /** * If connection pooling is enabled, how long will we wait to get a connection? * @param timeout the timeout * @param unit the units the timeout is in * @return this builder */ public ResteasyClientBuilder connectionCheckoutTimeout(long timeout, TimeUnit unit) { this.connectionCheckoutTimeoutMs = (int) TimeUnit.MILLISECONDS.convert(timeout, unit); return this; } /** * Number of connections allowed to pool * * @param connectionPoolSize * @return */ public ResteasyClientBuilder connectionPoolSize(int connectionPoolSize) { this.connectionPoolSize = connectionPoolSize; return this; } /** * Response stream is wrapped in a BufferedInputStream. Default is 8192. Value of 0 will not wrap it. * Value of -1 will use a SelfExpandingBufferedInputStream * * @param size * @return */ public ResteasyClientBuilder responseBufferSize(int size) { this.responseBufferSize = size; return this; } /** * Disable trust management and hostname verification. <i>NOTE</i> this is a security * hole, so only set this option if you cannot or do not want to verify the identity of the * host you are communicating with. */ public ResteasyClientBuilder disableTrustManager() { this.disableTrustManager = true; return this; } /** * SSL policy used to verify hostnames * * @param policy * @return */ public ResteasyClientBuilder hostnameVerification(HostnameVerificationPolicy policy) { this.policy = policy; return this; } /** * Negates all ssl and connection specific configuration * * @param httpEngine * @return */ public ResteasyClientBuilder httpEngine(ClientHttpEngine httpEngine) { this.httpEngine = httpEngine; return this; } @Override public ResteasyClientBuilder sslContext(SSLContext sslContext) { this.sslContext = sslContext; return this; } @Override public ResteasyClientBuilder trustStore(KeyStore truststore) { this.truststore = truststore; return this; } @Override public ResteasyClientBuilder keyStore(KeyStore keyStore, String password) { this.clientKeyStore = keyStore; this.clientPrivateKeyPassword = password; return this; } @Override public ResteasyClientBuilder keyStore(KeyStore keyStore, char[] password) { this.clientKeyStore = keyStore; this.clientPrivateKeyPassword = new String(password); return this; } @Override public ResteasyClientBuilder property(String name, Object value) { getProviderFactory().property(name, value); return this; } /** * Adds a TLS/SSL SNI Host Name for authentication. * * @param sniHostNames * @return */ public ResteasyClientBuilder sniHostNames(String... sniHostNames) { this.sniHostNames.addAll(Arrays.asList(sniHostNames)); return this; } /** * Specify a default proxy. Default port and schema will be used * * @param hostname * @return */ public ResteasyClientBuilder defaultProxy(String hostname) { return defaultProxy(hostname, -1, null); } /** * Specify a default proxy host and port. Default schema will be used * * @param hostname * @param port * @return */ public ResteasyClientBuilder defaultProxy(String hostname, int port) { return defaultProxy(hostname, port, null); } /** * Specify default proxy. * * @param hostname * @param port * @param scheme * @return */ public ResteasyClientBuilder defaultProxy(String hostname, int port, final String scheme) { this.defaultProxy = new HttpHost(hostname, port, scheme); return this; } protected ResteasyProviderFactory getProviderFactory() { if (providerFactory == null) { // create a new one providerFactory = new LocalResteasyProviderFactory(ResteasyProviderFactory.newInstance()); RegisterBuiltin.register(providerFactory); } return providerFactory; } @Deprecated public ResteasyClient buildOld() { ClientConfiguration config = new ClientConfiguration(getProviderFactory()); for (Map.Entry<String, Object> entry : properties.entrySet()) { config.property(entry.getKey(), entry.getValue()); } ExecutorService executor = asyncExecutor; if (executor == null) { cleanupExecutor = true; executor = Executors.newFixedThreadPool(10); } ClientHttpEngine engine = httpEngine; if (engine == null) { engine = initDefaultEngine(); } return new ResteasyClient(engine, executor, cleanupExecutor, config); } @Override public ResteasyClient build() { if (useOldHTTPClient || !newHTTPClientAvailable) { return buildOld(); } ClientConfiguration config = new ClientConfiguration(getProviderFactory()); for (Map.Entry<String, Object> entry : properties.entrySet()) { config.property(entry.getKey(), entry.getValue()); } ExecutorService executor = asyncExecutor; if (executor == null) { cleanupExecutor = true; executor = Executors.newFixedThreadPool(10); } ClientHttpEngine engine = httpEngine; if (engine == null) { engine = HttpClientBuilder43.initDefaultEngine43(this); } return new ResteasyClient(engine, executor, cleanupExecutor, config); } static class VerifierWrapper implements X509HostnameVerifier { protected HostnameVerifier verifier; VerifierWrapper(HostnameVerifier verifier) { this.verifier = verifier; } @Override public void verify(String host, SSLSocket ssl) throws IOException { if (!verifier.verify(host, ssl.getSession())) throw new SSLException(Messages.MESSAGES.hostnameVerificationFailure()); } @Override public void verify(String host, X509Certificate cert) throws SSLException { throw new SSLException(Messages.MESSAGES.verificationPathNotImplemented()); } @Override public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException { throw new SSLException(Messages.MESSAGES.verificationPathNotImplemented()); } @Override public boolean verify(String s, SSLSession sslSession) { return verifier.verify(s, sslSession); } } protected ClientHttpEngine initDefaultEngine() { DefaultHttpClient httpClient = null; X509HostnameVerifier verifier = null; if (this.verifier != null) verifier = new VerifierWrapper(this.verifier); else { switch (policy) { case ANY: verifier = new AllowAllHostnameVerifier(); break; case WILDCARD: verifier = new BrowserCompatHostnameVerifier(); break; case STRICT: verifier = new StrictHostnameVerifier(); break; } } try { SSLSocketFactory sslsf = null; SSLContext theContext = sslContext; if (disableTrustManager) { theContext = SSLContext.getInstance("SSL"); theContext.init(null, new TrustManager[]{new PassthroughTrustManager()}, new SecureRandom()); verifier = new AllowAllHostnameVerifier(); sslsf = new SSLSocketFactory(theContext, verifier); } else if (theContext != null) { sslsf = new SSLSocketFactory(theContext, verifier) { @Override protected void prepareSocket(SSLSocket socket) throws IOException { prepareSocketForSni(socket); } }; } else if (clientKeyStore != null || truststore != null) { sslsf = new SSLSocketFactory(SSLSocketFactory.TLS, clientKeyStore, clientPrivateKeyPassword, truststore, null, verifier) { @Override protected void prepareSocket(SSLSocket socket) throws IOException { prepareSocketForSni(socket); } }; } else { //sslsf = new SSLSocketFactory(SSLContext.getInstance(SSLSocketFactory.TLS), verifier); final SSLContext tlsContext = SSLContext.getInstance(SSLSocketFactory.TLS); tlsContext.init(null, null, null); sslsf = new SSLSocketFactory(tlsContext, verifier); } SchemeRegistry registry = new SchemeRegistry(); registry.register( new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); Scheme httpsScheme = new Scheme("https", 443, sslsf); registry.register(httpsScheme); ClientConnectionManager cm = null; if (connectionPoolSize > 0) { PoolingClientConnectionManager tcm = new PoolingClientConnectionManager(registry, connectionTTL, connectionTTLUnit); tcm.setMaxTotal(connectionPoolSize); if (maxPooledPerRoute == 0) maxPooledPerRoute = connectionPoolSize; tcm.setDefaultMaxPerRoute(maxPooledPerRoute); cm = tcm; } else { cm = new BasicClientConnectionManager(registry); } BasicHttpParams params = new BasicHttpParams(); if (socketTimeout > -1) { HttpConnectionParams.setSoTimeout(params, (int) socketTimeoutUnits.toMillis(socketTimeout)); } if (establishConnectionTimeout > -1) { HttpConnectionParams.setConnectionTimeout(params, (int)establishConnectionTimeoutUnits.toMillis(establishConnectionTimeout)); } if (connectionCheckoutTimeoutMs > -1) { HttpClientParams.setConnectionManagerTimeout(params, connectionCheckoutTimeoutMs); } params.setParameter(ConnRoutePNames.DEFAULT_PROXY, defaultProxy); httpClient = new DefaultHttpClient(cm, params); ApacheHttpClient4Engine engine = (ApacheHttpClient4Engine) ApacheHttpClient4EngineFactory.create(httpClient, true); engine.setResponseBufferSize(responseBufferSize); engine.setHostnameVerifier(verifier); // this may be null. We can't really support this with Apache Client. engine.setSslContext(theContext); return engine; } catch (Exception e) { throw new RuntimeException(e); } } protected void prepareSocketForSni(SSLSocket socket) { if(!sniHostNames.isEmpty()) { List<SNIServerName> sniNames = new ArrayList<>(sniHostNames.size()); for(String sniHostName : sniHostNames) { sniNames.add(new SNIHostName(sniHostName)); } SSLParameters sslParameters = socket.getSSLParameters(); sslParameters.setServerNames(sniNames); socket.setSSLParameters(sslParameters); } } @Override public ResteasyClientBuilder hostnameVerifier(HostnameVerifier verifier) { this.verifier = verifier; return this; } @Override public Configuration getConfiguration() { return getProviderFactory().getConfiguration(); } @Override public ResteasyClientBuilder register(Class<?> componentClass) { getProviderFactory().register(componentClass); return this; } @Override public ResteasyClientBuilder register(Class<?> componentClass, int priority) { getProviderFactory().register(componentClass, priority); return this; } @Override public ResteasyClientBuilder register(Class<?> componentClass, Class<?>... contracts) { getProviderFactory().register(componentClass, contracts); return this; } @Override public ResteasyClientBuilder register(Class<?> componentClass, Map<Class<?>, Integer> contracts) { getProviderFactory().register(componentClass, contracts); return this; } @Override public ResteasyClientBuilder register(Object component) { getProviderFactory().register(component); return this; } @Override public ResteasyClientBuilder register(Object component, int priority) { getProviderFactory().register(component, priority); return this; } @Override public ResteasyClientBuilder register(Object component, Class<?>... contracts) { getProviderFactory().register(component, contracts); return this; } @Override public ResteasyClientBuilder register(Object component, Map<Class<?>, Integer> contracts) { getProviderFactory().register(component, contracts); return this; } @Override public ResteasyClientBuilder withConfig(Configuration config) { providerFactory = new LocalResteasyProviderFactory(new ResteasyProviderFactory()); // return to original above rls //providerFactory = new ResteasyProviderFactory(); providerFactory.setProperties(config.getProperties()); for (Class clazz : config.getClasses()) { Map<Class<?>, Integer> contracts = config.getContracts(clazz); try { register(clazz, contracts); } catch (RuntimeException e) { throw new RuntimeException(Messages.MESSAGES.failedOnRegisteringClass(clazz.getName()), e); } } for (Object obj : config.getInstances()) { Map<Class<?>, Integer> contracts = config.getContracts(obj.getClass()); register(obj, contracts); } return this; } }