mirror of https://github.com/shred/acme4j
Add Order resource
parent
4fe4c12c62
commit
4772488896
|
@ -19,9 +19,6 @@ import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp;
|
|||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -46,7 +43,6 @@ public class Authorization extends AcmeResource {
|
|||
private Status status;
|
||||
private Instant expires;
|
||||
private List<Challenge> challenges;
|
||||
private List<List<Challenge>> combinations;
|
||||
private boolean loaded = false;
|
||||
|
||||
protected Authorization(Session session, URL location) {
|
||||
|
@ -101,17 +97,8 @@ public class Authorization extends AcmeResource {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets all combinations of challenges supported by the server.
|
||||
*/
|
||||
public List<List<Challenge>> getCombinations() {
|
||||
load();
|
||||
return combinations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single {@link Challenge} of the given type. Responding to this
|
||||
* {@link Challenge} is sufficient for authorization. This is a convenience call to
|
||||
* {@link #findCombination(String...)}.
|
||||
* Finds a {@link Challenge} of the given type. Responding to this {@link Challenge}
|
||||
* is sufficient for authorization.
|
||||
*
|
||||
* @param type
|
||||
* Challenge name (e.g. "http-01")
|
||||
|
@ -121,45 +108,11 @@ public class Authorization extends AcmeResource {
|
|||
* if the type does not match the expected Challenge class type
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Challenge> T findChallenge(String type) {
|
||||
return (T) findCombination(type).stream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a combination of {@link Challenge} types that the client supports. The client
|
||||
* has to respond to <em>all</em> of the {@link Challenge}s returned. However, this
|
||||
* method attempts to find the combination with the smallest number of
|
||||
* {@link Challenge}s.
|
||||
*
|
||||
* @param types
|
||||
* Challenge name or names (e.g. "http-01"), in no particular order.
|
||||
* Basically this is a collection of all challenge types supported by your
|
||||
* implementation.
|
||||
* @return Matching {@link Challenge} combination, or an empty collection if the ACME
|
||||
* server does not support any of your challenges. The challenges are returned
|
||||
* in no particular order. The result may be a subset of the types you have
|
||||
* provided, if fewer challenges are actually required for a successful
|
||||
* validation.
|
||||
*/
|
||||
public Collection<Challenge> findCombination(String... types) {
|
||||
Collection<String> available = Arrays.asList(types);
|
||||
Collection<String> combinationTypes = new ArrayList<>();
|
||||
|
||||
Collection<Challenge> result = Collections.emptyList();
|
||||
|
||||
for (List<Challenge> combination : getCombinations()) {
|
||||
combinationTypes.clear();
|
||||
for (Challenge c : combination) {
|
||||
combinationTypes.add(c.getType());
|
||||
}
|
||||
|
||||
if (available.containsAll(combinationTypes) &&
|
||||
(result.isEmpty() || result.size() > combination.size())) {
|
||||
result = combination;
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.unmodifiableCollection(result);
|
||||
public <T extends Challenge> T findChallenge(final String type) {
|
||||
return (T) getChallenges().stream()
|
||||
.filter(ch -> type.equals(ch.getType()))
|
||||
.reduce((a, b) -> {throw new AcmeProtocolException("Found more than one challenge of type " + type);})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -240,7 +193,6 @@ public class Authorization extends AcmeResource {
|
|||
}
|
||||
|
||||
challenges = fetchChallenges(json);
|
||||
combinations = fetchCombinations(json, challenges);
|
||||
|
||||
loaded = true;
|
||||
}
|
||||
|
@ -261,39 +213,4 @@ public class Authorization extends AcmeResource {
|
|||
.collect(toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all possible combination of {@link Challenge} that are defined in the JSON.
|
||||
*
|
||||
* @param json
|
||||
* {@link JSON} to read
|
||||
* @param challenges
|
||||
* List of available {@link Challenge}
|
||||
* @return List of {@link Challenge} combinations
|
||||
*/
|
||||
private List<List<Challenge>> fetchCombinations(JSON json, List<Challenge> challenges) {
|
||||
JSON.Array jsonCombinations = json.get("combinations").asArray();
|
||||
if (jsonCombinations.isEmpty()) {
|
||||
return Arrays.asList(challenges);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(jsonCombinations.stream()
|
||||
.map(JSON.Value::asArray)
|
||||
.map(this::findChallenges)
|
||||
.collect(toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of challenge indexes to a list of matching {@link Challenge}.
|
||||
*
|
||||
* @param combination
|
||||
* {@link Array} of the challenge indexes
|
||||
* @return List of matching {@link Challenge}
|
||||
*/
|
||||
private List<Challenge> findChallenges(JSON.Array combination) {
|
||||
return combination.stream()
|
||||
.mapToInt(JSON.Value::asInt)
|
||||
.mapToObj(challenges::get)
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2017 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;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.util.JSON;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Represents a certificate order.
|
||||
*/
|
||||
public class Order extends AcmeResource {
|
||||
private static final long serialVersionUID = 5435808648658292177L;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Order.class);
|
||||
|
||||
private Status status;
|
||||
private Instant expires;
|
||||
private byte[] csr;
|
||||
private Instant notBefore;
|
||||
private Instant notAfter;
|
||||
private List<URL> authorizations;
|
||||
private URL certificate;
|
||||
private boolean loaded = false;
|
||||
|
||||
protected Order(Session session, URL location) {
|
||||
super(session);
|
||||
setLocation(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@link Order} and binds it to the {@link Session}.
|
||||
*
|
||||
* @param session
|
||||
* {@link Session} to be used
|
||||
* @param location
|
||||
* Location URL of the order
|
||||
* @return {@link Order} bound to the session and location
|
||||
*/
|
||||
public static Order bind(Session session, URL location) {
|
||||
return new Order(session, location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current status of the order.
|
||||
*/
|
||||
public Status getStatus() {
|
||||
load();
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expiry date of the authorization, if set by the server.
|
||||
*/
|
||||
public Instant getExpires() {
|
||||
load();
|
||||
return expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the CSR that was used for the order.
|
||||
*/
|
||||
public byte[] getCsr() {
|
||||
load();
|
||||
return csr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "not before" date that was used for the order, or {@code null}.
|
||||
*/
|
||||
public Instant getNotBefore() {
|
||||
load();
|
||||
return notBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "not after" date that was used for the order, or {@code null}.
|
||||
*/
|
||||
public Instant getNotAfter() {
|
||||
load();
|
||||
return notAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Authorization} required for this order.
|
||||
*/
|
||||
public List<Authorization> getAuthorizations() {
|
||||
load();
|
||||
Session session = getSession();
|
||||
return authorizations.stream()
|
||||
.map(loc -> Authorization.bind(session, loc))
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link URL} where the certificate can be downloaded from, if it is
|
||||
* available. {@code null} otherwise.
|
||||
*/
|
||||
public URL getCertificateLocation() {
|
||||
load();
|
||||
return certificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the order to the current account status.
|
||||
*/
|
||||
public void update() throws AcmeException {
|
||||
LOG.debug("update");
|
||||
try (Connection conn = getSession().provider().connect()) {
|
||||
conn.sendRequest(getLocation(), getSession());
|
||||
conn.accept(HttpURLConnection.HTTP_OK);
|
||||
|
||||
JSON json = conn.readJsonResponse();
|
||||
unmarshal(json);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily updates the object's state when one of the getters is invoked.
|
||||
*/
|
||||
protected void load() {
|
||||
if (!loaded) {
|
||||
try {
|
||||
update();
|
||||
} catch (AcmeException ex) {
|
||||
throw new AcmeProtocolException("Could not load lazily", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets order properties according to the given JSON data.
|
||||
*
|
||||
* @param json
|
||||
* JSON data
|
||||
*/
|
||||
public void unmarshal(JSON json) {
|
||||
this.status = json.get("status").asStatusOrElse(Status.UNKNOWN);
|
||||
this.expires = json.get("expires").asInstant();
|
||||
this.csr = json.get("csr").asBinary();
|
||||
this.notBefore = json.get("notBefore").asInstant();
|
||||
this.notAfter = json.get("notAfter").asInstant();
|
||||
this.certificate = json.get("certificate").asURL();
|
||||
this.authorizations = json.get("authorizations").asArray().stream()
|
||||
.map(JSON.Value::asURL)
|
||||
.collect(toList());
|
||||
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
}
|
|
@ -158,6 +158,42 @@ public class Registration extends AcmeResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders a certificate. The certificate will be associated with this registration.
|
||||
*
|
||||
* @param csr
|
||||
* CSR containing the parameters for the certificate being requested
|
||||
* @param notBefore
|
||||
* Requested "notBefore" date in the certificate, or {@code null}
|
||||
* @param notAfter
|
||||
* Requested "notAfter" date in the certificate, or {@code null}
|
||||
* @return {@link Order} object for this domain
|
||||
*/
|
||||
public Order orderCertificate(byte[] csr, Instant notBefore, Instant notAfter) throws AcmeException {
|
||||
Objects.requireNonNull(csr, "csr");
|
||||
|
||||
LOG.debug("orderCertificate");
|
||||
try (Connection conn = getSession().provider().connect()) {
|
||||
JSONBuilder claims = new JSONBuilder();
|
||||
claims.putBase64("csr", csr);
|
||||
if (notBefore != null) {
|
||||
claims.put("notBefore", notBefore);
|
||||
}
|
||||
if (notAfter != null) {
|
||||
claims.put("notAfter", notAfter);
|
||||
}
|
||||
|
||||
conn.sendSignedRequest(getSession().resourceUrl(Resource.NEW_ORDER), claims, getSession());
|
||||
conn.accept(HttpURLConnection.HTTP_CREATED);
|
||||
|
||||
JSON json = conn.readJsonResponse();
|
||||
|
||||
Order order = new Order(getSession(), conn.getLocation());
|
||||
order.unmarshal(json);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorizes a domain. The domain is associated with this registration.
|
||||
* <p>
|
||||
|
|
|
@ -23,6 +23,7 @@ public enum Resource {
|
|||
NEW_AUTHZ("new-authz"),
|
||||
NEW_CERT("new-cert"),
|
||||
NEW_NONCE("new-nonce"),
|
||||
NEW_ORDER("new-order"),
|
||||
REVOKE_CERT("revoke-cert");
|
||||
|
||||
private final String path;
|
||||
|
|
|
@ -23,7 +23,6 @@ import java.net.HttpURLConnection;
|
|||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.Test;
|
||||
|
@ -32,6 +31,7 @@ import org.shredzone.acme4j.challenge.Dns01Challenge;
|
|||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||
import org.shredzone.acme4j.util.JSON;
|
||||
|
@ -43,12 +43,12 @@ import org.shredzone.acme4j.util.JSONBuilder;
|
|||
public class AuthorizationTest {
|
||||
|
||||
private static final String SNAILMAIL_TYPE = "snail-01"; // a non-existent challenge
|
||||
private static final String DUPLICATE_TYPE = "duplicate-01"; // a duplicate challenge
|
||||
|
||||
private URL locationUrl = url("http://example.com/acme/registration");
|
||||
|
||||
/**
|
||||
* Test that {@link Authorization#findChallenge(String)} does only find standalone
|
||||
* challenges, and nothing else.
|
||||
* Test that {@link Authorization#findChallenge(String)} finds challenges.
|
||||
*/
|
||||
@Test
|
||||
public void testFindChallenge() throws IOException {
|
||||
|
@ -58,60 +58,30 @@ public class AuthorizationTest {
|
|||
Challenge c1 = authorization.findChallenge(SNAILMAIL_TYPE);
|
||||
assertThat(c1, is(nullValue()));
|
||||
|
||||
// HttpChallenge is available as standalone challenge
|
||||
// HttpChallenge is available
|
||||
Challenge c2 = authorization.findChallenge(Http01Challenge.TYPE);
|
||||
assertThat(c2, is(notNullValue()));
|
||||
assertThat(c2, is(instanceOf(Http01Challenge.class)));
|
||||
|
||||
// TlsSniChallenge is available, but not as standalone challenge
|
||||
Challenge c3 = authorization.findChallenge(TlsSni02Challenge.TYPE);
|
||||
assertThat(c3, is(nullValue()));
|
||||
// Dns01Challenge is available
|
||||
Challenge c3 = authorization.findChallenge(Dns01Challenge.TYPE);
|
||||
assertThat(c3, is(notNullValue()));
|
||||
assertThat(c3, is(instanceOf(Dns01Challenge.class)));
|
||||
|
||||
// TlsSni02Challenge is available
|
||||
Challenge c4 = authorization.findChallenge(TlsSni02Challenge.TYPE);
|
||||
assertThat(c4, is(notNullValue()));
|
||||
assertThat(c4, is(instanceOf(TlsSni02Challenge.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that {@link Authorization#findCombination(String...)} does only find proper
|
||||
* combinations.
|
||||
* Test that {@link Authorization#findChallenge(String)} fails on duplicate
|
||||
* challenges.
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testFindCombination() throws IOException {
|
||||
@Test(expected = AcmeProtocolException.class)
|
||||
public void testFailDuplicateChallenges() throws IOException {
|
||||
Authorization authorization = createChallengeAuthorization();
|
||||
|
||||
// Standalone challenge
|
||||
Collection<Challenge> c1 = authorization.findCombination(Http01Challenge.TYPE);
|
||||
assertThat(c1, hasSize(1));
|
||||
assertThat(c1, contains(instanceOf(Http01Challenge.class)));
|
||||
|
||||
// Available combined challenge
|
||||
Collection<Challenge> c2 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE);
|
||||
assertThat(c2, hasSize(2));
|
||||
assertThat(c2, contains(instanceOf(Dns01Challenge.class),
|
||||
instanceOf(TlsSni02Challenge.class)));
|
||||
|
||||
// Order does not matter
|
||||
Collection<Challenge> c3 = authorization.findCombination(TlsSni02Challenge.TYPE, Dns01Challenge.TYPE);
|
||||
assertThat(c3, hasSize(2));
|
||||
assertThat(c3, contains(instanceOf(Dns01Challenge.class),
|
||||
instanceOf(TlsSni02Challenge.class)));
|
||||
|
||||
// Finds smaller combinations as well
|
||||
Collection<Challenge> c4 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE, SNAILMAIL_TYPE);
|
||||
assertThat(c4, hasSize(2));
|
||||
assertThat(c4, contains(instanceOf(Dns01Challenge.class),
|
||||
instanceOf(TlsSni02Challenge.class)));
|
||||
|
||||
// Finds the smallest possible combination
|
||||
Collection<Challenge> c5 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE, Http01Challenge.TYPE);
|
||||
assertThat(c5, hasSize(1));
|
||||
assertThat(c5, contains(instanceOf(Http01Challenge.class)));
|
||||
|
||||
// Finds only entire combinations
|
||||
Collection<Challenge> c6 = authorization.findCombination(Dns01Challenge.TYPE);
|
||||
assertThat(c6, is(empty()));
|
||||
|
||||
// Does not find challenges that have not been provided
|
||||
Collection<Challenge> c7 = authorization.findCombination(SNAILMAIL_TYPE);
|
||||
assertThat(c7, is(empty()));
|
||||
authorization.findChallenge(DUPLICATE_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -161,12 +131,6 @@ public class AuthorizationTest {
|
|||
assertThat(auth.getChallenges(), containsInAnyOrder(
|
||||
(Challenge) httpChallenge, (Challenge) dnsChallenge));
|
||||
|
||||
assertThat(auth.getCombinations(), hasSize(2));
|
||||
assertThat(auth.getCombinations().get(0), containsInAnyOrder(
|
||||
(Challenge) httpChallenge));
|
||||
assertThat(auth.getCombinations().get(1), containsInAnyOrder(
|
||||
(Challenge) httpChallenge, (Challenge) dnsChallenge));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
|
@ -279,12 +243,6 @@ public class AuthorizationTest {
|
|||
assertThat(auth.getChallenges(), containsInAnyOrder(
|
||||
(Challenge) httpChallenge, (Challenge) dnsChallenge));
|
||||
|
||||
assertThat(auth.getCombinations(), hasSize(2));
|
||||
assertThat(auth.getCombinations().get(0), containsInAnyOrder(
|
||||
(Challenge) httpChallenge));
|
||||
assertThat(auth.getCombinations().get(1), containsInAnyOrder(
|
||||
(Challenge) httpChallenge, (Challenge) dnsChallenge));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
|
@ -327,6 +285,7 @@ public class AuthorizationTest {
|
|||
provider.putTestChallenge(Http01Challenge.TYPE, new Http01Challenge(session));
|
||||
provider.putTestChallenge(Dns01Challenge.TYPE, new Dns01Challenge(session));
|
||||
provider.putTestChallenge(TlsSni02Challenge.TYPE, new TlsSni02Challenge(session));
|
||||
provider.putTestChallenge(DUPLICATE_TYPE, new Challenge(session));
|
||||
|
||||
Authorization authorization = new Authorization(session, locationUrl);
|
||||
authorization.unmarshalAuthorization(getJsonAsObject("authorizationChallenges"));
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2017 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;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp;
|
||||
import static org.shredzone.acme4j.util.TestUtils.*;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||
import org.shredzone.acme4j.util.JSON;
|
||||
import org.shredzone.acme4j.util.TestUtils;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Order}.
|
||||
*/
|
||||
public class OrderTest {
|
||||
|
||||
private URL locationUrl = url("http://example.com/acme/order/1234");
|
||||
|
||||
/**
|
||||
* Test that order is properly updated.
|
||||
*/
|
||||
@Test
|
||||
public void testUpdate() throws Exception {
|
||||
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
|
||||
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
public void sendRequest(URL url, Session session) {
|
||||
assertThat(url, is(locationUrl));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int accept(int... httpStatus) throws AcmeException {
|
||||
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_OK));
|
||||
return HttpURLConnection.HTTP_OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON readJsonResponse() {
|
||||
return getJsonAsObject("updateOrderResponse");
|
||||
}
|
||||
};
|
||||
|
||||
Session session = provider.createSession();
|
||||
|
||||
Order order = new Order(session, locationUrl);
|
||||
order.update();
|
||||
|
||||
assertThat(order.getStatus(), is(Status.PENDING));
|
||||
assertThat(order.getExpires(), is(parseTimestamp("2015-03-01T14:09:00Z")));
|
||||
assertThat(order.getLocation(), is(locationUrl));
|
||||
|
||||
assertThat(order.getNotBefore(), is(parseTimestamp("2016-01-01T00:00:00Z")));
|
||||
assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00:00:00Z")));
|
||||
assertThat(order.getCertificateLocation(), is(url("https://example.com/acme/cert/1234")));
|
||||
assertThat(order.getCsr(), is(csr));
|
||||
|
||||
List<Authorization> auths = order.getAuthorizations();
|
||||
assertThat(auths.size(), is(2));
|
||||
assertThat(auths.stream().map(Authorization::getLocation)::iterator,
|
||||
containsInAnyOrder(
|
||||
url("https://example.com/acme/authz/1234"),
|
||||
url("https://example.com/acme/authz/2345")));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test lazy loading.
|
||||
*/
|
||||
@Test
|
||||
public void testLazyLoading() throws Exception {
|
||||
final AtomicBoolean requestWasSent = new AtomicBoolean(false);
|
||||
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
public void sendRequest(URL url, Session session) {
|
||||
requestWasSent.set(true);
|
||||
assertThat(url, is(locationUrl));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int accept(int... httpStatus) throws AcmeException {
|
||||
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_OK));
|
||||
return HttpURLConnection.HTTP_OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON readJsonResponse() {
|
||||
return getJsonAsObject("updateOrderResponse");
|
||||
}
|
||||
};
|
||||
|
||||
Session session = provider.createSession();
|
||||
|
||||
Order order = new Order(session, locationUrl);
|
||||
|
||||
// Lazy loading
|
||||
assertThat(requestWasSent.get(), is(false));
|
||||
assertThat(order.getCertificateLocation(), is(url("https://example.com/acme/cert/1234")));
|
||||
assertThat(requestWasSent.get(), is(true));
|
||||
|
||||
// Subsequent queries do not trigger another load
|
||||
requestWasSent.set(false);
|
||||
assertThat(order.getCertificateLocation(), is(url("https://example.com/acme/cert/1234")));
|
||||
assertThat(order.getStatus(), is(Status.PENDING));
|
||||
assertThat(order.getExpires(), is(parseTimestamp("2015-03-01T14:09:00Z")));
|
||||
assertThat(requestWasSent.get(), is(false));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
}
|
|
@ -15,6 +15,7 @@ package org.shredzone.acme4j;
|
|||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp;
|
||||
import static org.shredzone.acme4j.util.TestUtils.*;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
||||
|
@ -31,6 +32,7 @@ import java.time.ZoneId;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.jose4j.jws.JsonWebSignature;
|
||||
|
@ -267,12 +269,6 @@ public class RegistrationTest {
|
|||
assertThat(auth.getChallenges(), containsInAnyOrder(
|
||||
(Challenge) httpChallenge, (Challenge) dnsChallenge));
|
||||
|
||||
assertThat(auth.getCombinations(), hasSize(2));
|
||||
assertThat(auth.getCombinations().get(0), containsInAnyOrder(
|
||||
(Challenge) httpChallenge));
|
||||
assertThat(auth.getCombinations().get(1), containsInAnyOrder(
|
||||
(Challenge) httpChallenge, (Challenge) dnsChallenge));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
|
@ -302,6 +298,66 @@ public class RegistrationTest {
|
|||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a order can be requested.
|
||||
*/
|
||||
@Test
|
||||
public void testOrder() throws AcmeException, IOException {
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
public void sendSignedRequest(URL url, JSONBuilder claims, Session session) {
|
||||
assertThat(url, is(resourceUrl));
|
||||
assertThat(claims.toString(), sameJSONAs(getJson("requestOrderRequest")));
|
||||
assertThat(session, is(notNullValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int accept(int... httpStatus) throws AcmeException {
|
||||
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_CREATED));
|
||||
return HttpURLConnection.HTTP_CREATED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON readJsonResponse() {
|
||||
return getJsonAsObject("requestOrderResponse");
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getLocation() {
|
||||
return locationUrl;
|
||||
}
|
||||
};
|
||||
|
||||
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
|
||||
|
||||
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
|
||||
ZoneId utc = ZoneId.of("UTC");
|
||||
Instant notBefore = LocalDate.of(2016, 1, 1).atStartOfDay(utc).toInstant();
|
||||
Instant notAfter = LocalDate.of(2016, 1, 8).atStartOfDay(utc).toInstant();
|
||||
|
||||
Registration registration = new Registration(provider.createSession(), locationUrl);
|
||||
|
||||
Order order = registration.orderCertificate(csr, notBefore, notAfter);
|
||||
|
||||
assertThat(order.getLocation(), is(locationUrl));
|
||||
assertThat(order.getCsr(), is(csr));
|
||||
assertThat(order.getStatus(), is(Status.PENDING));
|
||||
assertThat(order.getExpires(), is(parseTimestamp("2016-01-01T00:00:00Z")));
|
||||
assertThat(order.getLocation(), is(locationUrl));
|
||||
assertThat(order.getNotBefore(), is(notBefore));
|
||||
assertThat(order.getNotAfter(), is(notAfter));
|
||||
assertThat(order.getCertificateLocation(), is(nullValue()));
|
||||
|
||||
List<Authorization> auths = order.getAuthorizations();
|
||||
assertThat(auths.size(), is(2));
|
||||
assertThat(auths.stream().map(Authorization::getLocation)::iterator,
|
||||
containsInAnyOrder(
|
||||
url("https://example.com/acme/authz/1234"),
|
||||
url("https://example.com/acme/authz/2345")));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a certificate can be requested and is delivered synchronously.
|
||||
*/
|
||||
|
|
|
@ -32,11 +32,12 @@ public class ResourceTest {
|
|||
assertThat(Resource.NEW_AUTHZ.path(), is("new-authz"));
|
||||
assertThat(Resource.NEW_CERT.path(), is("new-cert"));
|
||||
assertThat(Resource.NEW_NONCE.path(), is("new-nonce"));
|
||||
assertThat(Resource.NEW_ORDER.path(), is("new-order"));
|
||||
assertThat(Resource.NEW_REG.path(), is("new-reg"));
|
||||
assertThat(Resource.REVOKE_CERT.path(), is("revoke-cert"));
|
||||
|
||||
// fails if there are untested future Resource values
|
||||
assertThat(Resource.values().length, is(6));
|
||||
assertThat(Resource.values().length, is(7));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2017 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.it;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.Authorization;
|
||||
import org.shredzone.acme4j.Order;
|
||||
import org.shredzone.acme4j.Registration;
|
||||
import org.shredzone.acme4j.RegistrationBuilder;
|
||||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.util.TestUtils;
|
||||
|
||||
/**
|
||||
* Tests the complete process of ordering a certificate.
|
||||
*/
|
||||
public class OrderIT extends AbstractPebbleIT {
|
||||
|
||||
@Test
|
||||
public void testOrder() throws AcmeException, IOException {
|
||||
KeyPair keyPair = createKeyPair();
|
||||
Session session = new Session(pebbleURI(), keyPair);
|
||||
|
||||
Registration reg = new RegistrationBuilder().agreeToTermsOfService().create(session);
|
||||
|
||||
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
|
||||
Instant notBefore = Instant.now();
|
||||
Instant notAfter = notBefore.plus(Duration.ofDays(20L));
|
||||
|
||||
Order order = reg.orderCertificate(csr, notBefore, notAfter);
|
||||
assertThat(order.getCsr(), is(csr));
|
||||
assertThat(order.getNotBefore(), is(notBefore));
|
||||
assertThat(order.getNotAfter(), is(notAfter));
|
||||
assertThat(order.getStatus(), is(Status.PENDING));
|
||||
|
||||
for (Authorization auth : order.getAuthorizations()) {
|
||||
processAuthorization(auth);
|
||||
}
|
||||
}
|
||||
|
||||
private void processAuthorization(Authorization auth) throws AcmeException {
|
||||
assertThat(auth.getDomain(), is("example.com"));
|
||||
if (auth.getStatus() == Status.PENDING) {
|
||||
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
|
||||
assertThat(challenge, is(notNullValue()));
|
||||
challenge.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -57,6 +57,7 @@ public class SessionIT extends AbstractPebbleIT {
|
|||
|
||||
assertIsPebbleUrl(session.resourceUrl(Resource.NEW_NONCE));
|
||||
assertIsPebbleUrl(session.resourceUrl(Resource.NEW_REG));
|
||||
assertIsPebbleUrl(session.resourceUrl(Resource.NEW_ORDER));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -100,8 +100,7 @@ newAuthorizationResponse = \
|
|||
"uri": "https://example.com/authz/asdf/1",\
|
||||
"token": "DGyRejmCefe7v4NfDGDKfA"\
|
||||
}\
|
||||
],\
|
||||
"combinations": [[0], [0,1]]\
|
||||
]\
|
||||
}
|
||||
|
||||
updateAuthorizationResponse = \
|
||||
|
@ -125,8 +124,41 @@ updateAuthorizationResponse = \
|
|||
"uri": "https://example.com/authz/asdf/1",\
|
||||
"token": "DGyRejmCefe7v4NfDGDKfA"\
|
||||
}\
|
||||
]\
|
||||
}
|
||||
|
||||
requestOrderRequest = \
|
||||
{\
|
||||
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb",\
|
||||
"notBefore": "2016-01-01T00:00:00Z",\
|
||||
"notAfter": "2016-01-08T00:00:00Z"\
|
||||
}
|
||||
|
||||
requestOrderResponse = \
|
||||
{\
|
||||
"status": "pending",\
|
||||
"expires": "2016-01-01T00:00:00Z",\
|
||||
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb",\
|
||||
"notBefore": "2016-01-01T00:00:00Z",\
|
||||
"notAfter": "2016-01-08T00:00:00Z",\
|
||||
"authorizations": [\
|
||||
"https://example.com/acme/authz/1234",\
|
||||
"https://example.com/acme/authz/2345"\
|
||||
]\
|
||||
}
|
||||
|
||||
updateOrderResponse = \
|
||||
{\
|
||||
"status": "pending",\
|
||||
"expires": "2015-03-01T14:09:00Z",\
|
||||
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb",\
|
||||
"notBefore": "2016-01-01T00:00:00Z",\
|
||||
"notAfter": "2016-01-08T00:00:00Z",\
|
||||
"authorizations": [\
|
||||
"https://example.com/acme/authz/1234",\
|
||||
"https://example.com/acme/authz/2345"\
|
||||
],\
|
||||
"combinations": [[0], [0,1]]\
|
||||
"certificate": "https://example.com/acme/cert/1234"\
|
||||
}
|
||||
|
||||
triggerHttpChallenge = \
|
||||
|
@ -207,9 +239,16 @@ authorizationChallenges = \
|
|||
"type": "tls-sni-02",\
|
||||
"uri": "https://example.com/authz/asdf/2",\
|
||||
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ"\
|
||||
},\
|
||||
{\
|
||||
"type": "duplicate-01",\
|
||||
"uri": "https://example.com/authz/asdf/3"\
|
||||
},\
|
||||
{\
|
||||
"type": "duplicate-01",\
|
||||
"uri": "https://example.com/authz/asdf/4"\
|
||||
}\
|
||||
],\
|
||||
"combinations": [[0], [1,2]]\
|
||||
]\
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue