From b0287d4d942c1a8fd36b6342d8da0e428ee82d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Mon, 24 Apr 2023 21:23:58 +0200 Subject: [PATCH] Accept gzip compression --- .../acme4j/connector/DefaultConnection.java | 48 ++++++++++++------- .../acme4j/connector/NetworkSettings.java | 20 ++++++++ .../acme4j/connector/NetworkSettingsTest.java | 4 ++ src/doc/docs/migration.md | 1 + src/doc/docs/usage/session.md | 1 + 5 files changed, 58 insertions(+), 16 deletions(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java index fb0f4ba1..96f8bae2 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java @@ -41,6 +41,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; import edu.umd.cs.findbugs.annotations.Nullable; import org.shredzone.acme4j.Login; @@ -75,6 +76,7 @@ public class DefaultConnection implements Connection { private static final String ACCEPT_HEADER = "Accept"; private static final String ACCEPT_CHARSET_HEADER = "Accept-Charset"; private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language"; + private static final String ACCEPT_ENCODING_HEADER = "Accept-Encoding"; private static final String CACHE_CONTROL_HEADER = "Cache-Control"; private static final String CONTENT_TYPE_HEADER = "Content-Type"; private static final String DATE_HEADER = "Date"; @@ -208,11 +210,7 @@ public class DefaultConnection implements Connection { public JSON readJsonResponse() throws AcmeException { expectContentType(Set.of(MIME_JSON, MIME_JSON_PROBLEM)); - try (var in = getResponse().body()) { - if (in == null) { - throw new AcmeProtocolException("JSON response is empty"); - } - + try (var in = getResponseBody()) { var result = JSON.parse(in); LOG.debug("Result JSON: {}", result); return result; @@ -225,17 +223,11 @@ public class DefaultConnection implements Connection { public List readCertificates() throws AcmeException { expectContentType(Set.of(MIME_CERTIFICATE_CHAIN)); - try (var in = getResponse().body()) { - if (in == null) { - throw new AcmeProtocolException("Certificate response is empty"); - } - - try (var ins = new TrimmingInputStream(in)) { - var cf = CertificateFactory.getInstance("X.509"); - return cf.generateCertificates(ins).stream() - .map(X509Certificate.class::cast) - .collect(toUnmodifiableList()); - } + try (var in = new TrimmingInputStream(getResponseBody())) { + var cf = CertificateFactory.getInstance("X.509"); + return cf.generateCertificates(in).stream() + .map(X509Certificate.class::cast) + .collect(toUnmodifiableList()); } catch (IOException ex) { throw new AcmeNetworkException(ex); } catch (CertificateException ex) { @@ -356,6 +348,11 @@ public class DefaultConnection implements Connection { var builder = httpConnector.createRequestBuilder(url) .header(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET) .header(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag()); + + if (session.networkSettings().isCompressionEnabled()) { + builder.header(ACCEPT_ENCODING_HEADER, "gzip"); + } + body.accept(builder); lastResponse = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofInputStream()); @@ -503,6 +500,25 @@ public class DefaultConnection implements Connection { } } + /** + * Provides an {@link InputStream} of the response body. If the stream is compressed, + * it will also take care for decompression. + */ + private InputStream getResponseBody() throws IOException { + var stream = getResponse().body(); + if (stream == null) { + throw new AcmeProtocolException("Unexpected empty response"); + } + + if (getResponse().headers().firstValue("Content-Encoding") + .filter("gzip"::equalsIgnoreCase) + .isPresent()) { + stream = new GZIPInputStream(stream); + } + + return stream; + } + /** * Throws an {@link AcmeException}. This method throws an exception that tries to * explain the error as precisely as possible. diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/NetworkSettings.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/NetworkSettings.java index 2c9d7d64..cee10a5c 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/NetworkSettings.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/NetworkSettings.java @@ -30,6 +30,7 @@ public class NetworkSettings { private ProxySelector proxySelector = HttpClient.Builder.NO_PROXY; private Duration timeout = Duration.ofSeconds(10); private @Nullable Authenticator authenticator = null; + private boolean compression = true; /** * Gets the {@link ProxySelector} to be used for connections. @@ -91,4 +92,23 @@ public class NetworkSettings { this.timeout = timeout; } + /** + * Checks if HTTP compression is enabled. + * + * @since 3.0.0 + */ + public boolean isCompressionEnabled() { + return compression; + } + + /** + * Sets if HTTP compression is enabled. It is enabled by default, but can be + * disabled e.g. for debugging purposes. + * + * @since 3.0.0 + */ + public void setCompressionEnabled(boolean compression) { + this.compression = compression; + } + } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/NetworkSettingsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/NetworkSettingsTest.java index 6988b566..5603fd07 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/NetworkSettingsTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/NetworkSettingsTest.java @@ -55,6 +55,10 @@ public class NetworkSettingsTest { assertThat(settings.getAuthenticator()).isNull(); settings.setAuthenticator(defaultAuthenticator); assertThat(settings.getAuthenticator()).isSameAs(defaultAuthenticator); + + assertThat(settings.isCompressionEnabled()).isTrue(); + settings.setCompressionEnabled(false); + assertThat(settings.isCompressionEnabled()).isFalse(); } @Test diff --git a/src/doc/docs/migration.md b/src/doc/docs/migration.md index 51aeedad..5e524f6d 100644 --- a/src/doc/docs/migration.md +++ b/src/doc/docs/migration.md @@ -6,6 +6,7 @@ This document will help you migrate your code to the latest _acme4j_ version. - Starting with _acme4j_ v3, we will require the smallest Java SE LTS version that is still receiving premier support according to the [Oracle Java SE Support Roadmap](https://www.oracle.com/java/technologies/java-se-support-roadmap.html). At the moment of writing, these are Java 11 and Java 17, so _acme4j_ requires Java 11 starting from now. With the prospected release of Java 21 (LTS) in September 2023, we will start to require Java 17, and so on. If you still need Java 8, you can use _acme4j_ v2, which will receive bugfixes until September 2023. - Changed to `java.net.http` client. Due to limitations of the API, HTTP errors are only thrown with the error code, but not with the error message. If you checked the message in unit tests, be prepared that the error message might have changed. +- acme4j now accepts HTTP gzip compression. It is enabled by default, but can be disabled in the `NetworkSettings` if it causes problems or impedes debugging. - All deprecated methods have been removed. ## Migration to Version 2.16 diff --git a/src/doc/docs/usage/session.md b/src/doc/docs/usage/session.md index 0ed07c0a..d52c73d5 100644 --- a/src/doc/docs/usage/session.md +++ b/src/doc/docs/usage/session.md @@ -48,3 +48,4 @@ You can use `Session.networkSettings()` to change some network parameters for th * If a proxy must be used for internet connections, you can set a `ProxySelector` instance via `setProxySelector()`. * To change network timeouts, use `setTimeout()`. The default timeout is 10 seconds. You can either increase the timeout on poor network connections, or reduce it to fail early on network errors. * If you need authentication (e.g. for the proxy), you can set an `Authenticator` via `setAuthenticator()`. Be careful here! Most code snippets I have found on the internet will send out the proxy credentials to anyone who is asking. You should check `Authenticator.getRequestorType()` and make sure it is `RequestorType.PROXY` before sending the proxy credentials. +* _acme4j_ accepts HTTP `gzip` compression by default. If it should impede debugging, it can be disabled via `setCompressionEnabled(false)`.