From a90b6fd82c725fdbda6b4733902f0dfa65b7c6e3 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Thu, 27 Mar 2014 12:46:44 -0700 Subject: [PATCH 1/7] Move clojure source into clj directory --- project.clj | 3 +++ src/{ => clj}/puppetlabs/http/client/async.clj | 0 src/{ => clj}/puppetlabs/http/client/sync.clj | 0 3 files changed, 3 insertions(+) rename src/{ => clj}/puppetlabs/http/client/async.clj (100%) rename src/{ => clj}/puppetlabs/http/client/sync.clj (100%) diff --git a/project.clj b/project.clj index 8bf4261..1627436 100644 --- a/project.clj +++ b/project.clj @@ -7,6 +7,9 @@ [http-kit "2.1.16"] [puppetlabs/kitchensink "0.5.2"]] + :source-paths ["src/clj"] + :java-source-paths ["src/java"] + :deploy-repositories [["releases" {:url "https://clojars.org/repo" :username :env/clojars_jenkins_username :password :env/clojars_jenkins_password diff --git a/src/puppetlabs/http/client/async.clj b/src/clj/puppetlabs/http/client/async.clj similarity index 100% rename from src/puppetlabs/http/client/async.clj rename to src/clj/puppetlabs/http/client/async.clj diff --git a/src/puppetlabs/http/client/sync.clj b/src/clj/puppetlabs/http/client/sync.clj similarity index 100% rename from src/puppetlabs/http/client/sync.clj rename to src/clj/puppetlabs/http/client/sync.clj From f2373565e5fb44600b0f4af0221ee4414504913d Mon Sep 17 00:00:00 2001 From: Chris Price Date: Thu, 27 Mar 2014 12:49:54 -0700 Subject: [PATCH 2/7] Add travis config --- .gitignore | 1 + .travis.yml | 10 ++++++++++ README.md | 2 ++ ext/travisci/test.sh | 3 +++ 4 files changed, 16 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100755 ext/travisci/test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e2cccfe --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: clojure +lein: lein2 +jdk: + - oraclejdk7 + - openjdk7 + - openjdk6 +script: ./ext/travisci/test.sh +notifications: + email: false + diff --git a/README.md b/README.md index 6ca8846..df1bafe 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ext/travisci/test.sh b/ext/travisci/test.sh new file mode 100755 index 0000000..db011da --- /dev/null +++ b/ext/travisci/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +lein2 test From 075afd5fdb995e48611ba485c5f870d94c23223d Mon Sep 17 00:00:00 2001 From: Chris Price Date: Thu, 27 Mar 2014 17:20:42 -0700 Subject: [PATCH 3/7] Initial java port of the clojure request code --- .../http/client/SyncHttpClient.java | 4 + .../http/client/impl/BasicAuth.java | 19 ++ .../client/impl/CoercedRequestOptions.java | 47 ++++ .../http/client/impl/DefaultClient.java | 20 ++ .../http/client/impl/DefaultWorkerPool.java | 21 ++ .../http/client/impl/HttpResponse.java | 43 ++++ .../http/client/impl/IResponseCallback.java | 5 + .../http/client/impl/JavaClient.java | 144 ++++++++++++ .../puppetlabs/http/client/impl/Promise.java | 26 +++ .../http/client/impl/RequestOptions.java | 215 ++++++++++++++++++ .../http/client/impl/ResponseBodyType.java | 19 ++ .../http/client/impl/ResponseHandler.java | 86 +++++++ .../http/client/impl/package-info.java | 6 + 13 files changed, 655 insertions(+) create mode 100644 src/java/com/puppetlabs/http/client/SyncHttpClient.java create mode 100644 src/java/com/puppetlabs/http/client/impl/BasicAuth.java create mode 100644 src/java/com/puppetlabs/http/client/impl/CoercedRequestOptions.java create mode 100644 src/java/com/puppetlabs/http/client/impl/DefaultClient.java create mode 100644 src/java/com/puppetlabs/http/client/impl/DefaultWorkerPool.java create mode 100644 src/java/com/puppetlabs/http/client/impl/HttpResponse.java create mode 100644 src/java/com/puppetlabs/http/client/impl/IResponseCallback.java create mode 100644 src/java/com/puppetlabs/http/client/impl/JavaClient.java create mode 100644 src/java/com/puppetlabs/http/client/impl/Promise.java create mode 100644 src/java/com/puppetlabs/http/client/impl/RequestOptions.java create mode 100644 src/java/com/puppetlabs/http/client/impl/ResponseBodyType.java create mode 100644 src/java/com/puppetlabs/http/client/impl/ResponseHandler.java create mode 100644 src/java/com/puppetlabs/http/client/impl/package-info.java diff --git a/src/java/com/puppetlabs/http/client/SyncHttpClient.java b/src/java/com/puppetlabs/http/client/SyncHttpClient.java new file mode 100644 index 0000000..8b356e1 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/SyncHttpClient.java @@ -0,0 +1,4 @@ +package com.puppetlabs.http.client; + +public class SyncHttpClient { +} diff --git a/src/java/com/puppetlabs/http/client/impl/BasicAuth.java b/src/java/com/puppetlabs/http/client/impl/BasicAuth.java new file mode 100644 index 0000000..4f12a28 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/BasicAuth.java @@ -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; + } +} diff --git a/src/java/com/puppetlabs/http/client/impl/CoercedRequestOptions.java b/src/java/com/puppetlabs/http/client/impl/CoercedRequestOptions.java new file mode 100644 index 0000000..1cb266e --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/CoercedRequestOptions.java @@ -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 headers; + private final Object body; + private final SSLEngine sslEngine; + + + public CoercedRequestOptions(String url, + HttpMethod method, + Map 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 getHeaders() { + return headers; + } + + public Object getBody() { + return body; + } + + public SSLEngine getSslEngine() { + return sslEngine; + } +} diff --git a/src/java/com/puppetlabs/http/client/impl/DefaultClient.java b/src/java/com/puppetlabs/http/client/impl/DefaultClient.java new file mode 100644 index 0000000..3dbb592 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/DefaultClient.java @@ -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; + } +} diff --git a/src/java/com/puppetlabs/http/client/impl/DefaultWorkerPool.java b/src/java/com/puppetlabs/http/client/impl/DefaultWorkerPool.java new file mode 100644 index 0000000..7087899 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/DefaultWorkerPool.java @@ -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 queue = new LinkedBlockingQueue(); + PrefixThreadFactory factory = new PrefixThreadFactory("client-worker-"); + + instance = new ThreadPoolExecutor(0, max, 60, TimeUnit.SECONDS, queue, factory); + } + return instance; + } +} diff --git a/src/java/com/puppetlabs/http/client/impl/HttpResponse.java b/src/java/com/puppetlabs/http/client/impl/HttpResponse.java new file mode 100644 index 0000000..20a0bdf --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/HttpResponse.java @@ -0,0 +1,43 @@ +package com.puppetlabs.http.client.impl; + +import java.util.Map; + +public class HttpResponse { + private RequestOptions options = null; + private Throwable error = null; + private Object body = null; + private Map headers = null; + private Integer status = null; + + public HttpResponse(RequestOptions options, Throwable error) { + this.options = options; + this.error = error; + } + + public HttpResponse(RequestOptions options, Object body, Map 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 getHeaders() { + return headers; + } + + public Integer getStatus() { + return status; + } +} diff --git a/src/java/com/puppetlabs/http/client/impl/IResponseCallback.java b/src/java/com/puppetlabs/http/client/impl/IResponseCallback.java new file mode 100644 index 0000000..0e83385 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/IResponseCallback.java @@ -0,0 +1,5 @@ +package com.puppetlabs.http.client.impl; + +public interface IResponseCallback { + HttpResponse handleResponse(HttpResponse response); +} diff --git a/src/java/com/puppetlabs/http/client/impl/JavaClient.java b/src/java/com/puppetlabs/http/client/impl/JavaClient.java new file mode 100644 index 0000000..d55534f --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/JavaClient.java @@ -0,0 +1,144 @@ +package com.puppetlabs.http.client.impl; + +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 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 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 prepareHeaders(RequestOptions options) { + Map result; + if (options.getHeaders() != null) { + result = (Map) options.getHeaders().clone(); + } else { + result = new HashMap(); + } + + 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(); + if (method == null) { + method = HttpMethod.GET; + } + + Map 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 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(); + } +} diff --git a/src/java/com/puppetlabs/http/client/impl/Promise.java b/src/java/com/puppetlabs/http/client/impl/Promise.java new file mode 100644 index 0000000..afc4976 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/Promise.java @@ -0,0 +1,26 @@ +package com.puppetlabs.http.client.impl; + +import java.util.concurrent.CountDownLatch; + +public class Promise { + 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; + } + +} diff --git a/src/java/com/puppetlabs/http/client/impl/RequestOptions.java b/src/java/com/puppetlabs/http/client/impl/RequestOptions.java new file mode 100644 index 0000000..c7fa8d1 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/RequestOptions.java @@ -0,0 +1,215 @@ +package com.puppetlabs.http.client.impl; + +import org.httpkit.HttpMethod; +import org.httpkit.client.HttpClient; +import org.httpkit.client.IFilter; +import org.httpkit.client.MultipartEntity; + +import javax.net.ssl.SSLEngine; +import java.util.ArrayList; +import java.util.HashMap; +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; + private IFilter filter = IFilter.ACCEPT_ALL; + private ExecutorService workerPool = DefaultWorkerPool.getInstance(); + private Promise promise = new Promise(); + private int keepalive = 120000; + private ResponseBodyType as = ResponseBodyType.AUTO; + + private String url; + private HttpMethod method = null; + private List traceRedirects = new ArrayList(); + private HashMap headers; + private Map formParams; + private BasicAuth basicAuth; + private String oauthToken; + private String userAgent; + private Map queryParams; + private SSLEngine sslEngine; + private boolean insecure = false; + private Object body; + private List 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 getPromise() { + return this.promise; + } + public RequestOptions setPromise(Promise promise) { + this.promise = promise; + return this; + } + + public List getTraceRedirects() { + return traceRedirects; + } + public RequestOptions addTraceRedirect(String url) { + traceRedirects.add(url); + return this; + } + + public HashMap getHeaders() { + return headers; + } + public RequestOptions setHeaders(HashMap headers) { + this.headers = headers; + return this; + } + + public Map getFormParams() { + return formParams; + } + public RequestOptions setFormParams(Map 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 getQueryParams() { + return queryParams; + } + public RequestOptions setQueryParams(Map queryParams) { + this.queryParams = queryParams; + return this; + } + + public SSLEngine getSslEngine() { + return sslEngine; + } + public RequestOptions setSslEngine(SSLEngine sslEngine) { + this.sslEngine = sslEngine; + 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 getMultipartEntities() { + return multipartEntities; + } + public RequestOptions setMultipartEntities(List entities) { + this.multipartEntities = entities; + return this; + } +} diff --git a/src/java/com/puppetlabs/http/client/impl/ResponseBodyType.java b/src/java/com/puppetlabs/http/client/impl/ResponseBodyType.java new file mode 100644 index 0000000..c8b1af9 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/ResponseBodyType.java @@ -0,0 +1,19 @@ +package com.puppetlabs.http.client.impl; + +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; + } + + +} diff --git a/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java b/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java new file mode 100644 index 0000000..f8e045a --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java @@ -0,0 +1,86 @@ +package com.puppetlabs.http.client.impl; + +import org.httpkit.HttpMethod; +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 REDIRECT_STATUS_CODES = + new HashSet(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 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)); + } +} diff --git a/src/java/com/puppetlabs/http/client/impl/package-info.java b/src/java/com/puppetlabs/http/client/impl/package-info.java new file mode 100644 index 0000000..915993f --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/package-info.java @@ -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; \ No newline at end of file From f93b3a9d9caef9193ad59392854141b838c30e30 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Thu, 27 Mar 2014 17:57:12 -0700 Subject: [PATCH 4/7] Initial Java implementation of synchronous GET --- .../http/client/SyncHttpClient.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/java/com/puppetlabs/http/client/SyncHttpClient.java b/src/java/com/puppetlabs/http/client/SyncHttpClient.java index 8b356e1..106026d 100644 --- a/src/java/com/puppetlabs/http/client/SyncHttpClient.java +++ b/src/java/com/puppetlabs/http/client/SyncHttpClient.java @@ -1,4 +1,38 @@ package com.puppetlabs.http.client; +import com.puppetlabs.http.client.impl.HttpResponse; +import com.puppetlabs.http.client.impl.JavaClient; +import com.puppetlabs.http.client.impl.Promise; +import com.puppetlabs.http.client.impl.RequestOptions; +import org.httpkit.HttpMethod; + +import java.io.IOException; + public class SyncHttpClient { + public static HttpResponse request(RequestOptions options) { + Promise promise = null; + try { + promise = JavaClient.request(options, null); + } catch (IOException e) { + throw new RuntimeException("Error submitting http request", e); + } + HttpResponse response = null; + try { + response = promise.deref(); + } catch (InterruptedException e) { + throw new RuntimeException("Error while waiting for http response", e); + } + if (response.getError() != null) { + throw new RuntimeException("Error in http request", response.getError()); + } + return response; + } + + public static HttpResponse get(String url) { + return get(new RequestOptions(url)); + } + + private static HttpResponse get(RequestOptions requestOptions) { + return request(requestOptions.setMethod(HttpMethod.GET)); + } } From 9020eed85f7afc14ce21157b1905445f00b82736 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Fri, 28 Mar 2014 15:33:23 -0700 Subject: [PATCH 5/7] Add support for configuring SSL via puppet pems --- project.clj | 4 +- .../puppetlabs/http/client/HttpMethod.java | 28 ++ .../client/{impl => }/RequestOptions.java | 55 ++- .../http/client/SyncHttpClient.java | 91 ++++- .../http/client/impl/HttpResponse.java | 2 + .../http/client/impl/JavaClient.java | 11 +- .../http/client/impl/ResponseHandler.java | 3 +- .../puppetlabs/http/client/impl/SslUtils.java | 349 ++++++++++++++++++ 8 files changed, 524 insertions(+), 19 deletions(-) create mode 100644 src/java/com/puppetlabs/http/client/HttpMethod.java rename src/java/com/puppetlabs/http/client/{impl => }/RequestOptions.java (78%) create mode 100644 src/java/com/puppetlabs/http/client/impl/SslUtils.java diff --git a/project.clj b/project.clj index 1627436..f09c030 100644 --- a/project.clj +++ b/project.clj @@ -5,7 +5,9 @@ :dependencies [[org.clojure/clojure "1.5.1"] [http-kit "2.1.16"] - [puppetlabs/kitchensink "0.5.2"]] + [puppetlabs/kitchensink "0.5.2"] + [org.clojure/tools.logging "0.2.6"] + [org.slf4j/slf4j-api "1.7.6"]] :source-paths ["src/clj"] :java-source-paths ["src/java"] diff --git a/src/java/com/puppetlabs/http/client/HttpMethod.java b/src/java/com/puppetlabs/http/client/HttpMethod.java new file mode 100644 index 0000000..55dee9f --- /dev/null +++ b/src/java/com/puppetlabs/http/client/HttpMethod.java @@ -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; + } + +} diff --git a/src/java/com/puppetlabs/http/client/impl/RequestOptions.java b/src/java/com/puppetlabs/http/client/RequestOptions.java similarity index 78% rename from src/java/com/puppetlabs/http/client/impl/RequestOptions.java rename to src/java/com/puppetlabs/http/client/RequestOptions.java index c7fa8d1..1a9ef99 100644 --- a/src/java/com/puppetlabs/http/client/impl/RequestOptions.java +++ b/src/java/com/puppetlabs/http/client/RequestOptions.java @@ -1,13 +1,14 @@ -package com.puppetlabs.http.client.impl; +package com.puppetlabs.http.client; -import org.httpkit.HttpMethod; +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.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; @@ -17,6 +18,8 @@ public class RequestOptions { 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 promise = new Promise(); @@ -26,17 +29,24 @@ public class RequestOptions { private String url; private HttpMethod method = null; private List traceRedirects = new ArrayList(); - private HashMap headers; + private Map headers; private Map formParams; private BasicAuth basicAuth; private String oauthToken; private String userAgent; private Map 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 multipartEntities; + public RequestOptions(String url) { this.url = url; } @@ -133,10 +143,10 @@ public class RequestOptions { return this; } - public HashMap getHeaders() { + public Map getHeaders() { return headers; } - public RequestOptions setHeaders(HashMap headers) { + public RequestOptions setHeaders(Map headers) { this.headers = headers; return this; } @@ -189,6 +199,38 @@ public class RequestOptions { 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; } @@ -212,4 +254,5 @@ public class RequestOptions { this.multipartEntities = entities; return this; } + } diff --git a/src/java/com/puppetlabs/http/client/SyncHttpClient.java b/src/java/com/puppetlabs/http/client/SyncHttpClient.java index 106026d..7589d34 100644 --- a/src/java/com/puppetlabs/http/client/SyncHttpClient.java +++ b/src/java/com/puppetlabs/http/client/SyncHttpClient.java @@ -3,36 +3,115 @@ package com.puppetlabs.http.client; import com.puppetlabs.http.client.impl.HttpResponse; import com.puppetlabs.http.client.impl.JavaClient; import com.puppetlabs.http.client.impl.Promise; -import com.puppetlabs.http.client.impl.RequestOptions; -import org.httpkit.HttpMethod; +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 promise = null; try { promise = JavaClient.request(options, null); } catch (IOException e) { - throw new RuntimeException("Error submitting http request", e); + logAndRethrow("Error submitting http request", e); + } HttpResponse response = null; try { response = promise.deref(); } catch (InterruptedException e) { - throw new RuntimeException("Error while waiting for http response", e); + logAndRethrow("Error while waiting for http response", e); } if (response.getError() != null) { - throw new RuntimeException("Error in http request", response.getError()); + logAndRethrow("Error executing http request", response.getError()); } return response; } + public static HttpResponse get(String url) { return get(new RequestOptions(url)); } - private static HttpResponse get(RequestOptions requestOptions) { + 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)); + } } diff --git a/src/java/com/puppetlabs/http/client/impl/HttpResponse.java b/src/java/com/puppetlabs/http/client/impl/HttpResponse.java index 20a0bdf..9528f95 100644 --- a/src/java/com/puppetlabs/http/client/impl/HttpResponse.java +++ b/src/java/com/puppetlabs/http/client/impl/HttpResponse.java @@ -1,5 +1,7 @@ package com.puppetlabs.http.client.impl; +import com.puppetlabs.http.client.RequestOptions; + import java.util.Map; public class HttpResponse { diff --git a/src/java/com/puppetlabs/http/client/impl/JavaClient.java b/src/java/com/puppetlabs/http/client/impl/JavaClient.java index d55534f..f68b8b6 100644 --- a/src/java/com/puppetlabs/http/client/impl/JavaClient.java +++ b/src/java/com/puppetlabs/http/client/impl/JavaClient.java @@ -1,5 +1,6 @@ package com.puppetlabs.http.client.impl; +import com.puppetlabs.http.client.RequestOptions; import org.httpkit.HttpMethod; import org.httpkit.client.*; @@ -53,11 +54,11 @@ public class JavaClient { } private static Map prepareHeaders(RequestOptions options) { - Map result; + Map result = new HashMap(); if (options.getHeaders() != null) { - result = (Map) options.getHeaders().clone(); - } else { - result = new HashMap(); + for (Map.Entry entry : options.getHeaders().entrySet()) { + result.put(entry.getKey(), entry.getValue()); + } } if (options.getFormParams() != null) { @@ -94,7 +95,7 @@ public class JavaClient { sslEngine = SslContextFactory.trustAnybody(); } - HttpMethod method = options.getMethod(); + HttpMethod method = options.getMethod().getValue(); if (method == null) { method = HttpMethod.GET; } diff --git a/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java b/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java index f8e045a..f485a3f 100644 --- a/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java +++ b/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java @@ -1,6 +1,7 @@ package com.puppetlabs.http.client.impl; -import org.httpkit.HttpMethod; +import com.puppetlabs.http.client.HttpMethod; +import com.puppetlabs.http.client.RequestOptions; import org.httpkit.HttpUtils; import org.httpkit.client.IResponseHandler; diff --git a/src/java/com/puppetlabs/http/client/impl/SslUtils.java b/src/java/com/puppetlabs/http/client/impl/SslUtils.java new file mode 100644 index 0000000..4417ec0 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/impl/SslUtils.java @@ -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 pemToObjects(Reader reader) + throws IOException + { + PEMParser parser = new PEMParser(reader); + List results = new ArrayList(); + 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 pemToPrivateKeys(Reader reader) + throws IOException, PEMException + { + List objects = pemToObjects(reader); + List results = new ArrayList(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 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 pemToCerts(Reader reader) + throws CertificateException, IOException + { + JcaX509CertificateConverter converter = new JcaX509CertificateConverter(); + List pemObjects = pemToObjects(reader); + List results = new ArrayList(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 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 certs = pemToCerts(pem); + ListIterator 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: + *
    + *
  • "keystore" - a keystore initialized with the cert and private key
  • + *
  • "keystore-pw" - a string containing a dynamically generated password for the keystore
  • + *
  • "truststore" - a keystore containing the CA cert
  • + *
      + * + * @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 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 result = new HashMap(); + 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 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; + } +} \ No newline at end of file From db53a75f3436aa3ff59f6318a2fc5b42651a2207 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Fri, 28 Mar 2014 17:33:27 -0700 Subject: [PATCH 6/7] Add integration tests for sync clients Prior to this commit, we didn't actually have any tests that started a real web server and made a real request. This commit adds tests that do this for the clojure and Java synchronous clients, using SSL. --- {test/resources => dev-resources/ssl}/ca.pem | 0 .../resources => dev-resources/ssl}/cert.pem | 0 {test/resources => dev-resources/ssl}/key.pem | 0 project.clj | 10 ++++- test/puppetlabs/http/client/async_test.clj | 18 ++++---- test/puppetlabs/http/client/sync_test.clj | 44 +++++++++++++++++++ 6 files changed, 62 insertions(+), 10 deletions(-) rename {test/resources => dev-resources/ssl}/ca.pem (100%) rename {test/resources => dev-resources/ssl}/cert.pem (100%) rename {test/resources => dev-resources/ssl}/key.pem (100%) create mode 100644 test/puppetlabs/http/client/sync_test.clj diff --git a/test/resources/ca.pem b/dev-resources/ssl/ca.pem similarity index 100% rename from test/resources/ca.pem rename to dev-resources/ssl/ca.pem diff --git a/test/resources/cert.pem b/dev-resources/ssl/cert.pem similarity index 100% rename from test/resources/cert.pem rename to dev-resources/ssl/cert.pem diff --git a/test/resources/key.pem b/dev-resources/ssl/key.pem similarity index 100% rename from test/resources/key.pem rename to dev-resources/ssl/key.pem diff --git a/project.clj b/project.clj index f09c030..a01e48f 100644 --- a/project.clj +++ b/project.clj @@ -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,13 +8,18 @@ :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 :password :env/clojars_jenkins_password diff --git a/test/puppetlabs/http/client/async_test.clj b/test/puppetlabs/http/client/async_test.clj index 2747c03..0504bb9 100644 --- a/test/puppetlabs/http/client/async_test.clj +++ b/test/puppetlabs/http/client/async_test.clj @@ -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" diff --git a/test/puppetlabs/http/client/sync_test.clj b/test/puppetlabs/http/client/sync_test.clj new file mode 100644 index 0000000..48089ce --- /dev/null +++ b/test/puppetlabs/http/client/sync_test.clj @@ -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))))))))) From c7101216acb6c5c07c6955778b3b554fc10ddf25 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Tue, 1 Apr 2014 08:28:35 -0700 Subject: [PATCH 7/7] Reorganize package structure to match API `HttpResponse` and `ResponseBodyType` were being exposed via the API, but were previously located in the 'impl' package. This commit moves them up to the main package. --- .../http/client/{impl => }/HttpResponse.java | 13 +++++++------ .../http/client/{impl => }/ResponseBodyType.java | 2 +- .../com/puppetlabs/http/client/SyncHttpClient.java | 2 +- .../http/client/impl/IResponseCallback.java | 2 ++ .../com/puppetlabs/http/client/impl/JavaClient.java | 1 + .../http/client/impl/ResponseHandler.java | 1 + 6 files changed, 13 insertions(+), 8 deletions(-) rename src/java/com/puppetlabs/http/client/{impl => }/HttpResponse.java (76%) rename src/java/com/puppetlabs/http/client/{impl => }/ResponseBodyType.java (85%) diff --git a/src/java/com/puppetlabs/http/client/impl/HttpResponse.java b/src/java/com/puppetlabs/http/client/HttpResponse.java similarity index 76% rename from src/java/com/puppetlabs/http/client/impl/HttpResponse.java rename to src/java/com/puppetlabs/http/client/HttpResponse.java index 9528f95..817dee7 100644 --- a/src/java/com/puppetlabs/http/client/impl/HttpResponse.java +++ b/src/java/com/puppetlabs/http/client/HttpResponse.java @@ -1,15 +1,16 @@ -package com.puppetlabs.http.client.impl; +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 = null; - private Throwable error = null; - private Object body = null; - private Map headers = null; - private Integer status = null; + private RequestOptions options; + private Throwable error; + private Object body; + private Map headers; + private Integer status; public HttpResponse(RequestOptions options, Throwable error) { this.options = options; diff --git a/src/java/com/puppetlabs/http/client/impl/ResponseBodyType.java b/src/java/com/puppetlabs/http/client/ResponseBodyType.java similarity index 85% rename from src/java/com/puppetlabs/http/client/impl/ResponseBodyType.java rename to src/java/com/puppetlabs/http/client/ResponseBodyType.java index c8b1af9..00e4b8d 100644 --- a/src/java/com/puppetlabs/http/client/impl/ResponseBodyType.java +++ b/src/java/com/puppetlabs/http/client/ResponseBodyType.java @@ -1,4 +1,4 @@ -package com.puppetlabs.http.client.impl; +package com.puppetlabs.http.client; public enum ResponseBodyType { AUTO(1), diff --git a/src/java/com/puppetlabs/http/client/SyncHttpClient.java b/src/java/com/puppetlabs/http/client/SyncHttpClient.java index 7589d34..7b60823 100644 --- a/src/java/com/puppetlabs/http/client/SyncHttpClient.java +++ b/src/java/com/puppetlabs/http/client/SyncHttpClient.java @@ -1,6 +1,6 @@ package com.puppetlabs.http.client; -import com.puppetlabs.http.client.impl.HttpResponse; +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; diff --git a/src/java/com/puppetlabs/http/client/impl/IResponseCallback.java b/src/java/com/puppetlabs/http/client/impl/IResponseCallback.java index 0e83385..19a096e 100644 --- a/src/java/com/puppetlabs/http/client/impl/IResponseCallback.java +++ b/src/java/com/puppetlabs/http/client/impl/IResponseCallback.java @@ -1,5 +1,7 @@ package com.puppetlabs.http.client.impl; +import com.puppetlabs.http.client.HttpResponse; + public interface IResponseCallback { HttpResponse handleResponse(HttpResponse response); } diff --git a/src/java/com/puppetlabs/http/client/impl/JavaClient.java b/src/java/com/puppetlabs/http/client/impl/JavaClient.java index f68b8b6..6374863 100644 --- a/src/java/com/puppetlabs/http/client/impl/JavaClient.java +++ b/src/java/com/puppetlabs/http/client/impl/JavaClient.java @@ -1,5 +1,6 @@ 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.*; diff --git a/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java b/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java index f485a3f..190dd13 100644 --- a/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java +++ b/src/java/com/puppetlabs/http/client/impl/ResponseHandler.java @@ -1,6 +1,7 @@ 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;