diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java index a49461a9..77e6810c 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java @@ -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.Collection; import java.util.Date; import java.util.Map; @@ -85,7 +86,8 @@ public interface Connection extends AutoCloseable { /** * Gets a relation link from the header. *

- * 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 * Link relation @@ -93,6 +95,17 @@ public interface Connection extends AutoCloseable { */ URI getLink(String relation); + /** + * Gets one or more relation link from the header. + *

+ * 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 getLinks(String relation); + /** * Returns the moment returned in a "Retry-After" header. * diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java index 2947187a..54eca085 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java @@ -27,6 +27,8 @@ import java.security.KeyPair; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; @@ -252,8 +254,24 @@ public class DefaultConnection implements Connection { @Override public URI getLink(String relation) { + Collection 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 getLinks(String relation) { assertConnectionIsOpen(); + List result = new ArrayList<>(); + List links = conn.getHeaderFields().get("Link"); if (links != null) { Pattern p = Pattern.compile("<(.*?)>\\s*;\\s*rel=\"?"+ Pattern.quote(relation) + "\"?"); @@ -262,12 +280,12 @@ public class DefaultConnection implements Connection { if (m.matches()) { String location = m.group(1); LOG.debug("Link: {} -> {}", relation, location); - return resolveRelative(location); + result.add(resolveRelative(location)); } } } - return null; + return (!result.isEmpty() ? result : null); } @Override @@ -324,7 +342,8 @@ public class DefaultConnection implements Connection { case ACME_ERROR_PREFIX + "rateLimited": case ACME_ERROR_PREFIX_DEPRECATED + "rateLimited": - throw new AcmeRateLimitExceededException(type, detail, getRetryAfterHeader()); + throw new AcmeRateLimitExceededException( + type, detail, getRetryAfterHeader(), getLinks("rate-limit")); default: throw new AcmeServerException(type, detail); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java index d8a282f5..19102d45 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java @@ -13,6 +13,9 @@ */ package org.shredzone.acme4j.exception; +import java.net.URI; +import java.util.Collection; +import java.util.Collections; import java.util.Date; /** @@ -22,6 +25,7 @@ public class AcmeRateLimitExceededException extends AcmeServerException { private static final long serialVersionUID = 4150484059796413069L; private final Date retryAfter; + private final Collection documents; /** * Creates a new {@link AcmeRateLimitExceededException}. @@ -34,10 +38,13 @@ public class AcmeRateLimitExceededException extends AcmeServerException { * @param retryAfter * The moment the request is expected to succeed again, may be {@code null} * 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 documents) { super(type, detail); 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); } + /** + * 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 getDocuments() { + return documents; + } + } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java index fe21b14d..8c4806da 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java @@ -189,6 +189,30 @@ public class DefaultConnectionTest { } } + /** + * Test that multiple link headers are evaluated. + */ + @Test + public void testGetMultiLink() { + Map> headers = new HashMap<>(); + headers.put("Link", Arrays.asList( + "; rel=\"terms-of-service\"", + "; rel=\"terms-of-service\"", + "; 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}. */ diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java index 80a9f79e..e356e794 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j.connector; import java.net.URI; import java.security.cert.X509Certificate; +import java.util.Collection; import java.util.Date; import java.util.Map; @@ -63,6 +64,11 @@ public class DummyConnection implements Connection { throw new UnsupportedOperationException(); } + @Override + public Collection getLinks(String relation) { + throw new UnsupportedOperationException(); + } + @Override public Date getRetryAfterHeader() { throw new UnsupportedOperationException();