mirror of https://github.com/shred/acme4j
Evaluate Retry-After header on rate limit excess
parent
5dc1b9314e
commit
279e0f3993
|
@ -16,6 +16,7 @@ package org.shredzone.acme4j.connector;
|
|||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import org.shredzone.acme4j.Registration;
|
||||
|
@ -100,6 +101,13 @@ public interface Connection extends AutoCloseable {
|
|||
*/
|
||||
URI getLink(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();
|
||||
|
||||
/**
|
||||
* Handles a problem by throwing an exception. If a JSON problem was returned, an
|
||||
* {@link AcmeServerException} will be thrown. Otherwise a generic
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
*/
|
||||
package org.shredzone.acme4j.exception;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* An exception that is thrown when a rate limit was exceeded.
|
||||
*
|
||||
|
@ -21,6 +23,8 @@ package org.shredzone.acme4j.exception;
|
|||
public class AcmeRateLimitExceededException extends AcmeServerException {
|
||||
private static final long serialVersionUID = 4150484059796413069L;
|
||||
|
||||
private final Date retryAfter;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AcmeRateLimitExceededException}.
|
||||
*
|
||||
|
@ -29,9 +33,21 @@ public class AcmeRateLimitExceededException extends AcmeServerException {
|
|||
* {@code "urn:ietf:params:acme:error:rateLimited"})
|
||||
* @param detail
|
||||
* Human readable error message
|
||||
* @param retryAfter
|
||||
* The moment the request is expected to succeed again, may be {@code null}
|
||||
* if not known
|
||||
*/
|
||||
public AcmeRateLimitExceededException(String type, String detail) {
|
||||
public AcmeRateLimitExceededException(String type, String detail, Date retryAfter) {
|
||||
super(type, detail);
|
||||
this.retryAfter = retryAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the moment the request is expected to succeed again. {@code null} if this
|
||||
* moment is not known.
|
||||
*/
|
||||
public Date getRetryAfter() {
|
||||
return (retryAfter != null ? new Date(retryAfter.getTime()) : null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.security.KeyPair;
|
|||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -314,6 +315,29 @@ public class DefaultConnection implements Connection {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getRetryAfterHeader() {
|
||||
assertConnectionIsOpen();
|
||||
|
||||
// See RFC 2616 section 14.37
|
||||
String header = conn.getHeaderField("Retry-After");
|
||||
|
||||
try {
|
||||
// delta-seconds
|
||||
if (header.matches("^\\d+$")) {
|
||||
int delta = Integer.parseInt(header);
|
||||
long date = conn.getHeaderFieldDate("Date", System.currentTimeMillis());
|
||||
return new Date(date + delta * 1000L);
|
||||
}
|
||||
|
||||
// HTTP-date
|
||||
long date = conn.getHeaderFieldDate("Retry-After", 0L);
|
||||
return (date != 0 ? new Date(date) : null);
|
||||
} catch (Exception ex) {
|
||||
throw new AcmeProtocolException("Bad retry-after header value: " + header, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void throwAcmeException() throws AcmeException, IOException {
|
||||
assertConnectionIsOpen();
|
||||
|
@ -338,7 +362,7 @@ public class DefaultConnection implements Connection {
|
|||
|
||||
case "urn:acme:error:rateLimited":
|
||||
case "urn:ietf:params:acme:error:rateLimited":
|
||||
throw new AcmeRateLimitExceededException(type, detail);
|
||||
throw new AcmeRateLimitExceededException(type, detail, getRetryAfterHeader());
|
||||
|
||||
default:
|
||||
throw new AcmeServerException(type, detail);
|
||||
|
|
|
@ -15,6 +15,7 @@ package org.shredzone.acme4j.impl;
|
|||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
||||
|
@ -27,6 +28,7 @@ import java.net.URISyntaxException;
|
|||
import java.security.KeyPair;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -189,6 +191,43 @@ public class DefaultConnectionTest {
|
|||
verifyNoMoreInteractions(mockUrlConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if Retry-After header with absolute date is correctly parsed.
|
||||
*/
|
||||
@Test
|
||||
public void testGetRetryAfterHeaderDate() {
|
||||
Date retryDate = new Date(System.currentTimeMillis() + 10 * 60 * 60 * 1000L);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
verify(mockUrlConnection, atLeastOnce()).getHeaderField("Retry-After");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if Retry-After header with relative timespan is correctly parsed.
|
||||
*/
|
||||
@Test
|
||||
public void testGetRetryAfterHeaderDelta() {
|
||||
int delta = 10 * 60 * 60;
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
when(mockUrlConnection.getHeaderField("Retry-After")).thenReturn(String.valueOf(delta));
|
||||
when(mockUrlConnection.getHeaderFieldDate(eq("Date"), anyLong())).thenReturn(now);
|
||||
|
||||
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||
conn.conn = mockUrlConnection;
|
||||
assertThat(conn.getRetryAfterHeader(), is(new Date(now + delta * 1000L)));
|
||||
}
|
||||
|
||||
verify(mockUrlConnection, atLeastOnce()).getHeaderField("Retry-After");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an {@link AcmeServerException} is thrown on an acme problem.
|
||||
*/
|
||||
|
|
|
@ -15,6 +15,7 @@ package org.shredzone.acme4j.impl;
|
|||
|
||||
import java.net.URI;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import org.shredzone.acme4j.Registration;
|
||||
|
@ -72,6 +73,11 @@ public class DummyConnection implements Connection {
|
|||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getRetryAfterHeader() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void throwAcmeException() throws AcmeException {
|
||||
throw new UnsupportedOperationException();
|
||||
|
|
Loading…
Reference in New Issue