org.shredzone.acme4j.challenge.Challenge Maven / Gradle / Ivy
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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.challenge;
import java.time.Instant;
import java.util.Optional;
import org.shredzone.acme4j.AcmeJsonResource;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSON.Value;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A generic challenge. It can be used as a base class for actual challenge
* implementations, but it is also used if the ACME server offers a proprietary challenge
* that is unknown to acme4j.
*
* Subclasses must override {@link Challenge#acceptable(String)} so it only accepts its
* own type. {@link Challenge#prepareResponse(JSONBuilder)} can be overridden to put all
* required data to the challenge response.
*/
public class Challenge extends AcmeJsonResource {
private static final long serialVersionUID = 2338794776848388099L;
private static final Logger LOG = LoggerFactory.getLogger(Challenge.class);
protected static final String KEY_TYPE = "type";
protected static final String KEY_URL = "url";
protected static final String KEY_STATUS = "status";
protected static final String KEY_VALIDATED = "validated";
protected static final String KEY_ERROR = "error";
/**
* Creates a new generic {@link Challenge} object.
*
* @param login
* {@link Login} the resource is bound with
* @param data
* {@link JSON} challenge data
*/
public Challenge(Login login, JSON data) {
super(login, data.get(KEY_URL).asURL());
setJSON(data);
}
/**
* Returns the challenge type by name (e.g. "http-01").
*/
public String getType() {
return getJSON().get(KEY_TYPE).asString();
}
/**
* Returns the current status of the challenge.
*
* Possible values are: {@link Status#PENDING}, {@link Status#PROCESSING},
* {@link Status#VALID}, {@link Status#INVALID}.
*
* A challenge is only completed when it reaches either status {@link Status#VALID} or
* {@link Status#INVALID}.
*/
public Status getStatus() {
return getJSON().get(KEY_STATUS).asStatus();
}
/**
* Returns the validation date, if returned by the server.
*/
public Optional getValidated() {
return getJSON().get(KEY_VALIDATED).map(Value::asInstant);
}
/**
* Returns a reason why the challenge has failed in the past, if returned by the
* server. If there are multiple errors, they can be found in
* {@link Problem#getSubProblems()}.
*/
public Optional getError() {
return getJSON().get(KEY_ERROR).map(it -> it.asProblem(getLocation()));
}
/**
* Prepares the response message for triggering the challenge. Subclasses can add
* fields to the {@link JSONBuilder} as required by the challenge. Implementations of
* subclasses should make sure that {@link #prepareResponse(JSONBuilder)} of the
* superclass is invoked.
*
* @param response
* {@link JSONBuilder} to write the response to
*/
protected void prepareResponse(JSONBuilder response) {
// Do nothing here...
}
/**
* Checks if the type is acceptable to this challenge. This generic class only checks
* if the type is not blank. Subclasses should instead check if the given type matches
* expected challenge type.
*
* @param type
* Type to check
* @return {@code true} if acceptable, {@code false} if not
*/
protected boolean acceptable(String type) {
return type != null && !type.trim().isEmpty();
}
@Override
protected void setJSON(JSON json) {
var type = json.get(KEY_TYPE).asString();
if (!acceptable(type)) {
throw new AcmeProtocolException("incompatible type " + type + " for this challenge");
}
var loc = json.get(KEY_URL).asString();
if (!loc.equals(getLocation().toString())) {
throw new AcmeProtocolException("challenge has changed its location");
}
super.setJSON(json);
}
/**
* Triggers this {@link Challenge}. The ACME server is requested to validate the
* response. Note that the validation is performed asynchronously by the ACME server.
*
* After a challenge is triggered, it changes to {@link Status#PENDING}. As soon as
* validation takes place, it changes to {@link Status#PROCESSING}. After validation
* the status changes to {@link Status#VALID} or {@link Status#INVALID}, depending on
* the outcome of the validation.
*
* If the challenge requires a resource to be set on your side (e.g. a DNS record or
* an HTTP file), it must be reachable from public before {@link #trigger()}
* is invoked, and must not be taken down until the challenge has reached
* {@link Status#VALID} or {@link Status#INVALID}.
*
* 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.
*/
public void trigger() throws AcmeException {
LOG.debug("trigger");
try (var conn = getSession().connect()) {
var claims = new JSONBuilder();
prepareResponse(claims);
conn.sendSignedRequest(getLocation(), claims, getLogin());
setJSON(conn.readJsonResponse());
}
}
}