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 e88c760e..cee5ad29 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java @@ -80,6 +80,19 @@ public interface AcmeClient { */ void updateChallenge(Account account, Challenge challenge) throws AcmeException; + /** + * Restore a {@link Challenge} instance if only the challenge URI is known. It + * contains the current state. + * + * @param account + * {@link Account} to be used for conversation + * @param challengeUri + * {@link URI} of the challenge to restore + * @throws ClassCastException + * if the challenge does not match the desired type + */ + T restoreChallenge(Account account, URI challengeUri) throws AcmeException; + /** * Request a certificate. * diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java index 2c518bcf..15151bc2 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java @@ -39,9 +39,9 @@ public interface Challenge { String getType(); /** - * Returns the {@link URI} of the challenge. + * Returns the location {@link URI} of the challenge. */ - URI getUri(); + URI getLocation(); /** * Returns the current status of the challenge. diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericChallenge.java index 968e462a..3121fcca 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericChallenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericChallenge.java @@ -58,7 +58,7 @@ public class GenericChallenge implements Challenge { } @Override - public URI getUri() { + public URI getLocation() { String uri = get(KEY_URI); if (uri == null) { return null; 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 675510db..26f939c4 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 @@ -202,7 +202,7 @@ public abstract class AbstractAcmeClient implements AcmeClient { claims.putResource("challenge"); challenge.marshall(claims); - int rc = conn.sendSignedRequest(challenge.getUri(), claims, session, account); + int rc = conn.sendSignedRequest(challenge.getLocation(), claims, session, account); if (rc != HttpURLConnection.HTTP_OK && rc != HttpURLConnection.HTTP_ACCEPTED) { conn.throwAcmeException(); } @@ -215,7 +215,7 @@ public abstract class AbstractAcmeClient implements AcmeClient { public void updateChallenge(Account account, Challenge challenge) throws AcmeException { LOG.debug("updateChallenge"); try (Connection conn = createConnection()) { - int rc = conn.sendRequest(challenge.getUri()); + int rc = conn.sendRequest(challenge.getLocation()); if (rc != HttpURLConnection.HTTP_ACCEPTED) { conn.throwAcmeException(); } @@ -224,6 +224,27 @@ public abstract class AbstractAcmeClient implements AcmeClient { } } + @Override + @SuppressWarnings("unchecked") + public T restoreChallenge(Account account, URI challengeUri) throws AcmeException { + LOG.debug("restoreChallenge"); + try (Connection conn = createConnection()) { + int rc = conn.sendRequest(challengeUri); + if (rc != HttpURLConnection.HTTP_ACCEPTED) { + conn.throwAcmeException(); + } + + Map json = conn.readJsonResponse(); + if (!(json.containsKey("type"))) { + throw new AcmeException("Provided URI is not a challenge URI"); + } + + T challenge = (T) createChallenge(json.get("type").toString()); + challenge.unmarshall(json); + return challenge; + } + } + @Override public URI requestCertificate(Account account, byte[] csr) throws AcmeException { LOG.debug("requestCertificate"); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/GenericChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/GenericChallengeTest.java index 09a23389..61f55ced 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/GenericChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/GenericChallengeTest.java @@ -49,7 +49,7 @@ public class GenericChallengeTest { // Test default values assertThat(challenge.getType(), is(nullValue())); assertThat(challenge.getStatus(), is(Status.PENDING)); - assertThat(challenge.getUri(), is(nullValue())); + assertThat(challenge.getLocation(), is(nullValue())); assertThat(challenge.getValidated(), is(nullValue())); // Unmarshall a challenge JSON @@ -58,7 +58,7 @@ public class GenericChallengeTest { // Test unmarshalled values assertThat(challenge.getType(), is("generic-01")); assertThat(challenge.getStatus(), is(Status.VALID)); - assertThat(challenge.getUri(), is(new URI("http://example.com/challenge/123"))); + assertThat(challenge.getLocation(), is(new URI("http://example.com/challenge/123"))); assertThat(challenge.getValidated(), is("2015-12-12T17:19:36.336785823Z")); } 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 a42ad234..e36068a2 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 @@ -224,7 +224,7 @@ public class AbstractAcmeClientTest { client.triggerChallenge(testAccount, challenge); assertThat(challenge.getStatus(), is(Status.PENDING)); - assertThat(challenge.getUri(), is(locationUri)); + assertThat(challenge.getLocation(), is(locationUri)); } /** @@ -253,7 +253,31 @@ public class AbstractAcmeClientTest { client.updateChallenge(testAccount, challenge); assertThat(challenge.getStatus(), is(Status.VALID)); - assertThat(challenge.getUri(), is(locationUri)); + assertThat(challenge.getLocation(), is(locationUri)); + } + + @Test + public void testRestoreChallenge() throws AcmeException { + Connection connection = new DummyConnection() { + @Override + public int sendRequest(URI uri) throws AcmeException { + assertThat(uri, is(locationUri)); + return HttpURLConnection.HTTP_ACCEPTED; + } + + @Override + public Map readJsonResponse() throws AcmeException { + return getJsonAsMap("updateHttpChallengeResponse"); + } + }; + + TestableAbstractAcmeClient client = new TestableAbstractAcmeClient(connection); + client.putTestChallenge(HttpChallenge.TYPE, new HttpChallenge()); + + Challenge challenge = client.restoreChallenge(testAccount, locationUri); + + assertThat(challenge.getStatus(), is(Status.VALID)); + assertThat(challenge.getLocation(), is(locationUri)); } /** diff --git a/src/site/markdown/usage/authorization.md b/src/site/markdown/usage/authorization.md index ad0553f2..6c50311d 100644 --- a/src/site/markdown/usage/authorization.md +++ b/src/site/markdown/usage/authorization.md @@ -62,3 +62,23 @@ As soon as the challenge is `VALID`, you have successfully associated the domain If your final certificate contains further domains or subdomains, repeat the authorization run with each of them. Note that wildcard certificates are not currently supported. + +## 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. + +To recreate a `Challenge` object at a later time, all you need is to store the original object's `location` property: + +```java +Challenge originalChallenge = ... // some Challenge instance +URI challengeUri = originalChallenge.getLocation(); +``` + +Later, you pass this `challengeUri` to `recreateChallenge()`: + +```java +URI challengeUri = ... // challenge URI +Challenge restoredChallenge = client.restoreChallenge(account, challengeUri); +``` + +The `restoredChallenge` already reflects the current state of the challenge.