Simplify handling of Retry-After header

pull/30/head
Richard Körber 2016-12-21 23:25:23 +01:00
parent 101801260f
commit 584452b079
10 changed files with 120 additions and 64 deletions

View File

@ -178,19 +178,11 @@ public class Authorization extends AcmeResource {
LOG.debug("update");
try (Connection conn = getSession().provider().connect()) {
conn.sendRequest(getLocation(), getSession());
int rc = conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED);
conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED);
JSON result = conn.readJsonResponse();
unmarshalAuthorization(result);
unmarshalAuthorization(conn.readJsonResponse());
if (rc == HttpURLConnection.HTTP_ACCEPTED) {
Date retryAfter = conn.getRetryAfterHeader();
if (retryAfter != null) {
throw new AcmeRetryAfterException(
"authorization is not completed yet",
retryAfter);
}
}
conn.handleRetryAfter("authorization is not completed yet");
}
}

View File

@ -18,7 +18,6 @@ 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;
@ -89,15 +88,8 @@ public class Certificate extends AcmeResource {
LOG.debug("download");
try (Connection conn = getSession().provider().connect()) {
conn.sendRequest(getLocation(), getSession());
int rc = conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED);
if (rc == HttpURLConnection.HTTP_ACCEPTED) {
Date retryAfter = conn.getRetryAfterHeader();
if (retryAfter != null) {
throw new AcmeRetryAfterException(
"certificate is not available for download yet",
retryAfter);
}
}
conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED);
conn.handleRetryAfter("certificate is not available for download yet");
chainCertUri = conn.getLink("up");
cert = conn.readCertificate();

View File

@ -204,17 +204,11 @@ public class Challenge extends AcmeResource {
LOG.debug("update");
try (Connection conn = getSession().provider().connect()) {
conn.sendRequest(getLocation(), getSession());
int rc = conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED);
conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED);
unmarshall(conn.readJsonResponse());
if (rc == HttpURLConnection.HTTP_ACCEPTED) {
Date retryAfter = conn.getRetryAfterHeader();
if (retryAfter != null) {
throw new AcmeRetryAfterException("challenge is not completed yet",
retryAfter);
}
}
conn.handleRetryAfter("challenge is not completed yet");
}
}

View File

@ -16,12 +16,12 @@ package org.shredzone.acme4j.connector;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.JSONBuilder;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.JSONBuilder;
/**
* Connects to the ACME server and offers different methods for invoking the API.
@ -74,6 +74,15 @@ public interface Connection extends AutoCloseable {
*/
X509Certificate readCertificate() throws AcmeException;
/**
* Throws an {@link AcmeRetryAfterException} if the last status was HTTP Accepted and
* a Retry-After header was received.
*
* @param message
* Message to be sent along with the {@link AcmeRetryAfterException}
*/
void handleRetryAfter(String message) throws AcmeException;
/**
* Updates a {@link Session} by evaluating the HTTP response header.
*
@ -114,13 +123,6 @@ public interface Connection extends AutoCloseable {
*/
Collection<URI> getLinks(String relation);
/**
* Returns the moment returned in a "Retry-After" header.
*
* @return Moment, or {@code null} if no "Retry-After" header was set.
*/
Date getRetryAfterHeader();
/**
* Closes the {@link Connection}, releasing all resources.
*/

View File

