mirror of https://github.com/shred/acme4j
Evaluate rate-limit relation when rate limit is exceeded
parent
57194ce0fc
commit
957dfd71a1
|
@ -16,6 +16,7 @@ package org.shredzone.acme4j.connector;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -85,7 +86,8 @@ public interface Connection extends AutoCloseable {
|
||||||
/**
|
/**
|
||||||
* Gets a relation link from the header.
|
* Gets a relation link from the header.
|
||||||
* <p>
|
* <p>
|
||||||
* Relative links are resolved against the last request's URL.
|
* Relative links are resolved against the last request's URL. If there is more than
|
||||||
|
* one relation, the first one is returned.
|
||||||
*
|
*
|
||||||
* @param relation
|
* @param relation
|
||||||
* Link relation
|
* Link relation
|
||||||
|
@ -93,6 +95,17 @@ public interface Connection extends AutoCloseable {
|
||||||
*/
|
*/
|
||||||
URI getLink(String relation);
|
URI getLink(String relation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets one or more relation link from the header.
|
||||||
|
* <p>
|
||||||
|
* Relative links are resolved against the last request's URL.
|
||||||
|
*
|
||||||
|
* @param relation
|
||||||
|
* Link relation
|
||||||
|
* @return Collection of links, or {@code null} if there was no such relation link
|
||||||
|
*/
|
||||||
|
Collection<URI> getLinks(String relation);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the moment returned in a "Retry-After" header.
|
* Returns the moment returned in a "Retry-After" header.
|
||||||
*
|
*
|
||||||
|
|
|
@ -27,6 +27,8 @@ import java.security.KeyPair;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -252,8 +254,24 @@ public class DefaultConnection implements Connection {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI getLink(String relation) {
|
public URI getLink(String relation) {
|
||||||
|
Collection<URI> links = getLinks(relation);
|
||||||
|
if (links == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.size() > 1) {
|
||||||
|
LOG.debug("Link: {} - using the first of {}", relation, links.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
return links.iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<URI> getLinks(String relation) {
|
||||||
assertConnectionIsOpen();
|
assertConnectionIsOpen();
|
||||||
|
|
||||||
|
List<URI> result = new ArrayList<>();
|
||||||
|
|
||||||
List<String> links = conn.getHeaderFields().get("Link");
|
List<String> links = conn.getHeaderFields().get("Link");
|
||||||
if (links != null) {
|
if (links != null) {
|
||||||
Pattern p = Pattern.compile("<(.*?)>\\s*;\\s*rel=\"?"+ Pattern.quote(relation) + "\"?");
|
Pattern p = Pattern.compile("<(.*?)>\\s*;\\s*rel=\"?"+ Pattern.quote(relation) + "\"?");
|
||||||
|
@ -262,12 +280,12 @@ public class DefaultConnection implements Connection {
|
||||||
if (m.matches()) {
|
if (m.matches()) {
|
||||||
String location = m.group(1);
|
String location = m.group(1);
|
||||||
LOG.debug("Link: {} -> {}", relation, location);
|
LOG.debug("Link: {} -> {}", relation, location);
|
||||||
return resolveRelative(location);
|
result.add(resolveRelative(location));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return (!result.isEmpty() ? result : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -324,7 +342,8 @@ public class DefaultConnection implements Connection {
|
||||||
|
|
||||||
case ACME_ERROR_PREFIX + "rateLimited":
|
case ACME_ERROR_PREFIX + "rateLimited":
|
||||||
case ACME_ERROR_PREFIX_DEPRECATED + "rateLimited":
|
case ACME_ERROR_PREFIX_DEPRECATED + "rateLimited":
|
||||||
throw new AcmeRateLimitExceededException(type, detail, getRetryAfterHeader());
|
throw new AcmeRateLimitExceededException(
|
||||||
|
type, detail, getRetryAfterHeader(), getLinks("rate-limit"));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new AcmeServerException(type, detail);
|
throw new AcmeServerException(type, detail);
|
||||||
|
|
|
@ -13,6 +13,9 @@
|
||||||
*/
|
*/
|
||||||
package org.shredzone.acme4j.exception;
|
package org.shredzone.acme4j.exception;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +25,7 @@ public class AcmeRateLimitExceededException extends AcmeServerException {
|
||||||
private static final long serialVersionUID = 4150484059796413069L;
|
private static final long serialVersionUID = 4150484059796413069L;
|
||||||
|
|
||||||
private final Date retryAfter;
|
private final Date retryAfter;
|
||||||
|
private final Collection<URI> documents;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link AcmeRateLimitExceededException}.
|
* Creates a new {@link AcmeRateLimitExceededException}.
|
||||||
|
@ -34,10 +38,13 @@ public class AcmeRateLimitExceededException extends AcmeServerException {
|
||||||
* @param retryAfter
|
* @param retryAfter
|
||||||
* The moment the request is expected to succeed again, may be {@code null}
|
* The moment the request is expected to succeed again, may be {@code null}
|
||||||
* if not known
|
* if not known
|
||||||
|
* @param documents
|
||||||
|
* URIs pointing to documents about the rate limit that was hit
|
||||||
*/
|
*/
|
||||||
public AcmeRateLimitExceededException(String type, String detail, Date retryAfter) {
|
public AcmeRateLimitExceededException(String type, String detail, Date retryAfter, Collection<URI> documents) {
|
||||||
super(type, detail);
|
super(type, detail);
|
||||||
this.retryAfter = retryAfter;
|
this.retryAfter = retryAfter;
|
||||||
|
this.documents = (documents != null ? Collections.unmodifiableCollection(documents) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,4 +55,12 @@ public class AcmeRateLimitExceededException extends AcmeServerException {
|
||||||
return (retryAfter != null ? new Date(retryAfter.getTime()) : null);
|
return (retryAfter != null ? new Date(retryAfter.getTime()) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of URIs pointing to documents about the rate limit that was hit.
|
||||||
|
* {@code null} if the server did not provide such URIs.
|
||||||
|
*/
|
||||||
|
public Collection<URI> getDocuments() {
|
||||||
|
return documents;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,6 +189,30 @@ public class DefaultConnectionTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that multiple link headers are evaluated.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testGetMultiLink() {
|
||||||
|
Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
headers.put("Link", Arrays.asList(
|
||||||
|
"<https://example.com/acme/terms1>; rel=\"terms-of-service\"",
|
||||||
|
"<https://example.com/acme/terms2>; rel=\"terms-of-service\"",
|
||||||
|
"<https://example.com/acme/terms3>; rel=\"terms-of-service\""
|
||||||
|
));
|
||||||
|
|
||||||
|
when(mockUrlConnection.getHeaderFields()).thenReturn(headers);
|
||||||
|
|
||||||
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
|
conn.conn = mockUrlConnection;
|
||||||
|
assertThat(conn.getLinks("terms-of-service"), containsInAnyOrder(
|
||||||
|
URI.create("https://example.com/acme/terms1"),
|
||||||
|
URI.create("https://example.com/acme/terms2"),
|
||||||
|
URI.create("https://example.com/acme/terms3")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that no Location header returns {@code null}.
|
* Test that no Location header returns {@code null}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -15,6 +15,7 @@ package org.shredzone.acme4j.connector;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -63,6 +64,11 @@ public class DummyConnection implements Connection {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<URI> getLinks(String relation) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Date getRetryAfterHeader() {
|
public Date getRetryAfterHeader() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
|
Loading…
Reference in New Issue