diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java index 0e04cb02..c2df9984 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java @@ -13,9 +13,16 @@ */ package org.shredzone.acme4j; +import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.toList; + import java.io.Serializable; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; +import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.toolbox.JSON; /** @@ -26,7 +33,7 @@ import org.shredzone.acme4j.toolbox.JSON; public class Problem implements Serializable { private static final long serialVersionUID = -8418248862966754214L; - private final URI baseUri; + private final URL baseUrl; private final JSON problemJson; /** @@ -34,26 +41,34 @@ public class Problem implements Serializable { * * @param problem * Problem as JSON structure - * @param baseUri - * Document's base {@link URI} to resolve relative URIs against + * @param baseUrl + * Document's base {@link URL} to resolve relative URIs against */ - public Problem(JSON problem, URI baseUri) { + public Problem(JSON problem, URL baseUrl) { this.problemJson = problem; - this.baseUri = baseUri; + this.baseUrl = baseUrl; } /** * Returns the problem type. It is always an absolute URI. */ public URI getType() { - String type = problemJson.get("type").asString(); - return type != null ? baseUri.resolve(type) : null; + try { + String type = problemJson.get("type").asString(); + return type != null ? baseUrl.toURI().resolve(type) : null; + } catch (URISyntaxException ex) { + throw new IllegalArgumentException("Bad base URL", ex); + } } /** * Returns a human-readable description of the problem. */ public String getDetail() { + String value = problemJson.get("value").asString(); + if (value != null) { + return value; + } return problemJson.get("detail").asString(); } @@ -62,8 +77,41 @@ public class Problem implements Serializable { * an absolute URI. */ public URI getInstance() { - String instance = problemJson.get("instance").asString(); - return instance != null ? baseUri.resolve(instance) : null; + try { + String instance = problemJson.get("instance").asString(); + return instance != null ? baseUrl.toURI().resolve(instance) : null; + } catch (URISyntaxException ex) { + throw new IllegalArgumentException("Bad base URL", ex); + } + } + + /** + * Returns the domain this problem relates to. May be {@code null}. + */ + public String getDomain() { + JSON identifier = problemJson.get("identifier").asObject(); + if (identifier == null) { + return null; + } + + String type = identifier.get("type").asString(); + if (!"dns".equals(type)) { + throw new AcmeProtocolException("Cannot process a " + type + " identifier"); + } + + return identifier.get("value").asString(); + } + + /** + * Returns a list of sub-problems. May be empty, but is never {@code null}. + */ + public List getSubProblems() { + return unmodifiableList( + problemJson.get("sub-problems") + .asArray().stream() + .map(o -> o.asProblem(baseUrl)) + .collect(toList()) + ); } /** 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 0ce99c5c..5978a61c 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 @@ -238,10 +238,8 @@ public class DefaultConnection implements Connection { throw new AcmeException("HTTP " + rc + ": " + conn.getResponseMessage()); } - Problem problem = new Problem(readJsonResponse(), conn.getURL().toURI()); + Problem problem = new Problem(readJsonResponse(), conn.getURL()); throw createAcmeException(problem); - } catch (URISyntaxException ex) { - throw new AcmeProtocolException("Bad request URL: " + conn.getURL(), ex); } catch (IOException ex) { throw new AcmeNetworkException(ex); } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java index a495e9b7..e6b8bbae 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java @@ -315,11 +315,7 @@ public final class JSON implements Serializable { return null; } - try { - return new Problem(asObject(), baseUrl.toURI()); - } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Bad base URL", ex); - } + return new Problem(asObject(), baseUrl); } /** diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java index 32d59d1e..796c6c8a 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java @@ -13,11 +13,15 @@ */ package org.shredzone.acme4j; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; +import static org.shredzone.acme4j.toolbox.TestUtils.url; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import java.net.URI; +import java.net.URL; +import java.util.List; import org.junit.Test; import org.shredzone.acme4j.toolbox.JSON; @@ -30,16 +34,31 @@ public class ProblemTest { @Test public void testProblem() { - URI baseUri = URI.create("https://example.com/acme/1"); + URL baseUrl = url("https://example.com/acme/1"); JSON original = TestUtils.getJSON("problem"); - Problem problem = new Problem(original, baseUri); + Problem problem = new Problem(original, baseUrl); - assertThat(problem.getType(), is(URI.create("urn:ietf:params:acme:error:connection"))); - assertThat(problem.getDetail(), is("connection refused")); + assertThat(problem.getType(), is(URI.create("urn:ietf:params:acme:error:malformed"))); + assertThat(problem.getDetail(), is("Some of the identifiers requested were rejected")); assertThat(problem.getInstance(), is(URI.create("https://example.com/documents/error.html"))); + assertThat(problem.getDomain(), is(nullValue())); assertThat(problem.asJSON().toString(), is(sameJSONAs(original.toString()))); assertThat(problem.toString(), is(sameJSONAs(original.toString()))); + + List subs = problem.getSubProblems(); + assertThat(subs, not(nullValue())); + assertThat(subs, hasSize(2)); + + Problem p1 = subs.get(0); + assertThat(p1.getType(), is(URI.create("urn:ietf:params:acme:error:malformed"))); + assertThat(p1.getDetail(), is("Invalid underscore in DNS name \"_example.com\"")); + assertThat(p1.getDomain(), is("_example.com")); + + Problem p2 = subs.get(1); + assertThat(p2.getType(), is(URI.create("urn:ietf:params:acme:error:rejectedIdentifier"))); + assertThat(p2.getDetail(), is("This CA will not issue for \"example.net\"")); + assertThat(p2.getDomain(), is("example.net")); } } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java index d8158d78..128e6dd8 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java @@ -302,8 +302,6 @@ public final class TestUtils { * @return Created {@link Problem} object */ public static Problem createProblem(URI type, String detail, URL instance) { - URI baseUri = URI.create("https://example.com/acme/1"); - JSONBuilder jb = new JSONBuilder(); jb.put("type", type); jb.put("detail", detail); @@ -311,7 +309,7 @@ public final class TestUtils { jb.put("instance", instance); } - return new Problem(jb.toJSON(), baseUri); + return new Problem(jb.toJSON(), url("https://example.com/acme/1")); } /** diff --git a/acme4j-client/src/test/resources/json/problem.json b/acme4j-client/src/test/resources/json/problem.json index f43a53fd..4fbd4fe6 100644 --- a/acme4j-client/src/test/resources/json/problem.json +++ b/acme4j-client/src/test/resources/json/problem.json @@ -1,5 +1,23 @@ { - "type": "urn:ietf:params:acme:error:connection", - "detail": "connection refused", - "instance": "/documents/error.html" + "type": "urn:ietf:params:acme:error:malformed", + "detail": "Some of the identifiers requested were rejected", + "instance": "/documents/error.html", + "sub-problems": [ + { + "type": "urn:ietf:params:acme:error:malformed", + "value": "Invalid underscore in DNS name \"_example.com\"", + "identifier": { + "type": "dns", + "value": "_example.com" + } + }, + { + "type": "urn:ietf:params:acme:error:rejectedIdentifier", + "value": "This CA will not issue for \"example.net\"", + "identifier": { + "type": "dns", + "value": "example.net" + } + } + ] }