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 26d755f2..9cc19a04 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 @@ -232,7 +232,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()); } @@ -249,7 +250,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); @@ -275,7 +276,7 @@ public class DefaultConnection implements Connection { public List readCertificates() throws AcmeException { assertConnectionIsOpen(); - String contentType = conn.getHeaderField(CONTENT_TYPE_HEADER); + String contentType = AcmeUtils.getContentType(conn.getHeaderField(CONTENT_TYPE_HEADER)); if (!("application/pem-certificate-chain".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 a7d651a6..2fd75083 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 @@ -58,6 +58,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 static final Base64.Encoder PEM_ENCODER = Base64.getMimeEncoder(64, "\n".getBytes()); /** @@ -326,4 +329,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 4ac316d1..50cb6146 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 @@ -43,6 +43,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}. @@ -299,6 +300,35 @@ public class AcmeUtilsTest { assertThat(AcmeUtils.toURL(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. */