@ -47,6 +47,7 @@ import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeNetworkException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.exception.AcmeRateLimitExceededException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.exception.AcmeServerException;
import org.shredzone.acme4j.exception.AcmeUnauthorizedException;
import org.shredzone.acme4j.util.JSON;
@ -228,6 +229,22 @@ public class DefaultConnection implements Connection {
}
}
@Override
public void handleRetryAfter(String message) throws AcmeException {
assertConnectionIsOpen();
try {
if (conn.getResponseCode() == HttpURLConnection.HTTP_ACCEPTED) {
Date retryAfter = getRetryAfterHeader();
if (retryAfter != null) {
throw new AcmeRetryAfterException(message, retryAfter);
}
}
} catch (IOException ex) {
throw new AcmeNetworkException(ex);
}
}
@Override
public void updateSession(Session session) {
assertConnectionIsOpen();
@ -296,9 +313,14 @@ public class DefaultConnection implements Connection {
}
@Override
public Date getRetryAfterHeader() {
assertConnectionIsOpen();
public void close() {
conn = null;
}
/**
* Gets the instant sent with the Retry-After header.
*/
private Date getRetryAfterHeader() {
// See RFC 2616 section 14.37
String header = conn.getHeaderField("Retry-After");
if (header == null) {
@ -321,11 +343,6 @@ public class DefaultConnection implements Connection {
}
}
@Override
public void close() {
conn = null;
}
/**
* Handles a problem by throwing an exception. If a JSON problem was returned, an
* {@link AcmeServerException} will be thrown. Otherwise a generic

View File

@ -135,6 +135,11 @@ public class AuthorizationTest {
public JSON readJsonResponse() {
return getJsonAsObject("updateAuthorizationResponse");
}
@Override
public void handleRetryAfter(String message) throws AcmeException {
// Just do nothing
}
};
Session session = provider.createSession();
@ -189,6 +194,11 @@ public class AuthorizationTest {
public JSON readJsonResponse() {
return getJsonAsObject("updateAuthorizationResponse");
}
@Override
public void handleRetryAfter(String message) throws AcmeException {
// Just do nothing
}
};
Session session = provider.createSession();
@ -239,8 +249,8 @@ public class AuthorizationTest {
}
@Override
public Date getRetryAfterHeader() {
return new Date(retryAfter);
public void handleRetryAfter(String message) throws AcmeException {
throw new AcmeRetryAfterException(message, new Date(retryAfter));
}
};

View File

@ -76,6 +76,11 @@ public class CertificateTest {
return originalCert;
}
@Override
public void handleRetryAfter(String message) throws AcmeException {
// Just do nothing
}
@Override
public URI getLink(String relation) {
switch(relation) {
@ -117,9 +122,10 @@ public class CertificateTest {
return HttpURLConnection.HTTP_ACCEPTED;
}
@Override
public Date getRetryAfterHeader() {
return new Date(retryAfter);
public void handleRetryAfter(String message) throws AcmeException {
throw new AcmeRetryAfterException(message, new Date(retryAfter));
}
};

View File

@ -204,6 +204,11 @@ public class ChallengeTest {
public JSON readJsonResponse() {
return getJsonAsObject("updateHttpChallengeResponse");
}
@Override
public void handleRetryAfter(String message) throws AcmeException {
// Just do nothing
}
};
Session session = provider.createSession();
@ -244,9 +249,10 @@ public class ChallengeTest {
return getJsonAsObject("updateHttpChallengeResponse");
}
@Override
public Date getRetryAfterHeader() {
return new Date(retryAfter);
public void handleRetryAfter(String message) throws AcmeException {
throw new AcmeRetryAfterException(message, new Date(retryAfter));
}
};

View File

@ -43,6 +43,7 @@ import org.shredzone.acme4j.Session;
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.exception.AcmeServerException;
import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.JSONBuilder;
@ -252,17 +253,24 @@ public class DefaultConnectionTest {
* Test if Retry-After header with absolute date is correctly parsed.
*/
@Test
public void testGetRetryAfterHeaderDate() {
public void testHandleRetryAfterHeaderDate() throws AcmeException, IOException {
Date retryDate = new Date(System.currentTimeMillis() + 10 * 60 * 60 * 1000L);
String retryMsg = "absolute date";
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_ACCEPTED);
when(mockUrlConnection.getHeaderField("Retry-After")).thenReturn(retryDate.toString());
when(mockUrlConnection.getHeaderFieldDate("Retry-After", 0L)).thenReturn(retryDate.getTime());
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
conn.conn = mockUrlConnection;
assertThat(conn.getRetryAfterHeader(), is(retryDate));
conn.handleRetryAfter(retryMsg);
fail("no AcmeRetryAfterException was thrown");
} catch (AcmeRetryAfterException ex) {
assertThat(ex.getRetryAfter(), is(retryDate));
assertThat(ex.getMessage(), is(retryMsg));
}
verify(mockUrlConnection, atLeastOnce()).getResponseCode();
verify(mockUrlConnection, atLeastOnce()).getHeaderField("Retry-After");
}
@ -270,10 +278,13 @@ public class DefaultConnectionTest {
* Test if Retry-After header with relative timespan is correctly parsed.
*/
@Test
public void testGetRetryAfterHeaderDelta() {
public void testHandleRetryAfterHeaderDelta() throws AcmeException, IOException {
int delta = 10 * 60 * 60;
long now = System.currentTimeMillis();
String retryMsg = "relative time";
when(mockUrlConnection.getResponseCode())
.thenReturn(HttpURLConnection.HTTP_ACCEPTED);
when(mockUrlConnection.getHeaderField("Retry-After"))
.thenReturn(String.valueOf(delta));
when(mockUrlConnection.getHeaderFieldDate(
@ -283,9 +294,14 @@ public class DefaultConnectionTest {
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
conn.conn = mockUrlConnection;
assertThat(conn.getRetryAfterHeader(), is(new Date(now + delta * 1000L)));
conn.handleRetryAfter(retryMsg);
fail("no AcmeRetryAfterException was thrown");
} catch (AcmeRetryAfterException ex) {
assertThat(ex.getRetryAfter(), is(new Date(now + delta * 1000L)));
assertThat(ex.getMessage(), is(retryMsg));
}
verify(mockUrlConnection, atLeastOnce()).getResponseCode();
verify(mockUrlConnection, atLeastOnce()).getHeaderField("Retry-After");
}
@ -293,18 +309,40 @@ public class DefaultConnectionTest {
* Test if no Retry-After header is correctly handled.
*/
@Test
public void testGetRetryAfterHeaderNull() {
public void testHandleRetryAfterHeaderNull() throws AcmeException, IOException {
when(mockUrlConnection.getResponseCode())
.thenReturn(HttpURLConnection.HTTP_ACCEPTED);
when(mockUrlConnection.getHeaderField("Retry-After"))
.thenReturn(null);
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
conn.conn = mockUrlConnection;
assertThat(conn.getRetryAfterHeader(), is(nullValue()));
conn.handleRetryAfter("no header");
} catch (AcmeRetryAfterException ex) {
fail("an AcmeRetryAfterException was thrown");
}
verify(mockUrlConnection, atLeastOnce()).getResponseCode();
verify(mockUrlConnection, atLeastOnce()).getHeaderField("Retry-After");
}
/**
* Test if no HTTP_ACCEPTED status is correctly handled.
*/
@Test
public void testHandleRetryAfterNotAccepted() throws AcmeException, IOException {
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
conn.conn = mockUrlConnection;
conn.handleRetryAfter("http ok");
} catch (AcmeRetryAfterException ex) {
fail("an AcmeRetryAfterException was thrown");
}
verify(mockUrlConnection, atLeastOnce()).getResponseCode();
}
/**
* Test if an {@link AcmeServerException} is thrown on an acme problem.
*/

View File

@ -16,12 +16,11 @@ package org.shredzone.acme4j.connector;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.JSONBuilder;
import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.JSONBuilder;
/**
* Dummy implementation of {@link Connection} that always fails. Single methods are
@ -54,6 +53,11 @@ public class DummyConnection implements Connection {
throw new UnsupportedOperationException();
}
@Override
public void handleRetryAfter(String message) throws AcmeException {
throw new UnsupportedOperationException();
}
@Override
public void updateSession(Session session) {
throw new UnsupportedOperationException();
@ -74,11 +78,6 @@ public class DummyConnection implements Connection {
throw new UnsupportedOperationException();
}
@Override
public Date getRetryAfterHeader() {
throw new UnsupportedOperationException();
}
@Override
public void close() {
// closing is always safe