From 41dabd0cfd3de84bbaf34a48daa906ca53df3ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Mon, 21 Dec 2015 00:10:03 +0100 Subject: [PATCH] Add updateAuthorization() method to get the current authorization state --- .../java/org/shredzone/acme4j/AcmeClient.java | 8 ++ .../org/shredzone/acme4j/Authorization.java | 30 ++++++ .../acme4j/impl/AbstractAcmeClient.java | 101 ++++++++++++------ .../shredzone/acme4j/AuthorizationTest.java | 14 +++ .../shredzone/acme4j/RegistrationTest.java | 2 +- .../acme4j/impl/AbstractAcmeClientTest.java | 50 +++++++++ .../src/test/resources/json.properties | 25 +++++ src/site/markdown/usage/authorization.md | 14 +++ 8 files changed, 213 insertions(+), 31 deletions(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java index 135d76c3..579526e0 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java @@ -59,6 +59,14 @@ public interface AcmeClient { */ void newAuthorization(Account account, Authorization auth) throws AcmeException; + /** + * Updates an {@link Authorization} to the current server state. + * + * @param auth + * {@link Authorization} to update + */ + void updateAuthorization(Authorization auth) throws AcmeException; + /** * Triggers a {@link Challenge}. The ACME server is requested to validate the * response. Note that the validation is performed asynchronously. diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java index 0ef50388..5fdb1f0b 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java @@ -14,6 +14,7 @@ package org.shredzone.acme4j; import java.io.Serializable; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -29,12 +30,41 @@ import org.shredzone.acme4j.challenge.Challenge; public class Authorization implements Serializable { private static final long serialVersionUID = -3116928998379417741L; + private URI location; private String domain; private String status; private String expires; private List challenges; private List> combinations; + /** + * Create an empty {@link Authorization}. + */ + public Authorization() { + // default constructor + } + + /** + * Create an {@link Authorization} for the given location URI. + */ + public Authorization(URI location) { + this.location = location; + } + + /** + * Gets the server URI for the authorization. + */ + public URI getLocation() { + return location; + } + + /** + * Sets the server URI for the authorization. + */ + public void setLocation(URI location) { + this.location = location; + } + /** * Gets the domain name to be authorized. */ diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java index 9b6cf648..934b2059 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java @@ -186,41 +186,33 @@ public abstract class AbstractAcmeClient implements AcmeClient { conn.throwAcmeException(); } + auth.setLocation(conn.getLocation()); + Map result = conn.readJsonResponse(); + unmarshalAuthorization(result, auth); + } + } - auth.setStatus((String) result.get("status")); + @Override + public void updateAuthorization(Authorization auth) throws AcmeException { + if (auth == null) { + throw new NullPointerException("auth must not be null"); + } + if (auth.getLocation() == null) { + throw new IllegalArgumentException("auth location must not be null. Use newAuthorization() if not known."); + } - @SuppressWarnings("unchecked") - Collection> challenges = - (Collection>) result.get("challenges"); - List cr = new ArrayList<>(); - for (Map c : challenges) { - Challenge ch = createChallenge((String) c.get("type")); - if (ch != null) { - ch.unmarshall(c); - cr.add(ch); - } + LOG.debug("updateAuthorization"); + try (Connection conn = createConnection()) { + int rc = conn.sendRequest(auth.getLocation()); + if (rc != HttpURLConnection.HTTP_OK && rc != HttpURLConnection.HTTP_ACCEPTED) { + conn.throwAcmeException(); } - auth.setChallenges(cr); - @SuppressWarnings("unchecked") - Collection> combinations = - (Collection>) result.get("combinations"); - if (combinations != null) { - List> cmb = new ArrayList<>(combinations.size()); - for (List c : combinations) { - List clist = new ArrayList<>(c.size()); - for (Number n : c) { - clist.add(cr.get(n.intValue())); - } - cmb.add(clist); - } - auth.setCombinations(cmb); - } else { - List> cmb = new ArrayList<>(1); - cmb.add(cr); - auth.setCombinations(cmb); - } + // HTTP_ACCEPTED requires Retry-After header to be set + + Map result = conn.readJsonResponse(); + unmarshalAuthorization(result, auth); } } @@ -371,4 +363,53 @@ public abstract class AbstractAcmeClient implements AcmeClient { } } + /** + * Sets {@link Authorization} properties according to the given JSON data. + * + * @param json + * JSON data + * @param auth + * {@link Authorization} to update + */ + @SuppressWarnings("unchecked") + private void unmarshalAuthorization(Map json, Authorization auth) { + auth.setStatus((String) json.get("status")); + auth.setExpires((String) json.get("expires")); + + Map identifier = (Map) json.get("identifier"); + if (identifier != null) { + auth.setDomain((String) identifier.get("value")); + } + + Collection> challenges = + (Collection>) json.get("challenges"); + List cr = new ArrayList<>(); + for (Map c : challenges) { + Challenge ch = createChallenge((String) c.get("type")); + if (ch != null) { + ch.unmarshall(c); + cr.add(ch); + } + } + auth.setChallenges(cr); + + Collection> combinations = + (Collection>) json.get("combinations"); + if (combinations != null) { + List> cmb = new ArrayList<>(combinations.size()); + for (List c : combinations) { + List clist = new ArrayList<>(c.size()); + for (Number n : c) { + clist.add(cr.get(n.intValue())); + } + cmb.add(clist); + } + auth.setCombinations(cmb); + } else { + List> cmb = new ArrayList<>(1); + cmb.add(cr); + auth.setCombinations(cmb); + } + } + } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java index 77146881..6c8d8e0b 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java @@ -16,6 +16,8 @@ package org.shredzone.acme4j; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -154,6 +156,18 @@ public class AuthorizationTest { assertThat(c7, is(nullValue())); } + /** + * Test constructors. + */ + @Test + public void testConstructor() throws URISyntaxException { + Authorization auth1 = new Authorization(); + assertThat(auth1.getLocation(), is(nullValue())); + + Authorization auth2 = new Authorization(new URI("http://example.com/acme/12345")); + assertThat(auth2.getLocation(), is(new URI("http://example.com/acme/12345"))); + } + /** * Sets up a {@link Challenge}. * diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java index 4910b8bb..6881294c 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java @@ -53,7 +53,7 @@ public class RegistrationTest { } /** - * Test constructors; + * Test constructors. */ @Test public void testConstructor() throws URISyntaxException { diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java index 732dc850..c215f6e6 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java @@ -168,6 +168,11 @@ public class AbstractAcmeClientTest { public Map readJsonResponse() throws AcmeException { return getJsonAsMap("newAuthorizationResponse"); } + + @Override + public URI getLocation() throws AcmeException { + return locationUri; + } }; HttpChallenge httpChallenge = new HttpChallenge(); @@ -183,6 +188,51 @@ public class AbstractAcmeClientTest { assertThat(auth.getDomain(), is("example.org")); assertThat(auth.getStatus(), is("pending")); assertThat(auth.getExpires(), is(nullValue())); + assertThat(auth.getLocation(), is(locationUri)); + + assertThat(auth.getChallenges(), containsInAnyOrder( + (Challenge) httpChallenge, (Challenge) dnsChallenge)); + + assertThat(auth.getCombinations(), hasSize(2)); + assertThat(auth.getCombinations().get(0), containsInAnyOrder( + (Challenge) httpChallenge)); + assertThat(auth.getCombinations().get(1), containsInAnyOrder( + (Challenge) httpChallenge, (Challenge) dnsChallenge)); + } + + /** + * Test that {@link Authorization} are properly updated. + */ + @Test + public void testUpdateAuthorization() throws AcmeException { + Authorization auth = new Authorization(locationUri); + + Connection connection = new DummyConnection() { + @Override + public int sendRequest(URI uri) throws AcmeException { + assertThat(uri, is(locationUri)); + return HttpURLConnection.HTTP_OK; + } + + @Override + public Map readJsonResponse() throws AcmeException { + return getJsonAsMap("updateAuthorizationResponse"); + } + }; + + HttpChallenge httpChallenge = new HttpChallenge(); + DnsChallenge dnsChallenge = new DnsChallenge(); + + TestableAbstractAcmeClient client = new TestableAbstractAcmeClient(connection); + client.putTestChallenge("http-01", httpChallenge); + client.putTestChallenge("dns-01", dnsChallenge); + + client.updateAuthorization(auth); + + assertThat(auth.getDomain(), is("example.org")); + assertThat(auth.getStatus(), is("valid")); + assertThat(auth.getExpires(), is("2015-03-01")); + assertThat(auth.getLocation(), is(locationUri)); assertThat(auth.getChallenges(), containsInAnyOrder( (Challenge) httpChallenge, (Challenge) dnsChallenge)); diff --git a/acme4j-client/src/test/resources/json.properties b/acme4j-client/src/test/resources/json.properties index 07be989a..72273b3a 100644 --- a/acme4j-client/src/test/resources/json.properties +++ b/acme4j-client/src/test/resources/json.properties @@ -49,6 +49,31 @@ newAuthorizationResponse = \ "combinations": [[0], [0,1]]\ } +updateAuthorizationResponse = \ + {\ + "status": "valid",\ + "expires": "2015-03-01",\ + "identifier": {\ + "type": "dns",\ + "value": "example.org"\ + },\ + "challenges": [\ + {\ + "type": "http-01",\ + "status":"pending",\ + "uri": "https://example.com/authz/asdf/0",\ + "token": "IlirfxKKXAsHtmzK29Pj8A"\ + },\ + {\ + "type": "dns-01",\ + "status":"pending",\ + "uri": "https://example.com/authz/asdf/1",\ + "token": "DGyRejmCefe7v4NfDGDKfA"\ + }\ + ],\ + "combinations": [[0], [0,1]]\ + } + triggerHttpChallenge = \ {\ "type": "http-01",\ diff --git a/src/site/markdown/usage/authorization.md b/src/site/markdown/usage/authorization.md index 6c50311d..29328a4a 100644 --- a/src/site/markdown/usage/authorization.md +++ b/src/site/markdown/usage/authorization.md @@ -63,6 +63,20 @@ If your final certificate contains further domains or subdomains, repeat the aut Note that wildcard certificates are not currently supported. +## Update an Authorization + +For each authorization, the server provides an URI where the status of the authorization can be queried. It can be retrieved from `Authorization.getLocation()` after `newAuthorization()` returned. + +To get a status overview of your authorization and all challenges, create a new `Authorization` object and pass the location URI to the constructor: + +```java +URI authUri = ... // Authorization URI +Authorization auth = new Authorization(authUri); +client.updateAuthorization(auth); +``` + +After that call, the `Authorization` object contains the current server state about your authorization, including the domain name, the overall status, and an expiry date. + ## Restore a Challenge Validating a challenge can take a considerable amount of time and is a candidate for asynchronous execution. This can be a problem if you need to keep the `Challenge` object for a later time or a different Java environment.