package org.wiztools.restclient;
import java.io.*;
import java.net.HttpCookie;
import java.net.URL;
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 java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import org.apache.http.*;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.NTCredentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
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.AuthSchemeBase;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.auth.DigestScheme;
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.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.wiztools.commons.MultiValueMap;
import org.wiztools.commons.StreamUtil;
import org.wiztools.commons.StringUtil;
import org.wiztools.restclient.bean.*;
import org.wiztools.restclient.http.RESTClientCookieStore;
import org.wiztools.restclient.util.HttpUtil;
import org.wiztools.restclient.util.IDNUtil;
import org.wiztools.restclient.util.Util;
/**
*
* @author subwiz
*/
public class HTTPClientRequestExecuter implements RequestExecuter {
private static final Logger LOG = Logger.getLogger(HTTPClientRequestExecuter.class.getName());
private CloseableHttpClient httpClient;
private boolean interruptedShutdown = false;
private boolean isRequestCompleted = false;
/*
* This instance variable is for avoiding multiple execution of requests
* on the same RequestExecuter object. We know it is not the perfect solution
* (as it does not synchronize access to shared variable), but is
* fine for finding this type of error during development phase.
*/
private boolean isRequestStarted = false;
@Override
public void execute(Request request, View... views) {
// Verify if this is the first call to this object:
if(isRequestStarted){
throw new MultipleRequestInSameRequestExecuterException(
"A RequestExecuter object can be used only once!");
}
isRequestStarted = true;
// Proceed with execution:
for(View view: views){
view.doStart(request);
}
// Needed for specifying HTTP pre-emptive authentication:
HttpContext httpContext = null;
// Create all the builder objects:
final HttpClientBuilder hcBuilder = HttpClientBuilder.create();
final RequestConfig.Builder rcBuilder = RequestConfig.custom();
final RequestBuilder reqBuilder;
switch(request.getMethod()){
case GET:
reqBuilder = RequestBuilder.get();
break;
case POST:
reqBuilder = RequestBuilder.post();
break;
case PUT:
reqBuilder = RequestBuilder.put();
break;
case PATCH:
reqBuilder = RequestBuilder.create("PATCH");
break;
case DELETE:
reqBuilder = RequestBuilder.delete();
break;
case HEAD:
reqBuilder = RequestBuilder.head();
break;
case OPTIONS:
reqBuilder = RequestBuilder.options();
break;
case TRACE:
reqBuilder = RequestBuilder.trace();
break;
default:
throw new IllegalStateException("Method not defined!");
}
// Retry handler (no-retries):
hcBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
// Url:
final URL url = IDNUtil.getIDNizedURL(request.getUrl());
final String urlHost = url.getHost();
final int urlPort = url.getPort()==-1?url.getDefaultPort():url.getPort();
final String urlProtocol = url.getProtocol();
final String urlStr = url.toString();
reqBuilder.setUri(urlStr);
// Set HTTP version:
HTTPVersion httpVersion = request.getHttpVersion();
ProtocolVersion protocolVersion =
httpVersion==HTTPVersion.HTTP_1_1? new ProtocolVersion("HTTP", 1, 1):
new ProtocolVersion("HTTP", 1, 0);
reqBuilder.setVersion(protocolVersion);
// Set request timeout (default 1 minute--60000 milliseconds)
IGlobalOptions options = ServiceLocator.getInstance(IGlobalOptions.class);
rcBuilder.setConnectionRequestTimeout(
Integer.parseInt(options.getProperty("request-timeout-in-millis")));
// Set proxy
ProxyConfig proxy = ProxyConfig.getInstance();
proxy.acquire();
if (proxy.isEnabled()) {
final HttpHost proxyHost = new HttpHost(proxy.getHost(), proxy.getPort(), "http");
if (proxy.isAuthEnabled()) {
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
new AuthScope(proxy.getHost(), proxy.getPort()),
new UsernamePasswordCredentials(proxy.getUsername(), new String(proxy.getPassword())));
hcBuilder.setDefaultCredentialsProvider(credsProvider);
}
hcBuilder.setProxy(proxyHost);
}
proxy.release();
// HTTP Authentication
if(request.getAuth() != null) {
// Add auth preference:
Auth auth = request.getAuth();
List<String> authPrefs = new ArrayList<>();
if(auth instanceof BasicAuth) {
authPrefs.add(AuthSchemes.BASIC);
}
else if(auth instanceof DigestAuth) {
authPrefs.add(AuthSchemes.DIGEST);
}
else if(auth instanceof NtlmAuth) {
authPrefs.add(AuthSchemes.NTLM);
}
rcBuilder.setTargetPreferredAuthSchemes(authPrefs);
// BASIC & DIGEST:
if(auth instanceof BasicAuth || auth instanceof DigestAuth) {
BasicDigestAuth a = (BasicDigestAuth) auth;
String uid = a.getUsername();
String pwd = new String(a.getPassword());
String host = StringUtil.isEmpty(a.getHost()) ? urlHost : a.getHost();
String realm = StringUtil.isEmpty(a.getRealm()) ? AuthScope.ANY_REALM : a.getRealm();
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
new AuthScope(host, urlPort, realm),
new UsernamePasswordCredentials(uid, pwd));
hcBuilder.setDefaultCredentialsProvider(credsProvider);
// preemptive mode:
if (a.isPreemptive()) {
AuthCache authCache = new BasicAuthCache();
AuthSchemeBase authScheme = a instanceof BasicAuth?
new BasicScheme(): new DigestScheme();
authCache.put(new HttpHost(urlHost, urlPort, urlProtocol), authScheme);
HttpClientContext localContext = HttpClientContext.create();
localContext.setAuthCache(authCache);
httpContext = localContext;
}
}
// NTLM:
if(auth instanceof NtlmAuth) {
NtlmAuth a = (NtlmAuth) auth;
String uid = a.getUsername();
String pwd = new String(a.getPassword());
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
AuthScope.ANY,
new NTCredentials(
uid, pwd, a.getWorkstation(), a.getDomain()));
hcBuilder.setDefaultCredentialsProvider(credsProvider);
}
// Authorization header
// Logic written in same place where Header is processed--a little down!
}
try {
{ // Authorization Header Authentication:
Auth auth = request.getAuth();
if(auth != null && auth instanceof AuthorizationHeaderAuth) {
AuthorizationHeaderAuth a = (AuthorizationHeaderAuth) auth;
final String authHeader = a.getAuthorizationHeaderValue();
if(StringUtil.isNotEmpty(authHeader)) {
Header header = new BasicHeader("Authorization", authHeader);
reqBuilder.addHeader(header);
}
}
}
// Get request headers
MultiValueMap<String, String> header_data = request.getHeaders();
for (String key : header_data.keySet()) {
for(String value: header_data.get(key)) {
Header header = new BasicHeader(key, value);
reqBuilder.addHeader(header);
}
}
// Cookies
{
// Set cookie policy:
rcBuilder.setCookieSpec(CookieSpecs.BEST_MATCH);
// Add to CookieStore:
CookieStore store = new RESTClientCookieStore();
List<HttpCookie> cookies = request.getCookies();
for(HttpCookie cookie: cookies) {
BasicClientCookie c = new BasicClientCookie(
cookie.getName(), cookie.getValue());
c.setVersion(cookie.getVersion());
c.setDomain(urlHost);
c.setPath("/");
store.addCookie(c);
}
// Attach store to client:
hcBuilder.setDefaultCookieStore(store);
}
// POST/PUT/PATCH/DELETE method specific logic
if (HttpUtil.isEntityEnclosingMethod(reqBuilder.getMethod())) {
// Create and set RequestEntity
ReqEntity bean = request.getBody();
if (bean != null) {
try {
if(bean instanceof ReqEntitySimple) {
AbstractHttpEntity e = HTTPClientUtil.getEntity((ReqEntitySimple)bean);
reqBuilder.setEntity(e);
}
else if(bean instanceof ReqEntityMultipart) {
ReqEntityMultipart multipart = (ReqEntityMultipart)bean;
MultipartEntityBuilder meb = MultipartEntityBuilder.create();
// Format:
MultipartMode mpMode = multipart.getMode();
switch(mpMode) {
case BROWSER_COMPATIBLE:
meb.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
break;
case RFC_6532:
meb.setMode(HttpMultipartMode.RFC6532);
break;
case STRICT:
meb.setMode(HttpMultipartMode.STRICT);
break;
}
// Parts:
for(ReqEntityPart part: multipart.getBody()) {
if(part instanceof ReqEntityStringPart) {
ReqEntityStringPart p = (ReqEntityStringPart)part;
String body = p.getPart();
ContentType ct = p.getContentType();
final StringBody sb;
if(ct != null) {
sb = new StringBody(body, HTTPClientUtil.getContentType(ct));
}
else {
sb = new StringBody(body, org.apache.http.entity.ContentType.DEFAULT_TEXT);
}
meb.addPart(part.getName(), sb);
}
else if(part instanceof ReqEntityFilePart) {
ReqEntityFilePart p = (ReqEntityFilePart)part;
File body = p.getPart();
ContentType ct = p.getContentType();
final FileBody fb;
if(ct != null) {
fb = new FileBody(body, HTTPClientUtil.getContentType(ct), p.getFilename());
}
else {
fb = new FileBody(body, org.apache.http.entity.ContentType.DEFAULT_BINARY, p.getFilename());
}
meb.addPart(p.getName(), fb);
}
}
reqBuilder.setEntity(meb.build());
}
}
catch (UnsupportedEncodingException ex) {
for(View view: views){
view.doError(Util.getStackTrace(ex));
view.doEnd();
}
return;
}
}
}
// SSL
// Set the hostname verifier:
final SSLReq sslReq = request.getSslReq();
if(sslReq != null) {
SSLHostnameVerifier verifier = sslReq.getHostNameVerifier();
final X509HostnameVerifier hcVerifier;
switch(verifier){
case STRICT:
hcVerifier = SSLConnectionSocketFactory.STRICT_HOSTNAME_VERIFIER;
break;
case BROWSER_COMPATIBLE:
hcVerifier = SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
break;
case ALLOW_ALL:
hcVerifier = SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
break;
default:
hcVerifier = SSLConnectionSocketFactory.STRICT_HOSTNAME_VERIFIER;
break;
}
// Register the SSL Scheme:
final KeyStore trustStore = sslReq.getTrustStore() == null?
null:
sslReq.getTrustStore().getKeyStore();
final KeyStore keyStore = sslReq.getKeyStore() == null?
null:
sslReq.getKeyStore().getKeyStore();
final TrustStrategy trustStrategy = sslReq.isTrustSelfSignedCert()
? new TrustSelfSignedStrategy(): null;
SSLContext ctx = new SSLContextBuilder()
.loadKeyMaterial(keyStore, sslReq.getKeyStore()!=null? sslReq.getKeyStore().getPassword(): null)
.loadTrustMaterial(trustStore, trustStrategy)
.setSecureRandom(null)
.useTLS()
.build();
SSLConnectionSocketFactory sf = new SSLConnectionSocketFactory(ctx, hcVerifier);
hcBuilder.setSSLSocketFactory(sf);
}
// How to handle redirects:
rcBuilder.setRedirectsEnabled(request.isFollowRedirect());
// Now Execute:
long startTime = System.currentTimeMillis();
RequestConfig rc = rcBuilder.build();
reqBuilder.setConfig(rc);
HttpUriRequest req = reqBuilder.build();
httpClient = hcBuilder.build();
HttpResponse http_res = httpClient.execute(req, httpContext);
long endTime = System.currentTimeMillis();
// Create response:
ResponseBean response = new ResponseBean();
response.setExecutionTime(endTime - startTime);
response.setStatusCode(http_res.getStatusLine().getStatusCode());
response.setStatusLine(http_res.getStatusLine().toString());
final Header[] responseHeaders = http_res.getAllHeaders();
for (Header header : responseHeaders) {
response.addHeader(header.getName(), header.getValue());
}
// Response body:
final HttpEntity entity = http_res.getEntity();
if(entity != null) {
if(request.isIgnoreResponseBody()) {
EntityUtils.consumeQuietly(entity);
}
else {
InputStream is = entity.getContent();
try{
byte[] responseBody = StreamUtil.inputStream2Bytes(is);
if (responseBody != null) {
response.setResponseBody(responseBody);
}
}
catch(IOException ex) {
for(View view: views) {
view.doError("Byte array conversion from response body stream failed.");
}
LOG.log(Level.WARNING, ex.getMessage(), ex);
}
}
}
// Now execute tests:
try {
junit.framework.TestSuite suite = TestUtil.getTestSuite(request, response);
if (suite != null) { // suite will be null if there is no associated script
TestResult testResult = TestUtil.execute(suite);
response.setTestResult(testResult);
}
} catch (TestException ex) {
for(View view: views){
view.doError(Util.getStackTrace(ex));
}
}
for(View view: views){
view.doResponse(response);
}
}
catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyManagementException | IllegalStateException ex) {
if(!interruptedShutdown){
for(View view: views){
view.doError(Util.getStackTrace(ex));
}
}
else{
for(View view: views){
view.doCancelled();
}
}
}
finally {
if (!interruptedShutdown) {
// for interrupted shutdown, httpClient is already closed
// close it only when otherwise:
try {
if(httpClient != null) httpClient.close();
}
catch(IOException ex) {
LOG.log(Level.WARNING, "Exception when closing httpClient", ex);
}
}
else {
// reset value to default:
interruptedShutdown = false;
}
for(View view: views){
view.doEnd();
}
isRequestCompleted = true;
}
}
@Override
public void abortExecution(){
if(!isRequestCompleted){
interruptedShutdown = true;
try {
if(httpClient != null) httpClient.close();
}
catch(IOException ex) {
LOG.log(Level.WARNING, "Exception when closing httpClient", ex);
}
}
else{
LOG.info("Request already completed. Doing nothing.");
}
}
}