package com.bluevia.commons.connector.http;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
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.HttpUriRequest;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.scheme.LayeredSchemeSocketFactory;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.RequestWrapper;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.apache.log4j.Logger;
import com.bluevia.commons.GsdpConstants;
import com.bluevia.commons.Utils;
import com.bluevia.commons.connector.GenericResponse;
import com.bluevia.commons.connector.IAuth;
import com.bluevia.commons.connector.IConnector;
import com.bluevia.commons.connector.http.multipart.BlueviaPartBase;
import com.bluevia.commons.connector.http.multipart.MultipartEntity;
import com.bluevia.commons.exception.BlueviaException;
import com.bluevia.commons.parser.ParseException;
/**
* HttpConnector that allows to send requests via HTTP REST
*
* @author Telefonica R&D
*/
public class HttpConnector implements IConnector, IAuth {
private static Logger log = Logger.getLogger(HttpConnector.class.getName());
private static final int MAX_REDIRECTS_COUNT = 5;
/**
* Type to set the REST HttpMethod
*/
private enum RestHttpMethod {POST, GET, PUT, DELETE};
/**
* Internal HTTP client.
*/
protected DefaultHttpClient mHttpClient;
/**
* Internal HTTP Request.
*/
protected HttpUriRequest mRequest;
/**
* Enable certificate validation
*/
private boolean enableCertificateValidation = true;
/**
* The path to client certificate
*/
protected String mKeyStorePath;
/**
* The password of client certificate
*/
protected String mKeyStorePass;
/**
* Close the HTTP connections
*/
public void close() {
if (mHttpClient != null)
mHttpClient.getConnectionManager().shutdown();
}
public GenericResponse create(String address, HashMap<String, String> parameters, byte[] content,
HashMap<String, String> headers)throws BlueviaException, IOException {
GenericResponse res = null;
HttpEntity httpEntity = null;
if (content != null && headers != null){
httpEntity = createHttpEntity(content, headers);
}
res = createAndExecuteRequest(address, RestHttpMethod.POST, httpEntity, parameters, headers);
return res;
}
public GenericResponse create(String address, HashMap<String, String> parameters, BlueviaPartBase[] parts, HashMap<String, String> headers) throws BlueviaException, IOException {
GenericResponse res = null;
HttpEntity httpEntity = new MultipartEntity(parts);
res = createAndExecuteRequest(address, RestHttpMethod.POST, httpEntity, parameters, headers);
return res;
}
public GenericResponse retrieve(String feedUri) throws BlueviaException, IOException {
return retrieve(feedUri, null);
}
public GenericResponse retrieve(String feedUri, HashMap<String, String> parameters) throws BlueviaException, IOException {
return retrieve(feedUri, parameters, null);
}
public GenericResponse retrieve(String feedUri, HashMap<String, String> parameters, HashMap<String, String> headers) throws BlueviaException, IOException {
GenericResponse res = createAndExecuteRequest(feedUri, RestHttpMethod.GET, null, parameters, headers);
return res;
}
public GenericResponse update(String feedUri, HashMap<String, String> parameters, byte[] body, HashMap<String, String> headers)throws BlueviaException, IOException {
GenericResponse res = null;
HttpEntity httpEntity = null;
if (body != null && headers != null){
httpEntity = createHttpEntity(body, headers);
}
res = createAndExecuteRequest(feedUri, RestHttpMethod.PUT, httpEntity, parameters, headers);
return res;
}
public GenericResponse delete(String feedUri) throws BlueviaException, IOException {
return delete(feedUri, null);
}
public GenericResponse delete(String feedUri, HashMap<String, String> parameters) throws BlueviaException, IOException {
return delete(feedUri, parameters, null);
}
public GenericResponse delete(String feedUri, HashMap<String, String> parameters, HashMap<String, String> headers) throws BlueviaException, IOException {
GenericResponse res = createAndExecuteRequest(feedUri, RestHttpMethod.DELETE, null, parameters, headers);
return res;
}
/**
* Creates an HttpClient
*
* @return the created HTTP Client
*/
protected HttpClient lazyInitHttpClient() throws BlueviaException, IOException {
if (mHttpClient == null) {
HttpParams params = new BasicHttpParams();
// Default connection and socket timeout of 20 seconds.
HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
HttpConnectionParams.setSoTimeout(params, 20 * 1000);
HttpConnectionParams.setSocketBufferSize(params, 8192);
//Default timeout used when retrieving a ManagedClientConnection from the ClientConnectionManager.
//ConnManagerParams.setTimeout(params, 20 * 1000);
HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
// Don't handle redirects -- return them to the caller.
HttpClientParams.setRedirecting(params, false);
// Set the specified user agent and register standard protocols.
//HttpProtocolParams.setUserAgent(params, getCurrentUserAgent());
if (enableCertificateValidation)
mHttpClient = createHttpClientWithCertificateValidation (params);
else
mHttpClient = createHttpClientWithNoCertificateValidation (params);
mHttpClient.removeRequestInterceptorByClass(
org.apache.http.protocol.RequestExpectContinue.class);
mHttpClient.addRequestInterceptor(new CurlLogger());
}
return mHttpClient;
}
private HttpEntity createHttpEntity(byte[] body, HashMap<String, String> headers) throws IOException, ParseException {
if (body != null) {
try {
log.debug("Serialized entry: " + new String(body, "UTF-8"));
} catch (UnsupportedEncodingException uee) {
// should not happen
throw new IllegalStateException("UTF-8 should be supported!",
uee);
}
}
String contentType = headers.get(GsdpConstants.HEADER_CONTENT_TYPE);
AbstractHttpEntity httpEntity = new ByteArrayEntity(body);
httpEntity.setContentType(contentType);
return httpEntity;
}
/**
* Create an HttpRequest with headers and execute it
* @param clientUrl endpoint to send the request
* @param httpMethod HTTP method (POST|GET)
* @param httpEntity HTTP entity to send in the request
* @param parameters HTTP query parameters
* @param headers HTTP headers
* @return the body of the response to this request
* @throws IOException
* @throws BlueviaException
*/
private GenericResponse createAndExecuteRequest(String clientUrl, RestHttpMethod httpMethod,
HttpEntity httpEntity, HashMap<String, String> parameters, HashMap<String, String> headers) throws IOException, BlueviaException {
HttpResponse response = null;
HttpClient client = lazyInitHttpClient();
int redirectsLeft = MAX_REDIRECTS_COUNT;
String url;
if (parameters != null)
url = generateQueryUrl(clientUrl, parameters);
else url = clientUrl;
//We must follow redirects ourselves, since we want to follow redirects even on POSTs, which
// the HTTP library does not do.
while (redirectsLeft > 0) {
//Verify Uri
URI uri = null;
try {
uri = new URI(url);
} catch (URISyntaxException use) {
log.debug("Unable to parse " + url + " as URI.", use);
throw new IOException("Unable to parse " + url
+ " as URI.");
}
//Create the request
switch (httpMethod) {
case POST:
mRequest = new HttpPost(uri);
//include the entity
if (httpEntity != null) {
((HttpPost)mRequest).setEntity(httpEntity);
}
break;
case GET:
mRequest = new HttpGet(uri);
break;
case PUT:
mRequest = new HttpPut(uri);
//include the entity
if (httpEntity != null) {
((HttpPost)mRequest).setEntity(httpEntity);
}
break;
case DELETE:
mRequest = new HttpDelete(uri);
break;
default:
throw new IllegalStateException("Not expected method"); //--Should not happen
}
//Add request headers
if (headers != null){
Iterator<Entry<String, String>> headerSet = headers.entrySet().iterator();
while (headerSet.hasNext()){
Entry<String,String> thisHeader = headerSet.next();
mRequest.addHeader(thisHeader.getKey(), thisHeader.getValue());
}
}
//Authenticate request - abstract method
authenticate();
//Execute the request
response = client.execute(mRequest);
//Analyze the response
StatusLine statusLine = response.getStatusLine();
if (statusLine == null) {
log.warn("StatusLine is null.");
throw new NullPointerException("StatusLine is null -- should not happen.");
}
HashMap<String, String> responseHeaders = new HashMap<String, String>();
log.debug(response.getStatusLine().toString());
for (Header h : response.getAllHeaders()) {
log.debug(h.getName() + ": " + h.getValue());
if (responseHeaders!= null && h != null && h.getName() != null && h.getValue() != null)
responseHeaders.put(h.getName(), h.getValue());
}
int status = statusLine.getStatusCode();
HttpEntity responseHttpEntity = response.getEntity();
InputStream content = null;
if (responseHttpEntity != null)
content = responseHttpEntity.getContent();
if ((status >= 200) && (status < 300)) {
return new GenericResponse(status, statusLine.getReasonPhrase(), content, responseHeaders);
} else if (status == 302) { //manage redirects
//if (responseHttpEntity != null) responseHttpEntity.consumeContent(); //Otherwise, the http lib cannot close connection
// responseHttpEntity.consumeContent() is deprecated:
// Either use getContent() and call InputStream.close() on that;
// otherwise call writeTo(OutputStream) which is required to free the resources.
if (responseHttpEntity != null) content.close(); //Otherwise, the http lib cannot close connection
Header location = response.getFirstHeader("Location");
if (location == null) {
log.debug("Redirect requested but no Location "
+ "specified.");
throw new HttpException("Http 302 Response Error: Redirect requested but no Location Header found", HttpException.BAD_REQUEST_EXCEPTION);
}
log.debug("Following redirect to " + location.getValue());
//Add default query parameters
url = generateQueryUrl(location.getValue(), parameters);
} else {
throw new HttpException(statusLine.getReasonPhrase(), status, content, responseHeaders);
}
redirectsLeft --;
}
return null;
}
/**
* Disables the validation of certificates for SSL connections.
*/
private DefaultHttpClient createHttpClientWithNoCertificateValidation(HttpParams params) throws IOException {
// Create a specific protocol socket factory, for https, based on java default
// socket factories.
LayeredSchemeSocketFactory sslSocketFactory = null;
if (Utils.isEmpty(mKeyStorePath))
sslSocketFactory = new SSLForNonValidCertsSocketFactory();
else sslSocketFactory = new SSL2wayForNonValidCertsSocketFactory(mKeyStorePath, mKeyStorePass);
SchemeRegistry sr = new SchemeRegistry();
sr.register(new Scheme("https", 8443, sslSocketFactory));
sr.register(new Scheme("https", 443, sslSocketFactory));
sr.register(new Scheme("http", 8080, PlainSocketFactory.getSocketFactory()));
sr.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(sr);
return new DefaultHttpClient(manager, params);
}
/**
* Enables the validation of certificates for SSL connections.
* @throws IOException
*/
private DefaultHttpClient createHttpClientWithCertificateValidation(HttpParams params) throws IOException {
// Create a specific protocol socket factory, for https, based on java default
// socket factories.
LayeredSchemeSocketFactory sslSocketFactory = null;
if (Utils.isEmpty(mKeyStorePath))
sslSocketFactory = SSLSocketFactory.getSocketFactory();
else sslSocketFactory = createSSLSocketFactoryWithKeystore();
SchemeRegistry sr = new SchemeRegistry();
sr.register(new Scheme("https", 8443, sslSocketFactory));
sr.register(new Scheme("https", 443, sslSocketFactory));
sr.register(new Scheme("http", 8080, PlainSocketFactory.getSocketFactory()));
sr.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(sr);
return new DefaultHttpClient(manager, params);
}
private SSLSocketFactory createSSLSocketFactoryWithKeystore() throws IOException {
SSLSocketFactory sslSocketFactory = null;
try {
KeyStore keystore = KeyStore.getInstance(GsdpConstants.KEYSTORE_TYPE_PKCS12);
FileInputStream fis = new FileInputStream(new File(mKeyStorePath));
keystore.load(fis, mKeyStorePass.toCharArray());
sslSocketFactory = new SSLSocketFactory(keystore, mKeyStorePass);
} catch (GeneralSecurityException e) {
log.error("Error in certificate. " + e.getMessage(), e);
throw new IOException(e);
}
return sslSocketFactory;
}
/**
* Generates the url query string including the parameters
* @param url the base url
* @return the url including the query parameters
* @throws UnsupportedEncodingException
*/
private String generateQueryUrl(String url, HashMap<String, String> params) throws UnsupportedEncodingException {
StringBuffer res = new StringBuffer(url);
res.append(url.indexOf('?') >= 0 ? '&' : '?');
Set<String> keys = params.keySet();
int i = 0;
for (String name : keys) {
if (i > 0) {
res.append('&');
}
res.append(encodeUri(name)).append('=');
res.append(encodeUri(params.get(name)));
i++;
}
return res.toString();
}
/**
* Encodes the uri in UTF-8
*
* @param uri
* @return
* @throws UnsupportedEncodingException
*/
private static String encodeUri(String uri) throws UnsupportedEncodingException {
String encodedUri;
try {
encodedUri = URLEncoder.encode(uri, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// should not happen.
log.error("[QueryParams]" +
"UTF-8 not supported -- should not happen. "
+ "Using default encoding.", uee);
encodedUri = URLEncoder.encode(uri, "UTF-8");
}
return encodedUri;
}
@Override
public void authenticate() throws BlueviaException {
//No authentication for default HTTPConnector, do nothing
}
/**
* Logs cURL commands equivalent to requests.
*/
private class CurlLogger implements HttpRequestInterceptor {
public CurlLogger() {
super();
}
public void process(HttpRequest request, HttpContext context)
throws org.apache.http.HttpException, IOException {
log.info(toCurl((HttpUriRequest) request, true));
}
}
/**
* Generates a cURL command equivalent to the given request.
*/
private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException {
StringBuilder builder = new StringBuilder();
builder.append("curl ");
for (Header header: request.getAllHeaders()) {
if (!logAuthToken
&& (header.getName().equals("Authorization") ||
header.getName().equals("Cookie"))) {
continue;
}
builder.append("--header \"");
builder.append(header.toString().trim());
builder.append("\" ");
}
URI uri = request.getURI();
// If this is a wrapped request, use the URI from the original
// request instead. getURI() on the wrapper seems to return a
// relative URI. We want an absolute URI.
if (request instanceof RequestWrapper) {
HttpRequest original = ((RequestWrapper) request).getOriginal();
if (original instanceof HttpUriRequest) {
uri = ((HttpUriRequest) original).getURI();
}
}
builder.append("\"");
builder.append(uri);
builder.append("\"");
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest entityRequest =
(HttpEntityEnclosingRequest) request;
HttpEntity entity = entityRequest.getEntity();
if (entity != null && entity.isRepeatable()) {
//if (entity.getContentLength() < 1024) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
entity.writeTo(stream);
String entityString = stream.toString();
// TODO: Check the content type, too.
builder.append(" --data-ascii \"")
.append(entityString)
.append("\"");
//} else {
// builder.append(" [TOO MUCH DATA TO INCLUDE]");
//}
}
}
return builder.toString();
}
}