| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to you 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 |
| * |
| * http://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.apache.calcite.avatica.remote; |
| |
| import org.apache.http.HttpHost; |
| import org.apache.http.NoHttpResponseException; |
| import org.apache.http.auth.AuthSchemeProvider; |
| import org.apache.http.auth.AuthScope; |
| import org.apache.http.auth.UsernamePasswordCredentials; |
| import org.apache.http.client.ClientProtocolException; |
| import org.apache.http.client.CredentialsProvider; |
| import org.apache.http.client.config.AuthSchemes; |
| import org.apache.http.client.methods.CloseableHttpResponse; |
| import org.apache.http.client.methods.HttpPost; |
| import org.apache.http.client.protocol.HttpClientContext; |
| import org.apache.http.config.Lookup; |
| import org.apache.http.config.Registry; |
| import org.apache.http.config.RegistryBuilder; |
| import org.apache.http.conn.socket.ConnectionSocketFactory; |
| import org.apache.http.conn.socket.PlainConnectionSocketFactory; |
| import org.apache.http.conn.ssl.NoopHostnameVerifier; |
| import org.apache.http.conn.ssl.SSLConnectionSocketFactory; |
| import org.apache.http.entity.ByteArrayEntity; |
| import org.apache.http.entity.ContentType; |
| import org.apache.http.impl.auth.BasicSchemeFactory; |
| import org.apache.http.impl.auth.DigestSchemeFactory; |
| import org.apache.http.impl.client.BasicAuthCache; |
| import org.apache.http.impl.client.BasicCredentialsProvider; |
| import org.apache.http.impl.client.CloseableHttpClient; |
| import org.apache.http.impl.client.HttpClients; |
| import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; |
| import org.apache.http.ssl.SSLContextBuilder; |
| import org.apache.http.ssl.SSLContexts; |
| import org.apache.http.util.EntityUtils; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.net.HttpURLConnection; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.util.Objects; |
| import javax.net.ssl.HostnameVerifier; |
| import javax.net.ssl.SSLContext; |
| |
| /** |
| * A common class to invoke HTTP requests against the Avatica server agnostic of the data being |
| * sent and received across the wire. |
| */ |
| public class AvaticaCommonsHttpClientImpl implements AvaticaHttpClient, |
| UsernamePasswordAuthenticateable, TrustStoreConfigurable, |
| KeyStoreConfigurable, HostnameVerificationConfigurable { |
| private static final Logger LOG = LoggerFactory.getLogger(AvaticaCommonsHttpClientImpl.class); |
| |
| // Some basic exposed configurations |
| private static final String MAX_POOLED_CONNECTION_PER_ROUTE_KEY = |
| "avatica.pooled.connections.per.route"; |
| private static final String MAX_POOLED_CONNECTION_PER_ROUTE_DEFAULT = "25"; |
| private static final String MAX_POOLED_CONNECTIONS_KEY = "avatica.pooled.connections.max"; |
| private static final String MAX_POOLED_CONNECTIONS_DEFAULT = "100"; |
| |
| protected final HttpHost host; |
| protected final URI uri; |
| protected BasicAuthCache authCache; |
| protected CloseableHttpClient client; |
| protected Registry<ConnectionSocketFactory> socketFactoryRegistry; |
| protected PoolingHttpClientConnectionManager pool; |
| |
| protected UsernamePasswordCredentials credentials = null; |
| protected CredentialsProvider credentialsProvider = null; |
| protected Lookup<AuthSchemeProvider> authRegistry = null; |
| protected Object userToken; |
| |
| protected File truststore = null; |
| protected File keystore = null; |
| protected String truststorePassword = null; |
| protected String keystorePassword = null; |
| protected String keyPassword = null; |
| protected HostnameVerification hostnameVerification = null; |
| |
| public AvaticaCommonsHttpClientImpl(URL url) { |
| this.host = new HttpHost(url.getHost(), url.getPort(), url.getProtocol()); |
| this.uri = toURI(Objects.requireNonNull(url)); |
| initializeClient(); |
| } |
| |
| protected void initializeClient() { |
| socketFactoryRegistry = this.configureSocketFactories(); |
| configureConnectionPool(socketFactoryRegistry); |
| this.authCache = new BasicAuthCache(); |
| // A single thread-safe HttpClient, pooling connections via the ConnectionManager |
| this.client = HttpClients.custom().setConnectionManager(pool).build(); |
| } |
| |
| protected void configureConnectionPool(Registry<ConnectionSocketFactory> registry) { |
| pool = new PoolingHttpClientConnectionManager(registry); |
| // Increase max total connection to 100 |
| final String maxCnxns = |
| System.getProperty(MAX_POOLED_CONNECTIONS_KEY, |
| MAX_POOLED_CONNECTIONS_DEFAULT); |
| pool.setMaxTotal(Integer.parseInt(maxCnxns)); |
| // Increase default max connection per route to 25 |
| final String maxCnxnsPerRoute = System.getProperty(MAX_POOLED_CONNECTION_PER_ROUTE_KEY, |
| MAX_POOLED_CONNECTION_PER_ROUTE_DEFAULT); |
| pool.setDefaultMaxPerRoute(Integer.parseInt(maxCnxnsPerRoute)); |
| } |
| |
| protected Registry<ConnectionSocketFactory> configureSocketFactories() { |
| RegistryBuilder<ConnectionSocketFactory> registryBuilder = RegistryBuilder.create(); |
| if (host.getSchemeName().equalsIgnoreCase("https")) { |
| configureHttpsRegistry(registryBuilder); |
| } else { |
| configureHttpRegistry(registryBuilder); |
| } |
| return registryBuilder.build(); |
| } |
| |
| protected void configureHttpsRegistry(RegistryBuilder<ConnectionSocketFactory> registryBuilder) { |
| try { |
| SSLContext sslContext = getSSLContext(); |
| final HostnameVerifier verifier = getHostnameVerifier(hostnameVerification); |
| SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext, verifier); |
| registryBuilder.register("https", sslFactory); |
| } catch (Exception e) { |
| LOG.error("HTTPS registry configuration failed"); |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private SSLContext getSSLContext() throws Exception { |
| SSLContextBuilder sslContextBuilder = SSLContexts.custom(); |
| if (null != truststore && null != truststorePassword) { |
| loadTrustStore(sslContextBuilder); |
| } |
| if (null != keystore && null != keystorePassword && null != keyPassword) { |
| loadKeyStore(sslContextBuilder); |
| } |
| return sslContextBuilder.build(); |
| } |
| |
| protected void loadKeyStore(SSLContextBuilder sslContextBuilder) throws Exception { |
| sslContextBuilder.loadKeyMaterial(keystore, |
| keystorePassword.toCharArray(), keyPassword.toCharArray()); |
| } |
| |
| protected void loadTrustStore(SSLContextBuilder sslContextBuilder) throws Exception { |
| sslContextBuilder.loadTrustMaterial(truststore, truststorePassword.toCharArray()); |
| } |
| |
| protected void configureHttpRegistry(RegistryBuilder<ConnectionSocketFactory> registryBuilder) { |
| registryBuilder.register("http", PlainConnectionSocketFactory.getSocketFactory()); |
| } |
| |
| /** |
| * Creates the {@code HostnameVerifier} given the provided {@code verification}. |
| * |
| * @param verification The intended hostname verification action. |
| * @return A verifier for the request verification. |
| * @throws IllegalArgumentException if the provided verification cannot be handled. |
| */ |
| HostnameVerifier getHostnameVerifier(HostnameVerification verification) { |
| // Normally, the configuration logic would give us a default of STRICT if it was not |
| // provided by the user. It's easy for us to do a double-check. |
| if (verification == null) { |
| verification = HostnameVerification.STRICT; |
| } |
| switch (verification) { |
| case STRICT: |
| return SSLConnectionSocketFactory.getDefaultHostnameVerifier(); |
| case NONE: |
| return NoopHostnameVerifier.INSTANCE; |
| default: |
| throw new IllegalArgumentException("Unhandled HostnameVerification: " |
| + hostnameVerification); |
| } |
| } |
| |
| @Override public byte[] send(byte[] request) { |
| while (true) { |
| HttpClientContext context = HttpClientContext.create(); |
| |
| context.setTargetHost(host); |
| |
| // Set the credentials if they were provided. |
| if (null != this.credentials) { |
| context.setCredentialsProvider(credentialsProvider); |
| context.setAuthSchemeRegistry(authRegistry); |
| context.setAuthCache(authCache); |
| } |
| |
| if (null != userToken) { |
| context.setUserToken(userToken); |
| } |
| |
| ByteArrayEntity entity = new ByteArrayEntity(request, ContentType.APPLICATION_OCTET_STREAM); |
| |
| // Create the client with the AuthSchemeRegistry and manager |
| HttpPost post = new HttpPost(uri); |
| post.setEntity(entity); |
| |
| try (CloseableHttpResponse response = execute(post, context)) { |
| final int statusCode = response.getStatusLine().getStatusCode(); |
| if (HttpURLConnection.HTTP_OK == statusCode |
| || HttpURLConnection.HTTP_INTERNAL_ERROR == statusCode) { |
| userToken = context.getUserToken(); |
| return EntityUtils.toByteArray(response.getEntity()); |
| } else if (HttpURLConnection.HTTP_UNAVAILABLE == statusCode) { |
| LOG.debug("Failed to connect to server (HTTP/503), retrying"); |
| continue; |
| } |
| |
| throw new RuntimeException("Failed to execute HTTP Request, got HTTP/" + statusCode); |
| } catch (NoHttpResponseException e) { |
| // This can happen when sitting behind a load balancer and a backend server dies |
| LOG.debug("The server failed to issue an HTTP response, retrying"); |
| continue; |
| } catch (RuntimeException e) { |
| throw e; |
| } catch (Exception e) { |
| LOG.debug("Failed to execute HTTP request", e); |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| // Visible for testing |
| CloseableHttpResponse execute(HttpPost post, HttpClientContext context) |
| throws IOException, ClientProtocolException { |
| return client.execute(post, context); |
| } |
| |
| @Override public void setUsernamePassword(AuthenticationType authType, String username, |
| String password) { |
| this.credentials = new UsernamePasswordCredentials( |
| Objects.requireNonNull(username), Objects.requireNonNull(password)); |
| |
| this.credentialsProvider = new BasicCredentialsProvider(); |
| credentialsProvider.setCredentials(AuthScope.ANY, credentials); |
| |
| RegistryBuilder<AuthSchemeProvider> authRegistryBuilder = RegistryBuilder.create(); |
| switch (authType) { |
| case BASIC: |
| authRegistryBuilder.register(AuthSchemes.BASIC, new BasicSchemeFactory()); |
| break; |
| case DIGEST: |
| authRegistryBuilder.register(AuthSchemes.DIGEST, new DigestSchemeFactory()); |
| break; |
| default: |
| throw new IllegalArgumentException("Unsupported authentiation type: " + authType); |
| } |
| this.authRegistry = authRegistryBuilder.build(); |
| } |
| |
| private static URI toURI(URL url) throws RuntimeException { |
| try { |
| return url.toURI(); |
| } catch (URISyntaxException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Override public void setTrustStore(File truststore, String password) { |
| this.truststore = Objects.requireNonNull(truststore); |
| if (!truststore.exists() || !truststore.isFile()) { |
| throw new IllegalArgumentException( |
| "Truststore is must be an existing, regular file: " + truststore); |
| } |
| this.truststorePassword = Objects.requireNonNull(password); |
| initializeClient(); |
| } |
| |
| @Override public void setKeyStore(File keystore, String keystorepassword, String keypassword) { |
| this.keystore = Objects.requireNonNull(keystore); |
| if (!keystore.exists() || !keystore.isFile()) { |
| throw new IllegalArgumentException( |
| "Keystore is must be an existing, regular file: " + keystore); |
| } |
| this.keystorePassword = Objects.requireNonNull(keystorepassword); |
| this.keyPassword = Objects.requireNonNull(keypassword); |
| initializeClient(); |
| } |
| |
| @Override public void setHostnameVerification(HostnameVerification verification) { |
| this.hostnameVerification = Objects.requireNonNull(verification); |
| initializeClient(); |
| } |
| } |
| |
| // End AvaticaCommonsHttpClientImpl.java |