mirror of https://github.com/shred/acme4j
Offer iterators of authorizations and certificates
parent
68b7560f2f
commit
cef5984f81
|
@ -23,6 +23,7 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -31,6 +32,7 @@ import org.jose4j.jws.JsonWebSignature;
|
|||
import org.jose4j.lang.JoseException;
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.Resource;
|
||||
import org.shredzone.acme4j.connector.ResourceIterator;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeNetworkException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
|
@ -50,6 +52,8 @@ public class Registration extends AcmeResource {
|
|||
|
||||
private final List<URI> contacts = new ArrayList<>();
|
||||
private URI agreement;
|
||||
private URI authorizations;
|
||||
private URI certificates;
|
||||
private Status status;
|
||||
|
||||
/**
|
||||
|
@ -96,6 +100,48 @@ public class Registration extends AcmeResource {
|
|||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Iterator} of all {@link Authorization} belonging to this
|
||||
* {@link Registration}.
|
||||
* <p>
|
||||
* Using the iterator will initiate one or more requests to the ACME server.
|
||||
*
|
||||
* @return {@link Iterator} instance that returns {@link Authorization} objects.
|
||||
* {@link Iterator#hasNext()} and {@link Iterator#next()} may throw
|
||||
* {@link AcmeProtocolException} if a batch of authorization URIs could not be
|
||||
* fetched from the server.
|
||||
*/
|
||||
public Iterator<Authorization> getAuthorizations() throws AcmeException {
|
||||
LOG.debug("getAuthorizations");
|
||||
return new ResourceIterator<Authorization>(getSession(), "authorizations", authorizations) {
|
||||
@Override
|
||||
protected Authorization create(Session session, URI uri) {
|
||||
return Authorization.bind(session, uri);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Iterator} of all {@link Certificate} belonging to this
|
||||
* {@link Registration}.
|
||||
* <p>
|
||||
* Using the iterator will initiate one or more requests to the ACME server.
|
||||
*
|
||||
* @return {@link Iterator} instance that returns {@link Certificate} objects.
|
||||
* {@link Iterator#hasNext()} and {@link Iterator#next()} may throw
|
||||
* {@link AcmeProtocolException} if a batch of certificate URIs could not be
|
||||
* fetched from the server.
|
||||
*/
|
||||
public Iterator<Certificate> getCertificates() throws AcmeException {
|
||||
LOG.debug("getCertificates");
|
||||
return new ResourceIterator<Certificate>(getSession(), "certificates", certificates) {
|
||||
@Override
|
||||
protected Certificate create(Session session, URI uri) {
|
||||
return Certificate.bind(session, uri);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the registration to the current account status.
|
||||
*/
|
||||
|
@ -294,6 +340,26 @@ public class Registration extends AcmeResource {
|
|||
}
|
||||
}
|
||||
|
||||
if (json.containsKey("authorizations")) {
|
||||
try {
|
||||
this.authorizations = new URI((String) json.get("authorizations"));
|
||||
} catch (ClassCastException | URISyntaxException ex) {
|
||||
throw new AcmeProtocolException("Illegal authorizations URI", ex);
|
||||
}
|
||||
} else {
|
||||
this.authorizations = null;
|
||||
}
|
||||
|
||||
if (json.containsKey("certificates")) {
|
||||
try {
|
||||
this.certificates = new URI((String) json.get("certificates"));
|
||||
} catch (ClassCastException | URISyntaxException ex) {
|
||||
throw new AcmeProtocolException("Illegal certificates URI", ex);
|
||||
}
|
||||
} else {
|
||||
this.certificates = null;
|
||||
}
|
||||
|
||||
if (json.containsKey("status")) {
|
||||
this.status = Status.parse((String) json.get("status"));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2016 Richard "Shred" Körber
|
||||
* http://acme4j.shredzone.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
package org.shredzone.acme4j.connector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Collection;
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.shredzone.acme4j.AcmeResource;
|
||||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeNetworkException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
|
||||
/**
|
||||
* An {@link Iterator} that fetches a batch of URIs from the ACME server, and
|
||||
* generates {@link AcmeResource} instances.
|
||||
*
|
||||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
public abstract class ResourceIterator<T extends AcmeResource> implements Iterator<T> {
|
||||
|
||||
private final Session session;
|
||||
private final String field;
|
||||
private final Deque<URI> uriList = new ArrayDeque<>();
|
||||
private boolean eol = false;
|
||||
private URI nextUri;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ResourceIterator}.
|
||||
*
|
||||
* @param session
|
||||
* {@link Session} to bind this iterator to
|
||||
* @param field
|
||||
* Field name to be used in the JSON response
|
||||
* @param start
|
||||
* URI of the first JSON array, may be {@code null} for an empty iterator
|
||||
*/
|
||||
public ResourceIterator(Session session, String field, URI start) {
|
||||
this.session = session;
|
||||
this.field = field;
|
||||
this.nextUri = start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is another object in the result.
|
||||
*
|
||||
* @throws AcmeProtocolException
|
||||
* if the next batch of URIs could not be fetched from the server
|
||||
*/
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (eol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uriList.isEmpty()) {
|
||||
fetch();
|
||||
}
|
||||
|
||||
if (uriList.isEmpty()) {
|
||||
eol = true;
|
||||
}
|
||||
|
||||
return !uriList.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next object of the result.
|
||||
*
|
||||
* @throws AcmeProtocolException
|
||||
* if the next batch of URIs could not be fetched from the server
|
||||
* @throws NoSuchElementException
|
||||
* if there are no more entries
|
||||
*/
|
||||
@Override
|
||||
public T next() {
|
||||
if (!eol && uriList.isEmpty()) {
|
||||
fetch();
|
||||
}
|
||||
|
||||
URI next = uriList.poll();
|
||||
if (next == null) {
|
||||
eol = true;
|
||||
throw new NoSuchElementException("no more " + field);
|
||||
}
|
||||
|
||||
return create(session, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation, only here to satisfy the {@link Iterator} interface.
|
||||
*/
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException("cannot remove " + field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link AcmeResource} object by binding it to the {@link Session} and
|
||||
* using the given {@link URI}.
|
||||
*
|
||||
* @param session
|
||||
* {@link Session} to bind the object to
|
||||
* @param uri
|
||||
* {@link URI} of the resource
|
||||
* @return Created object
|
||||
*/
|
||||
protected abstract T create(Session session, URI uri);
|
||||
|
||||
/**
|
||||
* Fetches the next batch of URIs. Handles exceptions. Does nothing if there is no
|
||||
* URI of the next batch.
|
||||
*/
|
||||
private void fetch() {
|
||||
if (nextUri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
readAndQueue();
|
||||
} catch (AcmeException ex) {
|
||||
throw new AcmeProtocolException("failed to read next set of " + field, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next batch of URIs from the server, and fills the queue with the URIs. If
|
||||
* there is a "next" header, it is used for the next batch of URIs.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void readAndQueue() throws AcmeException {
|
||||
try (Connection conn = session.provider().connect()) {
|
||||
int rc = conn.sendRequest(nextUri);
|
||||
if (rc != HttpURLConnection.HTTP_OK) {
|
||||
conn.throwAcmeException();
|
||||
}
|
||||
|
||||
Map<String, Object> json = conn.readJsonResponse();
|
||||
try {
|
||||
Collection<String> array = (Collection<String>) json.get(field);
|
||||
if (array != null) {
|
||||
for (String uri : array) {
|
||||
uriList.add(new URI(uri));
|
||||
}
|
||||
}
|
||||
} catch (ClassCastException ex) {
|
||||
throw new AcmeProtocolException("Expected an array");
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new AcmeProtocolException("Invalid URI", ex);
|
||||
}
|
||||
|
||||
nextUri = conn.getLink("next");
|
||||
} catch (IOException ex) {
|
||||
throw new AcmeNetworkException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -24,6 +24,9 @@ import java.net.URI;
|
|||
import java.net.URISyntaxException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jose4j.jws.JsonWebSignature;
|
||||
|
@ -77,17 +80,39 @@ public class RegistrationTest {
|
|||
@Test
|
||||
public void testUpdateRegistration() throws AcmeException, IOException {
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
private Map<String, Object> jsonResponse;
|
||||
|
||||
@Override
|
||||
public int sendSignedRequest(URI uri, ClaimBuilder claims, Session session) {
|
||||
assertThat(uri, is(locationUri));
|
||||
assertThat(claims.toString(), sameJSONAs(getJson("updateRegistration")));
|
||||
assertThat(session, is(notNullValue()));
|
||||
jsonResponse = getJsonAsMap("updateRegistrationResponse");
|
||||
return HttpURLConnection.HTTP_ACCEPTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sendRequest(URI uri) {
|
||||
if (URI.create("https://example.com/acme/reg/1/authz").equals(uri)) {
|
||||
jsonResponse = new HashMap<>();
|
||||
jsonResponse.put("authorizations",
|
||||
Arrays.asList("https://example.com/acme/auth/1"));
|
||||
return HttpURLConnection.HTTP_OK;
|
||||
}
|
||||
|
||||
if (URI.create("https://example.com/acme/reg/1/cert").equals(uri)) {
|
||||
jsonResponse = new HashMap<>();
|
||||
jsonResponse.put("certificates",
|
||||
Arrays.asList("https://example.com/acme/cert/1"));
|
||||
return HttpURLConnection.HTTP_OK;
|
||||
}
|
||||
|
||||
return HttpURLConnection.HTTP_NOT_FOUND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> readJsonResponse() {
|
||||
return getJsonAsMap("updateRegistrationResponse");
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -99,6 +124,7 @@ public class RegistrationTest {
|
|||
public URI getLink(String relation) {
|
||||
switch(relation) {
|
||||
case "terms-of-service": return agreementUri;
|
||||
case "next": return null;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +139,18 @@ public class RegistrationTest {
|
|||
assertThat(registration.getContacts().get(0), is(URI.create("mailto:foo2@example.com")));
|
||||
assertThat(registration.getStatus(), is(Status.GOOD));
|
||||
|
||||
Iterator<Authorization> authIt = registration.getAuthorizations();
|
||||
assertThat(authIt, not(nullValue()));
|
||||
assertThat(authIt.next().getLocation(),
|
||||
is(URI.create("https://example.com/acme/auth/1")));
|
||||
assertThat(authIt.hasNext(), is(false));
|
||||
|
||||
Iterator<Certificate> certIt = registration.getCertificates();
|
||||
assertThat(certIt, not(nullValue()));
|
||||
assertThat(certIt.next().getLocation(),
|
||||
is(URI.create("https://example.com/acme/cert/1")));
|
||||
assertThat(certIt.hasNext(), is(false));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -193,8 +193,8 @@ public class SessionTest {
|
|||
|
||||
final AcmeProvider mockProvider = mock(AcmeProvider.class);
|
||||
when(mockProvider.directory(
|
||||
org.mockito.Matchers.any(Session.class),
|
||||
org.mockito.Matchers.eq(serverUri)))
|
||||
ArgumentMatchers.any(Session.class),
|
||||
ArgumentMatchers.eq(serverUri)))
|
||||
.thenReturn(TestUtils.getJsonAsMap("directoryNoMeta"));
|
||||
|
||||
Session session = new Session(serverUri, keyPair) {
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2016 Richard "Shred" Körber
|
||||
* http://acme4j.shredzone.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
package org.shredzone.acme4j.connector;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.jose4j.json.JsonUtil;
|
||||
import org.jose4j.lang.JoseException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.Authorization;
|
||||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
|
||||
/**
|
||||
* Unit test for {@link ResourceIterator}.
|
||||
*
|
||||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
public class ResourceIteratorTest {
|
||||
|
||||
private final int PAGES = 4;
|
||||
private final int RESOURCES_PER_PAGE = 5;
|
||||
private final String TYPE = "authorizations";
|
||||
|
||||
private List<URI> resourceURIs = new ArrayList<>(PAGES * RESOURCES_PER_PAGE);
|
||||
private List<URI> pageURIs = new ArrayList<>(PAGES);
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
resourceURIs.clear();
|
||||
for (int ix = 0; ix < RESOURCES_PER_PAGE * PAGES; ix++) {
|
||||
resourceURIs.add(URI.create("https://example.com/acme/auth/" + ix));
|
||||
}
|
||||
|
||||
pageURIs.clear();
|
||||
for (int ix = 0; ix < PAGES; ix++) {
|
||||
pageURIs.add(URI.create("https://example.com/acme/batch/" + ix));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the {@link ResourceIterator} handles a {@code null} start URI.
|
||||
*/
|
||||
@Test(expected = NoSuchElementException.class)
|
||||
public void nullTest() throws IOException {
|
||||
Iterator<Authorization> it = createIterator(null);
|
||||
|
||||
assertThat(it, not(nullValue()));
|
||||
assertThat(it.hasNext(), is(false));
|
||||
it.next(); // throws NoSuchElementException
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the {@link ResourceIterator} returns all objects in the correct order.
|
||||
*/
|
||||
@Test
|
||||
public void iteratorTest() throws IOException {
|
||||
List<URI> result = new ArrayList<>();
|
||||
|
||||
Iterator<Authorization> it = createIterator(pageURIs.get(0));
|
||||
while (it.hasNext()) {
|
||||
result.add(it.next().getLocation());
|
||||
}
|
||||
|
||||
assertThat(result, is(equalTo(resourceURIs)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test unusual {@link Iterator#next()} and {@link Iterator#hasNext()} usage.
|
||||
*/
|
||||
@Test
|
||||
public void nextHasNextTest() throws IOException {
|
||||
List<URI> result = new ArrayList<>();
|
||||
|
||||
Iterator<Authorization> it = createIterator(pageURIs.get(0));
|
||||
assertThat(it.hasNext(), is(true));
|
||||
assertThat(it.hasNext(), is(true));
|
||||
|
||||
// don't try this at home, kids...
|
||||
try {
|
||||
for (;;) {
|
||||
result.add(it.next().getLocation());
|
||||
}
|
||||
} catch (NoSuchElementException ex) {
|
||||
assertThat(it.hasNext(), is(false));
|
||||
assertThat(it.hasNext(), is(false));
|
||||
}
|
||||
|
||||
assertThat(result, is(equalTo(resourceURIs)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that {@link Iterator#remove()} fails.
|
||||
*/
|
||||
@Test(expected = UnsupportedOperationException.class)
|
||||
public void removeTest() throws IOException {
|
||||
Iterator<Authorization> it = createIterator(pageURIs.get(0));
|
||||
it.next();
|
||||
it.remove(); // throws UnsupportedOperationException
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Iterator} of {@link Authorization} objects.
|
||||
*
|
||||
* @param first
|
||||
* URI of the first page
|
||||
* @return Created {@link Iterator}
|
||||
*/
|
||||
private Iterator<Authorization> createIterator(URI first) throws IOException {
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
private int ix;
|
||||
|
||||
@Override
|
||||
public int sendRequest(URI uri) {
|
||||
ix = pageURIs.indexOf(uri);
|
||||
assertThat(ix, is(greaterThanOrEqualTo(0)));
|
||||
return HttpURLConnection.HTTP_OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> readJsonResponse() {
|
||||
try {
|
||||
int start = ix * RESOURCES_PER_PAGE;
|
||||
int end = (ix + 1) * RESOURCES_PER_PAGE;
|
||||
|
||||
ClaimBuilder cb = new ClaimBuilder();
|
||||
cb.array(TYPE, resourceURIs.subList(start, end).toArray());
|
||||
|
||||
// Make sure to use the JSON parser
|
||||
return JsonUtil.parseJson(cb.toString());
|
||||
} catch (JoseException ex) {
|
||||
throw new AcmeProtocolException("Invalid JSON", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getLink(String relation) {
|
||||
if ("next".equals(relation) && (ix + 1 < pageURIs.size())) {
|
||||
return pageURIs.get(ix + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
Session session = provider.createSession();
|
||||
|
||||
provider.close();
|
||||
|
||||
return new ResourceIterator<Authorization>(session, TYPE, first) {
|
||||
@Override
|
||||
protected Authorization create(Session session, URI uri) {
|
||||
return Authorization.bind(session, uri);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -54,7 +54,9 @@ updateRegistration = \
|
|||
updateRegistrationResponse = \
|
||||
{"agreement":"http://example.com/agreement.pdf",\
|
||||
"contact":["mailto:foo2@example.com"],\
|
||||
"status":"good"}
|
||||
"status":"good",\
|
||||
"authorizations":"https://example.com/acme/reg/1/authz",\
|
||||
"certificates":"https://example.com/acme/reg/1/cert"}
|
||||
|
||||
newAuthorizationRequest = \
|
||||
{"resource":"new-authz",\
|
||||
|
|
Loading…
Reference in New Issue