Client.java [src/csip/utils] Revision: default Date:
/*
* $Id$
*
* This file is part of the Cloud Services Integration Platform (CSIP),
* a Model-as-a-Service framework, API and application suite.
*
* 2012-2022, Olaf David and others, OMSLab, Colorado State University.
*
* OMSLab licenses this file to you under the MIT license.
* See the LICENSE file in the project root for more information.
*/
package csip.utils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_OK;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
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.config.RegistryBuilder;
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.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.auth.BasicScheme;
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.conn.PoolingHttpClientConnectionManager;
import org.apache.http.pool.PoolStats;
import org.apache.http.protocol.HttpContext;
import org.codehaus.jettison.json.JSONObject;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.codehaus.jettison.json.JSONException;
/**
* HTTP Client for easier CSIP service calling.
*
* @author od
*/
public class Client implements AutoCloseable {
private Logger log;
private static final PoolingHttpClientConnectionManager cm
= new PoolingHttpClientConnectionManager(
RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", new SSLConnectionSocketFactory(SSLContexts.createSystemDefault()))
.build()
);
private CloseableHttpClient httpClient;
public static final int DEF_TIMEOUT = 3600; // 1 h timeout
private CredentialsProvider creds;
// private BasicHttpContext context;
private static class HttpsTrustManager implements X509TrustManager {
static TrustManager[] trustManagers;
static final X509Certificate[] _AcceptedIssuers = new X509Certificate[]{};
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
throws java.security.cert.CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
throws java.security.cert.CertificateException {
}
public boolean isClientTrusted(X509Certificate[] chain) {
return true;
}
public boolean isServerTrusted(X509Certificate[] chain) {
return true;
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return _AcceptedIssuers;
}
/**
* it's not safe for production phase, because it will allow all SSL
* certificate although the SSL certificate is not valid
*/
public static void allowAllSSL() {
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}
});
SSLContext context = null;
if (trustManagers == null)
trustManagers = new TrustManager[]{new HttpsTrustManager()};
try {
context = SSLContext.getInstance("TLS");
context.init(null, trustManagers, new SecureRandom());
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
}
}
private static class DummyTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
static {
try {
cm.setMaxTotal(250);
// Increase default max connection per route to 75
cm.setDefaultMaxPerRoute(100);
// Increase max connections for localhost:80 to 125
HttpHost localhost = new HttpHost("locahost", 80);
cm.setMaxPerRoute(new HttpRoute(localhost), 150);
} catch (Exception E) {
Logger.getLogger(Client.class.getName()).log(Level.SEVERE, "Static Init Error", E);
}
}
private static final PoolingHttpClientConnectionManager connPM = createCM();
private static SSLConnectionSocketFactory factory;
@SuppressWarnings("unchecked")
private static PoolingHttpClientConnectionManager createCM() {
try {
SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(new TrustSelfSignedStrategy()).build();
sslcontext.init(new KeyManager[0], new TrustManager[]{new DummyTrustManager()}, new SecureRandom());
sslcontext.init(null, new X509TrustManager[]{new HttpsTrustManager()}, new SecureRandom());
factory = new SSLConnectionSocketFactory(sslcontext, new HostnameVerifier() {
@Override
public boolean verify(final String s, final SSLSession sslSession) {
return true;
}
});
org.apache.http.config.Registry r = RegistryBuilder.create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", factory).build();
PoolingHttpClientConnectionManager connPool = new PoolingHttpClientConnectionManager(r);
// Increase max total connection to 200
connPool.setMaxTotal(250);
connPool.setDefaultMaxPerRoute(100);
return connPool;
} catch (Exception e) {
System.err.println("Error initiliazing ConnectionPool :: " + e.toString());
return null;
}
}
/**
* Create a Client
*/
public Client() {
this(DEF_TIMEOUT, null);
}
/**
* Create a client with a logger.
* @param log the Logger
*
*/
public Client(Logger log) {
this(DEF_TIMEOUT, log);
}
/**
* Create a client with a custom timeout.
*
* @param timeout the timeout in
*/
public Client(int timeout) {
this(timeout, null);
}
public Client(int timeout, Logger log) {
this.log = log;
RequestConfig c = RequestConfig.custom()
.setConnectTimeout(timeout * 1000)
.setConnectionRequestTimeout(timeout * 1000)
.setSocketTimeout(timeout * 1000)
.build();
httpClient = HttpClients.custom()
.setDefaultRequestConfig(c)
.setConnectionManagerShared(true)
.setConnectionManager(connPM)
.setSSLSocketFactory(factory)
.build();
}
public Client(int timeout, Logger log, String username, String password) {
this.log = log;
RequestConfig reqConfig = RequestConfig.custom()
.setConnectTimeout(timeout * 1000)
.setConnectionRequestTimeout(timeout * 1000)
.setSocketTimeout(timeout * 1000) // 10 minute max per model run
// .setCookieSpec(CookieSpecs.BEST_MATCH)
.setExpectContinueEnabled(true)
// .setStaleConnectionCheckEnabled(true)
.setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.NTLM, AuthSchemes.DIGEST))
.setProxyPreferredAuthSchemes(Arrays.asList(AuthSchemes.BASIC))
.build();
// context = new BasicHttpContext();
// context.setAttribute("preemptive-auth", new BasicScheme());
creds = new BasicCredentialsProvider();
creds.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(reqConfig)
.setConnectionManager(cm)
.addInterceptorFirst(new PreemptiveAuthInterceptor())
.build();
}
@Override
public void close() {
try {
httpClient.close();
} catch (IOException ex) {
if (log != null)
log.log(Level.SEVERE, null, ex);
}
}
// private CloseableHttpClient createHttpClient(int timeout, Logger log) {
// this.log = log;
// RequestConfig c = RequestConfig.custom()
// .setConnectTimeout(timeout * 1000)
// .setSocketTimeout(timeout * 1000)
// .build();
//
// return HttpClients.custom()
// .setDefaultRequestConfig(c)
// .setConnectionManagerShared(true)
// .setConnectionManager(connPM)
// .setSSLSocketFactory(factory)
// .build();
// }
private class PreemptiveAuthInterceptor implements HttpRequestInterceptor {
@Override
@SuppressWarnings("deprecation")
public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
request.addHeader(BasicScheme.authenticate(creds.getCredentials(AuthScope.ANY), "US-ASCII", false));
}
}
/**
* Pings a HTTP URL. This effectively sends a HEAD request and returns
* <code>true</code> if the response code is in the 200-399 range.
* @param url The HTTP URL to be pinged.
* @param timeout The timeout in millis for both the connection timeout and
* the response read timeout. Note that the total timeout is effectively two
* times the given timeout.
* @return the time to respond if the given HTTP URL has returned response
* code 200-399 on a HEAD request within the given timeout, otherwise
* <code>-1</code>.
*/
public static long ping(String url, int timeout) {
// url = url.replaceFirst("^https", "http");
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(timeout);
conn.setReadTimeout(timeout);
conn.setRequestMethod("HEAD");
long start = System.currentTimeMillis();
int responseCode = conn.getResponseCode();
long end = System.currentTimeMillis();
return (200 <= responseCode && responseCode <= 399) ? (end - start) : -1;
} catch (Exception E) {
return -1;
}
}
public static boolean ping0(String url, int timeout) {
try {
HttpURLConnection.setFollowRedirects(true);
HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
con.setConnectTimeout(timeout);
con.setRequestMethod("HEAD");
return (con.getResponseCode() == HttpURLConnection.HTTP_OK);
} catch (Exception E) {
return false;
}
}
////////////////// GET
public File doGET(String url, File file) throws Exception {
return (File) _doGET(url, fw(t -> {
FileUtils.copyInputStreamToFile(t, file);
return file;
}));
}
public String doGET(String url) throws Exception {
return (String) _doGET(url, fw(t -> {
return IOUtils.toString(t, "UTF-8");
}));
}
private Object _doGET(String url, Function<InputStream, Object> c) throws Exception {
if (log != null && log.isLoggable(Level.INFO))
log.info("GET : " + url);
HttpGet get = new HttpGet(url);
try (CloseableHttpResponse httpResponse = httpClient.execute(get)) {
int statusCode = httpResponse.getStatusLine().getStatusCode();
if (statusCode < HTTP_BAD_REQUEST) // < 400
return c.apply(httpResponse.getEntity().getContent());
throw new IOException(url + ": error " + statusCode);
} finally {
get.releaseConnection();
}
}
@FunctionalInterface
interface ThrowingFunction<T, R, E extends Exception> {
R apply(T t) throws E;
}
static <T, R> Function<T, R> fw(ThrowingFunction<T, R, Exception> throwingFunction) {
return i -> {
try {
return throwingFunction.apply(i);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}
///////////// DELETE
public int doDelete(String url) throws Exception {
HttpDelete del = new HttpDelete(url);
int status = 0;
try {
HttpResponse resultsResponse = httpClient.execute(del);
status = resultsResponse.getStatusLine().getStatusCode();
} finally {
del.releaseConnection();
}
if (log != null && log.isLoggable(Level.INFO)) {
log.info("DELETE : " + url + " -> " + status);
}
return status;
}
///////////// POST
/**
* HTTP Post with the request JSON and files attached.
*
* @param uri the endpoint
* @param req the request JSON
* @param files the files to attach
* @param header the header map
* @return the response JSON
*/
public JSONObject doPOST(String uri, JSONObject req, File[] files, Map<String, String> header) {
return doPOST(uri, req, files, null, header);
}
/**
* HTTP Post with the request JSON.
*
* @param uri the endpoint
* @param req the request JSON
* @return the response JSON
* @throws Exception if something goes wrong
*/
public JSONObject doPOST(String uri, JSONObject req) throws Exception {
return doPOST(uri, req, null, null, null);
}
public JSONObject doPOST(String uri, JSONObject req, Map<String, String> header) throws Exception {
return doPOST(uri, req, null, null, header);
}
public String doPOST(String uri, String req) throws Exception {
return doPOST(uri, req, null, null, null);
}
public JSONObject doPOST(String uri, JSONObject req, File[] files, String[] names) throws Exception {
return doPOST(uri, req, files, names, null);
}
public JSONObject doPOST(String uri, JSONObject req, File[] files) throws Exception {
return doPOST(uri, req, files, null, null);
}
public JSONObject doPOST(String uri, JSONObject req, File[] files, String[] names, Map<String, String> header) {
try {
String r = doPOST(uri, req.toString(), files, names, header);
return new JSONObject(r);
} catch (JSONException ex) {
return new JSONObject(new HashMap<String, String>() {
{
put("error", ex.getMessage());
}
});
}
}
public String doPOST(String uri, String req, File[] files, String[] names, Map<String, String> header) {
if (uri == null || uri.isEmpty())
throw new IllegalArgumentException("URI is null or empty.");
if (req == null || req.isEmpty())
throw new IllegalArgumentException("Invalid request, null or empty.");
String response;
HttpPost post = new HttpPost(uri);
if (files != null && files.length > 0) {
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.addPart("param", new StringBody(req, ContentType.APPLICATION_JSON));
if (log != null && log.isLoggable(Level.INFO))
log.info(" attaching request as 'param':" + req);
for (int i = 0; i < files.length; i++) {
FileBody b = new FileBody(files[i], ContentType.APPLICATION_OCTET_STREAM,
names == null ? files[i].getName() : names[i]);
builder.addPart("file" + (i + 1), b);
if (log != null && log.isLoggable(Level.INFO))
log.info(" attached file" + b.getFile().toString() + " as " + b.getFilename());
}
post.setEntity(builder.build());
} else {
post.addHeader("Content-Type", ContentType.APPLICATION_JSON.toString());
StringEntity entity = new StringEntity(req, "UTF-8");
entity.setContentType(ContentType.APPLICATION_JSON.toString());
if (log != null && log.isLoggable(Level.INFO))
log.info("string entity : " + req);
post.setEntity(entity);
}
if (header != null) {
for (Map.Entry<String, String> entry : header.entrySet()) {
post.addHeader(entry.getKey(), entry.getValue());
if (log != null && log.isLoggable(Level.INFO))
log.info("adding header: " + entry.getKey() + ": " + entry.getValue());
}
}
if (log != null && log.isLoggable(Level.INFO))
log.info("POST: " + uri + " ...");
try (CloseableHttpResponse httpResponse = httpClient.execute(post)) {
int statusCode = httpResponse.getStatusLine().getStatusCode();
if (statusCode == HTTP_OK) {
InputStream responseStream = httpResponse.getEntity().getContent();
response = IOUtils.toString(responseStream, "UTF-8");
if (log != null && log.isLoggable(Level.INFO)) {
log.info("response: " + response);
PoolStats ps = cm.getTotalStats();
log.info("http-client-connections pool [avail=" + ps.getAvailable()
+ " leased=" + ps.getLeased() + " pending=" + ps.getPending() + "] ");
}
} else {
if (log != null)
log.severe("response code: " + statusCode);
response = "{'error': " + statusCode + "}";
}
} catch (Exception E) {
if (log != null)
log.log(Level.SEVERE, "Client Error: ", E);
response = "{'error': '" + E.getMessage() + "'}";
} finally {
post.releaseConnection();
}
return response;
}
//////////////////// PUT
public int doPUT(String uri, JSONObject req) throws Exception {
HttpPut put = new HttpPut(uri);
put.addHeader("Content-Type", "application/json");
StringEntity entity = new StringEntity(req.toString(), "UTF-8");
entity.setContentType("application/json");
if (log != null && log.isLoggable(Level.INFO))
log.info("attaching : " + req.toString());
put.setEntity(entity);
if (log != null && log.isLoggable(Level.INFO))
log.info("PUT : " + uri);
int statusCode = 0;
try (CloseableHttpResponse response = httpClient.execute(put)) {
statusCode = response.getStatusLine().getStatusCode();
} finally {
put.releaseConnection();
}
return statusCode;
}
}