Merge pull request #1 from cprice404/feature/master/java-api

Add java API and integration tests
This commit is contained in:
Chris Price 2014-04-01 11:39:43 -07:00
commit 7196f316c5
27 changed files with 1280 additions and 10 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target

10
.travis.yml Normal file
View file

@ -0,0 +1,10 @@
language: clojure
lein: lein2
jdk:
- oraclejdk7
- openjdk7
- openjdk6
script: ./ext/travisci/test.sh
notifications:
email: false

View file

@ -1,3 +1,5 @@
[![Build Status](https://travis-ci.org/puppetlabs/clj-http-client.png?branch=master)](https://travis-ci.org/puppetlabs/clj-http-client)
# puppetlabs/http-client
This is a wrapper around the [http-kit](http://http-kit.org/) client

3
ext/travisci/test.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
lein2 test

View file

@ -1,3 +1,6 @@
(def ks-version "0.5.3")
(def tk-version "0.3.8")
(defproject puppetlabs/http-client "0.1.3-SNAPSHOT"
:description "HTTP client wrapper"
:license {:name "Apache License, Version 2.0"
@ -5,7 +8,17 @@
:dependencies [[org.clojure/clojure "1.5.1"]
[http-kit "2.1.16"]
[puppetlabs/kitchensink "0.5.2"]]
[puppetlabs/kitchensink ~ks-version]
[org.clojure/tools.logging "0.2.6"]
[org.slf4j/slf4j-api "1.7.6"]]
:source-paths ["src/clj"]
:java-source-paths ["src/java"]
:profiles {:dev {:dependencies [[puppetlabs/kitchensink ~ks-version :classifier "test"]
[puppetlabs/trapperkeeper ~tk-version]
[puppetlabs/trapperkeeper ~tk-version :classifier "test"]
[puppetlabs/trapperkeeper-webserver-jetty9 "0.3.5"]]}}
:deploy-repositories [["releases" {:url "https://clojars.org/repo"
:username :env/clojars_jenkins_username

View file

@ -0,0 +1,28 @@
package com.puppetlabs.http.client;
// This is really dumb, but I didn't want to leak the HTTPKit class into the
// API for now.
public enum HttpMethod {
GET(org.httpkit.HttpMethod.GET),
HEAD(org.httpkit.HttpMethod.HEAD),
POST(org.httpkit.HttpMethod.POST),
PUT(org.httpkit.HttpMethod.PUT),
DELETE(org.httpkit.HttpMethod.DELETE),
TRACE(org.httpkit.HttpMethod.TRACE),
OPTIONS(org.httpkit.HttpMethod.OPTIONS),
CONNECT(org.httpkit.HttpMethod.CONNECT),
PATCH(org.httpkit.HttpMethod.PATCH);
private org.httpkit.HttpMethod httpKitMethod;
HttpMethod(org.httpkit.HttpMethod httpKitMethod) {
this.httpKitMethod = httpKitMethod;
}
public org.httpkit.HttpMethod getValue() {
return this.httpKitMethod;
}
}

View file

@ -0,0 +1,46 @@
package com.puppetlabs.http.client;
import com.puppetlabs.http.client.HttpResponse;
import com.puppetlabs.http.client.RequestOptions;
import java.util.Map;
public class HttpResponse {
private RequestOptions options;
private Throwable error;
private Object body;
private Map<String, Object> headers;
private Integer status;
public HttpResponse(RequestOptions options, Throwable error) {
this.options = options;
this.error = error;
}
public HttpResponse(RequestOptions options, Object body, Map<String, Object> headers, int status) {
this.options = options;
this.body = body;
this.headers = headers;
this.status = status;
}
public RequestOptions getOptions() {
return options;
}
public Throwable getError() {
return error;
}
public Object getBody() {
return body;
}
public Map<String, Object> getHeaders() {
return headers;
}
public Integer getStatus() {
return status;
}
}

View file

@ -0,0 +1,258 @@
package com.puppetlabs.http.client;
import com.puppetlabs.http.client.impl.*;
import org.httpkit.client.HttpClient;
import org.httpkit.client.IFilter;
import org.httpkit.client.MultipartEntity;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
public class RequestOptions {
private HttpClient client = DefaultClient.getInstance();
private int timeout = 60000;
private boolean followRedirects = true;
private int maxRedirects = 10;
// TODO: we are technically leaking this http-kit class into our API,
// but since we're not using it anywhere I decided not to worry about it yet.
private IFilter filter = IFilter.ACCEPT_ALL;
private ExecutorService workerPool = DefaultWorkerPool.getInstance();
private Promise<HttpResponse> promise = new Promise<HttpResponse>();
private int keepalive = 120000;
private ResponseBodyType as = ResponseBodyType.AUTO;
private String url;
private HttpMethod method = null;
private List<String> traceRedirects = new ArrayList<String>();
private Map<String, Object> headers;
private Map<String, String> formParams;
private BasicAuth basicAuth;
private String oauthToken;
private String userAgent;
private Map<String, String> queryParams;
private SSLEngine sslEngine;
private SSLContext sslContext;
private String sslCert;
private String sslKey;
private String sslCaCert;
private boolean insecure = false;
private Object body;
// TODO: we are technically leaking this http-kit class into our API,
// but since we're not using it anywhere I decided not to worry about it yet.
private List<MultipartEntity> multipartEntities;
public RequestOptions(String url) {
this.url = url;
}
public HttpClient getClient() {
return client;
}
public RequestOptions setClient(HttpClient client) {
this.client = client;
return this;
}
public String getUrl() {
return url;
}
public RequestOptions setUrl(String url) {
this.url = url;
return this;
}
public int getTimeout() {
return timeout;
}
public RequestOptions setTimeout(int timeout) {
this.timeout = timeout;
return this;
}
public int getKeepalive() {
return keepalive;
}
public RequestOptions setKeepalive(int keepalive) {
this.keepalive = keepalive;
return this;
}
public boolean getFollowRedirects() {
return followRedirects;
}
public RequestOptions setFollowRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
return this;
}
public int getMaxRedirects() {
return maxRedirects;
}
public RequestOptions setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}
public ResponseBodyType getAs() {
return as;
}
public RequestOptions setAs(ResponseBodyType as) {
this.as = as;
return this;
}
public HttpMethod getMethod() {
return method;
}
public RequestOptions setMethod(HttpMethod method) {
this.method = method;
return this;
}
public IFilter getFilter() {
return filter;
}
public RequestOptions setFilter(IFilter filter) {
this.filter = filter;
return this;
}
public ExecutorService getWorkerPool() {
return workerPool;
}
public Promise<HttpResponse> getPromise() {
return this.promise;
}
public RequestOptions setPromise(Promise<HttpResponse> promise) {
this.promise = promise;
return this;
}
public List<String> getTraceRedirects() {
return traceRedirects;
}
public RequestOptions addTraceRedirect(String url) {
traceRedirects.add(url);
return this;
}
public Map<String, Object> getHeaders() {
return headers;
}
public RequestOptions setHeaders(Map<String, Object> headers) {
this.headers = headers;
return this;
}
public Map<String, String> getFormParams() {
return formParams;
}
public RequestOptions setFormParams(Map<String, String> formParams) {
this.formParams = formParams;
return this;
}
public BasicAuth getBasicAuth() {
return basicAuth;
}
public RequestOptions setBasicAuth(BasicAuth basicAuth) {
this.basicAuth = basicAuth;
return this;
}
public String getOAuthToken() {
return oauthToken;
}
public RequestOptions setOAuthToken(String oauthToken) {
this.oauthToken = oauthToken;
return this;
}
public String getUserAgent() {
return userAgent;
}
public RequestOptions setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public Map<String, String> getQueryParams() {
return queryParams;
}
public RequestOptions setQueryParams(Map<String, String> queryParams) {
this.queryParams = queryParams;
return this;
}
public SSLEngine getSslEngine() {
return sslEngine;
}
public RequestOptions setSslEngine(SSLEngine sslEngine) {
this.sslEngine = sslEngine;
return this;
}
public SSLContext getSslContext() {
return sslContext;
}
public RequestOptions setSslContext(SSLContext sslContext) {
this.sslContext = sslContext;
return this;
}
public String getSslCert() {
return sslCert;
}
public RequestOptions setSslCert(String sslCert) {
this.sslCert = sslCert;
return this;
}
public String getSslKey() {
return sslKey;
}
public RequestOptions setSslKey(String sslKey) {
this.sslKey = sslKey;
return this;
}
public String getSslCaCert() {
return sslCaCert;
}
public RequestOptions setSslCaCert(String sslCaCert) {
this.sslCaCert = sslCaCert;
return this;
}
public boolean getInsecure() {
return insecure;
}
public RequestOptions setInsecure(boolean insecure) {
this.insecure = insecure;
return this;
}
public Object getBody() {
return body;
}
public RequestOptions setBody(Object body) {
this.body = body;
return this;
}
public List<MultipartEntity> getMultipartEntities() {
return multipartEntities;
}
public RequestOptions setMultipartEntities(List<MultipartEntity> entities) {
this.multipartEntities = entities;
return this;
}
}

View file

@ -0,0 +1,19 @@
package com.puppetlabs.http.client;
public enum ResponseBodyType {
AUTO(1),
TEXT(2),
STREAM(3),
BYTE_ARRAY(4);
private int value;
ResponseBodyType(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}

View file

@ -0,0 +1,117 @@
package com.puppetlabs.http.client;
import com.puppetlabs.http.client.HttpResponse;
import com.puppetlabs.http.client.impl.JavaClient;
import com.puppetlabs.http.client.impl.Promise;
import com.puppetlabs.http.client.impl.SslUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
public class SyncHttpClient {
private static final Logger LOGGER = LoggerFactory.getLogger(SyncHttpClient.class);
private static void logAndRethrow(String msg, Throwable t) {
LOGGER.error(msg, t);
throw new RuntimeException(msg, t);
}
private static RequestOptions configureSslFromContext(RequestOptions options) {
options.setSslEngine(options.getSslContext().createSSLEngine());
options.setSslContext(null);
return options;
}
// TODO: move this into the async java API if we ever add one
private static RequestOptions configureSsl(RequestOptions options) {
if (options.getSslEngine() != null) {
return options;
}
if (options.getSslContext() != null) {
return configureSslFromContext(options);
}
if ((options.getSslCert() != null) &&
(options.getSslKey() != null) &&
(options.getSslCaCert() != null)) {
try {
options.setSslContext(
SslUtils.pemsToSSLContext(
new FileReader(options.getSslCert()),
new FileReader(options.getSslKey()),
new FileReader(options.getSslCaCert()))
);
} catch (KeyStoreException e) {
logAndRethrow("Error while configuring SSL", e);
} catch (CertificateException e) {
logAndRethrow("Error while configuring SSL", e);
} catch (IOException e) {
logAndRethrow("Error while configuring SSL", e);
} catch (NoSuchAlgorithmException e) {
logAndRethrow("Error while configuring SSL", e);
} catch (KeyManagementException e) {
logAndRethrow("Error while configuring SSL", e);
} catch (UnrecoverableKeyException e) {
logAndRethrow("Error while configuring SSL", e);
}
options.setSslCert(null);
options.setSslKey(null);
options.setSslCaCert(null);
return configureSslFromContext(options);
}
return options;
}
public static HttpResponse request(RequestOptions options) {
// TODO: if we end up implementing an async version of the java API,
// we should refactor this implementation so that it is based on the
// async one, as Patrick has done in the clojure API.
options = configureSsl(options);
Promise<HttpResponse> promise = null;
try {
promise = JavaClient.request(options, null);
} catch (IOException e) {
logAndRethrow("Error submitting http request", e);
}
HttpResponse response = null;
try {
response = promise.deref();
} catch (InterruptedException e) {
logAndRethrow("Error while waiting for http response", e);
}
if (response.getError() != null) {
logAndRethrow("Error executing http request", response.getError());
}
return response;
}
public static HttpResponse get(String url) {
return get(new RequestOptions(url));
}
public static HttpResponse get(RequestOptions requestOptions) {
return request(requestOptions.setMethod(HttpMethod.GET));
}
public static HttpResponse post(String url) {
return post(new RequestOptions(url));
}
public static HttpResponse post(RequestOptions requestOptions) {
return request(requestOptions.setMethod(HttpMethod.POST));
}
}

View file

@ -0,0 +1,19 @@
package com.puppetlabs.http.client.impl;
public class BasicAuth {
private final String user;
private final String password;
public BasicAuth(String user, String password) {
this.user = user;
this.password = password;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
}

View file

@ -0,0 +1,47 @@
package com.puppetlabs.http.client.impl;
import org.httpkit.HttpMethod;
import javax.net.ssl.SSLEngine;
import java.util.Map;
public class CoercedRequestOptions {
private final String url;
private final HttpMethod method;
private final Map<String, Object> headers;
private final Object body;
private final SSLEngine sslEngine;
public CoercedRequestOptions(String url,
HttpMethod method,
Map<String, Object> headers,
Object body,
SSLEngine sslEngine) {
this.url = url;
this.method = method;
this.headers = headers;
this.body = body;
this.sslEngine = sslEngine;
}
public String getUrl() {
return url;
}
public HttpMethod getMethod() {
return method;
}
public Map<String, Object> getHeaders() {
return headers;
}
public Object getBody() {
return body;
}
public SSLEngine getSslEngine() {
return sslEngine;
}
}

View file

@ -0,0 +1,20 @@
package com.puppetlabs.http.client.impl;
import org.httpkit.client.HttpClient;
import java.io.IOException;
public class DefaultClient {
private static HttpClient instance;
public synchronized static HttpClient getInstance() {
if (instance == null) {
try {
instance = new HttpClient();
} catch (IOException e) {
throw new RuntimeException("Error attempting to instantiate HttpClient", e);
}
}
return instance;
}
}

View file

@ -0,0 +1,21 @@
package com.puppetlabs.http.client.impl;
import org.httpkit.PrefixThreadFactory;
import java.util.concurrent.*;
public class DefaultWorkerPool {
private static ExecutorService instance;
public static synchronized ExecutorService getInstance() {
if (instance == null) {
int max = Runtime.getRuntime().availableProcessors();
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
PrefixThreadFactory factory = new PrefixThreadFactory("client-worker-");
instance = new ThreadPoolExecutor(0, max, 60, TimeUnit.SECONDS, queue, factory);
}
return instance;
}
}

View file

@ -0,0 +1,7 @@
package com.puppetlabs.http.client.impl;
import com.puppetlabs.http.client.HttpResponse;
public interface IResponseCallback {
HttpResponse handleResponse(HttpResponse response);
}

View file

@ -0,0 +1,146 @@
package com.puppetlabs.http.client.impl;
import com.puppetlabs.http.client.HttpResponse;
import com.puppetlabs.http.client.RequestOptions;
import org.httpkit.HttpMethod;
import org.httpkit.client.*;
import javax.net.ssl.SSLEngine;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
public class JavaClient {
private static HttpClient defaultClient = null;
private static HttpClient getDefaultClient() throws IOException {
if (defaultClient == null) {
defaultClient = new HttpClient();
}
return defaultClient;
}
private static String buildQueryString(Map<String, String> params) {
// TODO: add support for nested query params. For now we assume a flat,
// String->String data structure.
StringBuilder sb = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> entry : params.entrySet()) {
if (!first) {
sb.append("&");
}
first = false;
try {
sb.append(URLEncoder.encode(entry.getKey(), "utf8"));
sb.append("=");
sb.append(URLEncoder.encode(entry.getValue(), "utf8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Error while url-encoding query string", e);
}
}
return sb.toString();
}
private static String getBasicAuthValue(BasicAuth auth) {
String userPasswordStr = auth.getUser() + ":" + auth.getPassword();
try {
return "Basic " + DatatypeConverter.printBase64Binary(userPasswordStr.getBytes("utf8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Error while attmempting to encode basic auth", e);
}
}
private static Map<String, Object> prepareHeaders(RequestOptions options) {
Map<String, Object> result = new HashMap<String, Object>();
if (options.getHeaders() != null) {
for (Map.Entry<String, Object> entry : options.getHeaders().entrySet()) {
result.put(entry.getKey(), entry.getValue());
}
}
if (options.getFormParams() != null) {
result.put("Content-Type", "application/x-www-form-urlencoded");
}
if (options.getBasicAuth() != null) {
result.put("Authorization", getBasicAuthValue(options.getBasicAuth()));
}
if (options.getOAuthToken() != null) {
result.put("Authorization", "Bearer " + options.getOAuthToken());
}
if (options.getUserAgent() != null) {
result.put("User-Agent", options.getUserAgent());
}
return result;
}
private static CoercedRequestOptions coerceRequestOptions(RequestOptions options) throws IOException {
String url;
if (options.getQueryParams() != null) {
if (options.getUrl().indexOf('?') == -1) {
url = options.getUrl() + "?" + buildQueryString(options.getQueryParams());
} else {
url = options.getUrl() + "&" + buildQueryString(options.getQueryParams());
}
} else {
url = options.getUrl();
}
SSLEngine sslEngine = null;
if (options.getSslEngine() != null) {
sslEngine = options.getSslEngine();
} else if (options.getInsecure()) {
sslEngine = SslContextFactory.trustAnybody();
}
HttpMethod method = options.getMethod().getValue();
if (method == null) {
method = HttpMethod.GET;
}
Map<String, Object> headers = prepareHeaders(options);
Object body;
if (options.getFormParams() != null) {
body = buildQueryString(options.getFormParams());
} else {
body = options.getBody();
}
if (options.getMultipartEntities() != null) {
String boundary = MultipartEntity.genBoundary(options.getMultipartEntities());
headers = options.getHeaders();
headers.put("Content-Type", "multipart/form-data; boundary=" + boundary);
body = MultipartEntity.encode(boundary, options.getMultipartEntities());
}
return new CoercedRequestOptions(url, method, headers, body, sslEngine);
}
public static Promise<HttpResponse> request(RequestOptions options, IResponseCallback callback)
throws IOException {
HttpClient client = options.getClient();
if (client == null) {
client = getDefaultClient();
}
CoercedRequestOptions coercedOptions = coerceRequestOptions(options);
RequestConfig config = new RequestConfig(coercedOptions.getMethod(),
coercedOptions.getHeaders(), coercedOptions.getBody(),
options.getTimeout(), options.getKeepalive());
RespListener listener = new RespListener(
new ResponseHandler(options, coercedOptions, callback), options.getFilter(),
options.getWorkerPool(), options.getAs().getValue());
client.exec(options.getUrl(), config, coercedOptions.getSslEngine(), listener);
return options.getPromise();
}
}

View file

@ -0,0 +1,26 @@
package com.puppetlabs.http.client.impl;
import java.util.concurrent.CountDownLatch;
public class Promise<T> {
private final CountDownLatch latch;
private T value = null;
public Promise() {
latch = new CountDownLatch(1);
}
public synchronized void deliver(T t) {
if (value != null) {
throw new IllegalStateException("Attempting to deliver value to a promise that has already been realized!");
}
value = t;
latch.countDown();
}
public T deref() throws InterruptedException {
latch.await();
return value;
}
}

View file

@ -0,0 +1,88 @@
package com.puppetlabs.http.client.impl;
import com.puppetlabs.http.client.HttpMethod;
import com.puppetlabs.http.client.HttpResponse;
import com.puppetlabs.http.client.RequestOptions;
import org.httpkit.HttpUtils;
import org.httpkit.client.IResponseHandler;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class ResponseHandler implements IResponseHandler {
private static final Set<Integer> REDIRECT_STATUS_CODES =
new HashSet<Integer>(Arrays.asList(301, 302, 303, 307, 308));
private final RequestOptions options;
private final CoercedRequestOptions coercedOptions;
private final IResponseCallback callback;
public ResponseHandler(RequestOptions options,
CoercedRequestOptions coercedOptions,
IResponseCallback callback) {
this.options = options;
this.coercedOptions = coercedOptions;
this.callback = callback;
}
private HttpMethod getNewMethod(int status) {
if (status == 301 || status == 302 || status == 303) {
return HttpMethod.GET;
} else {
return options.getMethod();
}
}
private void deliverResponse(HttpResponse response) {
HttpResponse finalResponse = response;
try {
if (callback != null) {
finalResponse = callback.handleResponse(response);
}
} catch (Exception e) {
// dump stacktrace to stderr
HttpUtils.printError(coercedOptions.getMethod() + " " +
coercedOptions.getUrl() + "'s callback", e);
// return the error
options.getPromise().deliver(new HttpResponse(options, e));
}
options.getPromise().deliver(finalResponse);
}
@Override
public void onSuccess(int status, Map<String, Object> headers, Object body) {
if (options.getFollowRedirects() && REDIRECT_STATUS_CODES.contains(status)) {
if (options.getMaxRedirects() >= options.getTraceRedirects().size()) {
// follow 301 and 302 redirect
try {
JavaClient.request(
options.setUrl(new URI(coercedOptions.getUrl()).resolve((String) headers.get("location")).toString())
.setMethod(getNewMethod(status))
.addTraceRedirect(coercedOptions.getUrl()),
callback);
} catch (IOException e) {
throw new RuntimeException("Error when attempting redirect", e);
} catch (URISyntaxException e) {
throw new RuntimeException("Error when attempting redirect", e);
}
} else {
deliverResponse(new HttpResponse(options,
new Exception("too many redirects: " + options.getTraceRedirects().size())));
}
} else {
deliverResponse(new HttpResponse(options, body, headers, status));
}
}
@Override
public void onThrowable(Throwable t) {
deliverResponse(new HttpResponse(options, t));
}
}

View file

@ -0,0 +1,349 @@
package com.puppetlabs.http.client.impl;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMException;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Map;
import java.util.HashMap;
import java.util.UUID;
public class SslUtils {
// TODO: this code is copied verbatim from the CA library; we should get rid
// of this file and just specify a dependency on that library once we release it
/**
* Given a PEM reader, decode the contents into a collection of objects of the corresponding
* type from the java.security package.
*
* @param reader Reader for a PEM-encoded stream
* @return The list of decoded objects from the stream
* @throws IOException
* @see #writeToPEM
*/
public static List<Object> pemToObjects(Reader reader)
throws IOException
{
PEMParser parser = new PEMParser(reader);
List<Object> results = new ArrayList<Object>();
for (Object o = parser.readObject(); o != null; o = parser.readObject())
results.add(o);
return results;
}
/**
* Decodes the provided object (read from a PEM stream via {@link #pemToObjects}) into a private key.
*
* @param obj The object to decode into a PrivateKey
* @return The PrivateKey decoded from the object
* @throws PEMException
* @see #pemToPrivateKey
* @see #pemToPrivateKeys
*/
public static PrivateKey objectToPrivateKey(Object obj)
throws PEMException
{
// Certain PEMs will hand back a keypair with a nil public key
if (obj instanceof PrivateKeyInfo)
return new JcaPEMKeyConverter().getPrivateKey((PrivateKeyInfo) obj);
else if (obj instanceof PEMKeyPair)
return new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) obj).getPrivate();
else
throw new IllegalArgumentException("Expected a KeyPair or PrivateKey, got " + obj);
}
/**
* Given a PEM reader, decode the contents into a list of private keys.
*
* @param reader Reader for a PEM-encoded stream
* @return The list of decoded private keys from the stream
* @throws IOException
* @throws PEMException
* @see #pemToPrivateKey
* @see #writeToPEM
*/
public static List<PrivateKey> pemToPrivateKeys(Reader reader)
throws IOException, PEMException
{
List<Object> objects = pemToObjects(reader);
List<PrivateKey> results = new ArrayList<PrivateKey>(objects.size());
for (Object o : objects)
results.add(objectToPrivateKey(o));
return results;
}
/**
* Given a PEM reader, decode the contents into a private key.
* Throws an exception if multiple keys are found.
*
* @param reader Reader for a PEM-encoded stream
* @return The decoded private key from the stream
* @throws IOException
* @throws IllegalArgumentException
* @see #pemToPrivateKeys
* @see #writeToPEM
*/
public static PrivateKey pemToPrivateKey(Reader reader)
throws IOException
{
List<PrivateKey> privateKeys = pemToPrivateKeys(reader);
if (privateKeys.size() != 1)
throw new IllegalArgumentException("The PEM stream must contain exactly one private key");
return privateKeys.get(0);
}
/**
* Given a PEM reader, decode the contents into a list of certificates.
*
* @param reader Reader for a PEM-encoded stream
* @return The list of decoded certificates from the stream
* @throws CertificateException
* @throws IOException
* @see #writeToPEM
*/
public static List<X509Certificate> pemToCerts(Reader reader)
throws CertificateException, IOException
{
JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
List<Object> pemObjects = pemToObjects(reader);
List<X509Certificate> results = new ArrayList<X509Certificate>(pemObjects.size());
for (Object o : pemObjects)
results.add(converter.getCertificate((X509CertificateHolder) o));
return results;
}
/**
* Add a private key to a keystore.
*
* @param keystore The keystore to add the private key to
* @param alias An alias to associate with the private key
* @param privateKey The private key to add to the keystore
* @param password To protect the key in the keystore
* @param cert The certificate for the private key; a private key cannot
* be added to a keystore without a signed certificate
* @return The provided keystore
* @throws KeyStoreException
* @see #associatePrivateKeyFromReader
*/
public static KeyStore associatePrivateKey(KeyStore keystore, String alias, PrivateKey privateKey,
String password, X509Certificate cert)
throws KeyStoreException
{
keystore.setKeyEntry(alias, privateKey, password.toCharArray(), new Certificate[]{cert});
return keystore;
}
/**
* Add the private key from a PEM reader to the keystore.
*
* @param keystore The keystore to add the private key to
* @param alias An alias to associate with the private key
* @param pemPrivateKey Reader for a PEM-encoded stream with the private key
* @param password To protect the key in the keystore
* @param pemCert Reader for a PEM-encoded stream with the certificate; a private
* key cannot be added to a keystore without a signed certificate
* @return The provided keystore
* @throws CertificateException
* @throws KeyStoreException
* @throws IOException
* @see #associatePrivateKey
*/
public static KeyStore associatePrivateKeyFromReader(KeyStore keystore, String alias, Reader pemPrivateKey,
String password, Reader pemCert)
throws CertificateException, KeyStoreException, IOException
{
PrivateKey privateKey = pemToPrivateKey(pemPrivateKey);
List<X509Certificate> certs = pemToCerts(pemCert);
if (certs.size() > 1)
throw new IllegalArgumentException("The PEM stream contains more than one certificate");
X509Certificate firstCert = certs.get(0);
return associatePrivateKey(keystore, alias, privateKey, password, firstCert);
}
/**
* Add a certificate to a keystore.
*
* @param keystore The keystore to add the certificate to
* @param alias An alias to associate with the certificate
* @param cert The certificate to add to the keystore
* @return The provided keystore
* @throws KeyStoreException
* @see #associateCertsFromReader
*/
public static KeyStore associateCert(KeyStore keystore, String alias, X509Certificate cert)
throws KeyStoreException
{
keystore.setCertificateEntry(alias, cert);
return keystore;
}
/**
* Add all certificates from a PEM reader to the keystore.
*
* @param keystore The keystore to add all the certificates to
* @param prefix An alias to associate with the certificates. Each certificate will
* have a numeric index appended to the prefix (starting with '-0')
* @param pem Reader for a PEM-encoded stream of certificates
* @return The provided keystore
* @throws CertificateException
* @throws KeyStoreException
* @throws IOException
* @see #associateCert
*/
public static KeyStore associateCertsFromReader(KeyStore keystore, String prefix, Reader pem)
throws CertificateException, KeyStoreException, IOException
{
List<X509Certificate> certs = pemToCerts(pem);
ListIterator<X509Certificate> iter = certs.listIterator();
for (int i = 0; iter.hasNext(); i++)
associateCert(keystore, prefix + "-" + i, iter.next());
return keystore;
}
/**
* Create an empty in-memory key store.
*
* @return New key store
* @throws KeyStoreException
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws CertificateException
*/
public static KeyStore createKeyStore()
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException
{
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(null);
return ks;
}
/**
* Given PEM readers for a certificate, private key, and CA certificate,
* create an in-memory keystore and truststore.
*
* Returns a map containing the following:
* <ul>
* <li>"keystore" - a keystore initialized with the cert and private key</li>
* <li>"keystore-pw" - a string containing a dynamically generated password for the keystore</li>
* <li>"truststore" - a keystore containing the CA cert</li>
* <ul>
*
* @param cert Reader for a PEM-encoded stream with the certificate
* @param privateKey Reader for a PEM-encoded stream with the correspnding private key
* @param caCert Reader for a PEM-encoded stream with the CA certificate
* @return Map containing the keystore, keystore password, and truststore
* @throws KeyStoreException
* @throws CertificateException
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public static Map<String, Object> pemsToKeyAndTrustStores(Reader cert, Reader privateKey, Reader caCert)
throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException
{
KeyStore truststore = createKeyStore();
associateCertsFromReader(truststore, "CA Certificate", caCert);
KeyStore keystore = createKeyStore();
String keystorePassword = UUID.randomUUID().toString();
associatePrivateKeyFromReader(keystore, "Private Key", privateKey, keystorePassword, cert);
Map<String, Object> result = new HashMap<String, Object>();
result.put("truststore", truststore);
result.put("keystore", keystore);
result.put("keystore-pw", keystorePassword);
return result;
}
/**
* Given a keystore and keystore password (as generated by {@link #pemsToKeyAndTrustStores}),
* return a key manager factory that contains the keystore.
*
* @param keystore The keystore to get a key manager for
* @param password The password for the keystore
* @return A key manager factory for the provided keystore
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws UnrecoverableKeyException
*/
public static KeyManagerFactory getKeyManagerFactory(KeyStore keystore, String password)
throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException
{
KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
factory.init(keystore, password.toCharArray());
return factory;
}
/**
* Given a truststore (as generated by {@link #pemsToKeyAndTrustStores}),
* return a trust manager factory that contains the truststore.
*
* @param truststore The truststore to get a trust manager for
* @return A trust manager factory for the provided truststore
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
*/
public static TrustManagerFactory getTrustManagerFactory(KeyStore truststore)
throws NoSuchAlgorithmException, KeyStoreException
{
TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
factory.init(truststore);
return factory;
}
/**
* Given PEM readers for a certificate, private key, and CA certificate, create an
* in-memory SSL context initialized with a keystore/truststore generated from the
* provided certificates and key.
*
* @param cert Reader for PEM-encoded stream with the certificate
* @param privateKey Reader for PEM-encoded stream with the corresponding private key
* @param caCert Reader for PEM-encoded stream with the CA certificate
* @return The configured SSLContext
* @throws KeyStoreException
* @throws CertificateException
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
* @throws UnrecoverableKeyException
*/
public static SSLContext pemsToSSLContext(Reader cert, Reader privateKey, Reader caCert)
throws KeyStoreException, CertificateException, IOException,
NoSuchAlgorithmException, KeyManagementException, UnrecoverableKeyException
{
Map<String, Object> stores = pemsToKeyAndTrustStores(cert, privateKey, caCert);
KeyStore keystore = (KeyStore) stores.get("keystore");
String password = (String) stores.get("keystore-pw");
KeyStore truststore = (KeyStore) stores.get("truststore");
KeyManagerFactory kmf = getKeyManagerFactory(keystore, password);
TrustManagerFactory tmf = getTrustManagerFactory(truststore);
SSLContext context = SSLContext.getInstance("SSL");
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return context;
}
}

View file

@ -0,0 +1,6 @@
/**
* This package is basically just a straight port of the clojure code from
* the org.httpkit.client namespace, so that we can make requests from Java
* using this same library.
*/
package com.puppetlabs.http.client.impl;

View file

@ -8,9 +8,9 @@
(deftest ssl-config-with-files
(let [req {:url "http://localhost"
:method :get
:ssl-cert (resource "resources/cert.pem")
:ssl-key (resource "resources/key.pem")
:ssl-ca-cert (resource "resources/ca.pem")}
:ssl-cert (resource "ssl/cert.pem")
:ssl-key (resource "ssl/key.pem")
:ssl-ca-cert (resource "ssl/ca.pem")}
configured-req (http/configure-ssl req)]
(testing "configure-ssl sets up an SSLEngine when given cert, key, ca-cert"
@ -33,9 +33,9 @@
(let [req {:url "http://localhost"
:method :get
:ssl-context (ks-ssl/pems->ssl-context
(resource "resources/cert.pem")
(resource "resources/key.pem")
(resource "resources/ca.pem"))}
(resource "ssl/cert.pem")
(resource "ssl/key.pem")
(resource "ssl/ca.pem"))}
configured-req (http/configure-ssl req)]
(testing "configure-ssl uses an existing ssl context"
@ -44,9 +44,9 @@
(deftest ssl-config-with-sslengine
(let [req {:url "http://localhost"
:method :get
:ssl-cert (resource "resources/cert.pem")
:ssl-key (resource "resources/key.pem")
:ssl-ca-cert (resource "resources/ca.pem")
:ssl-cert (resource "ssl/cert.pem")
:ssl-key (resource "ssl/key.pem")
:ssl-ca-cert (resource "ssl/ca.pem")
:sslengine "thing"}
configured-req (http/configure-ssl req)]
(testing "configure-ssl does nothing when :sslengine is given"

View file

@ -0,0 +1,44 @@
(ns puppetlabs.http.client.sync-test
(:import (com.puppetlabs.http.client SyncHttpClient RequestOptions))
(:require [clojure.test :refer :all]
[puppetlabs.trapperkeeper.core :as tk]
[puppetlabs.trapperkeeper.testutils.bootstrap :as testutils]
[puppetlabs.trapperkeeper.testutils.logging :as testlogging]
[puppetlabs.trapperkeeper.services.webserver.jetty9-service :as jetty9]
[puppetlabs.http.client.sync :as sync]))
(defn app
[req]
{:status 200
:body "Hello, World!"})
(tk/defservice test-web-service
[[:WebserverService add-ring-handler]]
(init [this context]
(add-ring-handler app "/hello")
context))
(deftest sync-client-test
(testlogging/with-test-logging
(testutils/with-app-with-config app
[jetty9/jetty9-service test-web-service]
{:webserver {:ssl-host "0.0.0.0"
:ssl-port 10080
:ssl-ca-cert "./dev-resources/ssl/ca.pem"
:ssl-cert "./dev-resources/ssl/cert.pem"
:ssl-key "./dev-resources/ssl/key.pem"}}
(testing "java sync client"
(let [options (.. (RequestOptions. "https://localhost:10080/hello/")
(setSslCert "./dev-resources/ssl/cert.pem")
(setSslKey "./dev-resources/ssl/key.pem")
(setSslCaCert "./dev-resources/ssl/ca.pem"))
response (SyncHttpClient/get options)]
(is (= 200 (.getStatus response)))
(is (= "Hello, World!" (slurp (.getBody response))))))
(testing "clojure sync client"
(let [response (sync/get "https://localhost:10080/hello/"
{:ssl-cert "./dev-resources/ssl/cert.pem"
:ssl-key "./dev-resources/ssl/key.pem"
:ssl-ca-cert "./dev-resources/ssl/ca.pem"})]
(is (= 200 (:status response)))
(is (= "Hello, World!" (slurp (:body response)))))))))