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