Java tutorial
/* * Copyright (c) 2016 Network New Technologies Inc. * * Licensed 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 com.networknt.client; import com.networknt.client.oauth.ClientCredentialsRequest; import com.networknt.client.oauth.TokenHelper; import com.networknt.client.oauth.TokenRequest; import com.networknt.client.oauth.TokenResponse; import com.networknt.config.Config; import com.networknt.exception.ApiException; import com.networknt.exception.ClientException; import com.networknt.status.Status; import com.networknt.utility.*; import org.apache.http.*; import org.apache.http.client.config.RequestConfig; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.routing.HttpRoute; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager; import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor; import org.apache.http.impl.nio.reactor.IOReactorConfig; import org.apache.http.message.BasicHeaderElementIterator; import org.apache.http.nio.conn.NoopIOSessionStrategy; import org.apache.http.nio.conn.SchemeIOSessionStrategy; import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; import org.apache.http.nio.reactor.ConnectingIOReactor; import org.apache.http.nio.reactor.IOReactorException; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContexts; import org.owasp.encoder.Encode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.security.*; import java.security.cert.CertificateException; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class Client { public static final String CONFIG_NAME = "client"; static final Logger logger = LoggerFactory.getLogger(Client.class); static final Integer DEFAULT_REACTOR_CONNECT_TIMEOUT = 10000; // 10 seconds static final Integer DEFAULT_REACTOR_SO_TIMEOUT = 10000; // 10 seconds static final String SYNC = "sync"; static final String ASYNC = "async"; static final String ROUTES = "routes"; static final String MAX_CONNECTION_TOTAL = "maxConnectionTotal"; static final String MAX_CONNECTION_PER_ROUTE = "maxConnectionPerRoute"; static final String TIMEOUT = "timeout"; static final String KEEP_ALIVE = "keepAlive"; static final String TLS = "tls"; static final String LOAD_TRUST_STORE = "loadTrustStore"; static final String LOAD_KEY_STORE = "loadKeyStore"; static final String VERIFY_HOSTNAME = "verifyHostname"; static final String TRUST_STORE = "trustStore"; static final String TRUST_PASS = "trustPass"; static final String KEY_STORE = "keyStore"; static final String KEY_PASS = "keyPass"; static final String TRUST_STORE_PROPERTY = "javax.net.ssl.trustStore"; static final String TRUST_STORE_PASSWORD_PROPERTY = "javax.net.ssl.trustStorePassword"; static final String REACTOR = "reactor"; static final String REACTOR_IO_THREAD_COUNT = "ioThreadCount"; static final String REACTOR_CONNECT_TIMEOUT = "connectTimeout"; static final String REACTOR_SO_TIMEOUT = "soTimeout"; static final String OAUTH = "oauth"; static final String TOKEN_RENEW_BEFORE_EXPIRED = "tokenRenewBeforeExpired"; static final String EXPIRED_REFRESH_RETRY_DELAY = "expiredRefreshRetryDelay"; static final String EARLY_REFRESH_RETRY_DELAY = "earlyRefreshRetryDelay"; static final String STATUS_CLIENT_CREDENTIALS_TOKEN_NOT_AVAILABLE = "ERR10009"; static Map<String, Object> config; static Map<String, Object> oauthConfig; private CloseableHttpClient httpClient = null; private CloseableHttpAsyncClient httpAsyncClient = null; // Cached jwt token for this client. private String jwt; private long expire; private volatile boolean renewing = false; private volatile long expiredRetryTimeout; private volatile long earlyRetryTimeout; private final Object lock = new Object(); static { List<String> masks = new ArrayList<String>(); masks.add("trustPass"); masks.add("keyPass"); masks.add("client_secret"); ModuleRegistry.registerModule(Client.class.getName(), Config.getInstance().getJsonMapConfigNoCache(CONFIG_NAME), masks); config = Config.getInstance().getJsonMapConfig(CONFIG_NAME); if (config != null) { oauthConfig = (Map<String, Object>) config.get(OAUTH); } } // This eager initialization. private static final Client instance = new Client(); // This is the only way to get instance public static Client getInstance() { return instance; } // private constructor to prevent create another instance. private Client() { } public CloseableHttpClient getSyncClient() throws ClientException { if (httpClient == null) { synchronized (Client.class) { if (httpClient == null) { httpClient = httpClient(); } } } return httpClient; } public CloseableHttpAsyncClient getAsyncClient() throws ClientException { if (httpAsyncClient == null) { synchronized (Client.class) { if (httpAsyncClient == null) { httpAsyncClient = httpAsyncClient(); } } } httpAsyncClient.start(); return httpAsyncClient; } /** * Add Authorization Code grant token the caller app gets from OAuth2 server. * * @param request * @param token */ public void addAuthorization(HttpRequest request, String token) { if (!token.startsWith("Bearer ")) { if (token.toUpperCase().startsWith("BEARER ")) { // other cases of Bearer token = "Bearer " + token.substring(7); } else { token = "Bearer " + token; } } request.addHeader(Constants.AUTHORIZATION, token); } /** * Add Client Credentials token cached in the client for standalone application * * @param request */ public void addAuthorization(HttpRequest request) throws ClientException, ApiException { checkCCTokenExpired(); request.addHeader(Constants.AUTHORIZATION, "Bearer " + jwt); } /** * Support API to API calls with scope token. The token is the original token from consumer and * the client credentials token of caller API is added from cache. * @param request * @param token * @throws ClientException */ public void addAuthorizationWithScopeToken(HttpRequest request, String token) throws ClientException, ApiException { addAuthorization(request, token); checkCCTokenExpired(); request.addHeader(Constants.SCOPE_TOKEN, "Bearer " + jwt); } private void getCCToken() throws ClientException { TokenRequest tokenRequest = new ClientCredentialsRequest(); TokenResponse tokenResponse = TokenHelper.getToken(tokenRequest); synchronized (lock) { jwt = tokenResponse.getAccessToken(); // the expiresIn is seconds and it is converted to millisecond in the future. expire = System.currentTimeMillis() + tokenResponse.getExpiresIn() * 1000; logger.info("Get client credentials token {} with expire_in {} seconds", jwt, tokenResponse.getExpiresIn()); } } private void checkCCTokenExpired() throws ClientException, ApiException { long tokenRenewBeforeExpired = (Integer) oauthConfig.get(TOKEN_RENEW_BEFORE_EXPIRED); long expiredRefreshRetryDelay = (Integer) oauthConfig.get(EXPIRED_REFRESH_RETRY_DELAY); long earlyRefreshRetryDelay = (Integer) oauthConfig.get(EARLY_REFRESH_RETRY_DELAY); boolean isInRenewWindow = expire - System.currentTimeMillis() < tokenRenewBeforeExpired; logger.trace("isInRenewWindow = " + isInRenewWindow); if (isInRenewWindow) { if (expire <= System.currentTimeMillis()) { logger.trace("In renew window and token is expired."); // block other request here to prevent using expired token. synchronized (Client.class) { if (expire <= System.currentTimeMillis()) { logger.trace("Within the synch block, check if the current request need to renew token"); if (!renewing || System.currentTimeMillis() > expiredRetryTimeout) { // if there is no other request is renewing or the renewing flag is true but renewTimeout is passed renewing = true; expiredRetryTimeout = System.currentTimeMillis() + expiredRefreshRetryDelay; logger.trace( "Current request is renewing token synchronously as token is expired already"); getCCToken(); renewing = false; } else { logger.trace("Circuit breaker is tripped and not timeout yet!"); // reject all waiting requests by thrown an exception. throw new ApiException(new Status(STATUS_CLIENT_CREDENTIALS_TOKEN_NOT_AVAILABLE)); } } } } else { // Not expired yet, try to renew async but let requests use the old token. logger.trace("In renew window but token is not expired yet."); synchronized (Client.class) { if (expire > System.currentTimeMillis()) { if (!renewing || System.currentTimeMillis() > earlyRetryTimeout) { renewing = true; earlyRetryTimeout = System.currentTimeMillis() + earlyRefreshRetryDelay; logger.trace("Retrieve token async is called while token is not expired yet"); ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); executor.schedule(new Runnable() { @Override public void run() { try { getCCToken(); renewing = false; logger.trace("Async get token is completed."); } catch (Exception e) { logger.error("Async retrieve token error", e); // swallow the exception here as it is on a best effort basis. } } }, 50, TimeUnit.MILLISECONDS); executor.shutdown(); } } } } } logger.trace("Check secondary token is done!"); } private CloseableHttpClient httpClient() throws ClientException { PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry()); Map<String, Object> httpClientMap = (Map<String, Object>) config.get(SYNC); connectionManager.setMaxTotal((Integer) httpClientMap.get(MAX_CONNECTION_TOTAL)); connectionManager.setDefaultMaxPerRoute((Integer) httpClientMap.get(MAX_CONNECTION_PER_ROUTE)); // Now handle all the specific route defined. Map<String, Object> routeMap = (Map<String, Object>) httpClientMap.get(ROUTES); Iterator<String> it = routeMap.keySet().iterator(); while (it.hasNext()) { String route = it.next(); Integer maxConnection = (Integer) routeMap.get(route); connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(route)), maxConnection); } final int timeout = (Integer) httpClientMap.get(TIMEOUT); RequestConfig config = RequestConfig.custom().setConnectTimeout(timeout).setSocketTimeout(timeout).build(); final long keepAliveMilliseconds = (Integer) httpClientMap.get(KEEP_ALIVE); return HttpClientBuilder.create().setConnectionManager(connectionManager) .setKeepAliveStrategy(new ConnectionKeepAliveStrategy() { @Override public long getKeepAliveDuration(HttpResponse response, HttpContext context) { HeaderElementIterator it = new BasicHeaderElementIterator( response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { HeaderElement he = it.nextElement(); String param = he.getName(); String value = he.getValue(); if (value != null && param.equalsIgnoreCase("timeout")) { try { logger.trace("Use server timeout for keepAliveMilliseconds"); return Long.parseLong(value) * 1000; } catch (NumberFormatException ignore) { } } } //logger.trace("Use keepAliveMilliseconds from config " + keepAliveMilliseconds); return keepAliveMilliseconds; } }).setDefaultRequestConfig(config).build(); } private CloseableHttpAsyncClient httpAsyncClient() throws ClientException { PoolingNHttpClientConnectionManager connectionManager = new PoolingNHttpClientConnectionManager(ioReactor(), asyncRegistry()); Map<String, Object> asyncHttpClientMap = (Map<String, Object>) config.get(ASYNC); connectionManager.setMaxTotal((Integer) asyncHttpClientMap.get(MAX_CONNECTION_TOTAL)); connectionManager.setDefaultMaxPerRoute((Integer) asyncHttpClientMap.get(MAX_CONNECTION_PER_ROUTE)); // Now handle all the specific route defined. Map<String, Object> routeMap = (Map<String, Object>) asyncHttpClientMap.get(ROUTES); Iterator<String> it = routeMap.keySet().iterator(); while (it.hasNext()) { String route = it.next(); Integer maxConnection = (Integer) routeMap.get(route); connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(route)), maxConnection); } final int timeout = (Integer) asyncHttpClientMap.get(TIMEOUT); RequestConfig config = RequestConfig.custom().setConnectTimeout(timeout).setSocketTimeout(timeout).build(); final long keepAliveMilliseconds = (Integer) asyncHttpClientMap.get(KEEP_ALIVE); return HttpAsyncClientBuilder.create().setConnectionManager(connectionManager) .setKeepAliveStrategy(new ConnectionKeepAliveStrategy() { @Override public long getKeepAliveDuration(HttpResponse response, HttpContext context) { HeaderElementIterator it = new BasicHeaderElementIterator( response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { HeaderElement he = it.nextElement(); String param = he.getName(); String value = he.getValue(); if (value != null && param.equalsIgnoreCase("timeout")) { try { logger.trace("Use server timeout for keepAliveMilliseconds"); return Long.parseLong(value) * 1000; } catch (NumberFormatException ignore) { } } } //logger.trace("Use keepAliveMilliseconds from config " + keepAliveMilliseconds); return keepAliveMilliseconds; } }).setDefaultRequestConfig(config).build(); } private Registry<SchemeIOSessionStrategy> asyncRegistry() throws ClientException { // Allow TLSv1 protocol only Registry<SchemeIOSessionStrategy> registry = null; try { SSLIOSessionStrategy sslSessionStrategy = new SSLIOSessionStrategy(sslContext(), new String[] { "TLSv1" }, null, hostnameVerifier()); // Create a registry of custom connection session strategies for supported // protocol schemes. registry = RegistryBuilder.<SchemeIOSessionStrategy>create() .register("http", NoopIOSessionStrategy.INSTANCE).register("https", sslSessionStrategy).build(); } catch (NoSuchAlgorithmException e) { logger.error("NoSuchAlgorithmException: ", e); throw new ClientException("NoSuchAlgorithmException: ", e); } catch (KeyManagementException e) { logger.error("KeyManagementException: ", e); throw new ClientException("KeyManagementException: ", e); } catch (IOException e) { logger.error("IOException: ", e); throw new ClientException("IOException: ", e); } return registry; } private ConnectingIOReactor ioReactor() throws ClientException { Map<String, Object> asyncMap = (Map) config.get(ASYNC); Map<String, Object> reactorMap = (Map) asyncMap.get(REACTOR); Integer ioThreadCount = (Integer) reactorMap.get(REACTOR_IO_THREAD_COUNT); IOReactorConfig.Builder builder = IOReactorConfig.custom(); builder.setIoThreadCount( ioThreadCount == null ? Runtime.getRuntime().availableProcessors() : ioThreadCount); Integer connectTimeout = (Integer) reactorMap.get(REACTOR_CONNECT_TIMEOUT); builder.setConnectTimeout(connectTimeout == null ? DEFAULT_REACTOR_CONNECT_TIMEOUT : connectTimeout); Integer soTimeout = (Integer) reactorMap.get(REACTOR_SO_TIMEOUT); builder.setSoTimeout(soTimeout == null ? DEFAULT_REACTOR_SO_TIMEOUT : soTimeout); ConnectingIOReactor reactor = null; try { reactor = new DefaultConnectingIOReactor(builder.build()); } catch (IOReactorException e) { logger.error("IOReactorException: ", e); throw new ClientException("IOReactorException: ", e); } return reactor; } private SSLContext sslContext() throws ClientException, IOException, NoSuchAlgorithmException, KeyManagementException { SSLContext sslContext = null; Map<String, Object> tlsMap = (Map) config.get(TLS); if (tlsMap != null) { SSLContextBuilder builder = SSLContexts.custom(); // load trust store, this is the server public key certificate // first check if javax.net.ssl.trustStore system properties is set. It is only necessary if the server // certificate doesn't have the entire chain. Boolean loadTrustStore = (Boolean) tlsMap.get(LOAD_TRUST_STORE); if (loadTrustStore != null && loadTrustStore == true) { String trustStoreName = System.getProperty(TRUST_STORE_PROPERTY); String trustStorePass = System.getProperty(TRUST_STORE_PASSWORD_PROPERTY); if (trustStoreName != null && trustStorePass != null) { logger.info("Loading trust store from system property at " + Encode.forJava(trustStoreName)); } else { trustStoreName = (String) tlsMap.get(TRUST_STORE); trustStorePass = (String) tlsMap.get(TRUST_PASS); logger.info("Loading trust store from config at " + Encode.forJava(trustStoreName)); } KeyStore trustStore = null; if (trustStoreName != null && trustStorePass != null) { InputStream trustStream = Config.getInstance().getInputStreamFromFile(trustStoreName); if (trustStream != null) { try { trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(trustStream, trustStorePass.toCharArray()); builder.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()); } catch (CertificateException ce) { logger.error("CertificateException: Unable to load trust store.", ce); throw new ClientException("CertificateException: Unable to load trust store.", ce); } catch (KeyStoreException kse) { logger.error("KeyStoreException: Unable to load trust store.", kse); throw new ClientException("KeyStoreException: Unable to load trust store.", kse); } finally { trustStream.close(); } } } } // load key store for client certificate if two way ssl is used. Boolean loadKeyStore = (Boolean) tlsMap.get(LOAD_KEY_STORE); if (loadKeyStore != null && loadKeyStore == true) { String keyStoreName = (String) tlsMap.get(KEY_STORE); String keyStorePass = (String) tlsMap.get(KEY_PASS); KeyStore keyStore = null; if (keyStoreName != null && keyStorePass != null) { InputStream keyStream = Config.getInstance().getInputStreamFromFile(keyStoreName); if (keyStream != null) { try { keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(keyStream, keyStorePass.toCharArray()); builder.loadKeyMaterial(keyStore, keyStorePass.toCharArray()); } catch (CertificateException ce) { logger.error("CertificateException: Unable to load key store.", ce); throw new ClientException("CertificateException: Unable to load key store.", ce); } catch (KeyStoreException kse) { logger.error("KeyStoreException: Unable to load key store.", kse); throw new ClientException("KeyStoreException: Unable to load key store.", kse); } catch (UnrecoverableKeyException uke) { logger.error("UnrecoverableKeyException: Unable to load key store.", uke); throw new ClientException("UnrecoverableKeyException: Unable to load key store.", uke); } finally { keyStream.close(); } } } } sslContext = builder.build(); } return sslContext; } private HostnameVerifier hostnameVerifier() { Map<String, Object> tlsMap = (Map) config.get(TLS); HostnameVerifier verifier = null; if (tlsMap != null) { Boolean verifyHostname = (Boolean) tlsMap.get(VERIFY_HOSTNAME); if (verifyHostname != null && verifyHostname == false) { verifier = new NoopHostnameVerifier(); } else { verifier = new DefaultHostnameVerifier(); } } return verifier; } private Registry<ConnectionSocketFactory> registry() throws ClientException { Registry<ConnectionSocketFactory> registry = null; try { // Allow TLSv1 protocol only SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext(), new String[] { "TLSv1" }, null, hostnameVerifier()); // Create a registry of custom connection factory registry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.INSTANCE).register("https", sslFactory).build(); } catch (NoSuchAlgorithmException e) { logger.error("NoSuchAlgorithmException: in registry", e); throw new ClientException("NoSuchAlgorithmException: in registry", e); } catch (KeyManagementException e) { logger.error("KeyManagementException: in registry", e); throw new ClientException("KeyManagementException: in registry", e); } catch (IOException e) { logger.error("IOException: in registry", e); throw new ClientException("IOException: in registry", e); } return registry; } }