/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.petra.json.web.service.client;
import com.liferay.petra.json.web.service.client.jcifs.JCIFSNTLMSchemeFactory;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ProxySelector;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthSchemeProvider;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.ChallengeState;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.config.Lookup;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
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.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Ivica Cardic
* @author Igor Beslic
*/
public class JSONWebServiceClientImpl implements JSONWebServiceClient {
public void afterPropertiesSet() {
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder = httpClientBuilder.useSystemProperties();
HttpClientConnectionManager httpClientConnectionManager =
getPoolingHttpClientConnectionManager();
httpClientBuilder.setConnectionManager(httpClientConnectionManager);
if ((!isNull(_login) && !isNull(_password)) ||
(!isNull(_proxyLogin) && !isNull(_proxyPassword))) {
CredentialsProvider credentialsProvider =
new BasicCredentialsProvider();
if (!isNull(_login)) {
credentialsProvider.setCredentials(
new AuthScope(_hostName, _hostPort),
new UsernamePasswordCredentials(_login, _password));
}
else {
if (_logger.isInfoEnabled()) {
_logger.info("No credentials are used");
}
}
if (!isNull(_proxyLogin)) {
credentialsProvider.setCredentials(
new AuthScope(_proxyHostName, _proxyHostPort),
new UsernamePasswordCredentials(
_proxyLogin, _proxyPassword));
}
httpClientBuilder.setDefaultCredentialsProvider(
credentialsProvider);
httpClientBuilder.setRetryHandler(
new HttpRequestRetryHandlerImpl());
}
try {
if (_proxySelector != null) {
httpClientBuilder.setRoutePlanner(
new SystemDefaultRoutePlanner(_proxySelector));
}
else {
setProxyHost(httpClientBuilder);
}
if (!isNull(_proxyAuthType) &&
_proxyAuthType.equalsIgnoreCase("ntlm")) {
RegistryBuilder registerBuilder =
RegistryBuilder.<AuthSchemeProvider>create();
registerBuilder = registerBuilder.register(
AuthSchemes.NTLM,
new JCIFSNTLMSchemeFactory(
_proxyDomain, _proxyWorkstation));
Lookup<AuthSchemeProvider> authSchemeRegistry =
registerBuilder.build();
httpClientBuilder.setDefaultAuthSchemeRegistry(
authSchemeRegistry);
}
_closeableHttpClient = httpClientBuilder.build();
_idleConnectionMonitorThread = new IdleConnectionMonitorThread(
httpClientConnectionManager);
_idleConnectionMonitorThread.start();
if (_logger.isDebugEnabled()) {
_logger.debug(
"Configured client for " + _protocol + "://" + _hostName);
}
}
catch (Exception e) {
_logger.error("Unable to configure client", e);
}
}
@Override
public void destroy() {
try {
_closeableHttpClient.close();
}
catch (IOException ioe) {
_logger.error("Unable to close client", ioe);
}
_closeableHttpClient = null;
_idleConnectionMonitorThread.shutdown();
}
@Override
public String doDelete(String url, Map<String, String> parameters)
throws JSONWebServiceTransportException {
return doDelete(
url, parameters, Collections.<String, String>emptyMap());
}
@Override
public String doDelete(
String url, Map<String, String> parameters,
Map<String, String> headers)
throws JSONWebServiceTransportException {
if (!isNull(_contextPath)) {
url = _contextPath + url;
}
List<NameValuePair> nameValuePairs = toNameValuePairs(parameters);
if (!nameValuePairs.isEmpty()) {
String queryString = URLEncodedUtils.format(
nameValuePairs, StandardCharsets.UTF_8);
url += "?" + queryString;
}
if (_logger.isDebugEnabled()) {
_logger.debug(
"Sending DELETE request to " + _login + "@" + _hostName + url);
log("HTTP parameters", parameters);
log("HTTP headers", headers);
}
HttpDelete httpDelete = new HttpDelete(url);
addHeaders(httpDelete, headers);
return execute(httpDelete);
}
@Override
public String doGet(String url, Map<String, String> parameters)
throws JSONWebServiceTransportException {
return doGet(url, parameters, Collections.<String, String>emptyMap());
}
@Override
public String doGet(
String url, Map<String, String> parameters,
Map<String, String> headers)
throws JSONWebServiceTransportException {
if (!isNull(_contextPath)) {
url = _contextPath + url;
}
List<NameValuePair> nameValuePairs = toNameValuePairs(parameters);
if (!nameValuePairs.isEmpty()) {
String queryString = URLEncodedUtils.format(
nameValuePairs, StandardCharsets.UTF_8);
url += "?" + queryString;
}
if (_logger.isDebugEnabled()) {
_logger.debug(
"Sending GET request to " + _login + "@" + _hostName + url);
log("HTTP parameters", parameters);
log("HTTP headers", headers);
}
HttpGet httpGet = new HttpGet(url);
addHeaders(httpGet, headers);
return execute(httpGet);
}
@Override
public String doPost(String url, Map<String, String> parameters)
throws JSONWebServiceTransportException {
return doPost(url, parameters, Collections.<String, String>emptyMap());
}
@Override
public String doPost(
String url, Map<String, String> parameters,
Map<String, String> headers)
throws JSONWebServiceTransportException {
if (!isNull(_contextPath)) {
url = _contextPath + url;
}
if (_logger.isDebugEnabled()) {
_logger.debug(
"Sending POST request to " + _login + "@" + _hostName + url);
log("HTTP parameters", parameters);
log("HTTP headers", headers);
}
HttpPost httpPost = new HttpPost(url);
List<NameValuePair> nameValuePairs = toNameValuePairs(parameters);
HttpEntity httpEntity = new UrlEncodedFormEntity(
nameValuePairs, StandardCharsets.UTF_8);
addHeaders(httpPost, headers);
httpPost.setEntity(httpEntity);
return execute(httpPost);
}
@Override
public String doPostAsJSON(String url, String json)
throws JSONWebServiceTransportException {
return doPostAsJSON(url, json, Collections.<String, String>emptyMap());
}
@Override
public String doPostAsJSON(
String url, String json, Map<String, String> headers)
throws JSONWebServiceTransportException {
HttpPost httpPost = new HttpPost(url);
addHeaders(httpPost, headers);
StringEntity stringEntity = new StringEntity(
json.toString(), StandardCharsets.UTF_8);
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
return execute(httpPost);
}
@Override
public String doPut(String url, Map<String, String> parameters)
throws JSONWebServiceTransportException {
return doPut(url, parameters, Collections.<String, String>emptyMap());
}
@Override
public String doPut(
String url, Map<String, String> parameters,
Map<String, String> headers)
throws JSONWebServiceTransportException {
if (!isNull(_contextPath)) {
url = _contextPath + url;
}
if (_logger.isDebugEnabled()) {
_logger.debug(
"Sending PUT request to " + _login + "@" + _hostName + url);
log("HTTP parameters", parameters);
log("HTTP headers", headers);
}
HttpPut httpPut = new HttpPut(url);
List<NameValuePair> nameValuePairs = toNameValuePairs(parameters);
HttpEntity httpEntity = new UrlEncodedFormEntity(
nameValuePairs, StandardCharsets.UTF_8);
addHeaders(httpPut, headers);
httpPut.setEntity(httpEntity);
return execute(httpPut);
}
public Map<String, String> getHeaders() {
return _headers;
}
@Override
public String getHostName() {
return _hostName;
}
@Override
public int getHostPort() {
return _hostPort;
}
@Override
public String getProtocol() {
return _protocol;
}
@Override
public void resetHttpClient() {
destroy();
afterPropertiesSet();
}
public void setContextPath(String contextPath) {
_contextPath = contextPath;
}
public void setHeaders(Map<String, String> headers) {
_headers = headers;
}
@Override
public void setHostName(String hostName) {
_hostName = hostName;
}
@Override
public void setHostPort(int hostPort) {
_hostPort = hostPort;
}
@Override
public void setKeyStore(KeyStore keyStore) {
_keyStore = keyStore;
}
@Override
public void setLogin(String login) {
_login = login;
}
@Override
public void setPassword(String password) {
_password = password;
}
@Override
public void setProtocol(String protocol) {
_protocol = protocol;
}
public void setProxyAuthType(String proxyAuthType) {
_proxyAuthType = proxyAuthType;
}
public void setProxyDomain(String proxyDomain) {
_proxyDomain = proxyDomain;
}
public void setProxyHostName(String proxyHostName) {
_proxyHostName = proxyHostName;
}
public void setProxyHostPort(int proxyHostPort) {
_proxyHostPort = proxyHostPort;
}
public void setProxyLogin(String proxyLogin) {
_proxyLogin = proxyLogin;
}
public void setProxyPassword(String proxyPassword) {
_proxyPassword = proxyPassword;
}
public void setProxySelector(ProxySelector proxySelector) {
_proxySelector = proxySelector;
}
public void setProxyWorkstation(String proxyWorkstation) {
_proxyWorkstation = proxyWorkstation;
}
protected void addHeaders(
HttpMessage httpMessage, Map<String, String> headers) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
httpMessage.addHeader(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : _headers.entrySet()) {
httpMessage.addHeader(entry.getKey(), entry.getValue());
}
}
protected String execute(HttpRequestBase httpRequestBase)
throws JSONWebServiceTransportException {
HttpHost httpHost = new HttpHost(_hostName, _hostPort, _protocol);
try {
if (_closeableHttpClient == null) {
afterPropertiesSet();
}
HttpResponse httpResponse = null;
if (!isNull(_login) && !isNull(_password)) {
HttpClientContext httpClientContext =
HttpClientContext.create();
AuthCache authCache = new BasicAuthCache();
AuthScheme authScheme = null;
if (!isNull(_proxyHostName)) {
authScheme = new BasicScheme(ChallengeState.PROXY);
}
else {
authScheme = new BasicScheme(ChallengeState.TARGET);
}
authCache.put(httpHost, authScheme);
httpClientContext.setAttribute(
ClientContext.AUTH_CACHE, authCache);
httpResponse = _closeableHttpClient.execute(
httpHost, httpRequestBase, httpClientContext);
}
else {
httpResponse = _closeableHttpClient.execute(
httpHost, httpRequestBase);
}
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
if ((statusCode == HttpServletResponse.SC_BAD_REQUEST) ||
(statusCode == HttpServletResponse.SC_FORBIDDEN) ||
(statusCode == HttpServletResponse.SC_NOT_ACCEPTABLE) ||
(statusCode == HttpServletResponse.SC_NOT_FOUND)) {
if (httpResponse.getEntity() != null) {
if (_logger.isDebugEnabled()) {
_logger.debug("Server returned status " + statusCode);
}
return EntityUtils.toString(
httpResponse.getEntity(), StandardCharsets.UTF_8);
}
}
else if (statusCode == HttpServletResponse.SC_OK) {
return EntityUtils.toString(
httpResponse.getEntity(), StandardCharsets.UTF_8);
}
else if (statusCode == HttpServletResponse.SC_UNAUTHORIZED) {
throw new JSONWebServiceTransportException.
AuthenticationFailure(
"Not authorized to access JSON web service");
}
throw new JSONWebServiceTransportException.CommunicationFailure(
"Server returned status " + statusCode, statusCode);
}
catch (IOException ioe) {
throw new JSONWebServiceTransportException.CommunicationFailure(
"Unable to transmit request", ioe);
}
finally {
httpRequestBase.releaseConnection();
}
}
protected PoolingHttpClientConnectionManager
getPoolingHttpClientConnectionManager() {
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager =
null;
if (_keyStore != null) {
poolingHttpClientConnectionManager =
new PoolingHttpClientConnectionManager(
getSocketFactoryRegistry(), null, null, null, 60000,
TimeUnit.MILLISECONDS);
}
else {
poolingHttpClientConnectionManager =
new PoolingHttpClientConnectionManager(
60000, TimeUnit.MILLISECONDS);
}
poolingHttpClientConnectionManager.setMaxTotal(20);
return poolingHttpClientConnectionManager;
}
protected Registry<ConnectionSocketFactory> getSocketFactoryRegistry() {
RegistryBuilder<ConnectionSocketFactory> registryBuilder =
RegistryBuilder.<ConnectionSocketFactory>create();
registryBuilder.register("http", new PlainConnectionSocketFactory());
registryBuilder.register("https", getSSLConnectionSocketFactory());
return registryBuilder.build();
}
protected SSLConnectionSocketFactory getSSLConnectionSocketFactory() {
SSLContextBuilder sslContextBuilder = SSLContexts.custom();
SSLContext sslContext = null;
try {
sslContextBuilder.loadTrustMaterial(_keyStore);
sslContext = sslContextBuilder.build();
sslContext.init(
null, new TrustManager[] {new X509TrustManagerImpl()}, null);
}
catch (Exception e) {
throw new RuntimeException(e);
}
return new SSLConnectionSocketFactory(
sslContext, new String[] {"TLSv1"}, null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
}
protected boolean isNull(String s) {
if ((s == null) || s.equals("")) {
return true;
}
return false;
}
protected void log(String message, Map<String, String> map) {
if (!_logger.isDebugEnabled() || map.isEmpty()) {
return;
}
StringBuilder sb = new StringBuilder((map.size() * 4) + 2);
sb.append(message);
sb.append(":");
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (value == null) {
key = "-" + key;
value = "";
}
sb.append("\n");
sb.append(key);
sb.append("=");
sb.append(value);
}
_logger.debug(sb.toString());
}
protected void setProxyHost(HttpClientBuilder httpClientBuilder) {
if ((_proxyHostName == null) || _proxyHostName.equals("")) {
return;
}
httpClientBuilder.setProxy(
new HttpHost(_proxyHostName, _proxyHostPort));
httpClientBuilder.setProxyAuthenticationStrategy(
new ProxyAuthenticationStrategy());
}
protected List<NameValuePair> toNameValuePairs(
Map<String, String> parameters) {
List<NameValuePair> nameValuePairs = new LinkedList<>();
for (Map.Entry<String, String> entry : parameters.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (value == null) {
key = "-" + key;
value = "";
}
NameValuePair nameValuePair = new BasicNameValuePair(key, value);
nameValuePairs.add(nameValuePair);
}
return nameValuePairs;
}
private static final Logger _logger = LoggerFactory.getLogger(
JSONWebServiceClientImpl.class);
private CloseableHttpClient _closeableHttpClient;
private String _contextPath;
private Map<String, String> _headers = Collections.emptyMap();
private String _hostName;
private int _hostPort = 80;
private IdleConnectionMonitorThread _idleConnectionMonitorThread;
private KeyStore _keyStore;
private String _login;
private String _password;
private String _protocol = "http";
private String _proxyAuthType;
private String _proxyDomain;
private String _proxyHostName;
private int _proxyHostPort;
private String _proxyLogin;
private String _proxyPassword;
private ProxySelector _proxySelector;
private String _proxyWorkstation;
private class HttpRequestRetryHandlerImpl
implements HttpRequestRetryHandler {
public boolean retryRequest(
IOException ioe, int retryCount, HttpContext httpContext) {
if (retryCount >= 5) {
return false;
}
if (ioe instanceof ConnectTimeoutException) {
return false;
}
if (ioe instanceof InterruptedIOException) {
return false;
}
if (ioe instanceof SocketException) {
return true;
}
if (ioe instanceof SSLException) {
return false;
}
if (ioe instanceof UnknownHostException) {
return false;
}
HttpClientContext httpClientContext = HttpClientContext.adapt(
httpContext);
HttpRequest httpRequest = httpClientContext.getRequest();
if (httpRequest instanceof HttpEntityEnclosingRequest) {
return false;
}
return true;
}
}
private class IdleConnectionMonitorThread extends Thread {
public IdleConnectionMonitorThread(
HttpClientConnectionManager httpClientConnectionManager) {
_httpClientConnectionManager = httpClientConnectionManager;
}
@Override
public void run() {
try {
while (!_shutdown) {
synchronized (this) {
wait(5000);
_httpClientConnectionManager.closeExpiredConnections();
_httpClientConnectionManager.closeIdleConnections(
30, TimeUnit.SECONDS);
}
}
}
catch (InterruptedException ie) {
}
}
public void shutdown() {
_shutdown = true;
synchronized (this) {
notifyAll();
}
}
private final HttpClientConnectionManager _httpClientConnectionManager;
private volatile boolean _shutdown;
}
private class X509TrustManagerImpl implements X509TrustManager {
public X509TrustManagerImpl() {
try {
X509TrustManager x509TrustManager = null;
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore)null);
for (TrustManager trustManager :
trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
x509TrustManager = (X509TrustManager)trustManager;
break;
}
}
_x509TrustManager = x509TrustManager;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void checkClientTrusted(
X509Certificate[] x509Certificates, String authType)
throws CertificateException {
if (x509Certificates.length != 1) {
_x509TrustManager.checkClientTrusted(
x509Certificates, authType);
}
}
@Override
public void checkServerTrusted(
X509Certificate[] x509Certificates, String authType)
throws CertificateException {
if (x509Certificates.length != 1) {
_x509TrustManager.checkServerTrusted(
x509Certificates, authType);
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return _x509TrustManager.getAcceptedIssuers();
}
private final X509TrustManager _x509TrustManager;
}
}