├── CODEOWNERS ├── Makefile ├── .gitignore ├── dev-resources ├── logging.properties ├── logback-test.xml ├── exts.cnf ├── ssl │ ├── cert.pem │ ├── key.pem │ ├── ca.pem │ └── alternate-ca.pem ├── gen-pki.sh └── Makefile.i18n ├── src ├── java │ └── com │ │ └── puppetlabs │ │ └── http │ │ └── client │ │ ├── CompressType.java │ │ ├── impl │ │ ├── Deliverable.java │ │ ├── IResponseCallback.java │ │ ├── FnDeliverable.java │ │ ├── Promise.java │ │ ├── metrics │ │ │ ├── UrlClientTimerFilter.java │ │ │ ├── CategoryClientTimerMetricFilter.java │ │ │ ├── MetricIdClientTimerFilter.java │ │ │ ├── UrlAndMethodClientTimerFilter.java │ │ │ ├── TimerMetricData.java │ │ │ └── TimerUtils.java │ │ ├── ResponseDeliveryDelegate.java │ │ ├── SafeLaxRedirectStrategy.java │ │ ├── SafeDefaultRedirectStrategy.java │ │ ├── TimedFutureCallback.java │ │ ├── ExceptionInsertingPipedInputStream.java │ │ ├── CoercedRequestOptions.java │ │ ├── CoercedClientOptions.java │ │ ├── JavaResponseDeliveryDelegate.java │ │ ├── SslUtils.java │ │ ├── SafeRedirectedRequest.java │ │ ├── StreamingAsyncResponseConsumer.java │ │ ├── CreateRedirectUtil.java │ │ ├── PersistentAsyncHttpClient.java │ │ └── PersistentSyncHttpClient.java │ │ ├── ResponseBodyType.java │ │ ├── HttpClientException.java │ │ ├── metrics │ │ ├── UrlClientMetricData.java │ │ ├── UrlClientTimer.java │ │ ├── UrlAndMethodClientMetricData.java │ │ ├── MetricIdClientMetricData.java │ │ ├── UrlAndMethodClientTimer.java │ │ ├── MetricIdClientTimer.java │ │ ├── ClientTimer.java │ │ ├── ClientMetricData.java │ │ ├── ClientTimerContainer.java │ │ ├── ClientMetricDataContainer.java │ │ └── Metrics.java │ │ ├── HttpMethod.java │ │ ├── Async.java │ │ ├── Response.java │ │ ├── RequestOptions.java │ │ ├── SimpleRequestOptions.java │ │ ├── SyncHttpClient.java │ │ ├── AsyncHttpClient.java │ │ └── ClientOptions.java └── clj │ └── puppetlabs │ └── http │ └── client │ ├── metrics.clj │ ├── sync.clj │ └── common.clj ├── jenkins └── deploy.sh ├── CONTRIBUTING.md ├── locales ├── http-client.pot └── eo.po ├── .github └── workflows │ ├── pr-testing.yaml │ ├── clojure-linting.yaml │ └── mend.yaml ├── .clj-kondo └── config.edn ├── .travis.yml ├── test ├── puppetlabs │ └── http │ │ └── client │ │ ├── test_common.clj │ │ ├── gzip_request_test.clj │ │ └── sync_ssl_test.clj └── com │ └── puppetlabs │ └── http │ └── client │ └── impl │ └── java_client_test.clj ├── README.md ├── doc ├── java-client.md └── clojure-client.md ├── project.clj ├── CHANGELOG.md └── LICENSE /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @puppetlabs/dumpling 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include dev-resources/Makefile.i18n 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | pom.xml 3 | .nrepl-port 4 | /.lein* 5 | /resources/locales.clj 6 | /resources/**/Messages*.class 7 | /dev-resources/i18n/bin 8 | -------------------------------------------------------------------------------- /dev-resources/logging.properties: -------------------------------------------------------------------------------- 1 | # register SLF4JBridgeHandler as handler for the j.u.l. root logger 2 | handlers = org.slf4j.bridge.SLF4JBridgeHandler -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/CompressType.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | public enum CompressType { 4 | GZIP, 5 | NONE 6 | } 7 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/Deliverable.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | public interface Deliverable { 4 | void deliver(T t); 5 | } 6 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/IResponseCallback.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import com.puppetlabs.http.client.Response; 4 | 5 | public interface IResponseCallback { 6 | Response handleResponse(Response response); 7 | } 8 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/ResponseBodyType.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | /** 4 | * This Enum represents the possible types of the body of a response. 5 | */ 6 | public enum ResponseBodyType { 7 | UNBUFFERED_STREAM, 8 | STREAM, 9 | TEXT; 10 | } 11 | -------------------------------------------------------------------------------- /jenkins/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | git fetch --tags 7 | 8 | lein test 9 | echo "Tests passed!" 10 | 11 | lein release 12 | echo "Release plugin successful, pushing changes to git" 13 | 14 | git push origin --tags HEAD:$BRANCH 15 | 16 | echo "git push successful." 17 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/FnDeliverable.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import clojure.lang.IFn; 4 | 5 | public class FnDeliverable implements Deliverable { 6 | 7 | private final IFn fn; 8 | 9 | public FnDeliverable(IFn fn) { 10 | this.fn = fn; 11 | } 12 | 13 | @Override 14 | public void deliver(T t) { 15 | fn.invoke(t); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/HttpClientException.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | /** 4 | * This class represents an exception caused by an error in 5 | * an HTTP request 6 | */ 7 | public class HttpClientException extends RuntimeException { 8 | public HttpClientException(String msg) { 9 | super(msg); 10 | } 11 | public HttpClientException(String msg, Throwable t) { 12 | super(msg, t); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /dev-resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d %-5p [%t] [%c{2}] %m%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Third-party patches are essential for keeping puppet open-source projects 4 | great. We want to keep it as easy as possible to contribute changes that 5 | allow you to get the most out of our projects. There are a few guidelines 6 | that we need contributors to follow so that we can have a chance of keeping on 7 | top of things. For more info, see our canonical guide to contributing: 8 | 9 | [https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md](https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md) 10 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/UrlClientMetricData.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | import com.puppetlabs.http.client.impl.metrics.TimerMetricData; 4 | 5 | public class UrlClientMetricData extends ClientMetricData { 6 | private final String url; 7 | 8 | public UrlClientMetricData(TimerMetricData timerMetricData, 9 | String url) { 10 | super(timerMetricData); 11 | this.url = url; 12 | } 13 | 14 | public String getUrl() { 15 | return url; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/UrlClientTimer.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | public class UrlClientTimer extends ClientTimer { 4 | private final String url; 5 | 6 | public UrlClientTimer(String metricName, String url, Metrics.MetricType metricType) { 7 | super(metricName, metricType); 8 | this.url = url; 9 | } 10 | 11 | public String getUrl() { 12 | return url; 13 | } 14 | 15 | @Override 16 | public boolean isCategory(Metrics.MetricCategory category) { 17 | return category.equals(Metrics.MetricCategory.URL); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/UrlAndMethodClientMetricData.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | import com.puppetlabs.http.client.impl.metrics.TimerMetricData; 4 | 5 | public class UrlAndMethodClientMetricData extends UrlClientMetricData { 6 | private final String method; 7 | 8 | public UrlAndMethodClientMetricData(TimerMetricData timerMetricData, 9 | String url, String method) { 10 | super(timerMetricData, url); 11 | this.method = method; 12 | } 13 | 14 | public String getMethod() { 15 | return method; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/MetricIdClientMetricData.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | import com.puppetlabs.http.client.impl.metrics.TimerMetricData; 4 | 5 | import java.util.List; 6 | 7 | public class MetricIdClientMetricData extends ClientMetricData { 8 | private final List metricId; 9 | 10 | public MetricIdClientMetricData(TimerMetricData timerMetricData, 11 | List metricId) { 12 | super(timerMetricData); 13 | this.metricId = metricId; 14 | } 15 | 16 | public List getMetricId() { 17 | return metricId; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/UrlAndMethodClientTimer.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | public class UrlAndMethodClientTimer extends UrlClientTimer { 4 | private final String method; 5 | 6 | public UrlAndMethodClientTimer(String metricName, String url, String method, 7 | Metrics.MetricType metricType) { 8 | super(metricName, url, metricType); 9 | this.method = method; 10 | } 11 | 12 | public String getMethod() { 13 | return method; 14 | } 15 | 16 | @Override 17 | public boolean isCategory(Metrics.MetricCategory category) { 18 | return category.equals(Metrics.MetricCategory.URL_AND_METHOD); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/MetricIdClientTimer.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | import java.util.List; 4 | 5 | public class MetricIdClientTimer extends ClientTimer { 6 | 7 | private final List metricId; 8 | 9 | public MetricIdClientTimer(String metricName, List metricId, 10 | Metrics.MetricType metricType) { 11 | super(metricName, metricType); 12 | this.metricId = metricId; 13 | } 14 | 15 | public List getMetricId() { 16 | return metricId; 17 | } 18 | 19 | @Override 20 | public boolean isCategory(Metrics.MetricCategory category) { 21 | return category.equals(Metrics.MetricCategory.METRIC_ID); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/ClientTimer.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | import com.codahale.metrics.Timer; 4 | 5 | public abstract class ClientTimer extends Timer { 6 | private final String metricName; 7 | 8 | private final Metrics.MetricType metricType; 9 | 10 | ClientTimer(String metricName, Metrics.MetricType metricType) { 11 | super(); 12 | this.metricName = metricName; 13 | this.metricType = metricType; 14 | } 15 | 16 | public String getMetricName() { 17 | return metricName; 18 | } 19 | 20 | public Metrics.MetricType getMetricType() { 21 | return metricType; 22 | } 23 | 24 | public abstract boolean isCategory(Metrics.MetricCategory category); 25 | } 26 | -------------------------------------------------------------------------------- /locales/http-client.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR Puppet 3 | # This file is distributed under the same license as the puppetlabs.http_client package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: puppetlabs.http_client \n" 10 | "Report-Msgid-Bugs-To: docs@puppet.com\n" 11 | "POT-Creation-Date: \n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/clj/puppetlabs/http/client/async.clj 21 | msgid "Unsupported request method: {0}" 22 | msgstr "" 23 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/Promise.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import java.util.concurrent.CountDownLatch; 4 | 5 | public class Promise implements Deliverable { 6 | private final CountDownLatch latch; 7 | private T value = null; 8 | 9 | public Promise() { 10 | latch = new CountDownLatch(1); 11 | } 12 | 13 | public synchronized void deliver(T t) { 14 | if (value != null) { 15 | throw new IllegalStateException("Attempting to deliver value to a promise that has already been realized!"); 16 | } 17 | value = t; 18 | latch.countDown(); 19 | } 20 | 21 | public T deref() throws InterruptedException { 22 | latch.await(); 23 | return value; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/pr-testing.yaml: -------------------------------------------------------------------------------- 1 | name: PR Testing 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: [opened, reopened, edited, synchronize] 7 | paths: ['src/**','test/**','project.clj'] 8 | 9 | jobs: 10 | pr-testing: 11 | name: PR Testing 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | version: ['8', '11', '17'] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: checkout repo 19 | uses: actions/checkout@v3 20 | with: 21 | submodules: recursive 22 | - name: setup java 23 | uses: actions/setup-java@v3 24 | with: 25 | distribution: 'temurin' 26 | java-version: ${{ matrix.version }} 27 | - name: clojure tests 28 | run: lein test 29 | timeout-minutes: 30 -------------------------------------------------------------------------------- /locales/eo.po: -------------------------------------------------------------------------------- 1 | # Esperanto translations for puppetlabs.http_client package. 2 | # Copyright (C) 2017 Puppet 3 | # This file is distributed under the same license as the puppetlabs.http_client package. 4 | # Automatically generated, 2017. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: puppetlabs.http_client \n" 9 | "Report-Msgid-Bugs-To: docs@puppet.com\n" 10 | "POT-Creation-Date: \n" 11 | "PO-Revision-Date: \n" 12 | "Last-Translator: Automatically generated\n" 13 | "Language-Team: none\n" 14 | "Language: eo\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: src/clj/puppetlabs/http/client/async.clj 21 | msgid "Unsupported request method: {0}" 22 | msgstr "" 23 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/metrics/UrlClientTimerFilter.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl.metrics; 2 | 3 | import com.codahale.metrics.Metric; 4 | import com.codahale.metrics.MetricFilter; 5 | import com.puppetlabs.http.client.metrics.UrlClientTimer; 6 | 7 | public class UrlClientTimerFilter implements MetricFilter { 8 | private final String url; 9 | 10 | public UrlClientTimerFilter(String url) { 11 | this.url = url; 12 | } 13 | 14 | protected String getUrl() { 15 | return url; 16 | } 17 | 18 | @Override 19 | public boolean matches(String s, Metric metric) { 20 | return metric.getClass().equals(UrlClientTimer.class) && 21 | ((UrlClientTimer) metric). 22 | getUrl().equals(url); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/metrics/CategoryClientTimerMetricFilter.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl.metrics; 2 | 3 | import com.codahale.metrics.Metric; 4 | import com.codahale.metrics.MetricFilter; 5 | import com.puppetlabs.http.client.metrics.ClientTimer; 6 | import com.puppetlabs.http.client.metrics.Metrics; 7 | 8 | public class CategoryClientTimerMetricFilter implements MetricFilter { 9 | private final Metrics.MetricCategory category; 10 | 11 | public CategoryClientTimerMetricFilter(Metrics.MetricCategory category) { 12 | this.category = category; 13 | } 14 | 15 | @Override 16 | public boolean matches(String s, Metric metric) { 17 | return metric instanceof ClientTimer && 18 | ((ClientTimer) metric).isCategory(category); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/ClientMetricData.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | import com.puppetlabs.http.client.impl.metrics.TimerMetricData; 4 | 5 | public abstract class ClientMetricData { 6 | private final TimerMetricData timerMetricData; 7 | 8 | ClientMetricData(TimerMetricData timerMetricData) { 9 | this.timerMetricData = timerMetricData; 10 | } 11 | 12 | public String getMetricName() { 13 | return timerMetricData.getMetricName(); 14 | } 15 | 16 | public Long getCount() { 17 | return timerMetricData.getCount(); 18 | } 19 | 20 | public Long getMean() { 21 | return timerMetricData.getMeanMillis(); 22 | } 23 | 24 | public Long getAggregate() { 25 | return timerMetricData.getAggregate(); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:refer-all {:exclude [clojure.test slingshot.test 2 | puppetlabs.http.client.test-common]} 3 | :unresolved-symbol {:level :warning :exclude [(puppetlabs.trapperkeeper.core/defservice) 4 | (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-config) 5 | (puppetlabs.trapperkeeper.testutils.webserver/with-test-webserver) 6 | (puppetlabs.trapperkeeper.testutils.webserver/with-test-webserver-and-config)]} 7 | :invalid-arity {:skip-args [puppetlabs.trapperkeeper.core/defservice]}} 8 | :output {:linter-name true} 9 | :lint-as {puppetlabs.trapperkeeper.core/defservice clojure.core/def}} 10 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/metrics/MetricIdClientTimerFilter.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl.metrics; 2 | 3 | import com.codahale.metrics.Metric; 4 | import com.codahale.metrics.MetricFilter; 5 | import com.puppetlabs.http.client.metrics.MetricIdClientTimer; 6 | 7 | import java.util.List; 8 | 9 | public class MetricIdClientTimerFilter implements MetricFilter { 10 | private final List metricId; 11 | 12 | public MetricIdClientTimerFilter(List metricId) { 13 | this.metricId = metricId; 14 | } 15 | 16 | @Override 17 | public boolean matches(String s, Metric metric) { 18 | return metric.getClass().equals(MetricIdClientTimer.class) && 19 | ((MetricIdClientTimer) metric). 20 | getMetricId().equals(metricId); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/HttpMethod.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | import org.apache.http.client.methods.*; 4 | 5 | /** 6 | * This enum represents the various HTTP methods that can be used to make requests. 7 | */ 8 | public enum HttpMethod { 9 | GET(HttpGet.class), 10 | HEAD(HttpHead.class), 11 | POST(HttpPost.class), 12 | PUT(HttpPut.class), 13 | DELETE(HttpDelete.class), 14 | TRACE(HttpTrace.class), 15 | OPTIONS(HttpOptions.class), 16 | PATCH(HttpPatch.class); 17 | 18 | private Class httpMethod; 19 | 20 | HttpMethod(Class httpMethod) { 21 | this.httpMethod = httpMethod; 22 | } 23 | 24 | public Class getValue() { 25 | return this.httpMethod; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/ResponseDeliveryDelegate.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import com.puppetlabs.http.client.RequestOptions; 4 | import org.apache.http.entity.ContentType; 5 | 6 | public interface ResponseDeliveryDelegate { 7 | 8 | void deliverResponse(RequestOptions requestOptions, 9 | String origContentEncoding, 10 | Object body, 11 | java.util.Map headers, 12 | int statusCode, 13 | String reasonPhrase, 14 | ContentType contentType, 15 | IResponseCallback callback); 16 | 17 | 18 | void deliverResponse(RequestOptions requestOptions, 19 | Exception e, 20 | IResponseCallback callback); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/metrics/UrlAndMethodClientTimerFilter.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl.metrics; 2 | 3 | import com.codahale.metrics.Metric; 4 | import com.puppetlabs.http.client.metrics.UrlAndMethodClientTimer; 5 | 6 | public class UrlAndMethodClientTimerFilter extends UrlClientTimerFilter { 7 | private final String method; 8 | 9 | public UrlAndMethodClientTimerFilter(String url, String method) { 10 | super(url); 11 | this.method = method; 12 | } 13 | 14 | @Override 15 | public boolean matches(String s, Metric metric) { 16 | if (metric.getClass().equals(UrlAndMethodClientTimer.class)) { 17 | UrlAndMethodClientTimer timer = (UrlAndMethodClientTimer) metric; 18 | return timer.getMethod().equals(this.method) && 19 | timer.getUrl().equals(this.getUrl()); 20 | } 21 | return false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/clojure-linting.yaml: -------------------------------------------------------------------------------- 1 | name: Clojure Linting 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited, synchronize] 6 | paths: ['src/**','test/**','.clj-kondo/config.edn','project.clj','.github/**'] 7 | 8 | jobs: 9 | clojure-linting: 10 | name: Clojure Linting 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: setup java 14 | uses: actions/setup-java@v3 15 | with: 16 | distribution: temurin 17 | java-version: 17 18 | - name: checkout repo 19 | uses: actions/checkout@v2 20 | - name: install clj-kondo (this is quite fast) 21 | run: | 22 | curl -sLO https://raw.githubusercontent.com/clj-kondo/clj-kondo/master/script/install-clj-kondo 23 | chmod +x install-clj-kondo 24 | ./install-clj-kondo --dir . 25 | - name: kondo lint 26 | run: ./clj-kondo --lint src test 27 | - name: eastwood lint 28 | run: | 29 | java -version 30 | lein eastwood -------------------------------------------------------------------------------- /dev-resources/exts.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | #default_bits = 2048 3 | #default_md = sha256 4 | #default_keyfile = privkey.pem 5 | distinguished_name = req_distinguished_name 6 | attributes = req_attributes 7 | 8 | [ req_distinguished_name ] 9 | countryName = Country Name (2 letter code) 10 | countryName_min = 2 11 | countryName_max = 2 12 | stateOrProvinceName = State or Province Name (full name) 13 | localityName = Locality Name (eg, city) 14 | 0.organizationName = Organization Name (eg, company) 15 | organizationalUnitName = Organizational Unit Name (eg, section) 16 | commonName = Common Name (eg, fully qualified host name) 17 | commonName_max = 64 18 | 19 | [ req_attributes ] 20 | 21 | # This section should be referenced when building an x509v3 CA 22 | # Certificate. 23 | # The default path length and the key usage can be overridden 24 | # modified by setting the CERTPATHLEN and CERTUSAGE environment 25 | # variables. 26 | [x509v3_CA] 27 | subjectKeyIdentifier = hash 28 | basicConstraints=critical,CA:true 29 | keyUsage=digitalSignature,keyCertSign,cRLSign 30 | authorityKeyIdentifier=keyid 31 | 32 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/ClientTimerContainer.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | import java.util.List; 4 | 5 | public class ClientTimerContainer { 6 | private final List urlTimers; 7 | private final List urlAndMethodTimers; 8 | private final List metricIdTimers; 9 | 10 | public ClientTimerContainer(List urlTimers, 11 | List urlAndMethodTimers, 12 | List metricIdTimers) { 13 | this.urlTimers = urlTimers; 14 | this.urlAndMethodTimers = urlAndMethodTimers; 15 | this.metricIdTimers = metricIdTimers; 16 | } 17 | 18 | public List getUrlTimers() { 19 | return urlTimers; 20 | } 21 | 22 | public List getUrlAndMethodTimers() { 23 | return urlAndMethodTimers; 24 | } 25 | 26 | public List getMetricIdTimers() { 27 | return metricIdTimers; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/ClientMetricDataContainer.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | import java.util.List; 4 | 5 | public class ClientMetricDataContainer { 6 | private final List urlData; 7 | private final List urlAndMethodData; 8 | private final List metricIdData; 9 | 10 | public ClientMetricDataContainer(List urlTimers, 11 | List urlAndMethodData, 12 | List metricIdData) { 13 | this.urlData = urlTimers; 14 | this.urlAndMethodData = urlAndMethodData; 15 | this.metricIdData = metricIdData; 16 | } 17 | 18 | public List getUrlData() { 19 | return urlData; 20 | } 21 | 22 | public List getUrlAndMethodData() { 23 | return urlAndMethodData; 24 | } 25 | 26 | public List getMetricIdData() { 27 | return metricIdData; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/SafeLaxRedirectStrategy.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import org.apache.http.HttpRequest; 4 | import org.apache.http.HttpResponse; 5 | import org.apache.http.ProtocolException; 6 | import org.apache.http.annotation.Contract; 7 | import org.apache.http.annotation.ThreadingBehavior; 8 | import org.apache.http.client.methods.HttpUriRequest; 9 | import org.apache.http.impl.client.LaxRedirectStrategy; 10 | import org.apache.http.protocol.HttpContext; 11 | 12 | // Shares behavior of CreateRedirectUtil.getRedirected() with SafeDefaultRedirectStrategy 13 | @Contract(threading = ThreadingBehavior.IMMUTABLE) 14 | public class SafeLaxRedirectStrategy extends LaxRedirectStrategy { 15 | 16 | // Used by the Apache HttpClientBuilder internally 17 | public static final SafeLaxRedirectStrategy INSTANCE = new SafeLaxRedirectStrategy(); 18 | 19 | @Override 20 | public HttpUriRequest getRedirect( 21 | final HttpRequest request, 22 | final HttpResponse response, 23 | final HttpContext context) throws ProtocolException { 24 | 25 | return CreateRedirectUtil.getRedirect(this, request, response, context); 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/SafeDefaultRedirectStrategy.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import org.apache.http.HttpRequest; 4 | import org.apache.http.HttpResponse; 5 | import org.apache.http.ProtocolException; 6 | import org.apache.http.annotation.Contract; 7 | import org.apache.http.annotation.ThreadingBehavior; 8 | import org.apache.http.client.methods.HttpUriRequest; 9 | import org.apache.http.impl.client.DefaultRedirectStrategy; 10 | import org.apache.http.protocol.HttpContext; 11 | 12 | // Shares behavior of CreateRedirectUtil.getRedirected() with SafeLaxRedirectStrategy 13 | @Contract(threading = ThreadingBehavior.IMMUTABLE) 14 | public class SafeDefaultRedirectStrategy extends DefaultRedirectStrategy { 15 | 16 | // Used by the Apache HttpClientBuilder internally 17 | public static final SafeDefaultRedirectStrategy INSTANCE = new SafeDefaultRedirectStrategy(); 18 | 19 | @Override 20 | public HttpUriRequest getRedirect( 21 | final HttpRequest request, 22 | final HttpResponse response, 23 | final HttpContext context) throws ProtocolException { 24 | 25 | return CreateRedirectUtil.getRedirect(this, request, response, context); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/Async.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | import com.puppetlabs.http.client.impl.JavaClient; 4 | import com.puppetlabs.http.client.impl.PersistentAsyncHttpClient; 5 | import com.puppetlabs.http.client.metrics.Metrics; 6 | 7 | /** 8 | * This class allows you to create an AsyncHttpClient for use in making 9 | * HTTP Requests. It consists exclusively of a static method to create 10 | * a client. 11 | */ 12 | public class Async { 13 | 14 | /** 15 | * Allows you to create an instance of an AsyncHttpClient for use in 16 | * making HTTP requests. 17 | * 18 | * @param clientOptions the list of options with which to configure the client 19 | * @return an AsyncHttpClient that can be used to make requests 20 | */ 21 | public static AsyncHttpClient createClient(ClientOptions clientOptions) { 22 | final String metricNamespace = Metrics.buildMetricNamespace(clientOptions.getMetricPrefix(), 23 | clientOptions.getServerId()); 24 | return new PersistentAsyncHttpClient(JavaClient.createClient(clientOptions), 25 | clientOptions.getMetricRegistry(), 26 | metricNamespace, 27 | clientOptions.isEnableURLMetrics()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/TimedFutureCallback.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import com.codahale.metrics.Timer; 4 | import org.apache.http.concurrent.FutureCallback; 5 | 6 | import java.util.ArrayList; 7 | 8 | public final class TimedFutureCallback implements FutureCallback { 9 | 10 | private final FutureCallback delegate; 11 | 12 | private final ArrayList timerContexts; 13 | 14 | public TimedFutureCallback(FutureCallback delegate, 15 | ArrayList timerContexts) { 16 | this.delegate = delegate; 17 | this.timerContexts = timerContexts; 18 | } 19 | 20 | public void completed(T result) { 21 | stopTimerContexts(); 22 | delegate.completed(result); 23 | } 24 | 25 | public void failed(Exception ex) { 26 | stopTimerContexts(); 27 | delegate.failed(ex); 28 | } 29 | 30 | public void cancelled() { 31 | stopTimerContexts(); 32 | delegate.cancelled(); 33 | } 34 | 35 | private void stopTimerContexts() { 36 | if (timerContexts != null) { 37 | for (Timer.Context timerContext : timerContexts) { 38 | timerContext.stop(); 39 | } 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/metrics/TimerMetricData.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl.metrics; 2 | 3 | import com.puppetlabs.http.client.metrics.ClientTimer; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | public class TimerMetricData { 8 | 9 | public static TimerMetricData fromTimer(ClientTimer timer) { 10 | Double mean = timer.getSnapshot().getMean(); 11 | Long count = timer.getCount(); 12 | Long meanMillis = TimeUnit.NANOSECONDS.toMillis(mean.longValue()); 13 | 14 | return new TimerMetricData( 15 | timer.getMetricName(), 16 | meanMillis, 17 | count, 18 | count * meanMillis); 19 | } 20 | 21 | 22 | private final String metricName; 23 | private final Long meanMillis; 24 | private final Long count; 25 | private final Long aggregate; 26 | 27 | public TimerMetricData(String metricName, Long meanMillis, 28 | Long count, Long aggregate) { 29 | this.metricName = metricName; 30 | this.meanMillis = meanMillis; 31 | this.count = count; 32 | this.aggregate = aggregate; 33 | } 34 | 35 | public String getMetricName() { 36 | return metricName; 37 | } 38 | 39 | public Long getMeanMillis() { 40 | return meanMillis; 41 | } 42 | 43 | public Long getCount() { 44 | return count; 45 | } 46 | 47 | public Long getAggregate() { 48 | return aggregate; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/ExceptionInsertingPipedInputStream.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import java.io.IOException; 4 | import java.io.PipedInputStream; 5 | 6 | public class ExceptionInsertingPipedInputStream extends PipedInputStream { 7 | 8 | private final Promise ioExceptionPromise; 9 | 10 | public ExceptionInsertingPipedInputStream(Promise ioExceptionPromise) { 11 | this.ioExceptionPromise = ioExceptionPromise; 12 | } 13 | 14 | private void checkFinalResult() throws IOException { 15 | try { 16 | IOException ioException = ioExceptionPromise.deref(); 17 | if (ioException != null) { 18 | throw ioException; 19 | } 20 | } catch (InterruptedException e) { 21 | throw new RuntimeException(e); 22 | } 23 | } 24 | 25 | @Override 26 | public synchronized int read() throws IOException { 27 | int read = super.read(); 28 | if (read == -1) { 29 | checkFinalResult(); 30 | } 31 | return read; 32 | } 33 | 34 | @Override 35 | public synchronized int read(byte[] b, int off, int len) throws IOException { 36 | int read = super.read(b, off, len); 37 | if (read == -1) { 38 | checkFinalResult(); 39 | } 40 | return read; 41 | } 42 | 43 | @Override 44 | public void close() throws IOException { 45 | super.close(); 46 | checkFinalResult(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /dev-resources/ssl/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEJzCCAg8CCQCgGYbfS6RdDzANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJV 3 | UzELMAkGA1UECAwCT1IxETAPBgNVBAcMCFBvcnRsYW5kMRQwEgYDVQQKDAtQdXBw 4 | ZXQsIEluYzEPMA0GA1UEAwwGcHVwcGV0MB4XDTIzMDcwNjIxNTUwMloXDTI4MDcw 5 | NDIxNTUwMlowVzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk9SMREwDwYDVQQHDAhQ 6 | b3J0bGFuZDEUMBIGA1UECgwLUHVwcGV0LCBJbmMxEjAQBgNVBAMMCWxvY2FsaG9z 7 | dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKUl/MYa3jHp2BiLidWZ 8 | 3NmUCSQV7gBUYQkYYAW+XzrSmACXzR/fzwNp4C1ypyHcvNbo/pcB6G6zfnrrfKlD 9 | XIgSXz0ty/YGYb04taiYdeF5myvXdd9moY60gTxmWYGlaJuj/9qm6Z2+46VZ5eF0 10 | nrZxWLatM7iIDcFWyvWlPdiiyQiYBerbxCn58LCwLxAucfrIxMsd2QtWRisNpnPE 11 | N7pZHjEBKghsgCzTsT1fOdVz2PY9VapCJJb78GavcNpY0RLTJLdRsmJ8490W5ruw 12 | meU0K35ZNuDMRC1IaqeedPxQzpNQATMN5IMho5QipG3DtLsKWKPV7XgAWp5+2AsO 13 | WekCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEANyLM5dVjjnxMEwX/zsVv1KZwfHp+ 14 | j7+RtfO1/zmmnaqIiQYq5MyYS6dCXZRMZsCLN0RKAOkn+IAi3QXRqoY6g9IrgmOq 15 | d+6QRfw2mJtkiAyrQ1IrpRhXcfsudJcTuqtQKez3pTfyQrMItzPQmRR10X854PT9 16 | JhQUuf+KSyoLoCO7VxETb14CNkPeDD8Ae7CUUWap86zmwxW4ssTLxfye5y4fwI0a 17 | I0b08AY5LBqk8J+yA314DXNQRC7SzLHxGYLigY3r6UN47yNINO7WSG1E/KA37rlR 18 | bkOnJpmwhv9uONlf7TWz0XR1i9b6utm2L2QSYPutOVMYlZq56qkW5wjODXUUR8J0 19 | eMp5s4IiSEIsyvTyN+jPoGGWYIDMdZvhKKl4pF3j8Zyu3P1tMGFJIGYSuo4HRmp4 20 | phYjW9NYzRqrFhldsO37r3y0E3t+Z93GshN2O1lyQCEPLQAQIKYje/1YRPxjxSfE 21 | t3UeshgJJ+AjVU8KbCXR3N2U2LyS1ZswlL7MB5ug61F3UBPBTmwm3+Wj/g1ysk+S 22 | awjSzwDZiwXOBThP/xXrGWj64hbOC6a0DdoulMf3jy5YMmdDrYlODTdFyCe/ESoX 23 | q0sSCh22ZuaPWzXmQ+WCxgW/skzlwxrTXLeF9/b26/slnIS9cReCKXPYc4U9A7vF 24 | HCACDPww8NaTkeQ= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/CoercedRequestOptions.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import com.puppetlabs.http.client.HttpMethod; 4 | import org.apache.http.Header; 5 | import org.apache.http.HttpEntity; 6 | 7 | import java.net.URI; 8 | import java.util.zip.GZIPOutputStream; 9 | 10 | class CoercedRequestOptions { 11 | private final URI uri; 12 | private final HttpMethod method; 13 | private final Header[] headers; 14 | private final HttpEntity body; 15 | private final GZIPOutputStream gzipOutputStream; 16 | private final byte[] bytesToGzip; 17 | 18 | public CoercedRequestOptions(URI uri, 19 | HttpMethod method, 20 | Header[] headers, 21 | HttpEntity body, 22 | GZIPOutputStream gzipOutputStream, 23 | byte[] bytesToGzip) { 24 | this.uri = uri; 25 | this.method = method; 26 | this.headers = headers; 27 | this.body = body; 28 | this.gzipOutputStream = gzipOutputStream; 29 | this.bytesToGzip = bytesToGzip; 30 | } 31 | 32 | public URI getUri() { 33 | return uri; 34 | } 35 | 36 | public HttpMethod getMethod() { 37 | return method; 38 | } 39 | 40 | public Header[] getHeaders() { 41 | return headers; 42 | } 43 | 44 | public HttpEntity getBody() { 45 | return body; 46 | } 47 | 48 | public GZIPOutputStream getGzipOutputStream() { return gzipOutputStream; }; 49 | 50 | public byte[] getBytesToGzip() { return bytesToGzip; } 51 | } 52 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: clojure 3 | lein: 2.9.10 4 | jobs: 5 | include: 6 | # The OpenJDK versions used by Travis are pretty old, which causes 7 | # problems with some of the tests. This pulls in a semi-recent version 8 | # from the Ubuntu repos. 9 | - name: jdk8 10 | before_install: 11 | - sudo rm -rf /usr/local/lib/jvm/ 12 | - sudo rm -rf /usr/lib/jvm/openjdk-8 13 | - sudo apt-get install -y openjdk-8-jdk-headless 14 | - export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/ 15 | script: 16 | - lein with-profile dev test 17 | #- # same as previous stage 18 | # script: lein with-profile fips test 19 | # jdk: openjdk8 20 | - name: jdk11 21 | before_install: 22 | - sudo rm -rf /usr/local/lib/jvm/ 23 | - sudo rm -rf /usr/lib/jvm/openjdk-11 24 | - sudo apt-get install -y openjdk-11-jdk-headless 25 | - export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/ 26 | script: 27 | - lein with-profile dev test 28 | #- # same as previous stage 29 | # script: lein with-profile fips test 30 | # jdk: openjdk11 31 | sudo: false 32 | # Java needs to be able to resolve its hostname when doing integration style 33 | # tests, which it cannot do in certain cases with travis-ci. If we need the 34 | # runtime/container to be able to resolve its own hostname we need to use 35 | # either the `hostname` or `hosts` "addon" for travis. Since we don't care 36 | # what the hostname is, here we just give it a garbage name based on the name 37 | # of the project. 38 | addons: 39 | hostname: cljhttpclient 40 | notifications: 41 | email: false 42 | -------------------------------------------------------------------------------- /dev-resources/gen-pki.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! [[ -d dev-resources/ssl ]]; then 4 | echo "This script must be called from the root of the project and dev-resources/ssl must already exist" 5 | exit 1 6 | fi 7 | 8 | echo 9 | echo "Generating primary self-signed CA" 10 | openssl req -x509 \ 11 | -newkey rsa:4096 \ 12 | -keyout dev-resources/ssl/ca.key \ 13 | -out dev-resources/ssl/ca.pem \ 14 | -days 1825 -nodes \ 15 | -extensions x509v3_CA \ 16 | -config dev-resources/exts.cnf \ 17 | -subj "/C=US/ST=OR/L=Portland/O=Puppet, Inc/CN=puppet" 18 | 19 | echo 20 | echo "Generating node cert" 21 | openssl genrsa -out dev-resources/ssl/key.pem 2048 22 | 23 | echo 24 | echo "Creating node CSR" 25 | openssl req -new -sha256 \ 26 | -key dev-resources/ssl/key.pem \ 27 | -out dev-resources/ssl/csr.pem \ 28 | -subj "/C=US/ST=OR/L=Portland/O=Puppet, Inc/CN=localhost" 29 | 30 | echo 31 | echo "Signing node CSR" 32 | openssl x509 -req \ 33 | -in dev-resources/ssl/csr.pem \ 34 | -CA dev-resources/ssl/ca.pem \ 35 | -CAkey dev-resources/ssl/ca.key \ 36 | -CAcreateserial \ 37 | -out dev-resources/ssl/cert.pem \ 38 | -days 1825 -sha256 39 | 40 | echo 41 | echo "Generating alternate self-signed CA" 42 | openssl req -x509 \ 43 | -newkey rsa:4096 \ 44 | -keyout dev-resources/ssl/alternate-ca.key \ 45 | -out dev-resources/ssl/alternate-ca.pem \ 46 | -days 1825 -nodes \ 47 | -extensions x509v3_CA \ 48 | -config dev-resources/exts.cnf \ 49 | -subj "/C=US/ST=OR/L=Portland/O=Puppet, Inc/CN=alternate" 50 | 51 | 52 | echo 53 | echo "Cleaning up files that will not be used by the tests" 54 | rm dev-resources/ssl/{alternate-ca.key,ca.key,ca.srl,csr.pem} 55 | -------------------------------------------------------------------------------- /dev-resources/ssl/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEApSX8xhreMenYGIuJ1Znc2ZQJJBXuAFRhCRhgBb5fOtKYAJfN 3 | H9/PA2ngLXKnIdy81uj+lwHobrN+eut8qUNciBJfPS3L9gZhvTi1qJh14XmbK9d1 4 | 32ahjrSBPGZZgaVom6P/2qbpnb7jpVnl4XSetnFYtq0zuIgNwVbK9aU92KLJCJgF 5 | 6tvEKfnwsLAvEC5x+sjEyx3ZC1ZGKw2mc8Q3ulkeMQEqCGyALNOxPV851XPY9j1V 6 | qkIklvvwZq9w2ljREtMkt1GyYnzj3Rbmu7CZ5TQrflk24MxELUhqp550/FDOk1AB 7 | Mw3kgyGjlCKkbcO0uwpYo9XteABann7YCw5Z6QIDAQABAoIBAGr2mqx41HcBnVvw 8 | bLqzKA6oSe9cYMVH4X1xkyfFMIq5wYeIymudmsf8OB1XiBZvkMOlLtgBfVjN2tqk 9 | UR6UKYdcVuEaG2NiqMVUyJvE+3ypNa6Eo6ypHXR/RsEnDUviJUnBQ8KLnv3zmCAN 10 | hM2sqetCqJHQxpH7VOVnYJcPimoXokMgAk9G/q4mAyZZqpvqME5crxxaK8E2wESp 11 | QYCEJXpTSsXhyeJQcqa0+1oZzCj9iSYvG4oJ3HJ14gJZVn9Gqr5ESWaNXd1vrv/n 12 | NCX1RzvJphEqekSshyvkgS68bYzXWqxGngbtBxba6GuHmhqfelBGj5axFlKRvYCe 13 | cIa+Yf0CgYEA2ocpyhxbG7bxQZ4A5ZHEUMKeO1nN+qBKAYcmh/0QLnFy2XVAAL4O 14 | vDhILfABQs2a5hN8ZfzCf2dWGcZ6J9p8DoIYfz/89kp8hjXpJNy7A8XwyeAw9sSJ 15 | 5z/9AP3gAQYnFxVb19QT/ZBCls4IYUz22n4y5jrKayuQfsmTOnOzIUMCgYEAwXeX 16 | olhHs3YQOIxwz8or/AdTp1s+H/SL+o7xFlv84qctoiEULhvSmWMdS4tugzlEiSzZ 17 | fxCooqg8lnKwkRjS6URaB+sAfZIv7I+vwyeSEqtAKV+TZTyTJ+fKSZm9afXhTIDe 18 | /BlBxJ7lHASHpMBaqsArIOlF6FV+xegcjPfzP2MCgYBM1OzqdKHL0rxsN6NVE7UU 19 | N/juIRr8nVKnyt7PPThtO9IHhuPj3u7LWnZ2QEYROLzXW86HBSFVLf3lvhTA4l5v 20 | s2ntg4/rADFb9qRsI3dVUkjgkYRlnqBlv+eya5BQi9s+kHHkJlqI8imXYAUuQKMi 21 | GvDGZbE5kO2SxkiPapJ2hQKBgCeED9q/AVYshT+nn9sxRi6iKHoEbvoD7xtsWt3g 22 | SBTbZLy62O5aDHf8AJ3PivEOn2sNWBdWBbvDdSydnGbmlR3EMkdvvpfnZhaBerr3 23 | uCRMCjzpIqgI6V27QFwsJL4h1LTVlbYUSpSoh89jNXl3OI2r1qNQQlEkNVKjCEHa 24 | 2zpVAoGAVVYrahxnn+jzUAkj62Z2DFZkWvf/c788vXp+gIWva7g1CoP822QukdoR 25 | po71a4oNeTyeOh0w055YrM8xHGjZt6ySzBK2dLVjyaQqbIz8gOzYsAwKit0mmeNw 26 | ZE/xL20E2hmkdh54mr2qeMzjf0D4vZR/FjXGyYaxrqhrrlSWraU= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/puppetlabs/http/client/test_common.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.http.client.test-common 2 | (:require [ring.middleware.params :as ring-params] 3 | [puppetlabs.trapperkeeper.core :as tk] 4 | [puppetlabs.trapperkeeper.testutils.logging :as testlogging]) 5 | (:import (java.net ConnectException SocketTimeoutException))) 6 | 7 | (defn query-params-test 8 | [req] 9 | {:status 200 10 | :body (str (:query-params req))}) 11 | 12 | (def app-wrapped 13 | (ring-params/wrap-params query-params-test)) 14 | 15 | (tk/defservice test-params-web-service 16 | [[:WebserverService add-ring-handler]] 17 | (init [this context] 18 | (add-ring-handler app-wrapped "/params") 19 | context)) 20 | 21 | (def queryparams {"foo" "bar" 22 | "baz" "lux"}) 23 | 24 | (def query-options {:method :get 25 | :url "http://localhost:8080/params/" 26 | :query-params queryparams 27 | :as :text}) 28 | 29 | (defn redirect-test-handler 30 | [req] 31 | (condp = (:uri req) 32 | "/hello/world" {:status 200 :body "Hello, World!"} 33 | "/hello" {:status 302 34 | :headers {"Location" "/hello/world"} 35 | :body ""} 36 | {:status 404 :body "D'oh"})) 37 | 38 | (tk/defservice redirect-web-service 39 | [[:WebserverService add-ring-handler]] 40 | (init [this context] 41 | (add-ring-handler redirect-test-handler "/hello") 42 | context)) 43 | 44 | (defmacro connect-exception-thrown? 45 | [& body] 46 | `(try 47 | (testlogging/with-test-logging ~@body) 48 | (catch SocketTimeoutException _# true) 49 | (catch ConnectException _# true))) 50 | 51 | (defn elapsed-within-range? 52 | [start-time-milliseconds duration-milliseconds] 53 | (<= (System/currentTimeMillis) (+ start-time-milliseconds 54 | duration-milliseconds))) 55 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/CoercedClientOptions.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import javax.net.ssl.SSLContext; 4 | 5 | public class CoercedClientOptions { 6 | private final SSLContext sslContext; 7 | private final String[] sslProtocols; 8 | private final String[] sslCipherSuites; 9 | private final boolean forceRedirects; 10 | private final boolean followRedirects; 11 | private final int connectTimeoutMilliseconds; 12 | private final int socketTimeoutMilliseconds; 13 | 14 | public CoercedClientOptions(SSLContext sslContext, 15 | String[] sslProtocols, 16 | String[] sslCipherSuites, 17 | boolean forceRedirects, 18 | boolean followRedirects, 19 | int connectTimeoutMilliseconds, 20 | int socketTimeoutMilliseconds) { 21 | this.sslContext = sslContext; 22 | this.sslProtocols = sslProtocols; 23 | this.sslCipherSuites = sslCipherSuites; 24 | this.forceRedirects = forceRedirects; 25 | this.followRedirects = followRedirects; 26 | this.connectTimeoutMilliseconds = connectTimeoutMilliseconds; 27 | this.socketTimeoutMilliseconds = socketTimeoutMilliseconds; 28 | } 29 | 30 | public SSLContext getSslContext() { return sslContext; } 31 | 32 | public String[] getSslProtocols() { return sslProtocols; } 33 | 34 | public String[] getSslCipherSuites() { return sslCipherSuites; } 35 | 36 | public boolean getForceRedirects() { return forceRedirects; } 37 | 38 | public boolean getFollowRedirects() { return followRedirects; } 39 | 40 | public int getConnectTimeoutMilliseconds() { 41 | return connectTimeoutMilliseconds; 42 | } 43 | 44 | public int getSocketTimeoutMilliseconds() { 45 | return socketTimeoutMilliseconds; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/Response.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | import com.puppetlabs.http.client.RequestOptions; 4 | import org.apache.http.entity.ContentType; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * This class represents a response from an HTTP request. 10 | */ 11 | public class Response { 12 | private RequestOptions options; 13 | private String origContentEncoding; 14 | private Throwable error; 15 | private Object body; 16 | private Map headers; 17 | private Integer status; 18 | private String reasonPhrase; 19 | private ContentType contentType; 20 | 21 | public Response(RequestOptions options, Throwable error) { 22 | this.options = options; 23 | this.error = error; 24 | } 25 | 26 | public Response(RequestOptions options, String origContentEncoding, 27 | Object body, Map headers, int status, 28 | String reasonPhrase, ContentType contentType) { 29 | this.options = options; 30 | this.origContentEncoding = origContentEncoding; 31 | this.body = body; 32 | this.headers = headers; 33 | this.status = status; 34 | this.reasonPhrase = reasonPhrase; 35 | this.contentType = contentType; 36 | } 37 | 38 | 39 | public RequestOptions getOptions() { 40 | return options; 41 | } 42 | 43 | public String getOrigContentEncoding() { return origContentEncoding; } 44 | 45 | public Throwable getError() { 46 | return error; 47 | } 48 | 49 | public Object getBody() { 50 | return body; 51 | } 52 | 53 | public Map getHeaders() { 54 | return headers; 55 | } 56 | 57 | public Integer getStatus() { 58 | return status; 59 | } 60 | 61 | public String getReasonPhrase() { 62 | return reasonPhrase; 63 | } 64 | 65 | public ContentType getContentType() { return contentType; } 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/mend.yaml: -------------------------------------------------------------------------------- 1 | name: mend_scan 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: connect_twingate 12 | uses: twingate/github-action@v1 13 | with: 14 | service-key: ${{ secrets.TWINGATE_PUBLIC_REPO_KEY }} 15 | - name: checkout repo content 16 | uses: actions/checkout@v2 # checkout the repository content to github runner. 17 | with: 18 | fetch-depth: 1 19 | # install java which is required for mend and clojure 20 | - name: setup java 21 | uses: actions/setup-java@v3 22 | with: 23 | distribution: temurin 24 | java-version: 17 25 | # install clojure tools 26 | - name: Install Clojure tools 27 | uses: DeLaGuardo/setup-clojure@10.1 28 | with: 29 | # Install just one or all simultaneously 30 | # The value must indicate a particular version of the tool, or use 'latest' 31 | # to always provision the latest version 32 | cli: latest # Clojure CLI based on tools.deps 33 | lein: latest # Leiningen 34 | boot: latest # Boot.clj 35 | bb: latest # Babashka 36 | clj-kondo: latest # Clj-kondo 37 | cljstyle: latest # cljstyle 38 | zprint: latest # zprint 39 | # run lein gen 40 | - name: create pom.xml 41 | run: lein pom 42 | # download mend 43 | - name: download_mend 44 | run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar 45 | - name: run mend 46 | run: env WS_INCLUDES=pom.xml java -jar wss-unified-agent.jar 47 | env: 48 | WS_APIKEY: ${{ secrets.MEND_API_KEY }} 49 | WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent 50 | WS_USERKEY: ${{ secrets.MEND_TOKEN }} 51 | WS_PRODUCTNAME: Puppet Enterprise 52 | WS_PROJECTNAME: ${{ github.event.repository.name }} 53 | -------------------------------------------------------------------------------- /dev-resources/ssl/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFizCCA3OgAwIBAgIJAODrD7R4ZHmfMA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJPUjERMA8GA1UEBwwIUG9ydGxhbmQxFDASBgNVBAoM 4 | C1B1cHBldCwgSW5jMQ8wDQYDVQQDDAZwdXBwZXQwHhcNMjMwNzA2MjE1NTAyWhcN 5 | MjgwNzA0MjE1NTAyWjBUMQswCQYDVQQGEwJVUzELMAkGA1UECAwCT1IxETAPBgNV 6 | BAcMCFBvcnRsYW5kMRQwEgYDVQQKDAtQdXBwZXQsIEluYzEPMA0GA1UEAwwGcHVw 7 | cGV0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAt4RXxU3eocqhDD+F 8 | zVU2vnELSEcwoHgD5VS2dtzjlEwv4g8L4Kds/FDoyaZt1846zvEC8+NkLIYypn0L 9 | PMuR0bO3ZAf4YTnSYYeleds1gKWjetgku2Wz4G/isfRQMYGanE7YyhvQA6MHnPy2 10 | Kvpy7TpQ0c+YVOdVto06I+Y36YDlljztfFEmAmyUb9kW1+SAwo/Je/PjJ5Gdav47 11 | rsTEASDe4D2ciVIZLbY8d1zeYvFPNW7K6yWCqxnJJxiNu2ODnO7T9O/jUz6RbzDH 12 | qbpK6dQoKW0ZCBge7wcQaNLROuAEdXP2zmMXFGIJcpSlYynKo+puVMkdReVWccRB 13 | VbBybkblhPXmVzd4ZFGbjkiQysLdPSfSfEs/r+r2+yfwzGAgMGmnK6+eIL6zO9PD 14 | nLEmXV7zCQXz6Ja8Nhx0RHdFBkHdbbxymM5GzIhglVKbx6NTIrnaAWciu5gGv+bI 15 | hpoIXelh2G87xFoASJbW0b0E6RWRGu7P4PwtEg7cgaUfC4d1auIa4uDv1oBSNYGD 16 | hd1WefKohJZMXWrCR0g9MEHO7Q0t1Ct7ZDXhRkBabcV6CZssixEY2MxsZvZ5n04m 17 | islnOvrVhgXM7rGvf3twzgcSAACIUxrf65E5+It6FpeM1bLi/Q/nty9z0GU2hZO8 18 | YedDSLhMacc97ga0NDoD+F5ms38CAwEAAaNgMF4wHQYDVR0OBBYEFMm/bWMZQQjp 19 | i8zVAim+VKMkIiYqMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgGGMB8GA1Ud 20 | IwQYMBaAFMm/bWMZQQjpi8zVAim+VKMkIiYqMA0GCSqGSIb3DQEBCwUAA4ICAQAT 21 | DlMpQSz1VnQH31Ez1ZFEhju0vzh2cVNvN5zB7+3NjYaYTfROWkOweoa+ZHi2uSuO 22 | x39mrBURLE4AsUhQYncmOi1EdPBoSUyBQcgVlurF8mfSpLkoo3+GiiBDOBTq6zip 23 | vQhOylmHfpUd/QdddHGhndmMxFMihIkJeqQY2p0NBRUNG/OeN/w9BNpW1yo7RZ9E 24 | yfeNnSmeMDFWLSMRVXBgd/Q0n5fjdCDKvATo6YcZAt5WO8h5SH86fBqJKg6dTwXf 25 | RvwX4c5fONTrUtkGHgxfTpRo6OVYMzG8AKJBqwtJha7vH1qXqPiwP5Wfr6OSL7rI 26 | G1sNPptHD9z7jTxM5eDtR/6YPf6NVwsp4VyuFawr7Lzo0NTAiBhZ87pv9YrsdlLo 27 | 5uEK0EZC0r9icPKIKqKe1DTBD+Vg0krqm+tCqFP0ma7DVC0POd9FSvOEHBP11O6J 28 | 5KZNIpZXQM8bljlrZCUumH6A6jLSs1YlLXb6H1mDD3b+jHfLnLpwp4+bIVH5myTo 29 | ZO2QI77eZgHRAoBu8J6j1eLO0ug4PYVzePD0kzghBe5WTMvYoOHU6J1+p4WgjWcQ 30 | 0K5UifVcKOvU+l7OLc5iNUc9bE1+rTm0TIxLkuUJApmConBE+dZis+vfoNR1iHni 31 | JADCb2Dn56aa8nkhHjqaKx+T8ez/dSZzvR//2Q4lCw== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /dev-resources/ssl/alternate-ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFkTCCA3mgAwIBAgIJANGPM8FNFV15MA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJPUjERMA8GA1UEBwwIUG9ydGxhbmQxFDASBgNVBAoM 4 | C1B1cHBldCwgSW5jMRIwEAYDVQQDDAlhbHRlcm5hdGUwHhcNMjMwNzA2MjE1NTAy 5 | WhcNMjgwNzA0MjE1NTAyWjBXMQswCQYDVQQGEwJVUzELMAkGA1UECAwCT1IxETAP 6 | BgNVBAcMCFBvcnRsYW5kMRQwEgYDVQQKDAtQdXBwZXQsIEluYzESMBAGA1UEAwwJ 7 | YWx0ZXJuYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApZv7/XJ7 8 | zlRovxHiaEusnA2tE9xrGN9hrgk4REUZpPmM+dthhqKyesJHuPSt0SPMhX8j93yF 9 | 8KRKHBsYdmSXiDB57w70q33Yb/V4fMv/IWRMuKJda/sS0kuMXmmsZIxQpaD5tOrm 10 | rfPaO6606OANBmjZdnDWpeCwna/87ojf0LLE77RvtYpbgwauGfMYU5Ae5mNSX19h 11 | T+e+qZPUIuxBFoHFXBXxothe/SeU7MvGPC1fRdKREhr1o19cMbchF2HS3LH37oIi 12 | cF3PRye80TMrr3fHHzExHEGhjUgDqnmSXPIRmP7S+Ma5tNnxxAp6LBw2dAdNWb6h 13 | FAZyIsU8F2z64aUA059J6PUivqRaeL05lrbqqdBCnEnEHqkex9b5NsCIx/o5O5Lj 14 | xVBSNW3u7kq+ooEcejENi16IWUSv+FVdtmzrWLAdu9nOrLLy2JRuvy6XVOxvkeiI 15 | RZ4HSxxTanN1jkUa6mW9NTpaJN4XSOVgQLRZZTekQWB2ySdrD/YxoE2H0PWQEn4L 16 | pW5WsONAx3okOEIV4NlBzqrsnDydwarJ9ABx3+3mCS7+rM5gC06xjON9fINjqA98 17 | Rgdn0MWvseN0TUDlaWQ5bZO6RTQcunm9hingjCDrSfssr8GJlJaOClSHZ/o53OzH 18 | Fp5L5bmf2BicV00OtaMWgyAxrjUjZohHooECAwEAAaNgMF4wHQYDVR0OBBYEFCjz 19 | hhlZIbhvIhBvYYuDZhPLpJ7nMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgGG 20 | MB8GA1UdIwQYMBaAFCjzhhlZIbhvIhBvYYuDZhPLpJ7nMA0GCSqGSIb3DQEBCwUA 21 | A4ICAQCJYsyfuMr9NJKPKxopd/1GjLsJGkdiWHmIdzfc6RDaiLEpZ2UnU5G1CUzn 22 | SXclD6SmCASfq/jqBMLyq/BOXAXBGOgslVxOBNTVSY5yFiEip+r9y29Erd+ngLr5 23 | ITE6+m8Wsq8fIShtcwEughKG2Gu6Lz8NQw+W5eLx+zztcKSZpnRv6zcBV/8sNmA2 24 | SRlNZwb2yJ6NlPJN8JdqnCn1Od+vxGhcg5aT7AqDQRQ46O2GbDS8f4y4Y0Di6tue 25 | muhR2CeCNJcDgLhCpZJ//35Ga2cZFz2ZDAdvsGGNdrePy6nK8/DZLTA4KiiZMei9 26 | D/i/gdfCdv5l/BJJzRCrG1J5BRz7j1OOyNZ3K+E3tTrU46aTt2N667KWmOQeFRI9 27 | A3KoBNIRV7assmFWSbuKU7bVhb4+7QHTx9NGOEil8gF/1gpbZ8/AvkwH0ba0au3d 28 | 5HPFXJkPis0L3FzHkNSS92pqNWPUyhxpIfcepD8/M9lqkHp+znUfm5drPWLbbO66 29 | MwgnFOyJR6leXR+ftbnPQ0k5wxETykjapddbbIuBH/uFsTgZYshOJWqvf0iPwYiu 30 | dTy7hv6Qovl+t93KILqL2nBxRzw117FkzuM50sSd6Uj933y5Am3eVn6ola8bXYHS 31 | P3qRiCFZQ3t5R/jLZqQKaxynLy/k9zDgNCYMQfvLh4fVdOu4eg== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/JavaResponseDeliveryDelegate.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import com.puppetlabs.http.client.RequestOptions; 4 | import com.puppetlabs.http.client.Response; 5 | import org.apache.http.entity.ContentType; 6 | 7 | import java.util.Map; 8 | 9 | public final class JavaResponseDeliveryDelegate implements ResponseDeliveryDelegate { 10 | 11 | private final Promise promise; 12 | 13 | public JavaResponseDeliveryDelegate(Promise promise) { 14 | this.promise = promise; 15 | } 16 | 17 | private void deliverResponse(Response response, RequestOptions requestOptions, IResponseCallback callback) { 18 | if (callback != null) { 19 | try { 20 | promise.deliver(callback.handleResponse(response)); 21 | } catch (Exception e) { 22 | promise.deliver(new Response(requestOptions, e)); 23 | } 24 | } else { 25 | promise.deliver(response); 26 | } 27 | } 28 | 29 | @Override 30 | public void deliverResponse(RequestOptions requestOptions, 31 | String origContentEncoding, 32 | Object body, 33 | Map headers, 34 | int statusCode, 35 | String reasonPhrase, 36 | ContentType contentType, 37 | IResponseCallback callback) { 38 | Response response = new Response(requestOptions, 39 | origContentEncoding, 40 | body, 41 | headers, 42 | statusCode, 43 | reasonPhrase, 44 | contentType); 45 | deliverResponse(response, requestOptions, callback); 46 | } 47 | 48 | @Override 49 | public void deliverResponse(RequestOptions requestOptions, 50 | Exception e, 51 | IResponseCallback callback) { 52 | deliverResponse(new Response(requestOptions, e), requestOptions, callback); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # puppetlabs/http-client 2 | 3 | [![Build Status](https://travis-ci.com/puppetlabs/clj-http-client.png?branch=master)](https://travis-ci.com/puppetlabs/clj-http-client) 4 | 5 | This is a wrapper around the [Apache HttpAsyncClient 6 | library](http://hc.apache.org/httpcomponents-asyncclient-4.0.x/) providing 7 | some extra functionality for configuring SSL in a way compatible with Puppet. 8 | 9 | ## Installation 10 | 11 | Add the following dependency to your `project.clj` file: 12 | 13 | [![Clojars Project](http://clojars.org/puppetlabs/http-client/latest-version.svg)](http://clojars.org/puppetlabs/http-client) 14 | 15 | ## Details 16 | 17 | Async versions of the http methods are exposed in 18 | puppetlabs.http.client.async, and synchronous versions are in 19 | puppetlabs.http.client.sync. For information on using these namespaces, see the page on 20 | [making requests with clojure clients](doc/clojure-client.md). 21 | 22 | Additionally, this library allows you to make requests using Java clients. For information 23 | on how to do this, see the page on [making requests with java clients](doc/java-client.md). 24 | 25 | ## Testing 26 | 27 | The tests require pki files in the `dev-resources/ssl/` directory of: 28 | * `ca.pem`: a CA cert with the CN of "puppet" 29 | * `key.pem`: a node private key 30 | * `cert.pem`: a cert signed by `ca.pem` for the private key at `key.pem` with a CN of "localhost" 31 | * `alternate-ca.pem`: a valid but untrusted CA cert 32 | 33 | The repo contains these files needed for testing, though if needed you may 34 | want to read `dev-resources/gen-pki.sh` for the commands to generate additional 35 | sets of files. 36 | 37 | ## Upgrading dependencies 38 | 39 | The APIs provided by httpclient change significantly in 5.0 and some of the 40 | internal classes that we've extended change within minor releases of the 4.5 41 | series. Whenever upgrading the apache/httpcomponents dependencies an audit of 42 | the Java classes should be undertaken, especially the classes pertaining to 43 | SafeRedirectedRequest and RedirectStrategy. 44 | 45 | ## Support 46 | 47 | We use the [Trapperkeeper project on JIRA](https://tickets.puppetlabs.com/browse/TK) 48 | for tickets on clj-http-client, although Github issues are welcome too. 49 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/SslUtils.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import com.puppetlabs.ssl_utils.SSLUtils; 4 | import com.puppetlabs.http.client.HttpClientException; 5 | import com.puppetlabs.http.client.ClientOptions; 6 | import com.puppetlabs.http.client.Sync; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.io.FileReader; 11 | import java.io.IOException; 12 | import java.security.*; 13 | import java.security.cert.CertificateException; 14 | 15 | public class SslUtils { 16 | private static final Logger LOGGER = LoggerFactory.getLogger(SslUtils.class); 17 | 18 | public static ClientOptions configureSsl(ClientOptions options) { 19 | if (options.getSslContext() != null) { 20 | return options; 21 | } 22 | 23 | if ((options.getSslCert() != null) && 24 | (options.getSslKey() != null) && 25 | (options.getSslCaCert() != null)) { 26 | try { 27 | options.setSslContext( 28 | SSLUtils.pemsToSSLContext( 29 | new FileReader(options.getSslCert()), 30 | new FileReader(options.getSslKey()), 31 | new FileReader(options.getSslCaCert())) 32 | ); 33 | } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | 34 | KeyManagementException | UnrecoverableKeyException | NoSuchProviderException e) { 35 | LOGGER.error("Error while configuring SSL", e); 36 | throw new HttpClientException("Error while configuring SSL", e); 37 | } 38 | options.setSslCert(null); 39 | options.setSslKey(null); 40 | options.setSslCaCert(null); 41 | return options; 42 | } 43 | 44 | if (options.getSslCaCert() != null) { 45 | try { 46 | options.setSslContext( 47 | SSLUtils.caCertPemToSSLContext( 48 | new FileReader(options.getSslCaCert())) 49 | ); 50 | } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | 51 | KeyManagementException | NoSuchProviderException e) { 52 | LOGGER.error("Error while configuring SSL", e); 53 | throw new HttpClientException("Error while configuring SSL", e); 54 | } 55 | options.setSslCaCert(null); 56 | return options; 57 | } 58 | 59 | return options; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/com/puppetlabs/http/client/impl/java_client_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.puppetlabs.http.client.impl.java-client-test 2 | (:import (com.puppetlabs.http.client.impl JavaClient) 3 | (org.apache.commons.io IOUtils) 4 | (com.puppetlabs.http.client ResponseBodyType RequestOptions) 5 | (org.apache.http.entity ContentType) 6 | (java.io ByteArrayInputStream)) 7 | (:require [clojure.test :refer :all])) 8 | 9 | ;; NOTE: there are more comprehensive, end-to-end tests for 10 | ;; the Java client functionality lumped in with the clojure 11 | ;; tests. This namespace is just for some Java-only unit tests. 12 | 13 | (deftest test-coerce-body-type 14 | (testing "Can handle a Content Type header with no charset" 15 | (let [body "foo" 16 | body-stream (IOUtils/toInputStream body "UTF-8")] 17 | (is (= "foo" (JavaClient/coerceBodyType 18 | body-stream 19 | ResponseBodyType/TEXT 20 | ContentType/WILDCARD)))))) 21 | 22 | (defn request-options 23 | [body content-type-value] 24 | (new RequestOptions 25 | nil {"content-type" content-type-value} body false nil)) 26 | 27 | (defn compute-content-type 28 | [body content-type-value] 29 | (-> 30 | (JavaClient/getContentType body (request-options body content-type-value)) 31 | ;; Calling .toString on an instance of org.apache.http.entity.ContentType 32 | ;; generates the string that'll actually end up in the header. 33 | .toString)) 34 | 35 | 36 | ;; This test case is 100% copypasta from puppetlabs.http.client.async-test 37 | (deftest content-type-test 38 | (testing "value of content-type header is computed correctly" 39 | (testing "a byte stream which specifies application/octet-stream" 40 | (let [body (ByteArrayInputStream. (byte-array [(byte 1) (byte 2)]))] 41 | (is (= (compute-content-type body "application/octet-stream") 42 | "application/octet-stream")))) 43 | 44 | (testing "the request body is a string" 45 | (testing "when a charset is specified, it is honored" 46 | (let [body "foo"] 47 | (is (= (compute-content-type body "text/plain; charset=US-ASCII") 48 | "text/plain; charset=US-ASCII")))) 49 | 50 | (testing "a missing charset yields a content-type that maintains 51 | the given mime-type but adds UTF-8 as the charset" 52 | (let [body "foo"] 53 | (is (= (compute-content-type body "text/html") 54 | "text/html; charset=UTF-8"))))))) 55 | 56 | 57 | (deftest null-response-body-coerced-as-text 58 | (testing "a null response body is coerced into a string by JavaClient.coerceBodyType" 59 | (let [body nil] 60 | (is (= "" (JavaClient/coerceBodyType body ResponseBodyType/TEXT nil)))))) 61 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/SafeRedirectedRequest.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import java.lang.reflect.Proxy; 4 | import java.lang.reflect.InvocationHandler; 5 | import java.lang.reflect.Method; 6 | import java.util.Arrays; 7 | 8 | import org.apache.http.Header; 9 | import org.apache.http.client.methods.HttpGet; 10 | import org.apache.http.client.methods.HttpUriRequest; 11 | 12 | // To use this class call SafeRedirectedRequest.wrap(HttpUriRequest). Will 13 | // wrap the given request and proxy all methods to it, except for setHeaders, 14 | // which we implement and filter out potentially unsafe headers from. 15 | // 16 | // See https://stackoverflow.com/questions/30344715/automatically-delegating-all-methods-of-a-java-class 17 | // for inspiration for this work. 18 | public class SafeRedirectedRequest 19 | // We extend HttpGet to satisfy the requirement to have implementations 20 | // of the HttpUriRequest, but proxy all invocations to the wrapped delegate. 21 | extends HttpGet 22 | implements HttpUriRequest, InvocationHandler { 23 | 24 | private final HttpUriRequest delegate; 25 | 26 | public SafeRedirectedRequest(HttpUriRequest delegate) { 27 | this.delegate = delegate; 28 | } 29 | 30 | public static HttpUriRequest wrap(HttpUriRequest wrapped) { 31 | return (HttpUriRequest) Proxy.newProxyInstance(HttpUriRequest.class.getClassLoader(), 32 | new Class[]{HttpUriRequest.class}, 33 | new SafeRedirectedRequest(wrapped)); 34 | } 35 | 36 | // There are other ways to set headers (setHeader(String, String), 37 | // setHeader(Header)), however this is the method currently used to 38 | // copy exiting headers when being redirected. 39 | @Override 40 | public void setHeaders(final Header[] headers) { 41 | final Header[] cleanedHeaders = Arrays.stream(headers).filter(header -> 42 | CreateRedirectUtil.SECURITY_RELATED_HEADERS.stream().noneMatch(mask -> 43 | mask.equalsIgnoreCase(header.getName()))) 44 | .toArray(Header[]::new); 45 | delegate.setHeaders(cleanedHeaders); 46 | } 47 | 48 | // Begin Proxy/InvocationHandler implementation 49 | @Override 50 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 51 | Method m = findMethod(this.getClass(), method); 52 | if (m != null) { 53 | return m.invoke(this, args); 54 | } else { 55 | return method.invoke(this.delegate, args); 56 | } 57 | } 58 | 59 | private Method findMethod(Class clazz, Method method) throws Throwable { 60 | try { 61 | return clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); 62 | } catch (NoSuchMethodException e) { 63 | return null; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/StreamingAsyncResponseConsumer.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import org.apache.http.HttpEntity; 4 | import org.apache.http.HttpResponse; 5 | import org.apache.http.entity.BasicHttpEntity; 6 | import org.apache.http.nio.IOControl; 7 | import org.apache.http.nio.client.methods.AsyncByteConsumer; 8 | import org.apache.http.protocol.HttpContext; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.io.IOException; 13 | import java.io.PipedInputStream; 14 | import java.io.PipedOutputStream; 15 | import java.nio.ByteBuffer; 16 | 17 | public class StreamingAsyncResponseConsumer extends AsyncByteConsumer { 18 | 19 | private volatile HttpResponse response; 20 | private volatile PipedOutputStream pos; 21 | private volatile Deliverable promise; 22 | private volatile Promise ioExceptionPromise = new Promise<>(); 23 | 24 | private static final Logger LOGGER = LoggerFactory.getLogger(StreamingAsyncResponseConsumer.class); 25 | 26 | public void setFinalResult(IOException ioException) { 27 | ioExceptionPromise.deliver(ioException); 28 | } 29 | 30 | public StreamingAsyncResponseConsumer(Deliverable promise) { 31 | this.promise = promise; 32 | } 33 | 34 | @Override 35 | protected void onResponseReceived(final HttpResponse response) throws IOException { 36 | HttpEntity entity = response.getEntity(); 37 | if (entity != null) { 38 | PipedInputStream pis = new ExceptionInsertingPipedInputStream(ioExceptionPromise); 39 | pos = new PipedOutputStream(); 40 | pos.connect(pis); 41 | ((BasicHttpEntity) entity).setContent(pis); 42 | } else { 43 | // this can happen if the server sends no response, like with a 204. 44 | LOGGER.debug("Null entity when processing response"); 45 | } 46 | this.response = response; 47 | promise.deliver(response); 48 | } 49 | 50 | @Override 51 | protected void onByteReceived(final ByteBuffer buf, final IOControl ioctrl) throws IOException { 52 | while (buf.hasRemaining()) { 53 | byte[] bs = new byte[buf.remaining()]; 54 | buf.get(bs); 55 | pos.write(bs); 56 | } 57 | } 58 | 59 | @Override 60 | protected void releaseResources() { 61 | super.releaseResources(); 62 | this.response = null; 63 | this.promise = null; 64 | try { 65 | if (pos != null) { 66 | this.pos.close(); 67 | this.pos = null; 68 | } 69 | } catch (IOException e) { 70 | throw new IllegalStateException(e); 71 | } 72 | } 73 | 74 | @Override 75 | protected HttpResponse buildResult(final HttpContext context) { 76 | return response; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /doc/java-client.md: -------------------------------------------------------------------------------- 1 | ## Making requests with the Java client 2 | 3 | Similarly to the way it is done in clojure code, clj-http-client allows you to make requests 4 | in two ways using Java: with and without a persistent client. 5 | 6 | ## `createClient(ClientOptions clientOptions)` 7 | 8 | clj-http-client allows you to create a persistent synchronous or asynchronous HTTP client using the static 9 | `createClient()` method in the [`Async`](../src/java/com/puppetlabs/http/client/Async.java) and 10 | [`Sync`](../src/java/com/puppetlabs/http/client/Sync.java) classes 11 | 12 | This method takes one argument, `clientOptions`, which is an instance of the 13 | [`ClientOptions`](../src/java/com/puppetlabs/http/client/ClientOptions.java) class, details on which can 14 | be found in its javadoc strings, linked above. 15 | 16 | ### Making requests with a persistent client 17 | 18 | The `createClient()` method returns an object implementing the [`SyncHttpClient`](../src/java/com/puppetlabs/http/client/SyncHttpClient.java) 19 | interface in the case of `Sync`, and the [`AsyncHttpClient`](../src/java/com/puppetlabs/http/client/AsyncHttpClient.java) interface 20 | in the case of `Async`. Information on the various methods available is detailed in the javadoc strings for the corresponding 21 | interfaces, which are linked above. The various request methods provided by these interfaces can take 22 | a [`RequestOptions`](../src/java/com/puppetlabs/http/client/RequestOptions.java) object, information on 23 | which can be found in that class' javadoc strings, linked above. 24 | 25 | For example, say you want to make a GET request 26 | against the URL `http://localhost:8080/test` with query parameter `abc` with value `def`. To make the request 27 | and print the body of the response with a persistent synchronous client, you could do the following: 28 | 29 | ```java 30 | ClientOptions options = new ClientOptions(); 31 | SyncHttpClient client = Sync.createClient(options); 32 | Response response = client.get(new URI("http://localhost:8080/test?abc=def")); 33 | System.out.println(response.getBody()); 34 | ``` 35 | 36 | If instead you wanted to use an asynchronous client, you could do the following: 37 | 38 | ```java 39 | ClientOptions options = new ClientOptions(); 40 | AsyncHttpClient client = Async.createClient(options); 41 | Promise response = client.get(new URI("http://localhost:8080/test?abc=def")); 42 | System.out.println(response.deref().getBody()); 43 | ``` 44 | 45 | ### Closing the client 46 | 47 | Each persistent client provides a `close` method, which can be used to close the client. This method will close 48 | the client and clean up all resources associated with it. It must be called by the caller when finished using the 49 | client to make requests, as there is no implicit cleanup of the associated resources when the client is garbage 50 | collected. Once the client is closed, it can no longer be used to make requests. 51 | 52 | ## Making a Request without a persistent client 53 | 54 | In addition to allowing you to create a persistent client with the `createClient()` method, the 55 | [`Sync`](../src/java/com/puppetlabs/http/client/Sync.java) class contains a number of simple request methods 56 | that allow for requests to be made without a persistent client. These are detailed in `Sync.java`'s 57 | javadoc strings, linked above. Many of the provided request methods take a 58 | [`SimpleRequestOptions`](../src/java/com/puppetlabs/http/client/SimpleRequestOptions.java) object. Information 59 | on this class can be found in its javadoc strings, linked above. 60 | 61 | As an example, say you wanted to make a request to the URL `http://localhost:8080/test` without a persistent client. 62 | You want the query parameter `abc` with value `def`, and you don't want redirects to be followed. In that case, you 63 | would do the following to print the body of the response: 64 | 65 | ```java 66 | SimpleRequestOptions options = new SimpleRequestOptions(new URI("http://localhost:8080/test?abc=def")); 67 | options = options.setFollowRedirects(false); 68 | Response response = Sync.get(options); 69 | System.out.println(response.getBody()); 70 | ``` -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/CreateRedirectUtil.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import org.apache.http.*; 4 | import org.apache.http.client.methods.HttpGet; 5 | import org.apache.http.client.methods.HttpHead; 6 | import org.apache.http.client.methods.HttpUriRequest; 7 | import org.apache.http.client.methods.RequestBuilder; 8 | import org.apache.http.client.protocol.HttpClientContext; 9 | import org.apache.http.impl.client.DefaultRedirectStrategy; 10 | import org.apache.http.protocol.HttpContext; 11 | 12 | import java.net.URI; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | 16 | // This class overrides the getRedirect() method of DefaultRedirectStrategy 17 | // (or LaxRedirectStrategy as it inherits this method from DefaultRedirectStrategy) 18 | // so that we do not copy the auth headers for non get or head requests, and the newly 19 | // created request is wrapped in a SafeRedirectedRequest. SafeRedirectedRequest 20 | // will proxy method invocations to the wrapped request, but intercepts setHeaders(), 21 | // which is used by callers to copy over headers from the original request. 22 | // 23 | // See https://stackoverflow.com/questions/17970633/header-values-overwritten-on-redirect-in-httpclient 24 | // for the inspiration for this work. 25 | // 26 | // Note: This implementation is VERY version specific and will need to be 27 | // revised whenever updating httpclient. 28 | public class CreateRedirectUtil { 29 | public static final int SC_PERMANENT_REDIRECT = 308; 30 | public static final List SECURITY_RELATED_HEADERS = Arrays.asList( 31 | "X-Authorization", "Authorization", 32 | "Cookie", "Set-Cookie", "WWW-Authenticate", 33 | "Proxy-Authorization", "Proxy-Authenticate" 34 | ); 35 | 36 | public static HttpUriRequest getRedirect( 37 | final DefaultRedirectStrategy redirectStrategy, 38 | final HttpRequest request, 39 | final HttpResponse response, 40 | final HttpContext context) throws ProtocolException { 41 | 42 | final URI uri = redirectStrategy.getLocationURI(request, response, context); 43 | final String method = request.getRequestLine().getMethod(); 44 | 45 | // This is new to allow Redirects to contain Auth headers IF they are to the same host/port/scheme 46 | final HttpClientContext clientContext = HttpClientContext.adapt(context); 47 | final HttpHost target = clientContext.getTargetHost(); 48 | final boolean localRedirect = target.getHostName().equals(uri.getHost()) && 49 | target.getPort() == uri.getPort() && 50 | target.getSchemeName().equals(uri.getScheme()); 51 | 52 | 53 | if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) { 54 | return localRedirect ? new HttpHead(uri) : SafeRedirectedRequest.wrap(new HttpHead(uri)); 55 | } else if (method.equalsIgnoreCase(HttpGet.METHOD_NAME)) { 56 | return localRedirect ? new HttpGet(uri) : SafeRedirectedRequest.wrap(new HttpGet(uri)); 57 | } else { 58 | final int status = response.getStatusLine().getStatusCode(); 59 | if (status == HttpStatus.SC_TEMPORARY_REDIRECT || status == SC_PERMANENT_REDIRECT) { 60 | 61 | if (! localRedirect) { 62 | // RequestBuilder.copy will copy any existing headers, which we don't want 63 | RequestBuilder builder = RequestBuilder.copy(request).setUri(uri); 64 | for (String header : SECURITY_RELATED_HEADERS) { 65 | // .removeHeaders does an equalsIgnoreCase() on the passed String. 66 | builder.removeHeaders(header); 67 | } 68 | 69 | return SafeRedirectedRequest.wrap(builder.build()); 70 | } else { 71 | return RequestBuilder.copy(request).setUri(uri).build(); 72 | } 73 | } else { 74 | return localRedirect ? new HttpGet(uri) : SafeRedirectedRequest.wrap(new HttpGet(uri)); 75 | } 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/RequestOptions.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | import java.net.URI; 4 | import java.net.URISyntaxException; 5 | import java.util.Map; 6 | 7 | /** 8 | * This class is a wrapper around a number of options for use in 9 | * configuring an HTTP request. 10 | */ 11 | public class RequestOptions { 12 | private URI uri; 13 | private Map headers; 14 | private Object body; 15 | private CompressType requestBodyCompression = CompressType.NONE; 16 | private boolean decompressBody = true; 17 | private ResponseBodyType as = ResponseBodyType.STREAM; 18 | private String[] metricId; 19 | 20 | /** 21 | * Constructor for the RequestOptions class. When this constructor is called, 22 | * decompressBody will default to true, and as will default to ResponseBodyType.STREAM 23 | * @param url the URL against which to make the request 24 | * @throws URISyntaxException 25 | */ 26 | public RequestOptions (String url) throws URISyntaxException { this.uri = new URI(url); } 27 | 28 | /** 29 | * Constructor for the RequestOptions class. When this constructor is called, 30 | * decompressBody will default to true, and as will default to ResponseBodyType.STREAM 31 | * @param uri the URI against which to make the request 32 | */ 33 | public RequestOptions(URI uri) { 34 | this.uri = uri; 35 | } 36 | 37 | /** 38 | * Constructor for the RequestOptions class 39 | * @param uri the URI against which to make the request 40 | * @param headers A map of headers. Can be null. 41 | * @param body The body of the request. Can be null. 42 | * @param decompressBody If true, an "accept-encoding" header with a value of "gzip, deflate" will be 43 | * automatically decompressed if it contains a recognized "content-encoding" header. 44 | * @param as Used to control the data type of the response body. 45 | */ 46 | public RequestOptions (URI uri, 47 | Map headers, 48 | Object body, 49 | boolean decompressBody, 50 | ResponseBodyType as) { 51 | this.uri = uri; 52 | this.headers = headers; 53 | this.body = body; 54 | this.decompressBody = decompressBody; 55 | this.as = as; 56 | } 57 | 58 | public URI getUri() { 59 | return uri; 60 | } 61 | public RequestOptions setUri(URI uri) { 62 | this.uri = uri; 63 | return this; 64 | } 65 | 66 | public Map getHeaders() { 67 | return headers; 68 | } 69 | public RequestOptions setHeaders(Map headers) { 70 | this.headers = headers; 71 | return this; 72 | } 73 | 74 | public Object getBody() { 75 | return body; 76 | } 77 | public RequestOptions setBody(Object body) { 78 | this.body = body; 79 | return this; 80 | } 81 | 82 | public boolean getDecompressBody() { return decompressBody; } 83 | public RequestOptions setDecompressBody(boolean decompressBody) { 84 | this.decompressBody = decompressBody; 85 | return this; 86 | } 87 | 88 | public CompressType getCompressRequestBody() { 89 | return requestBodyCompression; 90 | } 91 | public RequestOptions setCompressRequestBody( 92 | CompressType requestBodyCompression) { 93 | this.requestBodyCompression = requestBodyCompression; 94 | return this; 95 | } 96 | 97 | public ResponseBodyType getAs() { 98 | return as; 99 | } 100 | public RequestOptions setAs(ResponseBodyType as) { 101 | this.as = as; 102 | return this; 103 | } 104 | 105 | public String[] getMetricId() { 106 | return metricId; 107 | } 108 | 109 | public RequestOptions setMetricId(String[] metricId) { 110 | this.metricId = metricId; 111 | return this; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/PersistentAsyncHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import com.codahale.metrics.MetricRegistry; 4 | import com.puppetlabs.http.client.Response; 5 | import com.puppetlabs.http.client.RequestOptions; 6 | import com.puppetlabs.http.client.HttpMethod; 7 | import com.puppetlabs.http.client.AsyncHttpClient; 8 | import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; 9 | 10 | import java.io.IOException; 11 | import java.net.URI; 12 | import java.net.URISyntaxException; 13 | 14 | public class PersistentAsyncHttpClient implements AsyncHttpClient { 15 | private CloseableHttpAsyncClient client; 16 | private MetricRegistry metricRegistry; 17 | private String metricNamespace; 18 | private boolean enableURLMetrics; 19 | 20 | public PersistentAsyncHttpClient(CloseableHttpAsyncClient client, 21 | MetricRegistry metricRegistry, 22 | String metricNamespace, 23 | boolean enableURLMetrics) { 24 | this.client = client; 25 | this.metricRegistry = metricRegistry; 26 | this.metricNamespace = metricNamespace; 27 | this.enableURLMetrics = enableURLMetrics; 28 | } 29 | 30 | public void close() throws IOException { 31 | client.close(); 32 | } 33 | 34 | public MetricRegistry getMetricRegistry() { 35 | return metricRegistry; 36 | } 37 | 38 | public String getMetricNamespace() { 39 | return metricNamespace; 40 | } 41 | 42 | private Promise request(RequestOptions requestOptions, HttpMethod method) { 43 | final Promise promise = new Promise<>(); 44 | final JavaResponseDeliveryDelegate responseDelivery = new JavaResponseDeliveryDelegate(promise); 45 | JavaClient.requestWithClient(requestOptions, method, null, 46 | client, responseDelivery, metricRegistry, metricNamespace, enableURLMetrics); 47 | return promise; 48 | } 49 | 50 | public Promise get(String url) throws URISyntaxException { 51 | return get(new URI(url)); 52 | } 53 | public Promise get(URI uri) { 54 | return get(new RequestOptions(uri)); 55 | } 56 | public Promise get(RequestOptions requestOptions) { 57 | return request(requestOptions, HttpMethod.GET); 58 | } 59 | 60 | public Promise head(String url) throws URISyntaxException { 61 | return head(new URI(url)); 62 | } 63 | public Promise head(URI uri) { 64 | return head(new RequestOptions(uri)); 65 | } 66 | public Promise head(RequestOptions requestOptions) { 67 | return request(requestOptions, HttpMethod.HEAD); 68 | } 69 | 70 | public Promise post(String url) throws URISyntaxException { 71 | return post(new URI(url)); 72 | } 73 | public Promise post(URI uri) { 74 | return post(new RequestOptions(uri)); 75 | } 76 | public Promise post(RequestOptions requestOptions) { 77 | return request(requestOptions, HttpMethod.POST); 78 | } 79 | 80 | public Promise put(String url) throws URISyntaxException { 81 | return put(new URI(url)); 82 | } 83 | public Promise put(URI uri) { 84 | return put(new RequestOptions(uri)); 85 | } 86 | public Promise put(RequestOptions requestOptions) { 87 | return request(requestOptions, HttpMethod.PUT); 88 | } 89 | 90 | public Promise delete(String url) throws URISyntaxException { 91 | return delete(new URI(url)); 92 | } 93 | public Promise delete(URI uri) { 94 | return delete(new RequestOptions(uri)); 95 | } 96 | public Promise delete(RequestOptions requestOptions) { 97 | return request(requestOptions, HttpMethod.DELETE); 98 | } 99 | 100 | public Promise trace(String url) throws URISyntaxException { 101 | return trace(new URI(url)); 102 | } 103 | public Promise trace(URI uri) { 104 | return trace(new RequestOptions(uri)); 105 | } 106 | public Promise trace(RequestOptions requestOptions) { 107 | return request(requestOptions, HttpMethod.TRACE); 108 | } 109 | 110 | public Promise options(String url) throws URISyntaxException { 111 | return options(new URI(url)); 112 | } 113 | public Promise options(URI uri) { 114 | return options(new RequestOptions(uri)); 115 | } 116 | public Promise options(RequestOptions requestOptions) { 117 | return request(requestOptions, HttpMethod.OPTIONS); 118 | } 119 | 120 | public Promise patch(String url) throws URISyntaxException { 121 | return patch(new URI(url)); 122 | } 123 | public Promise patch(URI uri) { 124 | return patch(new RequestOptions(uri)); 125 | } 126 | public Promise patch(RequestOptions requestOptions) { 127 | return request(requestOptions, HttpMethod.PATCH); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/clj/puppetlabs/http/client/metrics.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.http.client.metrics 2 | (:require [clojure.string :as clj-string] 3 | [puppetlabs.http.client.common :as common] 4 | [schema.core :as schema]) 5 | (:import (com.codahale.metrics MetricRegistry) 6 | (com.puppetlabs.http.client.metrics ClientMetricData Metrics 7 | Metrics$MetricType))) 8 | 9 | (schema/defn get-base-metric-data :- common/BaseMetricData 10 | [data :- ClientMetricData] 11 | {:count (.getCount data) 12 | :mean (.getMean data) 13 | :aggregate (.getAggregate data) 14 | :metric-name (.getMetricName data)}) 15 | 16 | (schema/defn get-url-metric-data :- common/UrlMetricData 17 | [data :- ClientMetricData] 18 | (assoc (get-base-metric-data data) :url (.getUrl data))) 19 | 20 | (schema/defn get-url-and-method-metric-data :- common/UrlAndMethodMetricData 21 | [data :- ClientMetricData] 22 | (assoc (get-url-metric-data data) :method (.getMethod data))) 23 | 24 | (schema/defn get-metric-id-metric-data :- common/MetricIdMetricData 25 | [data :- ClientMetricData] 26 | (assoc (get-base-metric-data data) :metric-id (.getMetricId data))) 27 | 28 | (defn get-java-metric-type 29 | [metric-type] 30 | (case metric-type 31 | :full-response Metrics$MetricType/FULL_RESPONSE)) 32 | 33 | (defn uppercase-method 34 | [method] 35 | (clj-string/upper-case (name method))) 36 | 37 | (defn build-metric-namespace 38 | [metric-prefix server-id] 39 | (Metrics/buildMetricNamespace metric-prefix server-id)) 40 | 41 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 42 | ;;; Public 43 | 44 | (schema/defn ^:always-validate url->metric-url :- schema/Str 45 | [url :- schema/Str] 46 | (Metrics/urlToMetricUrl url)) 47 | 48 | (schema/defn ^:always-validate get-client-metrics 49 | :- (schema/maybe common/AllMetrics) 50 | "Returns the http client-specific metrics from the metric registry." 51 | [metric-registry :- MetricRegistry] 52 | (let [metrics (Metrics/getClientMetrics metric-registry)] 53 | {:url (.getUrlTimers metrics) 54 | :url-and-method (.getUrlAndMethodTimers metrics) 55 | :metric-id (.getMetricIdTimers metrics)})) 56 | 57 | (schema/defn ^:always-validate get-client-metrics-by-url 58 | :- common/Metrics 59 | "Returns the http client-specific url metrics matching the specified url." 60 | [metric-registry :- MetricRegistry 61 | url :- schema/Str] 62 | (Metrics/getClientMetricsByUrl 63 | metric-registry 64 | url)) 65 | 66 | (schema/defn ^:always-validate get-client-metrics-by-url-and-method 67 | :- common/Metrics 68 | "Returns the http client-specific url metrics matching the specified url." 69 | [metric-registry :- MetricRegistry 70 | url :- schema/Str 71 | method :- common/HTTPMethod] 72 | (Metrics/getClientMetricsByUrlAndMethod 73 | metric-registry 74 | url 75 | method)) 76 | 77 | (schema/defn ^:always-validate get-client-metrics-by-metric-id 78 | :- common/Metrics 79 | "Returns the http client-specific url metrics matching the specified url." 80 | [metric-registry :- MetricRegistry 81 | metric-id :- common/MetricId] 82 | (Metrics/getClientMetricsByMetricId 83 | metric-registry 84 | (into-array String (map name metric-id)))) 85 | 86 | (schema/defn ^:always-validate get-client-metrics-data 87 | :- common/AllMetricsData 88 | "Returns a summary of the metric data for all http client timers, organized 89 | in a map by category." 90 | [metric-registry :- MetricRegistry] 91 | (let [data (Metrics/getClientMetricsData metric-registry)] 92 | {:url (map get-url-metric-data (.getUrlData data)) 93 | :url-and-method (map get-url-and-method-metric-data (.getUrlAndMethodData data)) 94 | :metric-id (map get-metric-id-metric-data (.getMetricIdData data))})) 95 | 96 | (schema/defn ^:always-validate get-client-metrics-data-by-url 97 | :- [common/UrlMetricData] 98 | "Returns a summary of the metric data for all http client timers filtered by 99 | url." 100 | [metric-registry :- MetricRegistry 101 | url :- schema/Str] 102 | (let [data (Metrics/getClientMetricsDataByUrl 103 | metric-registry 104 | url)] 105 | (map get-url-metric-data data))) 106 | 107 | (schema/defn ^:always-validate get-client-metrics-data-by-url-and-method 108 | :- [common/UrlAndMethodMetricData] 109 | "Returns a summary of the metric data for all http client timers filtered by 110 | url and method." 111 | [metric-registry :- MetricRegistry 112 | url :- schema/Str 113 | method :- common/HTTPMethod] 114 | (let [data (Metrics/getClientMetricsDataByUrlAndMethod 115 | metric-registry 116 | url 117 | (uppercase-method method))] 118 | (map get-url-and-method-metric-data data))) 119 | 120 | (schema/defn ^:always-validate get-client-metrics-data-by-metric-id 121 | :- [common/MetricIdMetricData] 122 | "Returns a summary of the metric data for all http client timers filtered by 123 | metric-id." 124 | [metric-registry :- MetricRegistry 125 | metric-id :- common/MetricId] 126 | (let [data (Metrics/getClientMetricsDataByMetricId 127 | metric-registry 128 | (into-array String (map name metric-id)))] 129 | (map get-metric-id-metric-data data))) 130 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/PersistentSyncHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl; 2 | 3 | import com.codahale.metrics.MetricRegistry; 4 | import com.puppetlabs.http.client.HttpClientException; 5 | import com.puppetlabs.http.client.Response; 6 | import com.puppetlabs.http.client.RequestOptions; 7 | import com.puppetlabs.http.client.HttpMethod; 8 | import com.puppetlabs.http.client.SyncHttpClient; 9 | import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.io.IOException; 14 | import java.net.URI; 15 | import java.net.URISyntaxException; 16 | 17 | public class PersistentSyncHttpClient implements SyncHttpClient { 18 | private CloseableHttpAsyncClient client; 19 | private MetricRegistry metricRegistry; 20 | private String metricNamespace; 21 | private boolean enableURLMetrics; 22 | private static final Logger LOGGER = LoggerFactory.getLogger(PersistentSyncHttpClient.class); 23 | 24 | public PersistentSyncHttpClient(CloseableHttpAsyncClient client, 25 | MetricRegistry metricRegistry, 26 | String metricNamespace, 27 | boolean enableURLMetrics) { 28 | this.client = client; 29 | this.metricRegistry = metricRegistry; 30 | this.metricNamespace = metricNamespace; 31 | this.enableURLMetrics = enableURLMetrics; 32 | } 33 | 34 | public MetricRegistry getMetricRegistry() { 35 | return metricRegistry; 36 | } 37 | 38 | public String getMetricNamespace() { 39 | return metricNamespace; 40 | } 41 | 42 | public Response request(RequestOptions requestOptions, HttpMethod method) { 43 | final Promise promise = new Promise<>(); 44 | final JavaResponseDeliveryDelegate responseDelivery = new JavaResponseDeliveryDelegate(promise); 45 | JavaClient.requestWithClient(requestOptions, method, null, client, 46 | responseDelivery, metricRegistry, metricNamespace, enableURLMetrics); 47 | final Response response; 48 | try { 49 | response = promise.deref(); 50 | if (response.getError() != null) { 51 | LOGGER.warn("Error executing http request", response.getError()); 52 | throw new HttpClientException("Error executing http request", response.getError()); 53 | } 54 | } catch (InterruptedException e) { 55 | LOGGER.warn("Error while waiting for http response", e); 56 | throw new HttpClientException("Error while waiting for http response", e); 57 | } 58 | return response; 59 | } 60 | 61 | public void close() throws IOException { 62 | client.close(); 63 | } 64 | 65 | public Response get(String url) throws URISyntaxException { 66 | return get(new URI(url)); 67 | } 68 | public Response get(URI uri) { 69 | return get(new RequestOptions(uri)); 70 | } 71 | public Response get(RequestOptions requestOptions) { 72 | return request(requestOptions, HttpMethod.GET); 73 | } 74 | 75 | public Response head(String url) throws URISyntaxException { 76 | return head(new URI(url)); 77 | } 78 | public Response head(URI uri) { 79 | return head(new RequestOptions(uri)); 80 | } 81 | public Response head(RequestOptions requestOptions) { 82 | return request(requestOptions, HttpMethod.HEAD); 83 | } 84 | 85 | public Response post(String url) throws URISyntaxException { 86 | return post(new URI(url)); 87 | } 88 | public Response post(URI uri) { 89 | return post(new RequestOptions(uri)); 90 | } 91 | public Response post(RequestOptions requestOptions) { 92 | return request(requestOptions, HttpMethod.POST); 93 | } 94 | 95 | public Response put(String url) throws URISyntaxException { 96 | return put(new URI(url)); 97 | } 98 | public Response put(URI uri) { 99 | return put(new RequestOptions(uri)); 100 | } 101 | public Response put(RequestOptions requestOptions) { 102 | return request(requestOptions, HttpMethod.PUT); 103 | } 104 | 105 | public Response delete(String url) throws URISyntaxException { 106 | return delete(new URI(url)); 107 | } 108 | public Response delete(URI uri) { 109 | return delete(new RequestOptions(uri)); 110 | } 111 | public Response delete(RequestOptions requestOptions) { 112 | return request(requestOptions, HttpMethod.DELETE); 113 | } 114 | 115 | public Response trace(String url) throws URISyntaxException { 116 | return trace(new URI(url)); 117 | } 118 | public Response trace(URI uri) { 119 | return trace(new RequestOptions(uri)); 120 | } 121 | public Response trace(RequestOptions requestOptions) { 122 | return request(requestOptions, HttpMethod.TRACE); 123 | } 124 | 125 | public Response options(String url) throws URISyntaxException { 126 | return options(new URI(url)); 127 | } 128 | public Response options(URI uri) { 129 | return options(new RequestOptions(uri)); 130 | } 131 | public Response options(RequestOptions requestOptions) { 132 | return request(requestOptions, HttpMethod.OPTIONS); 133 | } 134 | 135 | public Response patch(String url) throws URISyntaxException { 136 | return patch(new URI(url)); 137 | } 138 | public Response patch(URI uri) { 139 | return patch(new RequestOptions(uri)); 140 | } 141 | public Response patch(RequestOptions requestOptions) { 142 | return request(requestOptions, HttpMethod.PATCH); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject puppetlabs/http-client "2.1.5-SNAPSHOT" 2 | :description "HTTP client wrapper" 3 | :license {:name "Apache License, Version 2.0" 4 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 5 | 6 | :min-lein-version "2.9.1" 7 | 8 | :parent-project {:coords [puppetlabs/clj-parent "5.6.7"] 9 | :inherit [:managed-dependencies]} 10 | 11 | ;; Abort when version ranges or version conflicts are detected in 12 | ;; dependencies. Also supports :warn to simply emit warnings. 13 | ;; requires lein 2.2.0+. 14 | :pedantic? :abort 15 | 16 | :dependencies [[org.clojure/clojure] 17 | 18 | [org.apache.httpcomponents/httpasyncclient] 19 | [prismatic/schema] 20 | [commons-io] 21 | [io.dropwizard.metrics/metrics-core] 22 | 23 | [puppetlabs/ssl-utils] 24 | [puppetlabs/i18n] 25 | 26 | [org.slf4j/jul-to-slf4j]] 27 | 28 | :source-paths ["src/clj"] 29 | :java-source-paths ["src/java"] 30 | :jar-exclusions [#".*\.java$"] 31 | 32 | ;; By declaring a classifier here and a corresponding profile below we'll get an additional jar 33 | ;; during `lein jar` that has all the source code (including the java source). Downstream projects can then 34 | ;; depend on this source jar using a :classifier in their :dependencies. 35 | :classifiers [["sources" :sources-jar]] 36 | 37 | :profiles {:provided {:dependencies [[org.bouncycastle/bcpkix-jdk18on]]} 38 | :defaults {:dependencies [[cheshire] 39 | [puppetlabs/kitchensink :classifier "test"] 40 | [puppetlabs/trapperkeeper] 41 | [puppetlabs/trapperkeeper :classifier "test"] 42 | [puppetlabs/trapperkeeper-webserver-jetty9] 43 | [puppetlabs/trapperkeeper-webserver-jetty9 :classifier "test"] 44 | [puppetlabs/ring-middleware]] 45 | :resource-paths ["dev-resources"] 46 | :jvm-opts ["-Djava.util.logging.config.file=dev-resources/logging.properties"]} 47 | :dev-deps {:dependencies [[org.bouncycastle/bcpkix-jdk18on]]} 48 | :dev [:defaults :dev-deps] 49 | :fips-deps {:dependencies [[org.bouncycastle/bcpkix-fips] 50 | [org.bouncycastle/bc-fips] 51 | [org.bouncycastle/bctls-fips]] 52 | ;; this only ensures that we run with the proper profiles 53 | ;; during testing. This JVM opt will be set in the puppet module 54 | ;; that sets up the JVM classpaths during installation. 55 | :jvm-opts ~(let [version (System/getProperty "java.version") 56 | [major minor _] (clojure.string/split version #"\.") 57 | unsupported-ex (ex-info "Unsupported major Java version. Expects 8 or 11." 58 | {:major major 59 | :minor minor})] 60 | (condp = (java.lang.Integer/parseInt major) 61 | 1 (if (= 8 (java.lang.Integer/parseInt minor)) 62 | ["-Djava.security.properties==dev-resources/jdk8-fips-security"] 63 | (throw unsupported-ex)) 64 | 11 ["-Djava.security.properties==dev-resources/jdk11-fips-security"] 65 | 17 ["-Djava.security.properties==dev-resources/jdk17-fips-security"] 66 | (throw unsupported-ex)))} 67 | :fips [:defaults :fips-deps] 68 | :sources-jar {:java-source-paths ^:replace [] 69 | :jar-exclusions ^:replace [] 70 | :source-paths ^:replace ["src/clj" "src/java"]}} 71 | 72 | :deploy-repositories [["releases" {:url "https://clojars.org/repo" 73 | :username :env/clojars_jenkins_username 74 | :password :env/clojars_jenkins_password 75 | :sign-releases false}]] 76 | 77 | :lein-release {:scm :git 78 | :deploy-via :lein-deploy} 79 | 80 | :plugins [[lein-parent "0.3.7"] 81 | [jonase/eastwood "1.2.2" :exclusions [org.clojure/clojure]] 82 | [puppetlabs/i18n "0.9.2"]] 83 | 84 | :eastwood {:continue-on-exception true 85 | :exclude-namespaces [;; linting this test throws and exception as test-utils/load-test-config 86 | ;; requires the addition of the config in /testutils, excluding for now 87 | puppetlabs.orchestrator.integration.migration-errors-test 88 | ;; The BoltClient protocol has more than 20 functions and therefore an exception is thrown 89 | ;; when compiling it for linting https://github.com/jonase/eastwood/issues/344 90 | puppetlabs.orchestrator.bolt.client] 91 | :exclude-linters [:no-ns-form-found :reflection :deprecations] 92 | :ignored-faults {:def-in-def {puppetlabs.http.client.async-plaintext-test [{:line 278}]}}} 93 | 94 | :repositories [["puppet-releases" "https://artifactory.delivery.puppetlabs.net/artifactory/clojure-releases__local/"] 95 | ["puppet-snapshots" "https://artifactory.delivery.puppetlabs.net/artifactory/clojure-snapshots__local/"]]) 96 | -------------------------------------------------------------------------------- /src/clj/puppetlabs/http/client/sync.clj: -------------------------------------------------------------------------------- 1 | ;; This namespace provides synchronous versions of the request functions 2 | ;; defined in puppetlabs.http.client 3 | 4 | (ns puppetlabs.http.client.sync 5 | (:require [puppetlabs.http.client.async :as async] 6 | [schema.core :as schema] 7 | [puppetlabs.http.client.common :as common] 8 | [puppetlabs.http.client.metrics :as metrics]) 9 | (:refer-clojure :exclude (get))) 10 | 11 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 12 | ;;; Private utility functions 13 | 14 | (schema/defn extract-client-opts :- common/ClientOptions 15 | [opts :- common/RawUserRequestClientOptions] 16 | (select-keys opts [:ssl-context :ssl-ca-cert :ssl-cert :ssl-key 17 | :ssl-protocols :cipher-suites 18 | :force-redirects :follow-redirects 19 | :connect-timeout-milliseconds 20 | :socket-timeout-milliseconds 21 | :max-connections-per-route 22 | :max-connections-total])) 23 | 24 | (schema/defn extract-request-opts :- common/RawUserRequestOptions 25 | [opts :- common/RawUserRequestClientOptions] 26 | (select-keys opts [:url :method :headers :body 27 | :decompress-body :compress-request-body 28 | :as :query-params])) 29 | 30 | (defn request-with-client 31 | ([req client] 32 | (request-with-client req client nil nil true)) 33 | ([req client metric-registry metric-namespace] 34 | (request-with-client req client metric-registry metric-namespace true)) 35 | ([req client metric-registry metric-namespace enable-url-metrics?] 36 | (let [{:keys [error] :as resp} @(async/request-with-client 37 | req nil client metric-registry metric-namespace enable-url-metrics?)] 38 | (if error 39 | (throw error) 40 | resp)))) 41 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 42 | ;;; Public 43 | 44 | (defn request 45 | [req] 46 | (with-open [client (async/create-default-client (extract-client-opts req))] 47 | (request-with-client (extract-request-opts req) client))) 48 | 49 | (schema/defn create-client :- (schema/protocol common/HTTPClient) 50 | [opts :- common/ClientOptions] 51 | (let [client (async/create-default-client opts) 52 | metric-registry (:metric-registry opts) 53 | metric-namespace (metrics/build-metric-namespace (:metric-prefix opts) (:server-id opts)) 54 | enable-url-metrics? (clojure.core/get opts :enable-url-metrics? true)] 55 | (reify common/HTTPClient 56 | (get [this url] (common/get this url {})) 57 | (get [this url opts] (common/make-request this url :get opts)) 58 | (head [this url] (common/head this url {})) 59 | (head [this url opts] (common/make-request this url :head opts)) 60 | (post [this url] (common/post this url {})) 61 | (post [this url opts] (common/make-request this url :post opts)) 62 | (put [this url] (common/put this url {})) 63 | (put [this url opts] (common/make-request this url :put opts)) 64 | (delete [this url] (common/delete this url {})) 65 | (delete [this url opts] (common/make-request this url :delete opts)) 66 | (trace [this url] (common/trace this url {})) 67 | (trace [this url opts] (common/make-request this url :trace opts)) 68 | (options [this url] (common/options this url {})) 69 | (options [this url opts] (common/make-request this url :options opts)) 70 | (patch [this url] (common/patch this url {})) 71 | (patch [this url opts] (common/make-request this url :patch opts)) 72 | (make-request [this url method] (common/make-request this url method {})) 73 | (make-request [_ url method opts] (request-with-client 74 | (assoc opts :method method :url url) 75 | client metric-registry metric-namespace enable-url-metrics?)) 76 | (close [_] (.close client)) 77 | (get-client-metric-registry [_] metric-registry) 78 | (get-client-metric-namespace [_] metric-namespace)))) 79 | 80 | (defn get 81 | "Issue a synchronous HTTP GET request. This will raise an exception if an 82 | error is returned." 83 | ([url] (get url {})) 84 | ([url opts] (request (assoc opts :method :get :url url)))) 85 | 86 | (defn head 87 | "Issue a synchronous HTTP head request. This will raise an exception if an 88 | error is returned." 89 | ([url] (head url {})) 90 | ([url opts] (request (assoc opts :method :head :url url)))) 91 | 92 | (defn post 93 | "Issue a synchronous HTTP POST request. This will raise an exception if an 94 | error is returned." 95 | ([url] (post url {})) 96 | ([url opts] (request (assoc opts :method :post :url url)))) 97 | 98 | (defn put 99 | "Issue a synchronous HTTP PUT request. This will raise an exception if an 100 | error is returned." 101 | ([url] (put url {})) 102 | ([url opts] (request (assoc opts :method :put :url url)))) 103 | 104 | (defn delete 105 | "Issue a synchronous HTTP DELETE request. This will raise an exception if an 106 | error is returned." 107 | ([url] (delete url {})) 108 | ([url opts] (request (assoc opts :method :delete :url url)))) 109 | 110 | (defn trace 111 | "Issue a synchronous HTTP TRACE request. This will raise an exception if an 112 | error is returned." 113 | ([url] (trace url {})) 114 | ([url opts] (request (assoc opts :method :trace :url url)))) 115 | 116 | (defn options 117 | "Issue a synchronous HTTP OPTIONS request. This will raise an exception if an 118 | error is returned." 119 | ([url] (options url {})) 120 | ([url opts] (request (assoc opts :method :options :url url)))) 121 | 122 | (defn patch 123 | "Issue a synchronous HTTP PATCH request. This will raise an exception if an 124 | error is returned." 125 | ([url] (patch url {})) 126 | ([url opts] (request (assoc opts :method :patch :url url)))) 127 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/impl/metrics/TimerUtils.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.impl.metrics; 2 | 3 | import com.codahale.metrics.Metric; 4 | import com.codahale.metrics.MetricRegistry; 5 | import com.codahale.metrics.Timer; 6 | import com.puppetlabs.http.client.metrics.ClientTimer; 7 | import com.puppetlabs.http.client.metrics.MetricIdClientTimer; 8 | import com.puppetlabs.http.client.metrics.Metrics; 9 | import com.puppetlabs.http.client.metrics.UrlAndMethodClientTimer; 10 | import com.puppetlabs.http.client.metrics.UrlClientTimer; 11 | import org.apache.http.HttpRequest; 12 | import org.apache.http.RequestLine; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.net.URISyntaxException; 17 | import java.util.ArrayList; 18 | import java.util.Map; 19 | 20 | public class TimerUtils { 21 | private static final Logger LOGGER = LoggerFactory.getLogger(TimerUtils.class); 22 | 23 | private static ClientTimer getOrAddTimer(MetricRegistry metricRegistry, 24 | String name, 25 | ClientTimer newTimer) { 26 | final Map metrics = metricRegistry.getMetrics(); 27 | final Metric metric = metrics.get(name); 28 | if ( metric instanceof ClientTimer ) { 29 | return (ClientTimer) metric; 30 | } else if ( metric == null ) { 31 | try { 32 | return metricRegistry.register(name, newTimer); 33 | } catch (IllegalArgumentException e) { 34 | final Metric added = metricRegistry.getMetrics().get(name); 35 | if ( added instanceof ClientTimer ) { 36 | return (ClientTimer) added; 37 | } 38 | } 39 | } 40 | throw new IllegalArgumentException(name +" is already used for a different type of metric"); 41 | } 42 | 43 | private static ArrayList startFullResponseMetricIdTimers(MetricRegistry registry, 44 | String[] metricId, 45 | String metricPrefix) { 46 | ArrayList timerContexts = new ArrayList<>(); 47 | for (int i = 0; i < metricId.length; i++) { 48 | ArrayList currentId = new ArrayList<>(); 49 | for (int j = 0; j <= i; j++) { 50 | currentId.add(metricId[j]); 51 | } 52 | ArrayList currentIdWithNamespace = new ArrayList<>(); 53 | currentIdWithNamespace.add(Metrics.NAMESPACE_METRIC_ID); 54 | currentIdWithNamespace.addAll(currentId); 55 | currentIdWithNamespace.add(Metrics.NAMESPACE_FULL_RESPONSE); 56 | String metric_name = MetricRegistry.name(metricPrefix, 57 | currentIdWithNamespace.toArray(new String[currentIdWithNamespace.size()])); 58 | 59 | ClientTimer timer = new MetricIdClientTimer(metric_name, currentId, Metrics.MetricType.FULL_RESPONSE); 60 | timerContexts.add(getOrAddTimer(registry, metric_name, timer).time()); 61 | } 62 | return timerContexts; 63 | } 64 | 65 | private static ArrayList startFullResponseUrlTimers(MetricRegistry registry, 66 | HttpRequest request, 67 | String metricPrefix, 68 | boolean enableURLMetrics) { 69 | ArrayList timerContexts = new ArrayList<>(); 70 | if (enableURLMetrics) { 71 | try { 72 | final RequestLine requestLine = request.getRequestLine(); 73 | final String strippedUrl = Metrics.urlToMetricUrl(requestLine.getUri()); 74 | final String method = requestLine.getMethod(); 75 | 76 | final String urlName = MetricRegistry.name(metricPrefix, Metrics.NAMESPACE_URL, 77 | strippedUrl, Metrics.NAMESPACE_FULL_RESPONSE); 78 | final String urlAndMethodName = MetricRegistry.name(metricPrefix, Metrics.NAMESPACE_URL_AND_METHOD, 79 | strippedUrl, method, Metrics.NAMESPACE_FULL_RESPONSE); 80 | 81 | ClientTimer urlTimer = new UrlClientTimer(urlName, strippedUrl, Metrics.MetricType.FULL_RESPONSE); 82 | timerContexts.add(getOrAddTimer(registry, urlName, urlTimer).time()); 83 | 84 | ClientTimer urlMethodTimer = new UrlAndMethodClientTimer(urlAndMethodName, strippedUrl, 85 | method, Metrics.MetricType.FULL_RESPONSE); 86 | timerContexts.add(getOrAddTimer(registry, urlAndMethodName, urlMethodTimer).time()); 87 | } catch (URISyntaxException e) { 88 | // this shouldn't be possible 89 | LOGGER.warn("Could not build URI out of the request URI. Will not create URI timers. " + 90 | "We recommend you read http://www.stilldrinking.com/programming-sucks. " + 91 | "'now all your snowflakes are urine and you can't even find the cat.'"); 92 | } 93 | } 94 | return timerContexts; 95 | } 96 | 97 | public static ArrayList startFullResponseTimers(MetricRegistry clientRegistry, 98 | HttpRequest request, 99 | String[] metricId, 100 | String metricNamespace, 101 | boolean enableURLMetrics) { 102 | if (clientRegistry != null) { 103 | ArrayList urlTimerContexts = startFullResponseUrlTimers(clientRegistry, request, metricNamespace, enableURLMetrics); 104 | ArrayList allTimerContexts = new ArrayList<>(urlTimerContexts); 105 | if (metricId != null) { 106 | ArrayList metricIdTimers = 107 | startFullResponseMetricIdTimers(clientRegistry, metricId, metricNamespace); 108 | allTimerContexts.addAll(metricIdTimers); 109 | } 110 | return allTimerContexts; 111 | } 112 | else { 113 | return null; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /dev-resources/Makefile.i18n: -------------------------------------------------------------------------------- 1 | # -*- Makefile -*- 2 | # This file was generated by the i18n leiningen plugin 3 | # Do not edit this file; it will be overwritten the next time you run 4 | # lein i18n init 5 | # 6 | 7 | # The name of the package into which the translations bundle will be placed 8 | BUNDLE=puppetlabs.http_client 9 | 10 | # The name of the POT file into which the gettext code strings (msgid) will be placed 11 | POT_NAME=http-client.pot 12 | 13 | # The list of names of packages covered by the translation bundle; 14 | # by default it contains a single package - the same where the translations 15 | # bundle itself is placed - but this can be overridden - preferably in 16 | # the top level Makefile 17 | PACKAGES?=$(BUNDLE) 18 | LOCALES=$(basename $(notdir $(wildcard locales/*.po))) 19 | BUNDLE_DIR=$(subst .,/,$(BUNDLE)) 20 | BUNDLE_FILES=$(patsubst %,resources/$(BUNDLE_DIR)/Messages_%.class,$(LOCALES)) 21 | FIND_SOURCES=find src -name \*.clj 22 | # xgettext before 0.19 does not understand --add-location=file. Even CentOS 23 | # 7 ships with an older gettext. We will therefore generate full location 24 | # info on those systems, and only file names where xgettext supports it 25 | LOC_OPT=$(shell xgettext --add-location=file -f - /dev/null 2>&1 && echo --add-location=file || echo --add-location) 26 | 27 | LOCALES_CLJ=resources/locales.clj 28 | define LOCALES_CLJ_CONTENTS 29 | { 30 | :locales #{$(patsubst %,"%",$(LOCALES))} 31 | :packages [$(patsubst %,"%",$(PACKAGES))] 32 | :bundle $(patsubst %,"%",$(BUNDLE).Messages) 33 | } 34 | endef 35 | export LOCALES_CLJ_CONTENTS 36 | 37 | 38 | i18n: msgfmt 39 | 40 | # Update locales/.pot 41 | update-pot: locales/$(POT_NAME) 42 | 43 | locales/$(POT_NAME): $(shell $(FIND_SOURCES)) | locales 44 | @tmp=$$(mktemp $@.tmp.XXXX); \ 45 | $(FIND_SOURCES) \ 46 | | xgettext --from-code=UTF-8 --language=lisp \ 47 | --copyright-holder='Puppet ' \ 48 | --package-name="$(BUNDLE)" \ 49 | --package-version="$(BUNDLE_VERSION)" \ 50 | --msgid-bugs-address="docs@puppet.com" \ 51 | -k \ 52 | -kmark:1 -ki18n/mark:1 \ 53 | -ktrs:1 -ki18n/trs:1 \ 54 | -ktru:1 -ki18n/tru:1 \ 55 | -ktrun:1,2 -ki18n/trun:1,2 \ 56 | -ktrsn:1,2 -ki18n/trsn:1,2 \ 57 | $(LOC_OPT) \ 58 | --add-comments --sort-by-file \ 59 | -o $$tmp -f -; \ 60 | sed -i.bak -e 's/charset=CHARSET/charset=UTF-8/' $$tmp; \ 61 | sed -i.bak -e 's/POT-Creation-Date: [^\\]*/POT-Creation-Date: /' $$tmp; \ 62 | rm -f $$tmp.bak; \ 63 | if ! diff -q -I POT-Creation-Date $$tmp $@ >/dev/null 2>&1; then \ 64 | mv $$tmp $@; \ 65 | else \ 66 | rm $$tmp; touch $@; \ 67 | fi 68 | 69 | # Run msgfmt over all .po files to generate Java resource bundles 70 | # and create the locales.clj file 71 | msgfmt: $(BUNDLE_FILES) $(LOCALES_CLJ) clean-orphaned-bundles 72 | 73 | # Force rebuild of locales.clj if its contents is not the the desired one. The 74 | # shell echo is used to add a trailing newline to match the one from `cat` 75 | ifneq ($(shell cat $(LOCALES_CLJ) 2> /dev/null),$(shell echo '$(LOCALES_CLJ_CONTENTS)')) 76 | .PHONY: $(LOCALES_CLJ) 77 | endif 78 | $(LOCALES_CLJ): | resources 79 | @echo "Writing $@" 80 | @echo "$$LOCALES_CLJ_CONTENTS" > $@ 81 | 82 | # Remove every resource bundle that wasn't generated from a PO file. 83 | # We do this because we used to generate the english bundle directly from the POT. 84 | .PHONY: clean-orphaned-bundles 85 | clean-orphaned-bundles: 86 | @for bundle in resources/$(BUNDLE_DIR)/Messages_*.class; do \ 87 | locale=$$(basename "$$bundle" | sed -E -e 's/\$$?1?\.class$$/_class/' | cut -d '_' -f 2;); \ 88 | if [ ! -f "locales/$$locale.po" ]; then \ 89 | rm "$$bundle"; \ 90 | fi \ 91 | done 92 | 93 | resources/$(BUNDLE_DIR)/Messages_%.class: locales/%.po | resources 94 | msgfmt --java2 -d resources -r $(BUNDLE).Messages -l $(*F) $< 95 | 96 | # Use this to initialize translations. Updating the PO files is done 97 | # automatically through a CI job that utilizes the scripts in the project's 98 | # `bin` file, which themselves come from the `clj-i18n` project. 99 | locales/%.po: | locales 100 | @if [ ! -f $@ ]; then \ 101 | touch $@ && msginit --no-translator -l $(*F) -o $@ -i locales/$(POT_NAME); \ 102 | fi 103 | 104 | resources locales: 105 | @mkdir $@ 106 | 107 | help: 108 | $(info $(HELP)) 109 | @echo 110 | 111 | .PHONY: help 112 | 113 | define HELP 114 | This Makefile assists in handling i18n related tasks during development. Files 115 | that need to be checked into source control are put into the locales/ directory. 116 | They are 117 | 118 | locales/$(POT_NAME) - the POT file generated by 'make update-pot' 119 | locales/$$LANG.po - the translations for $$LANG 120 | 121 | Only the $$LANG.po files should be edited manually; this is usually done by 122 | translators. 123 | 124 | You can use the following targets: 125 | 126 | i18n: refresh all the files in locales/ and recompile resources 127 | update-pot: extract strings and update locales/$(POT_NAME) 128 | locales/LANG.po: create translations for LANG 129 | msgfmt: compile the translations into Java classes; this step is 130 | needed to make translations available to the Clojure code 131 | and produces Java class files in resources/ 132 | endef 133 | # @todo lutter 2015-04-20: for projects that use libraries with their own 134 | # translation, we need to combine all their translations into one big po 135 | # file and then run msgfmt over that so that we only have to deal with one 136 | # resource bundle 137 | -------------------------------------------------------------------------------- /test/puppetlabs/http/client/gzip_request_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.http.client.gzip-request-test 2 | (:import (com.puppetlabs.http.client Sync 3 | SimpleRequestOptions 4 | ResponseBodyType 5 | CompressType) 6 | (java.io ByteArrayInputStream FilterInputStream) 7 | (java.net URI) 8 | (java.util.zip GZIPInputStream)) 9 | (:require [clojure.test :refer :all] 10 | [cheshire.core :as cheshire] 11 | [schema.test :as schema-test] 12 | [puppetlabs.http.client.sync :as http-client] 13 | [puppetlabs.http.client.test-common :refer [connect-exception-thrown?]] 14 | [puppetlabs.trapperkeeper.testutils.webserver :as testwebserver])) 15 | 16 | (use-fixtures :once schema-test/validate-schemas) 17 | 18 | (defn req-body-app 19 | [req] 20 | (let [response {:request-content-encoding (get-in req [:headers "content-encoding"]) 21 | :request-body-decompressed (slurp 22 | (GZIPInputStream. (:body req)) 23 | :encoding "utf-8")}] 24 | {:status 200 25 | :headers {"Content-Type" "application/json; charset=utf-8"} 26 | :body (cheshire/generate-string response)})) 27 | 28 | (def short-request-body "gzip me�") 29 | 30 | (def big-request-body 31 | (apply str (repeat 4000 "and�i�said�hey�yeah�yeah�whats�going�on"))) 32 | 33 | (defn string->byte-array-input-stream 34 | [source is-closed-atom] 35 | (let [bis (-> source 36 | (.getBytes) 37 | (ByteArrayInputStream.))] 38 | (proxy [FilterInputStream] [bis] 39 | (close [] 40 | (reset! is-closed-atom true) 41 | (proxy-super close))))) 42 | 43 | (defn post-gzip-clj-request 44 | [port body] 45 | (-> (http-client/post (format "http://localhost:%d" port) 46 | {:body body 47 | :headers {"Content-Type" "text/plain; charset=utf-8"} 48 | :compress-request-body :gzip 49 | :as :text}) 50 | :body 51 | (cheshire/parse-string true))) 52 | 53 | (defn post-gzip-java-request 54 | [port body] 55 | (-> (SimpleRequestOptions. (URI. (format "http://localhost:%d/hello/" port))) 56 | (.setBody body) 57 | (.setHeaders {"Content-Type" "text/plain; charset=utf-8"}) 58 | (.setRequestBodyCompression CompressType/GZIP) 59 | (.setAs ResponseBodyType/TEXT) 60 | (Sync/post) 61 | (.getBody) 62 | (cheshire/parse-string true))) 63 | 64 | (deftest clj-sync-client-gzip-requests 65 | (testing "for clojure sync client" 66 | (testwebserver/with-test-webserver 67 | req-body-app 68 | port 69 | (testing "short string body is gzipped in request" 70 | (let [response (post-gzip-clj-request port short-request-body)] 71 | (is (= "gzip" (:request-content-encoding response))) 72 | (is (= short-request-body (:request-body-decompressed response))))) 73 | (testing "big string body is gzipped in request" 74 | (let [response (post-gzip-clj-request port big-request-body)] 75 | (is (= "gzip" (:request-content-encoding response))) 76 | (is (= big-request-body (:request-body-decompressed response))))) 77 | (testing "short inputstream body is gzipped in request" 78 | (let [is-closed (atom false) 79 | response (post-gzip-clj-request 80 | port 81 | (string->byte-array-input-stream short-request-body 82 | is-closed))] 83 | (is (= "gzip" (:request-content-encoding response))) 84 | (is (= short-request-body (:request-body-decompressed response))) 85 | (is @is-closed "input stream was not closed after request"))) 86 | (testing "big inputstream body is gzipped in request" 87 | (let [is-closed (atom false) 88 | response (post-gzip-clj-request 89 | port 90 | (string->byte-array-input-stream big-request-body 91 | is-closed))] 92 | (is (= "gzip" (:request-content-encoding response))) 93 | (is (= big-request-body (:request-body-decompressed response))) 94 | (is @is-closed "input stream was not closed after request")))))) 95 | 96 | (deftest java-sync-client-gzip-requests 97 | (testing "for java sync client" 98 | (testwebserver/with-test-webserver 99 | req-body-app 100 | port 101 | (testing "short string body is gzipped in request" 102 | (let [response (post-gzip-java-request port short-request-body)] 103 | (is (= "gzip" (:request-content-encoding response))) 104 | (is (= short-request-body (:request-body-decompressed response))))) 105 | (testing "big string body is gzipped in request" 106 | (let [response (post-gzip-java-request port big-request-body)] 107 | (is (= "gzip" (:request-content-encoding response))) 108 | (is (= big-request-body (:request-body-decompressed response))))) 109 | (testing "short inputstream body is gzipped in request" 110 | (let [is-closed (atom false) 111 | response (post-gzip-java-request 112 | port 113 | (string->byte-array-input-stream short-request-body 114 | is-closed))] 115 | (is (= "gzip" (:request-content-encoding response))) 116 | (is (= short-request-body (:request-body-decompressed response))) 117 | (is @is-closed "input stream was not closed after request"))) 118 | (testing "big inputstream body is gzipped in request" 119 | (let [is-closed (atom false) 120 | response (post-gzip-java-request 121 | port 122 | (string->byte-array-input-stream big-request-body 123 | is-closed))] 124 | (is (= "gzip" (:request-content-encoding response))) 125 | (is (= big-request-body (:request-body-decompressed response))) 126 | (is @is-closed "input stream was not closed after request")))))) 127 | 128 | (deftest connect-exception-during-gzip-request-returns-failure 129 | (testing "connection exception during gzip request returns failure" 130 | (let [is-closed (atom false)] 131 | (is (connect-exception-thrown? 132 | (http-client/post "http://localhost:65535" 133 | {:body (string->byte-array-input-stream 134 | short-request-body 135 | is-closed) 136 | :compress-request-body :gzip 137 | :as :text}))) 138 | (is @is-closed "input stream was not closed after request")))) 139 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/SimpleRequestOptions.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | import com.puppetlabs.ssl_utils.SSLUtils; 4 | 5 | import javax.net.ssl.SSLContext; 6 | import java.net.URI; 7 | import java.net.URISyntaxException; 8 | import java.security.NoSuchAlgorithmException; 9 | import java.security.NoSuchProviderException; 10 | import java.util.Map; 11 | 12 | /** 13 | * This class represents is a wrapper around a number of options for use in 14 | * making requests with the simple request functions contained in the Sync class. 15 | * It is a combination of the options from ClientOptions and RequestOptions. 16 | * 17 | * @see com.puppetlabs.http.client.ClientOptions#ClientOptions(javax.net.ssl.SSLContext, String, String, String, String[], String[], boolean, boolean, boolean, int, int, int, int) 18 | * @see com.puppetlabs.http.client.RequestOptions#RequestOptions(java.net.URI, java.util.Map, Object, boolean, ResponseBodyType) 19 | */ 20 | public class SimpleRequestOptions { 21 | private URI uri; 22 | private Map headers; 23 | private SSLContext sslContext; 24 | private String sslCert; 25 | private String sslKey; 26 | private String sslCaCert; 27 | private String[] sslProtocols; 28 | private String[] sslCipherSuites; 29 | private boolean insecure = false; 30 | private Object body; 31 | private CompressType requestBodyCompression = CompressType.NONE; 32 | private boolean decompressBody = true; 33 | private ResponseBodyType as = ResponseBodyType.STREAM; 34 | private boolean forceRedirects = false; 35 | private boolean followRedirects = true; 36 | private int connectTimeoutMilliseconds = -1; 37 | private int socketTimeoutMilliseconds = -1; 38 | 39 | /** 40 | * Constructor for SimpleRequestOptions. When this constructor is used, 41 | * insecure and forceRedirects default to false, and followRedirects and decompressBody 42 | * default to true. as defaults to ResponseBodyType.STREAM. 43 | * @param url the URL against which to make the HTTP request 44 | * @throws URISyntaxException 45 | */ 46 | public SimpleRequestOptions (String url) throws URISyntaxException { 47 | this.uri = new URI(url); 48 | } 49 | 50 | /** 51 | * Constructor for SimpleRequestOptions. When this constructor is used, 52 | * insecure and forceRedirects default to false, and followRedirects and decompressBody 53 | * default to true. as defaults to ResponseBodyType.STREAM. 54 | * @param uri the URI against which to make the HTTP request 55 | */ 56 | public SimpleRequestOptions(URI uri) { 57 | this.uri = uri; 58 | } 59 | 60 | public URI getUri() { 61 | return uri; 62 | } 63 | public SimpleRequestOptions setUri(URI uri) { 64 | this.uri = uri; 65 | return this; 66 | } 67 | 68 | public Map getHeaders() { 69 | return headers; 70 | } 71 | public SimpleRequestOptions setHeaders(Map headers) { 72 | this.headers = headers; 73 | return this; 74 | } 75 | 76 | public SSLContext getSslContext() { 77 | return sslContext; 78 | } 79 | public SimpleRequestOptions setSslContext(SSLContext sslContext) { 80 | this.sslContext = sslContext; 81 | return this; 82 | } 83 | 84 | public String getSslCert() { 85 | return sslCert; 86 | } 87 | public SimpleRequestOptions setSslCert(String sslCert) { 88 | this.sslCert = sslCert; 89 | return this; 90 | } 91 | 92 | public String getSslKey() { 93 | return sslKey; 94 | } 95 | public SimpleRequestOptions setSslKey(String sslKey) { 96 | this.sslKey = sslKey; 97 | return this; 98 | } 99 | 100 | public String getSslCaCert() { 101 | return sslCaCert; 102 | } 103 | public SimpleRequestOptions setSslCaCert(String sslCaCert) { 104 | this.sslCaCert = sslCaCert; 105 | return this; 106 | } 107 | 108 | public String[] getSslProtocols() { 109 | return sslProtocols; 110 | } 111 | public SimpleRequestOptions setSslProtocols(String[] sslProtocols) { 112 | this.sslProtocols = sslProtocols; 113 | return this; 114 | } 115 | 116 | public String[] getSslCipherSuites() { 117 | return sslCipherSuites; 118 | } 119 | public SimpleRequestOptions setSslCipherSuites(String[] sslCipherSuites) { 120 | this.sslCipherSuites = sslCipherSuites; 121 | return this; 122 | } 123 | 124 | public boolean getInsecure() { 125 | return insecure; 126 | } 127 | public SimpleRequestOptions setInsecure(boolean insecure) { 128 | this.insecure = insecure; 129 | return this; 130 | } 131 | 132 | public Object getBody() { 133 | return body; 134 | } 135 | public SimpleRequestOptions setBody(Object body) { 136 | this.body = body; 137 | return this; 138 | } 139 | 140 | public boolean getDecompressBody() { return decompressBody; } 141 | public SimpleRequestOptions setDecompressBody(boolean decompressBody) { 142 | this.decompressBody = decompressBody; 143 | return this; 144 | } 145 | 146 | public CompressType getCompressRequestBody() { 147 | return requestBodyCompression; 148 | } 149 | public SimpleRequestOptions setRequestBodyCompression( 150 | CompressType requestBodyCompression) { 151 | this.requestBodyCompression = requestBodyCompression; 152 | return this; 153 | } 154 | 155 | public ResponseBodyType getAs() { 156 | return as; 157 | } 158 | public SimpleRequestOptions setAs(ResponseBodyType as) { 159 | this.as = as; 160 | return this; 161 | } 162 | 163 | public boolean getForceRedirects() { return forceRedirects; } 164 | public SimpleRequestOptions setForceRedirects(boolean forceRedirects) { 165 | this.forceRedirects = forceRedirects; 166 | return this; 167 | } 168 | 169 | public boolean getFollowRedirects() { return followRedirects; } 170 | public SimpleRequestOptions setFollowRedirects(boolean followRedirects) { 171 | this.followRedirects = followRedirects; 172 | return this; 173 | } 174 | 175 | public int getConnectTimeoutMilliseconds() { 176 | return connectTimeoutMilliseconds; 177 | } 178 | 179 | public SimpleRequestOptions setConnectTimeoutMilliseconds( 180 | int connectTimeoutMilliseconds) { 181 | this.connectTimeoutMilliseconds = connectTimeoutMilliseconds; 182 | return this; 183 | } 184 | 185 | public int getSocketTimeoutMilliseconds() { 186 | return socketTimeoutMilliseconds; 187 | } 188 | 189 | public SimpleRequestOptions setSocketTimeoutMilliseconds( 190 | int socketTimeoutMilliseconds) { 191 | this.socketTimeoutMilliseconds = socketTimeoutMilliseconds; 192 | return this; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/SyncHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | import com.codahale.metrics.MetricRegistry; 4 | 5 | import java.io.Closeable; 6 | import java.net.URI; 7 | import java.net.URISyntaxException; 8 | 9 | /** 10 | * This interface represents a synchronous HTTP client with which 11 | * requests can be made. An object implementing this interface is 12 | * returned by {@link com.puppetlabs.http.client.Sync#createClient(ClientOptions)} 13 | */ 14 | public interface SyncHttpClient extends Closeable { 15 | 16 | /** 17 | * @return the MetricRegistry instance associated with this Client 18 | */ 19 | public MetricRegistry getMetricRegistry(); 20 | 21 | /** 22 | * @return the String metricNamespace for this Client 23 | */ 24 | public String getMetricNamespace(); 25 | 26 | /** 27 | * Makes a configurable HTTP request 28 | * @param requestOptions the options to configure the request 29 | * @param method the type of the HTTP request 30 | * @return the HTTP response 31 | */ 32 | public Response request(RequestOptions requestOptions, HttpMethod method); 33 | 34 | /** 35 | * Makes an HTTP GET request 36 | * @param url the url against which to make the request 37 | * @return the HTTP response 38 | * @throws URISyntaxException 39 | */ 40 | public Response get(String url) throws URISyntaxException; 41 | 42 | /** 43 | * Makes an HTTP GET request 44 | * @param uri the uri against which to make the request 45 | * @return the HTTP response 46 | */ 47 | public Response get(URI uri); 48 | 49 | /** 50 | * Makes an HTTP GET request 51 | * @param requestOptions the options to configure the request 52 | * @return the HTTP response 53 | */ 54 | public Response get(RequestOptions requestOptions); 55 | 56 | /** 57 | * Makes an HTTP HEAD request 58 | * @param url the url against which to make the request 59 | * @return the HTTP response 60 | * @throws URISyntaxException 61 | */ 62 | public Response head(String url) throws URISyntaxException; 63 | 64 | /** 65 | * Makes an HTTP HEAD request 66 | * @param uri the uri against which to make the request 67 | * @return the HTTP response 68 | */ 69 | public Response head(URI uri); 70 | 71 | /** 72 | * Makes an HTTP HEAD request 73 | * @param requestOptions the options to configure the request 74 | * @return the HTTP response 75 | */ 76 | public Response head(RequestOptions requestOptions); 77 | 78 | /** 79 | * Makes an HTTP POST request 80 | * @param url the url against which to make the request 81 | * @return the HTTP response 82 | * @throws URISyntaxException 83 | */ 84 | public Response post(String url) throws URISyntaxException; 85 | 86 | /** 87 | * Makes an HTTP POST request 88 | * @param uri the uri against which to make the request 89 | * @return the HTTP response 90 | */ 91 | public Response post(URI uri); 92 | 93 | /** 94 | * Makes an HTTP POST request 95 | * @param requestOptions the options to configure the request 96 | * @return the HTTP response 97 | */ 98 | public Response post(RequestOptions requestOptions); 99 | 100 | /** 101 | * Makes an HTTP PUT request 102 | * @param url the url against which to make the request 103 | * @return the HTTP response 104 | * @throws URISyntaxException 105 | */ 106 | public Response put(String url) throws URISyntaxException; 107 | 108 | /** 109 | * Makes an HTTP PUT request 110 | * @param uri the uri against which to make the request 111 | * @return the HTTP response 112 | */ 113 | public Response put(URI uri); 114 | 115 | /** 116 | * Makes an HTTP PUT request 117 | * @param requestOptions the options to configure the request 118 | * @return the HTTP response 119 | */ 120 | public Response put(RequestOptions requestOptions); 121 | 122 | /** 123 | * Makes an HTTP DELETE request 124 | * @param url the url against which to make the request 125 | * @return the HTTP response 126 | * @throws URISyntaxException 127 | */ 128 | public Response delete(String url) throws URISyntaxException; 129 | 130 | /** 131 | * Makes an HTTP DELETE request 132 | * @param uri the uri against which to make the request 133 | * @return the HTTP response 134 | */ 135 | public Response delete(URI uri); 136 | 137 | /** 138 | * Makes an HTTP DELETE request 139 | * @param requestOptions the options to configure the request 140 | * @return the HTTP response 141 | */ 142 | public Response delete(RequestOptions requestOptions); 143 | 144 | /** 145 | * Makes an HTTP TRACE request 146 | * @param url the url against which to make the request 147 | * @return the HTTP response 148 | * @throws URISyntaxException 149 | */ 150 | public Response trace(String url) throws URISyntaxException; 151 | 152 | /** 153 | * Makes an HTTP TRACE request 154 | * @param uri the uri against which to make the request 155 | * @return the HTTP response 156 | */ 157 | public Response trace(URI uri); 158 | 159 | /** 160 | * Makes an HTTP TRACE request 161 | * @param requestOptions the options to configure the request 162 | * @return the HTTP response 163 | */ 164 | public Response trace(RequestOptions requestOptions); 165 | 166 | /** 167 | * Makes an HTTP OPTIONS request 168 | * @param url the url against which to make the request 169 | * @return the HTTP response 170 | * @throws URISyntaxException 171 | */ 172 | public Response options(String url) throws URISyntaxException; 173 | 174 | /** 175 | * Makes an HTTP OPTIONS request 176 | * @param uri the uri against which to make the request 177 | * @return the HTTP response 178 | */ 179 | public Response options(URI uri); 180 | 181 | /** 182 | * Makes an HTTP OPTIONS request 183 | * @param requestOptions the options to configure the request 184 | * @return the HTTP response 185 | */ 186 | public Response options(RequestOptions requestOptions); 187 | 188 | /** 189 | * Makes an HTTP PATCH request 190 | * @param url the url against which to make the request 191 | * @return the HTTP response 192 | * @throws URISyntaxException 193 | */ 194 | public Response patch(String url) throws URISyntaxException; 195 | 196 | /** 197 | * Makes an HTTP PATCH request 198 | * @param uri the uri against which to make the request 199 | * @return the HTTP response 200 | */ 201 | public Response patch(URI uri); 202 | 203 | /** 204 | * Makes an HTTP PATCH request 205 | * @param requestOptions the options to configure the request 206 | * @return the HTTP response 207 | */ 208 | public Response patch(RequestOptions requestOptions); 209 | } 210 | -------------------------------------------------------------------------------- /src/clj/puppetlabs/http/client/common.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.http.client.common 2 | (:import (java.net URL URI) 3 | (javax.net.ssl SSLContext) 4 | (com.codahale.metrics MetricRegistry) 5 | (clojure.lang IBlockingDeref) 6 | (java.io InputStream) 7 | (java.nio.charset Charset) 8 | (com.puppetlabs.http.client.metrics ClientTimer)) 9 | (:require [schema.core :as schema]) 10 | (:refer-clojure :exclude (get))) 11 | 12 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 13 | ;;; Client Protocol 14 | 15 | (defprotocol HTTPClient 16 | (get [this url] [this url opts]) 17 | (head [this url] [this url opts]) 18 | (post [this url] [this url opts]) 19 | (put [this url] [this url opts]) 20 | (delete [this url] [this url opts]) 21 | (trace [this url] [this url opts]) 22 | (options [this url] [this url opts]) 23 | (patch [this url] [this url opts]) 24 | (make-request [this url method] [this url method opts]) 25 | (close [this]) 26 | (get-client-metric-registry [this]) 27 | (get-client-metric-namespace [this])) 28 | 29 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 30 | ;;; Schemas 31 | 32 | (def ok schema/optional-key) 33 | 34 | (def UrlOrUriOrString (schema/either schema/Str URL URI)) 35 | (def UrlOrString (schema/either schema/Str URL)) 36 | 37 | (def Headers 38 | {schema/Str schema/Str}) 39 | 40 | (def Body 41 | (schema/maybe (schema/either String InputStream))) 42 | 43 | (def BodyType 44 | (schema/enum :text :stream :unbuffered-stream)) 45 | 46 | (def CompressType 47 | (schema/enum :gzip :none)) 48 | 49 | (def MetricId [(schema/either schema/Str schema/Keyword)]) 50 | 51 | (def RawUserRequestClientOptions 52 | "The list of request and client options passed by a user into 53 | the request function. Allows the user to configure 54 | both a client and a request." 55 | {:url UrlOrUriOrString 56 | :method schema/Keyword 57 | (ok :headers) Headers 58 | (ok :body) Body 59 | (ok :decompress-body) schema/Bool 60 | (ok :compress-request-body) CompressType 61 | (ok :as) BodyType 62 | (ok :query-params) {schema/Str schema/Str} 63 | (ok :metric-id) [schema/Str] 64 | 65 | (ok :ssl-context) SSLContext 66 | (ok :ssl-cert) UrlOrString 67 | (ok :ssl-key) UrlOrString 68 | (ok :ssl-ca-cert) UrlOrString 69 | (ok :ssl-protocols) [schema/Str] 70 | (ok :cipher-suites) [schema/Str] 71 | (ok :force-redirects) schema/Bool 72 | (ok :follow-redirects) schema/Bool 73 | (ok :connect-timeout-milliseconds) schema/Int 74 | (ok :socket-timeout-milliseconds) schema/Int}) 75 | 76 | (def RawUserRequestOptions 77 | "The list of request options passed by a user into the 78 | request function. Allows the user to configure a request." 79 | {:url UrlOrUriOrString 80 | :method schema/Keyword 81 | (ok :headers) Headers 82 | (ok :body) Body 83 | (ok :decompress-body) schema/Bool 84 | (ok :compress-request-body) CompressType 85 | (ok :as) BodyType 86 | (ok :query-params) {schema/Str schema/Str} 87 | (ok :metric-id) MetricId}) 88 | 89 | (def RequestOptions 90 | "The options from UserRequestOptions that have to do with the 91 | configuration and settings for an individual request. This is 92 | everything from UserRequestOptions not included in 93 | ClientOptions." 94 | {:url UrlOrUriOrString 95 | :method schema/Keyword 96 | :headers Headers 97 | :body Body 98 | :decompress-body schema/Bool 99 | :compress-request-body CompressType 100 | :as BodyType 101 | (ok :query-params) {schema/Str schema/Str} 102 | (ok :metric-id) MetricId}) 103 | 104 | (def SslContextOptions 105 | {:ssl-context SSLContext}) 106 | 107 | (def SslCaCertOptions 108 | {:ssl-ca-cert UrlOrString}) 109 | 110 | (def SslCertOptions 111 | {:ssl-cert UrlOrString 112 | :ssl-key UrlOrString 113 | :ssl-ca-cert UrlOrString}) 114 | 115 | (def SslProtocolOptions 116 | {(ok :ssl-protocols) [schema/Str] 117 | (ok :cipher-suites) [schema/Str]}) 118 | 119 | (def BaseClientOptions 120 | {(ok :force-redirects) schema/Bool 121 | (ok :follow-redirects) schema/Bool 122 | (ok :connect-timeout-milliseconds) schema/Int 123 | (ok :socket-timeout-milliseconds) schema/Int 124 | (ok :metric-registry) MetricRegistry 125 | (ok :server-id) schema/Str 126 | (ok :metric-prefix) schema/Str 127 | (ok :enable-url-metrics?) schema/Bool 128 | (ok :max-connections-total) schema/Int 129 | (ok :max-connections-per-route) schema/Int}) 130 | 131 | (def UserRequestOptions 132 | "A cleaned-up version of RawUserRequestClientOptions, which is formed after 133 | validating the RawUserRequestClientOptions and merging it with the defaults." 134 | (schema/either 135 | (merge RequestOptions BaseClientOptions) 136 | (merge RequestOptions SslContextOptions SslProtocolOptions BaseClientOptions) 137 | (merge RequestOptions SslCaCertOptions SslProtocolOptions BaseClientOptions) 138 | (merge RequestOptions SslCertOptions SslProtocolOptions BaseClientOptions))) 139 | 140 | (def ClientOptions 141 | "The options from UserRequestOptions that are related to the 142 | instantiation/management of a client. This is everything 143 | from UserRequestOptions not included in RequestOptions." 144 | (schema/either 145 | BaseClientOptions 146 | (merge SslContextOptions SslProtocolOptions BaseClientOptions) 147 | (merge SslCertOptions SslProtocolOptions BaseClientOptions) 148 | (merge SslCaCertOptions SslProtocolOptions BaseClientOptions))) 149 | 150 | (def ResponseCallbackFn 151 | (schema/maybe (schema/pred ifn?))) 152 | 153 | (def ResponsePromise 154 | IBlockingDeref) 155 | 156 | (def ContentType 157 | (schema/maybe {:mime-type schema/Str 158 | :charset (schema/maybe Charset)})) 159 | 160 | (def NormalResponse 161 | {:opts UserRequestOptions 162 | :orig-content-encoding (schema/maybe schema/Str) 163 | :body Body 164 | :headers Headers 165 | :status schema/Int 166 | :reason-phrase schema/Str 167 | :content-type ContentType}) 168 | 169 | (def ErrorResponse 170 | {:opts UserRequestOptions 171 | :error Exception}) 172 | 173 | (def Response 174 | (schema/either NormalResponse ErrorResponse)) 175 | 176 | (def HTTPMethod 177 | (schema/enum :delete :get :head :option :patch :post :put :trace)) 178 | 179 | (def Metrics 180 | [ClientTimer]) 181 | 182 | (def AllMetrics 183 | {:url Metrics 184 | :url-and-method Metrics 185 | :metric-id Metrics}) 186 | 187 | (def BaseMetricData 188 | {:metric-name schema/Str 189 | :count schema/Int 190 | :mean schema/Num 191 | :aggregate schema/Num}) 192 | 193 | (def UrlMetricData 194 | (assoc BaseMetricData :url schema/Str)) 195 | 196 | (def UrlAndMethodMetricData 197 | (assoc UrlMetricData :method schema/Str)) 198 | 199 | (def MetricIdMetricData 200 | (assoc BaseMetricData :metric-id [schema/Str])) 201 | 202 | (def AllMetricsData 203 | {:url [UrlMetricData] 204 | :url-and-method [UrlAndMethodMetricData] 205 | :metric-id [MetricIdMetricData]}) 206 | 207 | (def MetricTypes 208 | (schema/enum :full-response)) 209 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/AsyncHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | import com.codahale.metrics.MetricRegistry; 4 | import com.puppetlabs.http.client.impl.Promise; 5 | 6 | import java.io.Closeable; 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | 10 | /** 11 | * This interface represents an asynchronous HTTP client with which 12 | * requests can be made. An object implementing this interface is returned by 13 | * {@link com.puppetlabs.http.client.Async#createClient(ClientOptions)}. 14 | */ 15 | public interface AsyncHttpClient extends Closeable{ 16 | 17 | /** 18 | * @return the MetricRegistry instance associated with this Client 19 | */ 20 | public MetricRegistry getMetricRegistry(); 21 | 22 | /** 23 | * @return the String metricNamespace for this Client 24 | */ 25 | public String getMetricNamespace(); 26 | 27 | /** 28 | * Performs a GET request 29 | * @param url the URL against which to make the GET request 30 | * @return a Promise with the contents of the response 31 | * @throws URISyntaxException 32 | */ 33 | public Promise get(String url) throws URISyntaxException; 34 | 35 | /** 36 | * Performs a GET request 37 | * @param uri the URI against which to make the GET request 38 | * @return a Promise with the contents of the response 39 | */ 40 | public Promise get(URI uri); 41 | 42 | /** 43 | * Performs a GET request 44 | * @param requestOptions options to configure the GET request 45 | * @return a Promise with the contents of the response 46 | */ 47 | public Promise get(RequestOptions requestOptions); 48 | 49 | /** 50 | * Performs a HEAD request 51 | * @param url the URL against which to make the HEAD request 52 | * @return a Promise with the contents of the response 53 | * @throws URISyntaxException 54 | */ 55 | public Promise head(String url) throws URISyntaxException; 56 | 57 | /** 58 | * Performs a HEAD request 59 | * @param uri the URI against which to make the HEAD request 60 | * @return a Promise with the contents of the response 61 | */ 62 | public Promise head(URI uri); 63 | 64 | /** 65 | * Performs a HEAD request 66 | * @param requestOptions options to configure the HEAD request 67 | * @return a Promise with the contents of the response 68 | */ 69 | public Promise head(RequestOptions requestOptions); 70 | 71 | /** 72 | * Performs a POST request 73 | * @param url the URL against which to make the POST request 74 | * @return a Promise with the contents of the response 75 | * @throws URISyntaxException 76 | */ 77 | public Promise post(String url) throws URISyntaxException; 78 | 79 | /** 80 | * Performs a POST request 81 | * @param uri the URI against which to make the POST request 82 | * @return a Promise with the contents of the response 83 | */ 84 | public Promise post(URI uri); 85 | 86 | /** 87 | * Performs a POST request 88 | * @param requestOptions options to configure the POST request 89 | * @return a Promise with the contents of the response 90 | */ 91 | public Promise post(RequestOptions requestOptions); 92 | 93 | /** 94 | * Performs a PUT request 95 | * @param url the URL against which to make the PUT request 96 | * @return a Promise with the contents of the response 97 | * @throws URISyntaxException 98 | */ 99 | public Promise put(String url) throws URISyntaxException; 100 | 101 | /** 102 | * Performs a PUT request 103 | * @param uri the URI against which to make the PUT request 104 | * @return a Promise with the contents of the response 105 | */ 106 | public Promise put(URI uri); 107 | 108 | /** 109 | * Performs a PUT request 110 | * @param requestOptions options to configure the PUT request 111 | * @return a Promise with the contents of the response 112 | */ 113 | public Promise put(RequestOptions requestOptions); 114 | 115 | /** 116 | * Performs a DELETE request 117 | * @param url the URL against which to make the DELETE request 118 | * @return a Promise with the contents of the response 119 | * @throws URISyntaxException 120 | */ 121 | public Promise delete(String url) throws URISyntaxException; 122 | 123 | /** 124 | * Performs a DELETE request 125 | * @param uri the URI against which to make the DELETE request 126 | * @return a Promise with the contents of the response 127 | */ 128 | public Promise delete(URI uri); 129 | 130 | /** 131 | * Performs a DELETE request 132 | * @param requestOptions options to configure the DELETE request 133 | * @return a Promise with the contents of the response 134 | */ 135 | public Promise delete(RequestOptions requestOptions); 136 | 137 | /** 138 | * Performs a TRACE request 139 | * @param url the URL against which to make the TRACE request 140 | * @return a Promise with the contents of the response 141 | * @throws URISyntaxException 142 | */ 143 | public Promise trace(String url) throws URISyntaxException; 144 | 145 | /** 146 | * Performs a TRACE request 147 | * @param uri the URI against which to make the TRACE request 148 | * @return a Promise with the contents of the response 149 | */ 150 | public Promise trace(URI uri); 151 | 152 | /** 153 | * Performs a TRACE request 154 | * @param requestOptions options to configure the TRACE request 155 | * @return a Promise with the contents of the response 156 | */ 157 | public Promise trace(RequestOptions requestOptions); 158 | 159 | /** 160 | * Performs an OPTIONS request 161 | * @param url the URL against which to make the OPTIONS request 162 | * @return a Promise with the contents of the response 163 | * @throws URISyntaxException 164 | */ 165 | public Promise options(String url) throws URISyntaxException; 166 | 167 | /** 168 | * Performs an OPTIONS request 169 | * @param uri the URI against which to make the OPTIONS request 170 | * @return a Promise with the contents of the response 171 | */ 172 | public Promise options(URI uri); 173 | 174 | /** 175 | * Performs an OPTIONS request 176 | * @param requestOptions options to configure the OPTIONS request 177 | * @return a Promise with the contents of the response 178 | */ 179 | public Promise options(RequestOptions requestOptions); 180 | 181 | /** 182 | * Performs a PATCH request 183 | * @param url the URL against which to make the PATCH request 184 | * @return a Promise with the contents of the response 185 | * @throws URISyntaxException 186 | */ 187 | public Promise patch(String url) throws URISyntaxException; 188 | 189 | /** 190 | * Performs a PATCH request 191 | * @param uri the URI against which to make the PATCH request 192 | * @return a Promise with the contents of the response 193 | */ 194 | public Promise patch(URI uri); 195 | 196 | /** 197 | * Performs a PATCH request 198 | * @param requestOptions options to configure the PATCH request 199 | * @return a Promise with the contents of the response 200 | */ 201 | public Promise patch(RequestOptions requestOptions); 202 | } 203 | -------------------------------------------------------------------------------- /doc/clojure-client.md: -------------------------------------------------------------------------------- 1 | ## Making requests with clojure clients 2 | 3 | clj-http-client allows you to make requests in two ways with clojure clients: with and without a persistent HTTP client. 4 | 5 | ## `create-client` 6 | 7 | clj-http-client allows you to create a persistent synchronous or asynchronous HTTP client using the 8 | `create-client` function from the corresponding namespace. 9 | 10 | The `create-client` function takes one argument, a map called `options`. The available options 11 | for configuring the client are detailed below. 12 | 13 | ### Base Options 14 | 15 | The following are the base set of options supported by the `create-client` functions. 16 | 17 | * `:force-redirects`: used to set whether or not the client should follow 18 | redirects on POST or PUT requests. Defaults to false. 19 | * `:follow-redirects`: used to set whether or not the client should follow 20 | redirects in general. Defaults to true. If set to false, will override 21 | the :force-redirects setting. 22 | * `:connect-timeout-milliseconds`: maximum number of milliseconds that the 23 | client will wait for a connection to be established. A value of 0 is 24 | interpreted as infinite. A negative value for or the absence of this option 25 | is interpreted as undefined (system default). 26 | * `:socket-timeout-milliseconds`: maximum number of milliseconds that the 27 | client will allow for no data to be available on the socket before closing the 28 | underlying connection, 'SO_TIMEOUT' in socket terms. A timeout of zero is 29 | interpreted as an infinite timeout. A negative value for or the absence of 30 | this setting is interpreted as undefined (system default). 31 | * `:ssl-protocols`: an array used to set the list of SSL protocols that the client 32 | could select from when talking to the server. Defaults to 'TLSv1', 33 | 'TLSv1.1', and 'TLSv1.2'. 34 | * `:cipher-suites`: an array used to set the cipher suites that the client could 35 | select from when talking to the server. Defaults to the complete 36 | set of suites supported by the underlying language runtime. 37 | * `:metric-registry`: a Dropwizard `MetricRegistry` to register http metrics 38 | to. If provided, metrics will automatically be registered for all requests 39 | made by the client. See the [metrics documentation](./metrics.md) for more 40 | info. 41 | * `:server-id`: a string for the name of the server the request is being made 42 | from. If specified, used in the namespace for metrics: 43 | `puppetlabs..http-client.experimental`. 44 | * `:metric-prefix`: a string for the prefix for metrics. If specified, metric 45 | namespace is `.http-client.experimental`. If both 46 | `metric-prefix` and `server-id` are specified, `metric-prefix` takes 47 | precendence. 48 | * `:max-connections-per-route`: an integer to specify the maximum number 49 | of concurrent requests for a given route (host & port) for a given 50 | persistant client instance. Defaults to 2. If 0 is specified, it acts 51 | as the default. 52 | * `:max-connections-total`: an integer to specify the maximum number of 53 | concurrent requests for a given persistant client instance. Defaults 54 | to 20. If 0 is specified, it acts as the default. 55 | 56 | ### SSL Options 57 | 58 | The following options are SSL specific, and only one of the following combinations is permitted. 59 | 60 | * `:ssl-context`: an instance of [SSLContext](http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLContext.html) 61 | 62 | OR 63 | 64 | * `:ssl-cert`: path to a PEM file containing the client cert 65 | * `:ssl-key`: path to a PEM file containing the client private key 66 | * `:ssl-ca-cert`: path to a PEM file containing the CA cert 67 | 68 | OR 69 | 70 | * `:ssl-ca-cert`: path to a PEM file containing the CA cert 71 | 72 | ### Making requests with a persistent client 73 | 74 | The `create-client` functions return a client 75 | with the following protocol: 76 | 77 | ```clj 78 | (defprotocol HTTPClient 79 | (get [this url] [this url opts]) 80 | (head [this url] [this url opts]) 81 | (post [this url] [this url opts]) 82 | (put [this url] [this url opts]) 83 | (delete [this url] [this url opts]) 84 | (trace [this url] [this url opts]) 85 | (options [this url] [this url opts]) 86 | (patch [this url] [this url opts]) 87 | (close [this])) 88 | 89 | ``` 90 | 91 | Each function will execute the corresponding HTTP request, with the exception of `close`, which 92 | will close the client. 93 | 94 | Each request function takes one argument, `url`, which is the URL against which you want to make 95 | your HTTP request. Each request function also has a two-arity version with an extra parameter, `options`, 96 | which is a map containing options for the HTTP request. These options are as follows: 97 | 98 | * `:headers`: optional; a map of headers. By default an 'accept-language' header 99 | with a value of `puppetlabs.core.i18n/user-locale` will be added to the 100 | request. 101 | * `:body`: optional; may be a String or any type supported by clojure's reader 102 | * `:compress-request-body`: optional; used to control any additional compression 103 | which the client can apply to the request body before it is sent to the target 104 | server. Defaults to `:none`. Supported values are: 105 | * `:gzip` which will compress the request body as gzip 106 | * `:none` which will not apply any additional compression to the request body 107 | * `:decompress-body`: optional; if `true`, an 'accept-encoding' header with a value of 108 | 'gzip, deflate' will be added to the request, and the response will be 109 | automatically decompressed if it contains a recognized 'content-encoding' 110 | header. Defaults to `true`. 111 | * `:as`: optional; used to control the data type of the response body. Defaults to `:stream`. Supported values 112 | are: 113 | * `:text` which will return a `String` 114 | * `:stream` which will return an `InputStream` 115 | * `:unbuffered-stream` which is a variant of `:stream` that will buffer as little data as possible 116 | * `:query-params`: optional; used to set the query parameters of an http request. This should be 117 | a map, where each key and each value is a String. 118 | * `:metric-id`: optional; a vector of keywords or strings. A metric will be created for 119 | each element in the vector, with each appending to the previous. 120 | 121 | For example, say you want to make a GET request with 122 | query parameter `abc` with value `def` to the URL `http://localhost:8080/test`. If you wanted to use a 123 | persistent synchronous client, you could make the request and print the body of the response like so: 124 | 125 | ```clj 126 | (let [client (sync/create-client {}) 127 | response (get client "http://localhost:8080/test" {:query-params {"abc" "def"}})] 128 | (println (:body response)) 129 | ``` 130 | 131 | If you wanted to use an asynchronous client, you could make the request and print the body of the response like so: 132 | 133 | ```clj 134 | (let [client (async/create-client {}) 135 | response (get client "http://localhost:8080/test" {:query-params {"abc" "def"}})] 136 | (println (:body @response))) 137 | ``` 138 | 139 | ### Closing a persistent client 140 | 141 | The `close` function takes no arguments. This function closes the client, and causes 142 | all resources associated with it to be cleaned up. This function must be called by the caller when 143 | they are done making requests with the client, as no implicit cleanup of the associated resources 144 | is done when the client is garbage collected. Once a client is closed, it can no longer be used to 145 | make any requests. 146 | 147 | ## Making a Request without a persistent client 148 | 149 | In addition to allowing you to create a persistent client with the `create-client` function, the 150 | puppetlabs.http.client.sync namespace provides the following simple request functions that can be 151 | called without a client: 152 | 153 | ```clj 154 | (get [url] [url opts]) 155 | (head [url] [url opts]) 156 | (post [url] [url opts]) 157 | (put [url] [url opts]) 158 | (delete [url] [url opts]) 159 | (trace [url] [url opts]) 160 | (options [url] [url opts]) 161 | (patch [url] [url opts]) 162 | (request [req]) 163 | 164 | ``` 165 | These functions will, for every request, create a new client, make a new request with that client, and then 166 | close the client once the response is received. Each of these functions (barring `request`) take one argument, 167 | `url`, which is the URL to which you want to make the request, and can optionally take a second argument, `options`. 168 | `options` is a map of options to configure both the client and the request, and as such takes the union of all options 169 | accepted by the `create-client` function and all options accepted by the request functions for a persistent 170 | client. 171 | 172 | For example, say you want to make a GET request to the URL `http://localhost:8080/test` with query parameter 173 | `abc` with value `def`, and you do not want redirects to be followed. In that case, you could do the following 174 | to make the request and print the body of the response: 175 | 176 | ```clj 177 | (let [response (get "http://localhost:8080/test" {:follow-redirects false 178 | :query-params {"abc" "def"}})] 179 | (println (:body response))) 180 | ``` 181 | 182 | A `request` function is also provided, which allows you to make a request of any type. 183 | `request` takes one argument, `req`, which is a map of options. It takes the same options as the simple request 184 | functions, but also takes the following required options: 185 | 186 | * `:url`: the URL against which to make the request. This should be a string. 187 | * `:method`: the HTTP method (:get, :head, :post, :put, :delete, :trace, :options, :patch) 188 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | # 2.1.4 4 | * improve logging around non-successful outcomes 5 | 6 | # 2.1.3 7 | * ensure unbuffered-stream correctly handles 204 responses with no body in the response. 8 | 9 | # 2.1.2 10 | * add support for multiple `Set-Cookie` headers in a response separated by newlines. 11 | * update project.clj to remove composite profiles that include maps, it is deprecated in lein 2.11.0 12 | 13 | # 2.1.1 14 | * [PE-34843](https://tickets.puppetlabs.com/browse/PE-34843) Properly reuse connections when using a client certificate 15 | * Update to clj-parent 5.2.11 16 | 17 | # 2.1.0 18 | * update to clj-parent 5.2.6 to move from bouncy-castle `15on` libraries to the `18on` version 19 | * Use `OPTIONS` request method when calling the synchronous client's `options` function. 20 | 21 | ## 2.0.0 22 | * [PE-33177](https://tickets.puppetlabs.com/browse/PE-33177) Add TLSv1.3 to ClientOptions default SSL protocols and remove TLSv1 and TLSv1.1. 23 | 24 | ## 1.2.4 25 | * Public release of 1.2.3, no other changes. 26 | 27 | ## 1.2.3 28 | * [SERVER-3068](https://tickets.puppetlabs.com/browse/SERVER-3068) Conditionally allow auth headers when being redirected. 29 | 30 | ## 1.2.2 31 | * Internal release of 1.2.1, no other changes. 32 | 33 | ## 1.2.1 34 | * [SERVER-3068](https://tickets.puppetlabs.com/browse/SERVER-3068) Do not copy auth headers when being redirected. 35 | 36 | ## 1.2.0 37 | * [SERVER-2780](https://tickets.puppetlabs.com/browse/SERVER-2780) Add reason phrase to HTTP response 38 | 39 | ## 1.1.3 40 | * [PE-27189]((https://tickets.puppetlabs.com/browse/PE-27189) Disambiguate ambiguous type specifications 41 | 42 | ## 1.1.2 43 | * [PE-26658](https://tickets.puppetlabs.com/browse/PE-26658) Apply JVM security policy to FIPS 44 | 45 | ## 1.1.1 46 | * Released by automation w/o content 47 | 48 | ## 1.1.0 49 | * [PDB-4357](https://tickets.puppetlabs.com/browse/PDB-4357) FIPS support via bc-fips jar 50 | 51 | ## 1.0.0 52 | * Adds `::max-connections-total` and `::max-connections-per-route` to allow the Apache HTTP client values to be overridden. They default to 20 and 2 (to correlate to the Apache client behavior) respectively. 53 | * Update to clj-parent 1.7.12. This moves the library to require Java 8. 54 | 55 | ## 0.9.0 56 | This is a feature release 57 | * [TK-443](https://tickets.puppetlabs.com/browse/TK-443) Adds `:enable-url-metrics?` and `setEnableURLMetrics` to the clojure and java APIs respectively. This allows disabling autogenerated url based metrics. 58 | * [PE-19979](https://tickets.puppetlabs.com/browse/PE-19979) & [SERVER-1733](https://tickets.puppetlabs.com/browse/SERVER-1733) Update dependencies via clj-parent (i18n dependency specifically). 59 | 60 | ## 0.8.0 61 | This is a feature release 62 | * [PDB-2640](https://tickets.puppetlabs.com/browse/PDB-2640) Added a 63 | `:compress-request-body` request option which allows for the request 64 | body content to be gzip-compressed before forwarding it on to the server 65 | 66 | ## 0.7.0 67 | This is a feature release 68 | 69 | * [SERVER-1556](https://tickets.puppetlabs.com/browse/SERVER-1556) Add 70 | puppetlabs/clj-i18n support to clj-http-client. 71 | * Pass the global i18n locale binding in the "accept-language" header of Clojure 72 | client requests. 73 | 74 | ## 0.6.0 75 | This is a feature and maintenance release 76 | 77 | * [TK-179](https://tickets.puppetlabs.com/browse/TK-179) Refactor to eliminate Clojure/Java duplication 78 | * [TK-316](https://tickets.puppetlabs.com/browse/TK-316) Add support for metrics 79 | * [TK-317](https://tickets.puppetlabs.com/browse/TK-317) Add ability to pass a metrics-id 80 | * [TK-354](https://tickets.puppetlabs.com/browse/TK-354) Add [documentation on metrics](./doc/metrics.md) 81 | * [TK-308](https://tickets.puppetlabs.com/browse/TK-308) Bump Apache HttpAsyncClient library to 4.1.2 82 | * [TK-402](https://tickets.puppetlabs.com/browse/TK-402) Allow metric namespace to be configurable 83 | * Bump Trapperkeeper and Kitchensink dependencies from 1.1.1 to 1.5.1 (TK) and 84 | 1.2.0 to 1.3.0 (KS). 85 | 86 | ## 0.5.0 87 | This is a feature release. 88 | 89 | * [TK-312](https://tickets.puppetlabs.com/browse/TK-312) Unbuffered streams in Java to match clojure support released in 0.4.5. 90 | * Add request function to clojure client protocol. 91 | 92 | 93 | ## 0.4.6 94 | This is a maintenance release. 95 | 96 | * [TK-303](https://tickets.puppetlabs.com/browse/TK-303) - update dependencies to use Apache httpasyncclient v4.1.1. 97 | 98 | ## 0.4.5 99 | This is a feature release, and probably should have been 0.5.0 in 100 | order to comply with semantic versioning. 101 | 102 | * Add support for streaming responses in the Clojure API - (ca5ad63) Scott Walker 103 | 104 | ## 0.4.4 105 | This is a maintenance release. 106 | 107 | * [TK-196](https://tickets.puppetlabs.com/browse/TK-196) - update prismatic 108 | dependencies to the latest versions. 109 | 110 | ## 0.4.3 111 | This is a feature release. 112 | 113 | * [TK-134](https://tickets.puppetlabs.com/browse/TK-134) - Introduced two new 114 | optional client configuration settings: 115 | - `connect-timeout-milliseconds`: maximum number of milliseconds that the 116 | client will wait for a connection to be established. 117 | - `socket-timeout-milliseconds`: maximum number of milliseconds that the 118 | client will allow for no data to be available on the socket before 119 | closing the underlying connection, 'SO_TIMEOUT' in socket terms. 120 | 121 | ## 0.4.2 122 | This is a bugfix release. 123 | 124 | * [TK-145](https://tickets.puppetlabs.com/browse/TK-145) - Fixed a bug which 125 | caused some HTTP requests to incorrectly have `charset=UTF-8` added to their 126 | `Content-Type` headers. 127 | 128 | 129 | ## 0.4.1 130 | This is a maintenance release. 131 | 132 | * Add documentation for making requests using the Java and Clojure clients. 133 | * Upgrade jvm-ssl-utils (previously known as jvm-certificate-authority) 134 | dependency to 0.7.0. 135 | * Upgrade clj-kitchensink dependency to 1.0.0. 136 | * Upgrade trapperkeeper dependency to 1.0.1. 137 | 138 | ## 0.4.0 139 | This is a feature release which has some breaking changes. 140 | 141 | * Support for non-client bound asynchronous requests has been removed from both 142 | the Clojure and Java-layer APIs. This includes all of the request functions 143 | that previously existed in the `client.async` Clojure namespace and the 144 | request methods in the `AsyncHttpClient` Java class. 145 | * Add a Java-layer API for getting an instance of an HttpClient on which 146 | multiple requests -- e.g.., GET, POST -- can be made. Clients are created 147 | via the `createClient` method on the new `Async` and `Sync` classes, for 148 | a client that can make asynchronous or synchronous web requests, respectively. 149 | * Non-client bound synchronous requests can still be performed through the Java 150 | API but must now be done through the `Sync` classes rather than the 151 | `SyncHttpClient` class. The `SyncHttpClient` class is now used as the type 152 | of the instance that the `Sync.createClient()` method returns. 153 | * The Java `RequestOptions` class was refactored into new `ClientOptions` and 154 | `RequestOptions` classes which can be used with the client-bound `Async` 155 | and `Sync` APIs. For non-client bound requests, options are now defined 156 | via a `SimpleRequestOptions` class. 157 | * Reworked connection close behavior to more robustly handle successful and 158 | failed connections. 159 | 160 | ## 0.3.1 161 | This is a bugfix release. 162 | 163 | * Fix a memory leak that occurred as a result of connection failures. 164 | 165 | ## 0.3.0 166 | This is a feature release. 167 | 168 | * Add configuration settings for SSL Protocols and Cipher Suites to both the 169 | Java and Clojure clients. 170 | 171 | ## 0.2.8 172 | This is a bugfix release. 173 | 174 | * Fix a bug in the Java client API that caused a file descriptor leak each time 175 | a client was created for a new request. 176 | 177 | ## 0.2.7 178 | This is a bugfix release. 179 | 180 | * Fix a bug where the character encoding was always being set to ISO-8859-1 w/o 181 | a charset ever being explicitly specified in the Content-Type header. We now 182 | honor the existing charset if there is one in the header, and otherwise we 183 | use UTF-8 and explicitly add the charset to the header. 184 | 185 | ## 0.2.6 186 | * Add :follow-redirects and :force-redirects options to the clojure client. 187 | * Add followRedirects and forceRedirects options to the Java client. 188 | 189 | ## 0.2.5 190 | * Add an overloaded constructor for `RequestOptions` that accepts a String uri 191 | 192 | ## 0.2.4 193 | * Fix a bug in the Java client API that caused an NPE if a Content-Type header 194 | did not specify a charset 195 | 196 | ## 0.2.3 197 | * No changes 198 | 199 | ## 0.2.2 200 | * Add back in support for query-params map in Clojure API 201 | 202 | ## 0.2.1 203 | * Upgrade to Apache HttpAsyncClient v4.0.2 (fixes a bug where headers don't get 204 | included when following redirects). 205 | 206 | ## 0.2.0 207 | * Port the code to use the Apache HttpAsyncClient library instead of 208 | http-kit. 209 | * The API around creating a persistent client has changed and 210 | persistent clients are explicitly managed 211 | * The available request options have changed. Some convenience options 212 | have been removed. 213 | 214 | ## 0.1.7 215 | * Explicitly target JDK6 when building release jars 216 | 217 | ## 0.1.6 218 | * Add support for configuring client SSL context with a CA cert without a 219 | client cert/key 220 | 221 | ## 0.1.5 222 | * Update to latest version of puppetlabs/kitchensink 223 | * Use puppetlabs/certificate-authority for SSL tasks 224 | 225 | ## 0.1.4 226 | * Fix bug in sync.clj when excluding clojure.core/get 227 | 228 | ## 0.1.3 229 | * Added a Java API for the synchronous client 230 | 231 | -------------------------------------------------------------------------------- /test/puppetlabs/http/client/sync_ssl_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.http.client.sync-ssl-test 2 | (:require [clojure.test :refer :all] 3 | [puppetlabs.http.client.sync :as sync] 4 | [puppetlabs.trapperkeeper.core :as tk] 5 | [puppetlabs.trapperkeeper.services.webserver.jetty9-service :as jetty9] 6 | [puppetlabs.trapperkeeper.testutils.bootstrap :as testutils] 7 | [puppetlabs.trapperkeeper.testutils.logging :as testlogging] 8 | [schema.test :as schema-test]) 9 | (:import (com.puppetlabs.http.client HttpClientException 10 | SimpleRequestOptions 11 | Sync) 12 | (com.puppetlabs.ssl_utils SSLUtils) 13 | (java.net URI) 14 | (javax.net.ssl SSLException SSLHandshakeException) 15 | (org.apache.http ConnectionClosedException))) 16 | 17 | (use-fixtures :once schema-test/validate-schemas) 18 | 19 | (defn app 20 | [_req] 21 | {:status 200 22 | :body "Hello, World!"}) 23 | 24 | (tk/defservice test-web-service 25 | [[:WebserverService add-ring-handler]] 26 | (init [this context] 27 | (add-ring-handler app "/hello") 28 | context)) 29 | 30 | (deftest sync-client-test-from-pems 31 | (testlogging/with-test-logging 32 | (testutils/with-app-with-config app 33 | [jetty9/jetty9-service test-web-service] 34 | {:webserver {:ssl-host "0.0.0.0" 35 | :ssl-port 10080 36 | :ssl-ca-cert "./dev-resources/ssl/ca.pem" 37 | :ssl-cert "./dev-resources/ssl/cert.pem" 38 | :ssl-key "./dev-resources/ssl/key.pem"}} 39 | (testing "java sync client" 40 | (let [request-options (.. (SimpleRequestOptions. (URI. "https://localhost:10080/hello/")) 41 | (setSslCert "./dev-resources/ssl/cert.pem") 42 | (setSslKey "./dev-resources/ssl/key.pem") 43 | (setSslCaCert "./dev-resources/ssl/ca.pem")) 44 | response (Sync/get request-options)] 45 | (is (= 200 (.getStatus response))) 46 | (is (= "Hello, World!" (slurp (.getBody response)))))) 47 | (testing "clojure sync client" 48 | (let [response (sync/get "https://localhost:10080/hello/" 49 | {:ssl-cert "./dev-resources/ssl/cert.pem" 50 | :ssl-key "./dev-resources/ssl/key.pem" 51 | :ssl-ca-cert "./dev-resources/ssl/ca.pem"})] 52 | (is (= 200 (:status response))) 53 | (is (= "Hello, World!" (slurp (:body response))))))))) 54 | 55 | (deftest sync-client-test-from-ca-cert 56 | (testlogging/with-test-logging 57 | (testutils/with-app-with-config app 58 | [jetty9/jetty9-service test-web-service] 59 | {:webserver {:ssl-host "0.0.0.0" 60 | :ssl-port 10080 61 | :ssl-ca-cert "./dev-resources/ssl/ca.pem" 62 | :ssl-cert "./dev-resources/ssl/cert.pem" 63 | :ssl-key "./dev-resources/ssl/key.pem" 64 | :client-auth "want"}} 65 | (testing "java sync client" 66 | (let [request-options (.. (SimpleRequestOptions. (URI. "https://localhost:10080/hello/")) 67 | (setSslCaCert "./dev-resources/ssl/ca.pem")) 68 | response (Sync/get request-options)] 69 | (is (= 200 (.getStatus response))) 70 | (is (= "Hello, World!" (slurp (.getBody response)))))) 71 | (testing "clojure sync client" 72 | (let [response (sync/get "https://localhost:10080/hello/" 73 | {:ssl-ca-cert "./dev-resources/ssl/ca.pem"})] 74 | (is (= 200 (:status response))) 75 | (is (= "Hello, World!" (slurp (:body response))))))))) 76 | 77 | (deftest sync-client-test-with-invalid-ca-cert 78 | (testlogging/with-test-logging 79 | (testutils/with-app-with-config app 80 | [jetty9/jetty9-service test-web-service] 81 | {:webserver {:ssl-host "0.0.0.0" 82 | :ssl-port 10081 83 | :ssl-ca-cert "./dev-resources/ssl/ca.pem" 84 | :ssl-cert "./dev-resources/ssl/cert.pem" 85 | :ssl-key "./dev-resources/ssl/key.pem" 86 | :client-auth "want"}} 87 | (testing "java sync client" 88 | (let [request-options (.. (SimpleRequestOptions. (URI. "https://localhost:10081/hello/")) 89 | (setSslCaCert "./dev-resources/ssl/alternate-ca.pem"))] 90 | (try 91 | (Sync/get request-options) 92 | ; fail if we don't get an exception 93 | (is (not true) "expected HttpClientException") 94 | (catch HttpClientException e 95 | (if (SSLUtils/isFIPS) 96 | ;; in FIPS, the BC provider throws a different exception here, specifically: 97 | ;; javax.net.ssl.SSLException: org.bouncycastle.tls.TlsFatalAlert: certificate_unknown(46) 98 | (is (instance? SSLException (.getCause e))) 99 | (is (instance? SSLHandshakeException (.getCause e)))))))) 100 | (testing "clojure sync client" 101 | (if (SSLUtils/isFIPS) 102 | ;; in FIPS, the BC provider throws a different exception here, specifically: 103 | ;; javax.net.ssl.SSLException: org.bouncycastle.tls.TlsFatalAlert: certificate_unknown(46) 104 | (is (thrown? SSLException 105 | (sync/get "https://localhost:10081/hello/" 106 | {:ssl-ca-cert "./dev-resources/ssl/alternate-ca.pem"}))) 107 | (is (thrown? SSLHandshakeException 108 | (sync/get "https://localhost:10081/hello/" 109 | {:ssl-ca-cert "./dev-resources/ssl/alternate-ca.pem"})))))))) 110 | 111 | (defmacro with-server-with-protocols 112 | [server-protocols server-cipher-suites & body] 113 | `(testlogging/with-test-logging 114 | (testutils/with-app-with-config app# 115 | [jetty9/jetty9-service test-web-service] 116 | {:webserver (merge 117 | {:ssl-host "0.0.0.0" 118 | :ssl-port 10080 119 | :ssl-ca-cert "./dev-resources/ssl/ca.pem" 120 | :ssl-cert "./dev-resources/ssl/cert.pem" 121 | :ssl-key "./dev-resources/ssl/key.pem" 122 | :ssl-protocols ~server-protocols} 123 | (if ~server-cipher-suites 124 | {:cipher-suites ~server-cipher-suites}))} 125 | ~@body))) 126 | 127 | (defmacro java-unsupported-protocol-exception? 128 | [& body] 129 | `(try 130 | ~@body 131 | (catch HttpClientException e# 132 | (let [cause# (.getCause e#) 133 | message# (.getMessage cause#)] 134 | (or (and (instance? SSLHandshakeException cause#) 135 | (or ;; java 11 136 | (re-find #"protocol_version" message#) 137 | ;; java 8 138 | (re-find #"not supported by the client" message#))) 139 | (and (instance? SSLException cause#) 140 | (or (re-find #"handshake_failure" message#) 141 | (re-find #"internal_error" message#))) 142 | (instance? ConnectionClosedException cause#)))) 143 | (catch ConnectionClosedException cce# true))) 144 | 145 | (defn java-https-get-with-protocols 146 | [client-protocols client-cipher-suites] 147 | (let [request-options (.. (SimpleRequestOptions. (URI. "https://localhost:10080/hello/")) 148 | (setSslCert "./dev-resources/ssl/cert.pem") 149 | (setSslKey "./dev-resources/ssl/key.pem") 150 | (setSslCaCert "./dev-resources/ssl/ca.pem"))] 151 | (when client-protocols 152 | (.setSslProtocols request-options (into-array String client-protocols))) 153 | (when client-cipher-suites 154 | (.setSslCipherSuites request-options (into-array String client-cipher-suites))) 155 | (Sync/get request-options))) 156 | 157 | (defn clj-https-get-with-protocols 158 | [client-protocols client-cipher-suites] 159 | (let [ssl-opts (merge {:ssl-cert "./dev-resources/ssl/cert.pem" 160 | :ssl-key "./dev-resources/ssl/key.pem" 161 | :ssl-ca-cert "./dev-resources/ssl/ca.pem"} 162 | (when client-protocols 163 | {:ssl-protocols client-protocols}) 164 | (when client-cipher-suites 165 | {:cipher-suites client-cipher-suites}))] 166 | (sync/get "https://localhost:10080/hello/" ssl-opts))) 167 | 168 | (deftest sync-client-test-ssl-protocols 169 | (testing "should be able to connect to a TLSv1.2 server by default" 170 | (with-server-with-protocols ["TLSv1.2"] nil 171 | (testing "java sync client" 172 | (let [response (java-https-get-with-protocols nil nil)] 173 | (is (= 200 (.getStatus response))) 174 | (is (= "Hello, World!" (slurp (.getBody response)))))) 175 | (testing "clojure sync client" 176 | (let [response (clj-https-get-with-protocols nil nil)] 177 | (is (= 200 (:status response))) 178 | (is (= "Hello, World!" (slurp (:body response)))))))) 179 | 180 | (testing "should not connect to a server when protocols don't overlap" 181 | (with-server-with-protocols ["TLSv1.1"] ["TLS_RSA_WITH_AES_128_CBC_SHA"] 182 | (testing "java sync client" 183 | (is (java-unsupported-protocol-exception? 184 | (java-https-get-with-protocols ["TLSv1.2"] nil)))) 185 | (testing "clojure sync client" 186 | (is (java-unsupported-protocol-exception? (clj-https-get-with-protocols ["TLSv1.2"] nil))))))) 187 | 188 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/ClientOptions.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client; 2 | 3 | import com.codahale.metrics.MetricRegistry; 4 | import com.puppetlabs.ssl_utils.SSLUtils; 5 | 6 | import javax.net.ssl.SSLContext; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.security.NoSuchProviderException; 9 | import java.security.Provider; 10 | import java.security.Security; 11 | 12 | /** 13 | * This class is a wrapper around a number of options for use 14 | * in configuring a persistent HTTP Client. 15 | * 16 | * @see com.puppetlabs.http.client.Async#createClient(ClientOptions) 17 | * @see com.puppetlabs.http.client.Sync#createClient(ClientOptions) 18 | */ 19 | public class ClientOptions { 20 | public static final String[] DEFAULT_SSL_PROTOCOLS = 21 | new String[] {"TLSv1.3", "TLSv1.2"}; 22 | 23 | private SSLContext sslContext; 24 | private String sslCert; 25 | private String sslKey; 26 | private String sslCaCert; 27 | private String[] sslProtocols; 28 | private String[] sslCipherSuites; 29 | private boolean insecure = false; 30 | private boolean forceRedirects = false; 31 | private boolean followRedirects = true; 32 | private int connectTimeoutMilliseconds = -1; 33 | private int socketTimeoutMilliseconds = -1; 34 | private MetricRegistry metricRegistry; 35 | private String metricPrefix; 36 | private String serverId; 37 | private boolean enableURLMetrics = true; 38 | 39 | // defaults from apache connection manager 40 | private int maxConnectionsTotal = 20; 41 | private int maxConnectionsPerRoute = 2; 42 | 43 | /** 44 | * Constructor for the ClientOptions class. When this constructor is called, 45 | * insecure and forceRedirects will default to false, and followRedirects will default 46 | * to true. 47 | */ 48 | public ClientOptions() { } 49 | 50 | /** 51 | * Constructor for the ClientOptions class. 52 | * @param sslContext The SSL context for the client. 53 | * @param sslCert The path to a PEM file containing the client cert 54 | * @param sslKey The path to a PEM file containing the client private key 55 | * @param sslCaCert The path to a PEM file containing the CA cert 56 | * @param sslProtocols The SSL protocols that the client can select from when talking to the server 57 | * @param sslCipherSuites The cipher suites that the client can select from when talking to the server 58 | * @param insecure Whether or not the client should use an insecure connection. 59 | * @param forceRedirects Whether or not the client should follow redirects on POST or PUT requests. 60 | * @param followRedirects Whether or not the client should follow redirects in general. 61 | * @param connectTimeoutMilliseconds Maximum number of milliseconds that 62 | * the client will wait for a 63 | * connection to be established. A 64 | * value of zero is interpreted as 65 | * infinite. A negative value is 66 | * interpreted as undefined (system 67 | * default). 68 | * @param socketTimeoutMilliseconds Maximum number of milliseconds that 69 | * the client will allow for no data to 70 | * be available on the socket before 71 | * closing the underlying connection, 72 | * SO_TIMEOUT in socket 73 | * terms. A timeout of zero is 74 | * interpreted as an infinite timeout. 75 | * A negative value is interpreted as 76 | * undefined (system default). 77 | * @param maxConnectionsPerRoute Maxiumum number of connections allowed for a given route for a client instance, where 78 | * a route is a host / port combination. The apache default is 2 79 | * @param maxConnectionsTotal Maximum number of connections allowed for a given client instance. The apache default 80 | * is 20. 81 | * 82 | */ 83 | public ClientOptions(SSLContext sslContext, 84 | String sslCert, 85 | String sslKey, 86 | String sslCaCert, 87 | String[] sslProtocols, 88 | String[] sslCipherSuites, 89 | boolean insecure, 90 | boolean forceRedirects, 91 | boolean followRedirects, 92 | int connectTimeoutMilliseconds, 93 | int socketTimeoutMilliseconds, 94 | int maxConnectionsPerRoute, 95 | int maxConnectionsTotal) { 96 | this.sslContext = sslContext; 97 | this.sslCert = sslCert; 98 | this.sslKey = sslKey; 99 | this.sslCaCert = sslCaCert; 100 | this.sslProtocols = sslProtocols; 101 | this.sslCipherSuites = sslCipherSuites; 102 | this.insecure = insecure; 103 | this.forceRedirects = forceRedirects; 104 | this.followRedirects = followRedirects; 105 | this.connectTimeoutMilliseconds = connectTimeoutMilliseconds; 106 | this.socketTimeoutMilliseconds = socketTimeoutMilliseconds; 107 | this.maxConnectionsPerRoute = maxConnectionsPerRoute; 108 | this.maxConnectionsTotal = maxConnectionsTotal; 109 | } 110 | 111 | public SSLContext getSslContext() { 112 | return sslContext; 113 | } 114 | public ClientOptions setSslContext(SSLContext sslContext) { 115 | this.sslContext = sslContext; 116 | return this; 117 | } 118 | 119 | public String getSslCert() { 120 | return sslCert; 121 | } 122 | public ClientOptions setSslCert(String sslCert) { 123 | this.sslCert = sslCert; 124 | return this; 125 | } 126 | 127 | public String getSslKey() { 128 | return sslKey; 129 | } 130 | public ClientOptions setSslKey(String sslKey) { 131 | this.sslKey = sslKey; 132 | return this; 133 | } 134 | 135 | public String getSslCaCert() { 136 | return sslCaCert; 137 | } 138 | public ClientOptions setSslCaCert(String sslCaCert) { 139 | this.sslCaCert = sslCaCert; 140 | return this; 141 | } 142 | 143 | public String[] getSslProtocols() { 144 | return sslProtocols; 145 | } 146 | public ClientOptions setSslProtocols(String[] sslProtocols) { 147 | this.sslProtocols = sslProtocols; 148 | return this; 149 | } 150 | 151 | public String[] getSslCipherSuites() { 152 | return sslCipherSuites; 153 | } 154 | public ClientOptions setSslCipherSuites(String[] sslCipherSuites) { 155 | this.sslCipherSuites = sslCipherSuites; 156 | return this; 157 | } 158 | 159 | public boolean getInsecure() { 160 | return insecure; 161 | } 162 | public ClientOptions setInsecure(boolean insecure) { 163 | this.insecure = insecure; 164 | return this; 165 | } 166 | 167 | public boolean getForceRedirects() { return forceRedirects; } 168 | public ClientOptions setForceRedirects(boolean forceRedirects) { 169 | this.forceRedirects = forceRedirects; 170 | return this; 171 | } 172 | 173 | public boolean getFollowRedirects() { return followRedirects; } 174 | public ClientOptions setFollowRedirects(boolean followRedirects) { 175 | this.followRedirects = followRedirects; 176 | return this; 177 | } 178 | 179 | public int getConnectTimeoutMilliseconds() { 180 | return connectTimeoutMilliseconds; 181 | } 182 | 183 | public ClientOptions setConnectTimeoutMilliseconds( 184 | int connectTimeoutMilliseconds) { 185 | this.connectTimeoutMilliseconds = connectTimeoutMilliseconds; 186 | return this; 187 | } 188 | 189 | public int getSocketTimeoutMilliseconds() { 190 | return socketTimeoutMilliseconds; 191 | } 192 | 193 | public ClientOptions setSocketTimeoutMilliseconds( 194 | int socketTimeoutMilliseconds) { 195 | this.socketTimeoutMilliseconds = socketTimeoutMilliseconds; 196 | return this; 197 | } 198 | 199 | public MetricRegistry getMetricRegistry() { 200 | return metricRegistry; 201 | } 202 | 203 | public ClientOptions setMetricRegistry(MetricRegistry metricRegistry) { 204 | this.metricRegistry = metricRegistry; 205 | return this; 206 | } 207 | 208 | public String getMetricPrefix() { 209 | return metricPrefix; 210 | } 211 | 212 | public ClientOptions setMetricPrefix(String metricPrefix) { 213 | this.metricPrefix = metricPrefix; 214 | return this; 215 | } 216 | 217 | public String getServerId() { 218 | return serverId; 219 | } 220 | 221 | public ClientOptions setServerId(String serverId) { 222 | this.serverId = serverId; 223 | return this; 224 | } 225 | 226 | public boolean isEnableURLMetrics() { 227 | return enableURLMetrics; 228 | } 229 | 230 | public ClientOptions setEnableURLMetrics(boolean enableURLMetrics) { 231 | this.enableURLMetrics = enableURLMetrics; 232 | return this; 233 | } 234 | 235 | public int getMaxConnectionsTotal() {return this.maxConnectionsTotal; } 236 | public ClientOptions setMaxConnectionsTotal(int maxConnectionsTotal) { 237 | this.maxConnectionsTotal = maxConnectionsTotal; 238 | return this; 239 | } 240 | 241 | public int getMaxConnectionsPerRoute() {return this.maxConnectionsPerRoute; } 242 | public ClientOptions setMaxConnectionsPerRoute(int maxConnectionsPerRoute) { 243 | this.maxConnectionsPerRoute = maxConnectionsPerRoute; 244 | return this; 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /src/java/com/puppetlabs/http/client/metrics/Metrics.java: -------------------------------------------------------------------------------- 1 | package com.puppetlabs.http.client.metrics; 2 | 3 | import com.codahale.metrics.MetricFilter; 4 | import com.codahale.metrics.MetricRegistry; 5 | import com.codahale.metrics.Timer; 6 | import com.puppetlabs.http.client.impl.metrics.CategoryClientTimerMetricFilter; 7 | import com.puppetlabs.http.client.impl.metrics.MetricIdClientTimerFilter; 8 | import com.puppetlabs.http.client.impl.metrics.TimerMetricData; 9 | import com.puppetlabs.http.client.impl.metrics.UrlAndMethodClientTimerFilter; 10 | import com.puppetlabs.http.client.impl.metrics.UrlClientTimerFilter; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.net.URI; 15 | import java.net.URISyntaxException; 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | public class Metrics { 22 | public static final String PUPPETLABS_NAMESPACE_PREFIX = "puppetlabs"; 23 | public static final String HTTP_CLIENT_NAMESPACE_PREFIX = "http-client.experimental"; 24 | public static final String DEFAULT_NAMESPACE_PREFIX = PUPPETLABS_NAMESPACE_PREFIX + 25 | "." + HTTP_CLIENT_NAMESPACE_PREFIX; 26 | public static final String NAMESPACE_URL = "with-url"; 27 | public static final String NAMESPACE_URL_AND_METHOD = "with-url-and-method"; 28 | public static final String NAMESPACE_METRIC_ID = "with-metric-id"; 29 | public static final String NAMESPACE_FULL_RESPONSE = "full-response"; 30 | private static final Logger LOGGER = LoggerFactory.getLogger(Metrics.class); 31 | 32 | public static String buildMetricNamespace(String metricPrefix, String serverId) { 33 | if (metricPrefix != null) { 34 | if (serverId != null) { 35 | Metrics.LOGGER.warn("Metric prefix and server id both set. Using metric prefix '" 36 | + metricPrefix + "' for metric namespace."); 37 | } 38 | return metricPrefix + "." + HTTP_CLIENT_NAMESPACE_PREFIX; 39 | } else if (serverId != null) { 40 | return PUPPETLABS_NAMESPACE_PREFIX + "." + serverId + "." 41 | + HTTP_CLIENT_NAMESPACE_PREFIX; 42 | } else { 43 | return DEFAULT_NAMESPACE_PREFIX; 44 | } 45 | } 46 | 47 | public enum MetricType { FULL_RESPONSE } 48 | public enum MetricCategory { URL, URL_AND_METHOD, METRIC_ID } 49 | 50 | public static String urlToMetricUrl(String uriString) throws URISyntaxException { 51 | final URI uri = new URI(uriString); 52 | final URI convertedUri = new URI(uri.getScheme(), null, uri.getHost(), 53 | uri.getPort(), uri.getPath(), null, null); 54 | return convertedUri.toString(); 55 | } 56 | 57 | private static List getUrlClientTimerArray(MetricRegistry registry, 58 | MetricFilter filter) { 59 | List timerArray = new ArrayList<>(); 60 | for (Map.Entry entry : registry.getTimers(filter).entrySet()) { 61 | UrlClientTimer timer = (UrlClientTimer)entry.getValue(); 62 | timerArray.add(timer); 63 | } 64 | return timerArray; 65 | } 66 | 67 | private static List getUrlAndMethodClientTimerArray(MetricRegistry registry, 68 | MetricFilter filter) { 69 | List timerArray = new ArrayList<>(); 70 | for (Map.Entry entry : registry.getTimers(filter).entrySet()) { 71 | UrlAndMethodClientTimer timer = (UrlAndMethodClientTimer)entry.getValue(); 72 | timerArray.add(timer); 73 | } 74 | return timerArray; 75 | } 76 | 77 | private static List getMetricIdClientTimerArray(MetricRegistry registry, 78 | MetricFilter filter) { 79 | List timerArray = new ArrayList<>(); 80 | for (Map.Entry entry : registry.getTimers(filter).entrySet()) { 81 | MetricIdClientTimer timer = (MetricIdClientTimer)entry.getValue(); 82 | timerArray.add(timer); 83 | } 84 | return timerArray; 85 | } 86 | 87 | public static ClientTimerContainer getClientMetrics(MetricRegistry metricRegistry){ 88 | if (metricRegistry != null) { 89 | return new ClientTimerContainer( 90 | getUrlClientTimerArray(metricRegistry, 91 | new CategoryClientTimerMetricFilter(MetricCategory.URL)), 92 | getUrlAndMethodClientTimerArray(metricRegistry, 93 | new CategoryClientTimerMetricFilter(MetricCategory.URL_AND_METHOD)), 94 | getMetricIdClientTimerArray(metricRegistry, 95 | new CategoryClientTimerMetricFilter(MetricCategory.METRIC_ID))); 96 | } else { 97 | throw new IllegalArgumentException("Metric registry must not be null"); 98 | } 99 | } 100 | 101 | public static List getClientMetricsByUrl(MetricRegistry metricRegistry, 102 | final String url){ 103 | if (metricRegistry != null) { 104 | return getUrlClientTimerArray(metricRegistry, 105 | new UrlClientTimerFilter(url)); 106 | } else { 107 | throw new IllegalArgumentException("Metric registry must not be null"); 108 | } 109 | } 110 | 111 | public static List getClientMetricsByUrlAndMethod(MetricRegistry metricRegistry, 112 | final String url, 113 | final String method){ 114 | if (metricRegistry != null) { 115 | return getUrlAndMethodClientTimerArray(metricRegistry, 116 | new UrlAndMethodClientTimerFilter(url, method)); 117 | } else { 118 | throw new IllegalArgumentException("Metric registry must not be null"); 119 | } 120 | } 121 | 122 | public static List getClientMetricsByMetricId(MetricRegistry metricRegistry, 123 | final String[] metricId){ 124 | if (metricRegistry != null) { 125 | if (metricId.length == 0) { 126 | return getMetricIdClientTimerArray(metricRegistry, 127 | new CategoryClientTimerMetricFilter(MetricCategory.METRIC_ID)); 128 | } else { 129 | return getMetricIdClientTimerArray(metricRegistry, 130 | new MetricIdClientTimerFilter(new ArrayList(Arrays.asList(metricId)))); 131 | } 132 | } else { 133 | throw new IllegalArgumentException("Metric registry must not be null"); 134 | } 135 | } 136 | 137 | private static List computeUrlClientMetricsData(List timers) { 138 | if (timers != null) { 139 | List metricsData = new ArrayList<>(); 140 | for (UrlClientTimer timer: timers) { 141 | TimerMetricData timerMetricData = TimerMetricData.fromTimer(timer); 142 | String url = timer.getUrl(); 143 | 144 | metricsData.add(new UrlClientMetricData(timerMetricData, url)); 145 | } 146 | return metricsData; 147 | } else { 148 | return null; 149 | } 150 | } 151 | 152 | private static List computeUrlAndMethodClientMetricsData(List timers) { 153 | if (timers != null) { 154 | List metricsData = new ArrayList<>(); 155 | for (UrlAndMethodClientTimer timer: timers) { 156 | TimerMetricData timerMetricData = TimerMetricData.fromTimer(timer); 157 | String url = timer.getUrl(); 158 | String method = timer.getMethod(); 159 | 160 | metricsData.add(new UrlAndMethodClientMetricData(timerMetricData, url, method)); 161 | } 162 | return metricsData; 163 | } else { 164 | return null; 165 | } 166 | } 167 | 168 | private static List computeMetricIdClientMetricsData(List timers) { 169 | if (timers != null) { 170 | List metricsData = new ArrayList<>(); 171 | for (MetricIdClientTimer timer: timers) { 172 | TimerMetricData timerMetricData = TimerMetricData.fromTimer(timer); 173 | List metricId = timer.getMetricId(); 174 | 175 | metricsData.add(new MetricIdClientMetricData(timerMetricData, metricId)); 176 | } 177 | return metricsData; 178 | } else { 179 | return null; 180 | } 181 | } 182 | 183 | public static ClientMetricDataContainer getClientMetricsData(MetricRegistry metricRegistry){ 184 | if ( metricRegistry != null ) { 185 | ClientTimerContainer timers = getClientMetrics(metricRegistry); 186 | return new ClientMetricDataContainer(computeUrlClientMetricsData(timers.getUrlTimers()), computeUrlAndMethodClientMetricsData(timers.getUrlAndMethodTimers()), computeMetricIdClientMetricsData(timers.getMetricIdTimers()) 187 | ); 188 | } else { 189 | throw new IllegalArgumentException("Metric registry must not be null"); 190 | } 191 | } 192 | 193 | public static List getClientMetricsDataByUrl(MetricRegistry metricRegistry, 194 | String url){ 195 | List timers = getClientMetricsByUrl(metricRegistry, url); 196 | return computeUrlClientMetricsData(timers); 197 | } 198 | 199 | public static List getClientMetricsDataByUrlAndMethod(MetricRegistry metricRegistry, 200 | String url, 201 | String method){ 202 | List timers = getClientMetricsByUrlAndMethod(metricRegistry, url, method); 203 | return computeUrlAndMethodClientMetricsData(timers); 204 | } 205 | 206 | public static List getClientMetricsDataByMetricId(MetricRegistry metricRegistry, 207 | String[] metricId){ 208 | List timers = getClientMetricsByMetricId(metricRegistry, metricId); 209 | return computeMetricIdClientMetricsData(timers); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------