Add new methods for status change busy waiting

This commit is contained in:
Richard Körber
2024-08-17 17:20:52 +02:00
parent ae60431a79
commit b897dc277d
11 changed files with 258 additions and 217 deletions

View File

@@ -16,7 +16,9 @@ package org.shredzone.acme4j;
import static java.util.stream.Collectors.toUnmodifiableList;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
@@ -32,7 +34,7 @@ import org.slf4j.LoggerFactory;
/**
* Represents an authorization request at the ACME server.
*/
public class Authorization extends AcmeJsonResource {
public class Authorization extends AcmeJsonResource implements PollableResource {
private static final long serialVersionUID = -3116928998379417741L;
private static final Logger LOG = LoggerFactory.getLogger(Authorization.class);
@@ -60,6 +62,7 @@ public class Authorization extends AcmeJsonResource {
* {@link Status#INVALID}, {@link Status#DEACTIVATED}, {@link Status#EXPIRED},
* {@link Status#REVOKED}.
*/
@Override
public Status getStatus() {
return getJSON().get("status").asStatus();
}
@@ -151,6 +154,24 @@ public class Authorization extends AcmeJsonResource {
});
}
/**
* Waits until the authorization is completed.
* <p>
* Is is completed if it reaches either {@link Status#VALID} or
* {@link Status#INVALID}.
* <p>
* This method is synchronous and blocks the current thread.
*
* @param timeout
* Timeout until a terminal status must have been reached
* @return Status that was reached
* @since 3.4.0
*/
public Status waitForCompletion(Duration timeout)
throws AcmeException, InterruptedException {
return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);
}
/**
* Permanently deactivates the {@link Authorization}.
*/

View File

@@ -22,6 +22,7 @@ import java.net.URL;
import java.security.KeyPair;
import java.time.Duration;
import java.time.Instant;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
@@ -41,7 +42,7 @@ import org.slf4j.LoggerFactory;
/**
* A representation of a certificate order at the CA.
*/
public class Order extends AcmeJsonResource {
public class Order extends AcmeJsonResource implements PollableResource {
private static final long serialVersionUID = 5435808648658292177L;
private static final Logger LOG = LoggerFactory.getLogger(Order.class);
@@ -60,6 +61,7 @@ public class Order extends AcmeJsonResource {
* {@link Status#PROCESSING}, {@link Status#VALID}, {@link Status#INVALID}.
* If the server supports STAR, another possible value is {@link Status#CANCELED}.
*/
@Override
public Status getStatus() {
return getJSON().get("status").asStatus();
}
@@ -186,6 +188,8 @@ public class Order extends AcmeJsonResource {
* @see #execute(KeyPair, Consumer)
* @see #execute(PKCS10CertificationRequest)
* @see #execute(byte[])
* @see #waitUntilReady(Duration)
* @see #waitForCompletion(Duration)
* @since 3.0.0
*/
public void execute(KeyPair domainKeyPair) throws AcmeException {
@@ -208,6 +212,8 @@ public class Order extends AcmeJsonResource {
* @see #execute(KeyPair)
* @see #execute(PKCS10CertificationRequest)
* @see #execute(byte[])
* @see #waitUntilReady(Duration)
* @see #waitForCompletion(Duration)
* @since 3.0.0
*/
public void execute(KeyPair domainKeyPair, Consumer<CSRBuilder> builderConsumer) throws AcmeException {
@@ -235,6 +241,8 @@ public class Order extends AcmeJsonResource {
* @see #execute(KeyPair)
* @see #execute(KeyPair, Consumer)
* @see #execute(byte[])
* @see #waitUntilReady(Duration)
* @see #waitForCompletion(Duration)
* @since 3.0.0
*/
public void execute(PKCS10CertificationRequest csr) throws AcmeException {
@@ -256,6 +264,8 @@ public class Order extends AcmeJsonResource {
* @param csr
* Binary representation of a CSR containing the parameters for the
* certificate being requested, in DER format
* @see #waitUntilReady(Duration)
* @see #waitForCompletion(Duration)
*/
public void execute(byte[] csr) throws AcmeException {
LOG.debug("finalize");
@@ -268,6 +278,43 @@ public class Order extends AcmeJsonResource {
invalidate();
}
/**
* Waits until the order is ready for finalization.
* <p>
* Is is ready if it reaches {@link Status#READY}. The method will also return if the
* order already has another terminal state, which is either {@link Status#VALID} or
* {@link Status#INVALID}.
* <p>
* This method is synchronous and blocks the current thread.
*
* @param timeout
* Timeout until a terminal status must have been reached
* @return Status that was reached
* @since 3.4.0
*/
public Status waitUntilReady(Duration timeout)
throws AcmeException, InterruptedException {
return waitForStatus(EnumSet.of(Status.READY, Status.VALID, Status.INVALID), timeout);
}
/**
* Waits until the order finalization is completed.
* <p>
* Is is completed if it reaches either {@link Status#VALID} or
* {@link Status#INVALID}.
* <p>
* This method is synchronous and blocks the current thread.
*
* @param timeout
* Timeout until a terminal status must have been reached
* @return Status that was reached
* @since 3.4.0
*/
public Status waitForCompletion(Duration timeout)
throws AcmeException, InterruptedException {
return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);
}
/**
* Checks if this order is auto-renewing, according to the ACME STAR specifications.
*

View File

@@ -0,0 +1,107 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2024 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.time.Instant.now;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.shredzone.acme4j.exception.AcmeException;
/**
* Marks an ACME Resource with a pollable status.
* <p>
* The resource provides a status, and a method for updating the internal cache to read
* the current status from the server.
*
* @since 3.4.0
*/
public interface PollableResource {
/**
* Default delay between status polls if there is no Retry-After header.
*/
Duration DEFAULT_RETRY_AFTER = Duration.ofSeconds(3L);
/**
* Returns the current status of the resource.
*/
Status getStatus();
/**
* Fetches the current status from the server.
*
* @return Retry-After time, if given by the CA, otherwise empty.
*/
Optional<Instant> fetch() throws AcmeException;
/**
* Waits until a terminal status has been reached, by polling until one of the given
* status or the given timeout has been reached. This call honors the Retry-After
* header if set by the CA.
* <p>
* This method is synchronous and blocks the current thread.
* <p>
* If the resource is already in a terminal status, the method returns immediately.
*
* @param statusSet
* Set of {@link Status} that are accepted as terminal
* @param timeout
* Timeout until a terminal status must have been reached
* @return Status that was reached
*/
default Status waitForStatus(Set<Status> statusSet, Duration timeout)
throws AcmeException, InterruptedException {
Objects.requireNonNull(timeout, "timeout");
Objects.requireNonNull(statusSet, "statusSet");
if (statusSet.isEmpty()) {
throw new IllegalArgumentException("At least one Status is required");
}
var currentStatus = getStatus();
if (statusSet.contains(currentStatus)) {
return currentStatus;
}
var timebox = now().plus(timeout);
Instant now;
while ((now = now()).isBefore(timebox)) {
// Poll status and get the time of the next poll
var retryAfter = fetch()
.orElse(now.plus(DEFAULT_RETRY_AFTER));
currentStatus = getStatus();
if (statusSet.contains(currentStatus)) {
return currentStatus;
}
// Preemptively end the loop if the next iteration would be after timebox
if (retryAfter.isAfter(timebox)) {
break;
}
// Wait until retryAfter is reached
Thread.sleep(now.until(retryAfter, ChronoUnit.MILLIS));
}
throw new AcmeException("Timeout has been reached");
}
}

View File

@@ -13,11 +13,14 @@
*/
package org.shredzone.acme4j.challenge;
import java.time.Duration;
import java.time.Instant;
import java.util.EnumSet;
import java.util.Optional;
import org.shredzone.acme4j.AcmeJsonResource;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.PollableResource;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.exception.AcmeException;
@@ -37,7 +40,7 @@ import org.slf4j.LoggerFactory;
* own type. {@link Challenge#prepareResponse(JSONBuilder)} can be overridden to put all
* required data to the challenge response.
*/
public class Challenge extends AcmeJsonResource {
public class Challenge extends AcmeJsonResource implements PollableResource {
private static final long serialVersionUID = 2338794776848388099L;
private static final Logger LOG = LoggerFactory.getLogger(Challenge.class);
@@ -76,6 +79,7 @@ public class Challenge extends AcmeJsonResource {
* A challenge is only completed when it reaches either status {@link Status#VALID} or
* {@link Status#INVALID}.
*/
@Override
public Status getStatus() {
return getJSON().get(KEY_STATUS).asStatus();
}
@@ -155,6 +159,8 @@ public class Challenge extends AcmeJsonResource {
* If this method is invoked a second time, the ACME server is requested to retry the
* validation. This can be useful if the client state has changed, for example after a
* firewall rule has been updated.
*
* @see #waitForCompletion(Duration)
*/
public void trigger() throws AcmeException {
LOG.debug("trigger");
@@ -167,4 +173,22 @@ public class Challenge extends AcmeJsonResource {
}
}
/**
* Waits until the challenge is completed.
* <p>
* Is is completed if it reaches either {@link Status#VALID} or
* {@link Status#INVALID}.
* <p>
* This method is synchronous and blocks the current thread.
*
* @param timeout
* Timeout until a terminal status must have been reached
* @return Status that was reached
* @since 3.4.0
*/
public Status waitForCompletion(Duration timeout)
throws AcmeException, InterruptedException {
return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);
}
}