diff --git a/edgegrid-signer-apache-http-client/README.md b/edgegrid-signer-apache-http-client/README.md
index 63b3613..6aed718 100644
--- a/edgegrid-signer-apache-http-client/README.md
+++ b/edgegrid-signer-apache-http-client/README.md
@@ -4,7 +4,8 @@
-[![Javadoc](http://www.javadoc.io/badge/com.akamai.edgegrid/edgegrid-signer-apache-http-client.svg)](http://www.javadoc.io/doc/com.akamai.edgegrid/edgegrid-signer-apache-http-client)
This library implements [Akamai EdgeGrid Authentication](https://techdocs.akamai.com/developer/docs/authenticate-with-edgegrid) for Java.
-This particular module is a binding for the [Apache HTTP Client library](https://hc.apache.org/).
+This particular module is a binding for the [Apache HTTP Client library](https://hc.apache.org/) versions before 5.0.0.
+For Apache HTTP Client >= 5.0.0 support, use `edgegrid-signer-apache-http-client5` module.
This project contains installation and usage instructions in the [README.md](../README.md).
## Use Apache HTTP Client
diff --git a/edgegrid-signer-apache-http-client5/README.md b/edgegrid-signer-apache-http-client5/README.md
new file mode 100644
index 0000000..bf7da9f
--- /dev/null
+++ b/edgegrid-signer-apache-http-client5/README.md
@@ -0,0 +1,47 @@
+# Apache HTTP Client 5 module - EdgeGrid Client for Java
+
+-[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.akamai.edgegrid/edgegrid-signer-apache-http-client5/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.akamai.edgegrid/edgegrid-signer-apache-http-client5)
+-[![Javadoc](http://www.javadoc.io/badge/com.akamai.edgegrid/edgegrid-signer-apache-http-client5.svg)](http://www.javadoc.io/doc/com.akamai.edgegrid/edgegrid-signer-apache-http-client5)
+
+This library
+implements [Akamai EdgeGrid Authentication](https://techdocs.akamai.com/developer/docs/authenticate-with-edgegrid) for
+Java.
+This particular module is a binding for the [Apache HTTP Client library version 5.x](https://hc.apache.org/).
+This project contains installation and usage instructions in the [README.md](../README.md).
+
+## Use Apache HTTP Client
+
+Include the following Maven dependency in your project POM:
+
+```xml
+
+ com.akamai.edgegrid
+ edgegrid-signer-apache-http-client5
+ ${version}
+
+```
+
+Or in Gradle's `build.gradle.kts`
+```kotlin
+implementation("com.akamai.edgegrid:edgegrid-signer-apache-http-client5:$version")
+```
+
+Create an HTTP client that will sign your HTTP request with a defined client credential:
+
+```java
+var client=HttpClientBuilder.create()
+ .addRequestInterceptorFirst(new ApacheHttpClient5EdgeGridInterceptor(credential))
+ .setRoutePlanner(new ApacheHttpClient5EdgeGridRoutePlanner(credential))
+ .build();
+
+ var request=new HttpGet("http://endpoint.net/billing-usage/v1/reportSources");
+ client.execute(request,response->{
+ // response handler
+ });
+```
+
+## Use with REST-assured
+
+[REST-assured](https://github.com/rest-assured/rest-assured) doesn't currently support Apache HTTP Client 5. Refer to
+this [README](/edgegrid-signer-apache-http-client/README.md) in `edgegrid-signer-apache-http-client` module to set up
+an interceptor for a legacy client.
diff --git a/edgegrid-signer-apache-http-client5/pom.xml b/edgegrid-signer-apache-http-client5/pom.xml
new file mode 100644
index 0000000..c574c22
--- /dev/null
+++ b/edgegrid-signer-apache-http-client5/pom.xml
@@ -0,0 +1,59 @@
+
+
+
+
+ edgegrid-signer-parent
+ com.akamai.edgegrid
+ 5.0.0
+
+ 4.0.0
+
+ edgegrid-signer-apache-http-client5
+ jar
+ Apache HTTP Client 5 Library binding for EdgeGrid Client
+
+
+
+ com.akamai.edgegrid
+ edgegrid-signer-core
+ ${project.version}
+
+
+ ch.qos.logback
+ logback-classic
+
+
+ com.github.tomakehurst
+ wiremock
+ test
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
+ org.apache.httpcomponents.core5
+ httpcore5
+
+
+ org.hamcrest
+ hamcrest-all
+
+
+
+ org.slf4j
+ jcl-over-slf4j
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.testng
+ testng
+
+
+
+
diff --git a/edgegrid-signer-apache-http-client5/src/main/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridInterceptor.java b/edgegrid-signer-apache-http-client5/src/main/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridInterceptor.java
new file mode 100644
index 0000000..463bee7
--- /dev/null
+++ b/edgegrid-signer-apache-http-client5/src/main/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridInterceptor.java
@@ -0,0 +1,52 @@
+package com.akamai.edgegrid.signer.apachehttpclient5;
+
+import com.akamai.edgegrid.signer.ClientCredential;
+import com.akamai.edgegrid.signer.ClientCredentialProvider;
+import com.akamai.edgegrid.signer.Request;
+import com.akamai.edgegrid.signer.exceptions.RequestSigningException;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.protocol.HttpContext;
+
+/**
+ * Apache HTTP Client5 Library interceptor that signs a request using EdgeGrid V1 signing algorithm.
+ * Signing is a process of adding an Authorization header with a request signature. If signing fails then RuntimeException
is thrown.
+ */
+public class ApacheHttpClient5EdgeGridInterceptor implements HttpRequestInterceptor {
+
+ private final ApacheHttpClient5EdgeGridRequestSigner binding;
+
+ /**
+ * Creates an EdgeGrid signing interceptor using the same {@link ClientCredential} for each
+ * request.
+ *
+ * @param credential a {@link ClientCredential}
+ */
+ public ApacheHttpClient5EdgeGridInterceptor(ClientCredential credential) {
+ this.binding = new ApacheHttpClient5EdgeGridRequestSigner(credential);
+ }
+
+ /**
+ * Creates an EdgeGrid signing interceptor selecting a {@link ClientCredential} via
+ * {@link ClientCredentialProvider#getClientCredential(Request)} for each request.
+ *
+ * @param clientCredentialProvider a {@link ClientCredentialProvider}
+ */
+ public ApacheHttpClient5EdgeGridInterceptor(ClientCredentialProvider clientCredentialProvider) {
+ this.binding = new ApacheHttpClient5EdgeGridRequestSigner(clientCredentialProvider);
+ }
+
+ @Override
+ public void process(
+ HttpRequest request,
+ EntityDetails entityDetails,
+ HttpContext httpContext
+ ) {
+ try {
+ binding.sign(request, request);
+ } catch (RequestSigningException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/edgegrid-signer-apache-http-client5/src/main/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridRequestSigner.java b/edgegrid-signer-apache-http-client5/src/main/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridRequestSigner.java
new file mode 100644
index 0000000..1759991
--- /dev/null
+++ b/edgegrid-signer-apache-http-client5/src/main/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridRequestSigner.java
@@ -0,0 +1,94 @@
+package com.akamai.edgegrid.signer.apachehttpclient5;
+
+import com.akamai.edgegrid.signer.AbstractEdgeGridRequestSigner;
+import com.akamai.edgegrid.signer.ClientCredential;
+import com.akamai.edgegrid.signer.ClientCredentialProvider;
+import com.akamai.edgegrid.signer.Request;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntityContainer;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Apache HTTP Client5 binding for EdgeGrid signer for signing {@link HttpRequest}.
+ */
+public class ApacheHttpClient5EdgeGridRequestSigner extends AbstractEdgeGridRequestSigner {
+
+ public ApacheHttpClient5EdgeGridRequestSigner(ClientCredential clientCredential) {
+ super(clientCredential);
+ }
+
+ public ApacheHttpClient5EdgeGridRequestSigner(ClientCredentialProvider clientCredentialProvider) {
+ super(clientCredentialProvider);
+ }
+
+ @Override
+ protected URI requestUri(HttpRequest request) {
+ return getUri(request);
+ }
+
+ @Override
+ protected Request map(HttpRequest request) {
+ Request.RequestBuilder builder = Request.builder()
+ .method(request.getMethod())
+ .uri(getUri(request))
+ .body(serializeContent(request));
+ for (Header h : request.getHeaders()) {
+ builder.header(h.getName(), h.getValue());
+ }
+
+ return builder.build();
+ }
+
+ private URI getUri(HttpRequest request) {
+ try {
+ return request.getUri();
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e.getMessage(), e);
+ }
+ }
+
+ private byte[] serializeContent(HttpRequest request) {
+ if (!(request instanceof HttpEntityContainer)) {
+ return new byte[]{};
+ }
+
+ var entityWithRequest = (HttpEntityContainer) request;
+ var entity = entityWithRequest.getEntity();
+ if (entity == null) {
+ return new byte[]{};
+ }
+
+ try {
+ // Buffer non-repeatable entities
+ if (!entity.isRepeatable()) {
+ entityWithRequest.setEntity(new BufferedHttpEntity(entity));
+ }
+ return EntityUtils.toByteArray(entityWithRequest.getEntity());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ protected void setAuthorization(HttpRequest request, String signature) {
+ request.setHeader("Authorization", signature);
+ }
+
+ @Override
+ protected void setHost(HttpRequest request, String host, URI uri) {
+ request.setHeader("Host", host);
+ setRequestUri(request, uri);
+ }
+
+ private void setRequestUri(HttpRequest request, URI uri) {
+ // temporary workaround for https://issues.apache.org/jira/browse/HTTPCORE-742
+ request.setPath(uri.getPath());
+ request.setUri(uri);
+ }
+}
diff --git a/edgegrid-signer-apache-http-client5/src/main/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridRoutePlanner.java b/edgegrid-signer-apache-http-client5/src/main/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridRoutePlanner.java
new file mode 100644
index 0000000..2724125
--- /dev/null
+++ b/edgegrid-signer-apache-http-client5/src/main/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridRoutePlanner.java
@@ -0,0 +1,34 @@
+package com.akamai.edgegrid.signer.apachehttpclient5;
+
+import com.akamai.edgegrid.signer.ClientCredential;
+import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.protocol.HttpContext;
+
+import java.net.ProxySelector;
+
+public class ApacheHttpClient5EdgeGridRoutePlanner extends SystemDefaultRoutePlanner {
+
+ private final ClientCredential clientCredential;
+
+ public ApacheHttpClient5EdgeGridRoutePlanner(ClientCredential clientCredential) {
+ super(ProxySelector.getDefault());
+ this.clientCredential = clientCredential;
+ }
+
+ @Override
+ protected HttpHost determineProxy(HttpHost target, HttpContext context) {
+ var hostname = clientCredential.getHost();
+ int port = -1;
+ final int pos = hostname.lastIndexOf(":");
+ if (pos > 0) {
+ try {
+ port = Integer.parseInt(hostname.substring(pos + 1));
+ } catch (NumberFormatException ex) {
+ throw new IllegalArgumentException("Host contains invalid port number: " + hostname);
+ }
+ hostname = hostname.substring(0, pos);
+ }
+ return new HttpHost("https", hostname, port);
+ }
+}
diff --git a/edgegrid-signer-apache-http-client5/src/test/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridInterceptorIntegrationTest.java b/edgegrid-signer-apache-http-client5/src/test/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridInterceptorIntegrationTest.java
new file mode 100644
index 0000000..154bc39
--- /dev/null
+++ b/edgegrid-signer-apache-http-client5/src/test/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridInterceptorIntegrationTest.java
@@ -0,0 +1,95 @@
+package com.akamai.edgegrid.signer.apachehttpclient5;
+
+import com.akamai.edgegrid.signer.ClientCredential;
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.matching.RequestPattern;
+import com.github.tomakehurst.wiremock.verification.LoggedRequest;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+
+/**
+ * Integration tests for {@link ApacheHttpClient5EdgeGridInterceptor}.
+ */
+public class ApacheHttpClient5EdgeGridInterceptorIntegrationTest {
+
+ static final String SERVICE_MOCK_HOST = "localhost";
+
+ WireMockServer wireMockServer = new WireMockServer(wireMockConfig().dynamicHttpsPort());
+
+ ClientCredential credential;
+
+ private String getHost() {
+ return SERVICE_MOCK_HOST + ":" + wireMockServer.httpsPort();
+ }
+
+ @BeforeClass
+ public void setUp() {
+ wireMockServer.start();
+ credential = ClientCredential.builder()
+ .accessToken("akaa-dm5g2bfwoodqnc6k-ju7vlao2wz6oz2rp")
+ .clientToken("akaa-k7glklzuxkkh2ycw-oadjphopvpn6yjoj")
+ .clientSecret("SOMESECRET")
+ .host(getHost())
+ .build();
+ }
+
+ @BeforeMethod
+ public void reset() {
+ wireMockServer.resetMappings();
+ wireMockServer.resetRequests();
+ }
+
+ @AfterClass
+ public void tearDownAll() {
+ wireMockServer.stop();
+ }
+
+ @Test
+ public void testInterceptor() throws IOException {
+ wireMockServer.stubFor(get(urlPathEqualTo("/billing-usage/v1/reportSources"))
+ .withHeader("Authorization", matching(".*"))
+ .withHeader("Host", equalTo(getHost()))
+ .willReturn(aResponse()
+ .withStatus(302)
+ .withHeader("Location", "/billing-usage/v1/reportSources/alternative")));
+
+ wireMockServer.stubFor(get(urlPathEqualTo("/billing-usage/v1/reportSources/alternative"))
+ .withHeader("Authorization", matching(".*"))
+ .withHeader("Host", equalTo(getHost()))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/xml")
+ .withBody("Some content")));
+
+ var request = new HttpGet("http://endpoint.net/billing-usage/v1/reportSources");
+
+ var client = HttpClientSetup.getHttpClientWithRelaxedSsl()
+ .addRequestInterceptorFirst(new ApacheHttpClient5EdgeGridInterceptor(credential))
+ .setRoutePlanner(new ApacheHttpClient5EdgeGridRoutePlanner(credential))
+ .build();
+
+ client.execute(request, response -> null);
+
+ List loggedRequests = wireMockServer.findRequestsMatching(RequestPattern
+ .everything()).getRequests();
+
+ MatcherAssert.assertThat(loggedRequests.get(0).getHeader("Authorization"),
+ Matchers.not(CoreMatchers.equalTo(loggedRequests.get(1).getHeader("Authorization"))));
+ }
+}
diff --git a/edgegrid-signer-apache-http-client5/src/test/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridRequestSignerTest.java b/edgegrid-signer-apache-http-client5/src/test/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridRequestSignerTest.java
new file mode 100644
index 0000000..2e69e48
--- /dev/null
+++ b/edgegrid-signer-apache-http-client5/src/test/java/com/akamai/edgegrid/signer/apachehttpclient5/ApacheHttpClient5EdgeGridRequestSignerTest.java
@@ -0,0 +1,36 @@
+package com.akamai.edgegrid.signer.apachehttpclient5;
+
+import com.akamai.edgegrid.signer.ClientCredential;
+import com.akamai.edgegrid.signer.exceptions.RequestSigningException;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.testng.annotations.Test;
+
+import java.net.URISyntaxException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+/**
+ * Example of use of EdgeGrid signer with Apache HTTP Client5.
+ */
+public class ApacheHttpClient5EdgeGridRequestSignerTest {
+
+ private static final ClientCredential CREDENTIAL = ClientCredential.builder()
+ .accessToken("akaa-dm5g2bfwoodqnc6k-ju7vlao2wz6oz2rp")
+ .clientToken("akaa-k7glklzuxkkh2ycw-oadjphopvpn6yjoj")
+ .clientSecret("SOMESECRET")
+ .host("endpoint.net")
+ .build();
+
+ @Test
+ public void signEachRequest() throws URISyntaxException, RequestSigningException {
+ var request = new HttpGet("https://ignored-hostname.com/billing-usage/v1/reportSources");
+
+ var apacheHttpSinger = new ApacheHttpClient5EdgeGridRequestSigner(CREDENTIAL);
+ apacheHttpSinger.sign(request, request);
+
+ assertThat(request.getUri().getHost(), equalTo("endpoint.net"));
+ assertThat(request.getFirstHeader("Authorization"), notNullValue());
+ assertThat(request.getFirstHeader("Authorization").getValue(), not(isEmptyOrNullString()));
+ }
+}
diff --git a/edgegrid-signer-apache-http-client5/src/test/java/com/akamai/edgegrid/signer/apachehttpclient5/HttpClientSetup.java b/edgegrid-signer-apache-http-client5/src/test/java/com/akamai/edgegrid/signer/apachehttpclient5/HttpClientSetup.java
new file mode 100644
index 0000000..088392b
--- /dev/null
+++ b/edgegrid-signer-apache-http-client5/src/test/java/com/akamai/edgegrid/signer/apachehttpclient5/HttpClientSetup.java
@@ -0,0 +1,54 @@
+package com.akamai.edgegrid.signer.apachehttpclient5;
+
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+
+public class HttpClientSetup {
+
+ public static HttpClientBuilder getHttpClientWithRelaxedSsl() {
+ var sslConnectionSocketFactory = SSLConnectionSocketFactoryBuilder.create()
+ .setSslContext(trustAllCertificates())
+ .setHostnameVerifier(trustAllHosts())
+ .build();
+ var connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
+ .setSSLSocketFactory(sslConnectionSocketFactory)
+ .build();
+ return HttpClientBuilder.create()
+ .setConnectionManager(connectionManager);
+ }
+
+ private static HostnameVerifier trustAllHosts() {
+ return (s, sslSession) -> true;
+ }
+
+ private static SSLContext trustAllCertificates() {
+ // set up a TrustManager that trusts everything
+ try {
+ var sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null, new TrustManager[]{new X509TrustManager() {
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ public void checkClientTrusted(X509Certificate[] certs, String authType) {
+ }
+
+ public void checkServerTrusted(X509Certificate[] certs, String authType) {
+ }
+ }}, new SecureRandom());
+ return sslContext;
+ } catch (KeyManagementException | NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 3d879e4..bf66708 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,6 +10,7 @@
edgegrid-signer-apache-http-client
+ edgegrid-signer-apache-http-client5
edgegrid-signer-async-http-client
edgegrid-signer-core
edgegrid-signer-google-http-client
@@ -147,6 +148,16 @@
httpcore
4.4.14
+
+ org.apache.httpcomponents.core5
+ httpcore5
+ 5.2.1
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.2.1
+
org.asynchttpclient
async-http-client