Evaluate rate-limit relation when rate limit is exceeded

pull/30/head
Richard Körber 2016-07-27 22:58:02 +02:00
parent 57194ce0fc
commit 957dfd71a1
5 changed files with 82 additions and 5 deletions

View File

@ -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.
* *

View File

@ -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);

View File

@ -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;
}
} }

View File

@ -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}.
*/ */

View File

@ -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();