From 1289e2f5e81f16affbadda1981f7ad1ed91abee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Thu, 2 Nov 2017 23:19:31 +0100 Subject: [PATCH] Accept Content-Type header with charset parameter Backport of 42541ac2 --- .../acme4j/connector/DefaultConnection.java | 7 +++-- .../shredzone/acme4j/toolbox/AcmeUtils.java | 26 ++++++++++++++++ .../acme4j/toolbox/AcmeUtilsTest.java | 30 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 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 5e1e8f9e..245893d4 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 @@ -192,7 +192,8 @@ public class DefaultConnection implements Connection { return match.getAsInt(); } - if (!"application/problem+json".equals(conn.getHeaderField(CONTENT_TYPE_HEADER))) { + String contentType = AcmeUtils.getContentType(conn.getHeaderField(CONTENT_TYPE_HEADER)); + if (!"application/problem+json".equals(contentType)) { throw new AcmeException("HTTP " + rc + ": " + conn.getResponseMessage()); } @@ -212,7 +213,7 @@ public class DefaultConnection implements Connection { public JSON readJsonResponse() throws AcmeException { assertConnectionIsOpen(); - String contentType = conn.getHeaderField(CONTENT_TYPE_HEADER); + String contentType = AcmeUtils.getContentType(conn.getHeaderField(CONTENT_TYPE_HEADER)); if (!("application/json".equals(contentType) || "application/problem+json".equals(contentType))) { throw new AcmeProtocolException("Unexpected content type: " + contentType); @@ -238,7 +239,7 @@ public class DefaultConnection implements Connection { public X509Certificate readCertificate() throws AcmeException { assertConnectionIsOpen(); - String contentType = conn.getHeaderField(CONTENT_TYPE_HEADER); + String contentType = AcmeUtils.getContentType(conn.getHeaderField(CONTENT_TYPE_HEADER)); if (!("application/pkix-cert".equals(contentType))) { throw new AcmeProtocolException("Unexpected content type: " + contentType); } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java index 6e574f32..ee8af251 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java @@ -51,6 +51,9 @@ public final class AcmeUtils { private static final Pattern TZ_PATTERN = Pattern.compile( "([+-])(\\d{2}):?(\\d{2})$"); + private final static Pattern CONTENT_TYPE_PATTERN = Pattern.compile( + "([^;]+)(?:;.*?charset=(\"?)([a-z0-9_-]+)(\\2))?.*", Pattern.CASE_INSENSITIVE); + private AcmeUtils() { // Utility class without constructor } @@ -224,4 +227,27 @@ public final class AcmeUtils { } } + /** + * Extracts the content type of a Content-Type header. + * + * @param header + * Content-Type header + * @return Content-Type, or {@code null} if the header was invalid or empty + * @throws AcmeProtocolException + * if the Content-Type header contains a different charset than "utf-8". + */ + public static String getContentType(String header) { + if (header != null) { + Matcher m = CONTENT_TYPE_PATTERN.matcher(header); + if (m.matches()) { + String charset = m.group(3); + if (charset != null && !"utf-8".equalsIgnoreCase(charset)) { + throw new AcmeProtocolException("Unsupported charset " + charset); + } + return m.group(1).trim().toLowerCase(); + } + } + return null; + } + } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java index 70c338e3..84c25df8 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java @@ -32,6 +32,7 @@ import org.hamcrest.Description; import org.jose4j.jwk.PublicJsonWebKey; import org.junit.BeforeClass; import org.junit.Test; +import org.shredzone.acme4j.exception.AcmeProtocolException; /** * Unit tests for {@link AcmeUtils}. @@ -231,6 +232,35 @@ public class AcmeUtilsTest { assertThat(stripErrorPrefix(null), is(nullValue())); } + /** + * Test {@link AcmeUtils#getContentType(String)}. + */ + @Test + public void testGetContentType() { + assertThat(AcmeUtils.getContentType(null), is(nullValue())); + assertThat(AcmeUtils.getContentType("application/json"), + is("application/json")); + assertThat(AcmeUtils.getContentType("Application/Problem+JSON"), + is("application/problem+json")); + assertThat(AcmeUtils.getContentType("application/json; charset=utf-8"), + is("application/json")); + assertThat(AcmeUtils.getContentType("application/json; charset=utf-8 (Plain text)"), + is("application/json")); + assertThat(AcmeUtils.getContentType("application/json; charset=\"utf-8\""), + is("application/json")); + assertThat(AcmeUtils.getContentType("application/json; charset=\"UTF-8\"; foo=4"), + is("application/json")); + assertThat(AcmeUtils.getContentType(" application/json ;foo=4"), + is("application/json")); + + try { + AcmeUtils.getContentType("application/json; charset=\"iso-8859-1\""); + fail("Accepted bad charset"); + } catch (AcmeProtocolException ex) { + // expected + } + } + /** * Matches the given time. */