package at.gv.egiz.eaaf.core.impl.utils; import java.io.IOException; import java.io.InputStream; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import javax.annotation.PostConstruct; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import at.gv.egiz.eaaf.core.api.idp.IConfiguration; import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.ProtocolException; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.RedirectStrategy; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpUriRequest; 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.LayeredConnectionSocketFactory; 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.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.DefaultRedirectStrategy; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.protocol.HttpContext; import org.apache.http.ssl.SSLContexts; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; public class HttpClientFactory implements IHttpClientFactory { private static final Logger log = LoggerFactory.getLogger(HttpClientFactory.class); @Autowired(required = true) private IConfiguration basicConfig; @Autowired(required = true) ResourceLoader resourceLoader; public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_POOL_USE = "client.http.connection.pool.use"; public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXTOTAL = "client.http.connection.pool.maxtotal"; public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXPERROUTE = "client.http.connection.pool.maxperroute"; public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_SOCKET = "client.http.connection.timeout.socket"; public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_CONNECTION = "client.http.connection.timeout.connection"; public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_REQUEST = "client.http.connection.timeout.request"; public static final String PROP_CONFIG_CLIENT_HTTP_SSL_HOSTNAMEVERIFIER_TRUSTALL = "client.http.ssl.hostnameverifier.trustall"; public static final String PROP_CONFIG_CLIENT_MODE = "client.authmode"; public static final String PROP_CONFIG_CLIENT_AUTH_HTTP_USERNAME = "client.auth.http.username"; public static final String PROP_CONFIG_CLIENT_AUTH_HTTP_PASSORD = "client.auth.http.password"; public static final String PROP_CONFIG_CLIENT_AUTH_SSL_KEYSTORE_PATH = "client.auth.ssl.keystore.path"; public static final String PROP_CONFIG_CLIENT_AUTH_SSL_KEYSTORE_PASSORD = "client.auth.ssl.keystore.password"; public static final String PROP_CONFIG_CLIENT_AUTH_SSL_KEYSTORE_TYPE = "client.auth.ssl.keystore.type"; public static final String PROP_CONFIG_CLIENT_AUTH_SSL_KEY_PASSWORD = "client.auth.ssl.key.password"; public static final String PROP_CONFIG_CLIENT_AUTH_SSL_KEY_ALIAS = "client.auth.ssl.key.alias"; // default configuration values public static final String DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_SOCKET = "15"; public static final String DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_CONNECTION = "15"; public static final String DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_REQUEST = "30"; public static final String DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXTOTAL = "500"; public static final String DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXPERROUTE = "100"; public enum ClientAuthMode { NONE("none"), PASSWORD("password"), SSL("ssl"); private final String mode; ClientAuthMode(final String mode) { this.mode = mode; } /** * Get the PVP mode. * * @return */ public String getMode() { return this.mode; } /** * Get http-client authentication mode from String representation. * * @param s Config parameter * @return */ public static ClientAuthMode fromString(final String s) { try { return ClientAuthMode.valueOf(s.toUpperCase()); } catch (IllegalArgumentException | NullPointerException e) { return null; } } @Override public String toString() { return getMode(); } } public enum KeyStoreType { PKCS12("pkcs12"), JKS("jks"); private final String type; KeyStoreType(final String type) { this.type = type; } /** * Get the KeyStore type. * * @return */ public String getType() { return this.type; } /** * Get Keystore type from configuration. * * @param s String representation for keyStore type * @return */ public static KeyStoreType fromString(final String s) { try { return KeyStoreType.valueOf(s.toUpperCase()); } catch (IllegalArgumentException | NullPointerException e) { return null; } } @Override public String toString() { return getType(); } } private HttpClientBuilder httpClientBuilder = null; /* * (non-Javadoc) * * @see at.gv.egiz.eaaf.core.impl.utils.IHttpClientFactory#getHttpClient() */ @Override public CloseableHttpClient getHttpClient() { return getHttpClient(true); } @Override public CloseableHttpClient getHttpClient(final boolean followRedirects) { RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); if (!followRedirects) { redirectStrategy = new RedirectStrategy() { @Override public boolean isRedirected(final HttpRequest request, final HttpResponse response, final HttpContext context) throws ProtocolException { return false; } @Override public HttpUriRequest getRedirect(final HttpRequest request, final HttpResponse response, final HttpContext context) throws ProtocolException { return null; } }; } return httpClientBuilder.setRedirectStrategy(redirectStrategy).build(); } @PostConstruct private void initalize() { // initialize http client log.trace("Initializing HTTP Client-builder ... "); httpClientBuilder = HttpClients.custom(); // set default request configuration final RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout( Integer.parseInt(basicConfig.getBasicConfiguration( PROP_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_CONNECTION, DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_CONNECTION)) * 1000) .setConnectionRequestTimeout(Integer.parseInt(basicConfig.getBasicConfiguration( PROP_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_REQUEST, DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_REQUEST)) * 1000) .setSocketTimeout(Integer.parseInt( basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_SOCKET, DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_SOCKET)) * 1000) .build(); httpClientBuilder.setDefaultRequestConfig(requestConfig); ClientAuthMode clientAuthMode = ClientAuthMode.fromString( basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_MODE, ClientAuthMode.NONE.getMode())); if (clientAuthMode == null) { log.warn("Can Not parse ClientAuthMode! Set mode to default value"); clientAuthMode = ClientAuthMode.NONE; } // inject basic http authentication if required log.info("Client authentication-mode is set to: {}", clientAuthMode); injectBasicAuthenticationIfRequired(clientAuthMode); // inject authentication if required final LayeredConnectionSocketFactory sslConnectionFactory = getSslContext(clientAuthMode); // set pool connection if required injectConnectionPoolIfRequired(sslConnectionFactory); } private void injectBasicAuthenticationIfRequired(final ClientAuthMode clientAuthMode) { if (clientAuthMode.equals(ClientAuthMode.PASSWORD)) { final CredentialsProvider provider = new BasicCredentialsProvider(); final String username = basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_AUTH_HTTP_USERNAME); final String password = basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_AUTH_HTTP_PASSORD); if (StringUtils.isEmpty(username)) { log.warn("Http basic authentication was activated but NOT username was set!"); } log.trace("Injecting basic authentication with username: {} and password: {}", username, password); final UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password); provider.setCredentials(AuthScope.ANY, credentials); httpClientBuilder.setDefaultCredentialsProvider(provider); log.info("Basic http authentication was injected with username: {}", username); } else { log.trace("Injection of Http Basic authentication was skipped"); } } private SSLContext buildSslContextWithSslClientAuthentication() throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, EaafConfigurationException { log.trace("Injecting SSL client-authentication into http client ... "); final KeyStore keystore = getSslAuthKeyStore(); final String keyPasswordString = basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_AUTH_SSL_KEY_PASSWORD); log.trace("Open SSL Client-Auth keystore with password: {}", keyPasswordString); final char[] keyPassword = (keyPasswordString == null) ? StringUtils.EMPTY.toCharArray() : keyPasswordString.toCharArray(); return SSLContexts.custom().loadKeyMaterial(keystore, keyPassword).build(); } private KeyStore getSslAuthKeyStore() throws EaafConfigurationException { final KeyStoreType keyStoreType = KeyStoreType.fromString(basicConfig.getBasicConfiguration( PROP_CONFIG_CLIENT_AUTH_SSL_KEYSTORE_TYPE, KeyStoreType.PKCS12.getType())); final String localKeyStorePath = basicConfig .getBasicConfiguration(PROP_CONFIG_CLIENT_AUTH_SSL_KEYSTORE_PATH, StringUtils.EMPTY); final String keyStorePassword = basicConfig .getBasicConfiguration(PROP_CONFIG_CLIENT_AUTH_SSL_KEYSTORE_PASSORD, StringUtils.EMPTY); try { log.debug("Open keyStore with type: {}", keyStoreType); KeyStore clientStore; if (keyStoreType.equals(KeyStoreType.PKCS12)) { clientStore = KeyStore.getInstance("pkcs12"); } else { clientStore = KeyStore.getInstance("JKS"); } log.debug("Read keyStore path: {} from configuration", localKeyStorePath); if (StringUtils.isNotEmpty(localKeyStorePath)) { final String absFilePath = FileUtils.makeAbsoluteUrl(localKeyStorePath, basicConfig.getConfigurationRootDirectory()); final Resource ressource = resourceLoader.getResource(absFilePath); final InputStream is = ressource.getInputStream(); log.trace("Load keyStore: {} with password: {}", absFilePath, keyStorePassword); clientStore.load(is, keyStorePassword.toCharArray()); is.close(); return clientStore; } else { log.warn("Path to keyStore for SSL Client-Authentication is empty or null"); throw new EaafConfigurationException( "Path to keyStore for SSL Client-Authentication is empty or null", new Object[] {}); } } catch (final KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) { log.warn("Can NOT read keyStore: {} from filesystem", localKeyStorePath, null, e); throw new EaafConfigurationException("Can NOT read keyStore: {} from filesystem", new Object[] {localKeyStorePath}, e); } } private LayeredConnectionSocketFactory getSslContext(final ClientAuthMode clientAuthMode) { SSLContext sslContext = null; try { if (clientAuthMode.equals(ClientAuthMode.SSL)) { sslContext = buildSslContextWithSslClientAuthentication(); } else { log.trace("Initializing default SSL Context ... "); sslContext = SSLContext.getDefault(); } // set hostname verifier HostnameVerifier hostnameVerifier = null; if (basicConfig.getBasicConfigurationBoolean( PROP_CONFIG_CLIENT_HTTP_SSL_HOSTNAMEVERIFIER_TRUSTALL, false)) { hostnameVerifier = new NoopHostnameVerifier(); log.warn("HTTP client-builder deactivates SSL Host-name verification!"); } final LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier); return sslSocketFactory; } catch (final NoSuchAlgorithmException | KeyManagementException | UnrecoverableKeyException | KeyStoreException | EaafConfigurationException e) { log.warn("HTTP client-builder can NOT initialze SSL-Context", e); } log.info("HTTP client-builder successfuly initialized"); return null; } private void injectConnectionPoolIfRequired( final LayeredConnectionSocketFactory sslConnectionFactory) { if (basicConfig.getBasicConfigurationBoolean(PROP_CONFIG_CLIENT_HTTP_CONNECTION_POOL_USE, true)) { PoolingHttpClientConnectionManager pool; // set socketFactoryRegistry if SSLConnectionFactory is Set if (sslConnectionFactory != null) { final Registry socketFactoryRegistry = RegistryBuilder.create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslConnectionFactory).build(); log.trace("Inject SSLSocketFactory into pooled connection"); pool = new PoolingHttpClientConnectionManager(socketFactoryRegistry); } else { pool = new PoolingHttpClientConnectionManager(); } pool.setDefaultMaxPerRoute(Integer.parseInt( basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXPERROUTE, DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXPERROUTE))); pool.setMaxTotal(Integer.parseInt( basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXTOTAL, DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXTOTAL))); httpClientBuilder.setConnectionManager(pool); log.debug("Initalize http-client pool with, maxTotal: {} maxPerRoute: {}", pool.getMaxTotal(), pool.getDefaultMaxPerRoute()); } else if (sslConnectionFactory != null) { log.trace("Inject SSLSocketFactory without connection pool"); httpClientBuilder.setSSLSocketFactory(sslConnectionFactory); } } }