mirror of https://github.com/shred/acme4j
Evaluate retry-after header
parent
cef5984f81
commit
5049cd5ffd
|
@ -28,6 +28,7 @@ import org.shredzone.acme4j.connector.Connection;
|
|||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeNetworkException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
import org.shredzone.acme4j.util.TimestampParser;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -162,6 +163,13 @@ public class Authorization extends AcmeResource {
|
|||
/**
|
||||
* Updates the {@link Authorization}. After invocation, the {@link Authorization}
|
||||
* reflects the current state at the ACME server.
|
||||
*
|
||||
* @throws AcmeRetryAfterException
|
||||
* the auhtorization is still being validated, and the server returned an
|
||||
* estimated date when the validation will be completed. If you are
|
||||
* polling for the authorization to complete, you should wait for the date
|
||||
* given in {@link AcmeRetryAfterException#getRetryAfter()}. Note that the
|
||||
* authorization status is updated even if this exception was thrown.
|
||||
*/
|
||||
public void update() throws AcmeException {
|
||||
LOG.debug("update");
|
||||
|
@ -171,10 +179,15 @@ public class Authorization extends AcmeResource {
|
|||
conn.throwAcmeException();
|
||||
}
|
||||
|
||||
// HTTP_ACCEPTED requires Retry-After header to be set
|
||||
|
||||
Map<String, Object> result = conn.readJsonResponse();
|
||||
unmarshalAuthorization(result);
|
||||
|
||||
if (rc == HttpURLConnection.HTTP_ACCEPTED) {
|
||||
Date retryAfter = conn.getRetryAfterHeader();
|
||||
throw new AcmeRetryAfterException(
|
||||
"authorization is not completed yet",
|
||||
retryAfter);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new AcmeNetworkException(ex);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.net.URI;
|
|||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
|
@ -26,6 +27,7 @@ import org.shredzone.acme4j.connector.Resource;
|
|||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeNetworkException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -80,20 +82,29 @@ public class Certificate extends AcmeResource {
|
|||
* Downloads the certificate. The result is cached.
|
||||
*
|
||||
* @return {@link X509Certificate} that was downloaded
|
||||
* @throws AcmeRetryAfterException
|
||||
* the certificate is still being created, and the server returned an
|
||||
* estimated date when it will be ready for download. You should wait for
|
||||
* the date given in {@link AcmeRetryAfterException#getRetryAfter()}
|
||||
* before trying again.
|
||||
*/
|
||||
public X509Certificate download() throws AcmeException {
|
||||
if (cert == null) {
|
||||
LOG.debug("download");
|
||||
try (Connection conn = getSession().provider().connect()) {
|
||||
int rc = conn.sendRequest(getLocation());
|
||||
if (rc == HttpURLConnection.HTTP_ACCEPTED) {
|
||||
Date retryAfter = conn.getRetryAfterHeader();
|
||||
throw new AcmeRetryAfterException(
|
||||
"certificate is not available for download yet",
|
||||
retryAfter);
|
||||
}
|
||||
|
||||
if (rc != HttpURLConnection.HTTP_OK) {
|
||||
conn.throwAcmeException();
|
||||
}
|
||||
|
||||
// TODO: HTTP_ACCEPTED plus Retry-After header if not yet available
|
||||
|
||||
chainCertUri = conn.getLink("up");
|
||||
|
||||
cert = conn.readCertificate();
|
||||
} catch (IOException ex) {
|
||||
throw new AcmeNetworkException(ex);
|
||||
|
@ -106,6 +117,11 @@ public class Certificate extends AcmeResource {
|
|||
* Downloads the certificate chain. The result is cached.
|
||||
*
|
||||
* @return Chain of {@link X509Certificate}s
|
||||
* @throws AcmeRetryAfterException
|
||||
* the certificate is still being created, and the server returned an
|
||||
* estimated date when it will be ready for download. You should wait for
|
||||
* the date given in {@link AcmeRetryAfterException#getRetryAfter()}
|
||||
* before trying again.
|
||||
*/
|
||||
public X509Certificate[] downloadChain() throws AcmeException {
|
||||
if (chain == null) {
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.shredzone.acme4j.connector.Connection;
|
|||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeNetworkException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
import org.shredzone.acme4j.util.TimestampParser;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -228,16 +229,31 @@ public class Challenge extends AcmeResource {
|
|||
|
||||
/**
|
||||
* Updates the state of this challenge.
|
||||
*
|
||||
* @throws AcmeRetryAfterException
|
||||
* the challenge is still being validated, and the server returned an
|
||||
* estimated date when the challenge will be completed. If you are polling
|
||||
* for the challenge to complete, you should wait for the date given in
|
||||
* {@link AcmeRetryAfterException#getRetryAfter()}. Note that the
|
||||
* challenge status is updated even if this exception was thrown.
|
||||
*/
|
||||
public void update() throws AcmeException {
|
||||
LOG.debug("update");
|
||||
try (Connection conn = getSession().provider().connect()) {
|
||||
int rc = conn.sendRequest(getLocation());
|
||||
if (rc != HttpURLConnection.HTTP_ACCEPTED) {
|
||||
if (rc != HttpURLConnection.HTTP_OK && rc != HttpURLConnection.HTTP_ACCEPTED) {
|
||||
conn.throwAcmeException();
|
||||
}
|
||||
|
||||
unmarshall(conn.readJsonResponse());
|
||||
|
||||
if (rc == HttpURLConnection.HTTP_ACCEPTED) {
|
||||
Date retryAfter = conn.getRetryAfterHeader();
|
||||
if (retryAfter != null) {
|
||||
throw new AcmeRetryAfterException("challenge is not completed yet",
|
||||
retryAfter);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new AcmeNetworkException(ex);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2016 Richard "Shred" Körber
|
||||
* http://acme4j.shredzone.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
package org.shredzone.acme4j.exception;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* This exception is thrown when a server side process has not been completed yet, and the
|
||||
* server returned an estimated retry date.
|
||||
*
|
||||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
public class AcmeRetryAfterException extends AcmeException {
|
||||
private static final long serialVersionUID = 4461979121063649905L;
|
||||
|
||||
private final Date retryAfter;
|
||||
|
||||
public AcmeRetryAfterException(String msg, Date retryAfter) {
|
||||
super(msg);
|
||||
this.retryAfter = retryAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the retry-after date returned by the server.
|
||||
*/
|
||||
public Date getRetryAfter() {
|
||||
return (retryAfter != null ? new Date(retryAfter.getTime()) : null);
|
||||
}
|
||||
|
||||
}
|
|
@ -14,13 +14,14 @@
|
|||
package org.shredzone.acme4j;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.shredzone.acme4j.util.TestUtils.getJsonAsMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
@ -28,6 +29,7 @@ import org.shredzone.acme4j.challenge.Challenge;
|
|||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
||||
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
import org.shredzone.acme4j.util.TimestampParser;
|
||||
|
@ -156,6 +158,64 @@ public class AuthorizationTest {
|
|||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that authorization is properly updated, with retry-after header set.
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateRetryAfter() throws Exception {
|
||||
final long retryAfter = System.currentTimeMillis() + 30 * 1000L;
|
||||
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
public int sendRequest(URI uri) {
|
||||
assertThat(uri, is(locationUri));
|
||||
return HttpURLConnection.HTTP_ACCEPTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> readJsonResponse() {
|
||||
return getJsonAsMap("updateAuthorizationResponse");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getRetryAfterHeader() {
|
||||
return new Date(retryAfter);
|
||||
}
|
||||
};
|
||||
|
||||
Session session = provider.createSession();
|
||||
|
||||
Http01Challenge httpChallenge = new Http01Challenge(session);
|
||||
Dns01Challenge dnsChallenge = new Dns01Challenge(session);
|
||||
provider.putTestChallenge("http-01", httpChallenge);
|
||||
provider.putTestChallenge("dns-01", dnsChallenge);
|
||||
|
||||
Authorization auth = new Authorization(session, locationUri);
|
||||
|
||||
try {
|
||||
auth.update();
|
||||
fail("Expected AcmeRetryAfterException");
|
||||
} catch (AcmeRetryAfterException ex) {
|
||||
assertThat(ex.getRetryAfter(), is(new Date(retryAfter)));
|
||||
}
|
||||
|
||||
assertThat(auth.getDomain(), is("example.org"));
|
||||
assertThat(auth.getStatus(), is(Status.VALID));
|
||||
assertThat(auth.getExpires(), is(TimestampParser.parse("2016-01-02T17:12:40Z")));
|
||||
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));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an authorization can be deactivated.
|
||||
*/
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
package org.shredzone.acme4j;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.shredzone.acme4j.util.TestUtils.getJson;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
||||
|
@ -22,10 +22,12 @@ import java.io.IOException;
|
|||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.connector.Resource;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
import org.shredzone.acme4j.util.TestUtils;
|
||||
|
@ -84,6 +86,38 @@ public class CertificateTest {
|
|||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a {@link AcmeRetryAfterException} is thrown.
|
||||
*/
|
||||
@Test
|
||||
public void testRetryAfter() throws AcmeException, IOException {
|
||||
final long retryAfter = System.currentTimeMillis() + 30 * 1000L;
|
||||
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
public int sendRequest(URI uri) {
|
||||
assertThat(uri, is(locationUri));
|
||||
return HttpURLConnection.HTTP_ACCEPTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getRetryAfterHeader() {
|
||||
return new Date(retryAfter);
|
||||
}
|
||||
};
|
||||
|
||||
Certificate cert = new Certificate(provider.createSession(), locationUri);
|
||||
|
||||
try {
|
||||
cert.download();
|
||||
fail("Expected AcmeRetryAfterException");
|
||||
} catch (AcmeRetryAfterException ex) {
|
||||
assertThat(ex.getRetryAfter(), is(new Date(retryAfter)));
|
||||
}
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a certificate can be revoked.
|
||||
*/
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
package org.shredzone.acme4j.challenge;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.shredzone.acme4j.util.TestUtils.*;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
||||
|
@ -27,6 +27,7 @@ import java.net.HttpURLConnection;
|
|||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jose4j.base64url.Base64Url;
|
||||
|
@ -39,6 +40,7 @@ import org.junit.Test;
|
|||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
import org.shredzone.acme4j.util.SignatureUtils;
|
||||
|
@ -208,7 +210,7 @@ public class ChallengeTest {
|
|||
@Override
|
||||
public int sendRequest(URI uri) {
|
||||
assertThat(uri, is(locationUri));
|
||||
return HttpURLConnection.HTTP_ACCEPTED;
|
||||
return HttpURLConnection.HTTP_OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -230,6 +232,49 @@ public class ChallengeTest {
|
|||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a challenge is properly updated, with Retry-After header.
|
||||
*/
|
||||
@Test
|
||||
public void testUpdateRetryAfter() throws Exception {
|
||||
final long retryAfter = System.currentTimeMillis() + 30 * 1000L;
|
||||
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
public int sendRequest(URI uri) {
|
||||
assertThat(uri, is(locationUri));
|
||||
return HttpURLConnection.HTTP_ACCEPTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> readJsonResponse() {
|
||||
return getJsonAsMap("updateHttpChallengeResponse");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getRetryAfterHeader() {
|
||||
return new Date(retryAfter);
|
||||
}
|
||||
};
|
||||
|
||||
Session session = provider.createSession();
|
||||
|
||||
Challenge challenge = new Http01Challenge(session);
|
||||
challenge.unmarshall(getJsonAsMap("triggerHttpChallengeResponse"));
|
||||
|
||||
try {
|
||||
challenge.update();
|
||||
fail("Expected AcmeRetryAfterException");
|
||||
} catch (AcmeRetryAfterException ex) {
|
||||
assertThat(ex.getRetryAfter(), is(new Date(retryAfter)));
|
||||
}
|
||||
|
||||
assertThat(challenge.getStatus(), is(Status.VALID));
|
||||
assertThat(challenge.getLocation(), is(locationUri));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that challenge serialization works correctly.
|
||||
*/
|
||||
|
|
|
@ -48,6 +48,8 @@ while (challenge.getStatus() != Status.VALID) {
|
|||
|
||||
This is a very simple example. You should limit the number of loop iterations, and abort the loop when the status should turn to `INVALID`. If you know when the CA server actually requested your response (e.g. when you notice a HTTP request on the response file), you should start polling after that event.
|
||||
|
||||
`update()` may throw an `AcmeRetryAfterException`, giving an estimated time in `getRetryAfter()` for when the challenge is completed. You should then wait until that moment has been reached, before trying again. The challenge state is still updated when this exception is thrown.
|
||||
|
||||
As soon as all the necessary challenges are `VALID`, you have successfully associated the domain with your account.
|
||||
|
||||
If your final certificate will contain further domains or subdomains, repeat the authorization run with each of them.
|
||||
|
@ -67,6 +69,8 @@ auth.update();
|
|||
|
||||
After invoking `update()`, the `Authorization` object contains the current server state about your authorization, including the domain name, the overall status, and an expiry date.
|
||||
|
||||
`update()` may throw an `AcmeRetryAfterException`, giving an estimated time in `getRetryAfter()` for when all challenges are completed. You should then wait until that moment has been reached, before trying again. The authorization state is still updated when this exception is thrown.
|
||||
|
||||
## Deactivate an Authorization
|
||||
|
||||
It is possible to deactivate an `Authorization`, for example if you sell the associated domain.
|
||||
|
|
|
@ -45,6 +45,8 @@ X509Certificate[] chain = cert.downloadChain();
|
|||
|
||||
Congratulations! You have just created your first certificate via _acme4j_.
|
||||
|
||||
`download()` may throw an `AcmeRetryAfterException`, giving an estimated time in `getRetryAfter()` for when the certificate is ready for download. You should then wait until that moment has been reached, before trying again.
|
||||
|
||||
To recreate a `Certificate` object from the location, just bind it:
|
||||
|
||||
```java
|
||||
|
|
Loading…
Reference in New Issue