001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2015 Richard "Shred" Körber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j.challenge;
015
016import java.time.Duration;
017import java.time.Instant;
018import java.util.EnumSet;
019import java.util.Optional;
020
021import org.shredzone.acme4j.AcmeJsonResource;
022import org.shredzone.acme4j.Login;
023import org.shredzone.acme4j.PollableResource;
024import org.shredzone.acme4j.Problem;
025import org.shredzone.acme4j.Status;
026import org.shredzone.acme4j.exception.AcmeException;
027import org.shredzone.acme4j.exception.AcmeProtocolException;
028import org.shredzone.acme4j.toolbox.JSON;
029import org.shredzone.acme4j.toolbox.JSON.Value;
030import org.shredzone.acme4j.toolbox.JSONBuilder;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * A generic challenge. It can be used as a base class for actual challenge
036 * implementations, but it is also used if the ACME server offers a proprietary challenge
037 * that is unknown to acme4j.
038 * <p>
039 * Subclasses must override {@link Challenge#acceptable(String)} so it only accepts its
040 * own type. {@link Challenge#prepareResponse(JSONBuilder)} can be overridden to put all
041 * required data to the challenge response.
042 */
043public class Challenge extends AcmeJsonResource implements PollableResource {
044    private static final long serialVersionUID = 2338794776848388099L;
045    private static final Logger LOG = LoggerFactory.getLogger(Challenge.class);
046
047    protected static final String KEY_TYPE = "type";
048    protected static final String KEY_URL = "url";
049    protected static final String KEY_STATUS = "status";
050    protected static final String KEY_VALIDATED = "validated";
051    protected static final String KEY_ERROR = "error";
052
053    /**
054     * Creates a new generic {@link Challenge} object.
055     *
056     * @param login
057     *            {@link Login} the resource is bound with
058     * @param data
059     *            {@link JSON} challenge data
060     */
061    public Challenge(Login login, JSON data) {
062        super(login, data.get(KEY_URL).asURL());
063        setJSON(data);
064    }
065
066    /**
067     * Returns the challenge type by name (e.g. "http-01").
068     */
069    public String getType() {
070        return getJSON().get(KEY_TYPE).asString();
071    }
072
073    /**
074     * Returns the current status of the challenge.
075     * <p>
076     * Possible values are: {@link Status#PENDING}, {@link Status#PROCESSING},
077     * {@link Status#VALID}, {@link Status#INVALID}.
078     * <p>
079     * A challenge is only completed when it reaches either status {@link Status#VALID} or
080     * {@link Status#INVALID}.
081     */
082    @Override
083    public Status getStatus() {
084        return getJSON().get(KEY_STATUS).asStatus();
085    }
086
087    /**
088     * Returns the validation date, if returned by the server.
089     */
090    public Optional<Instant> getValidated() {
091        return getJSON().get(KEY_VALIDATED).map(Value::asInstant);
092    }
093
094    /**
095     * Returns a reason why the challenge has failed in the past, if returned by the
096     * server. If there are multiple errors, they can be found in
097     * {@link Problem#getSubProblems()}.
098     */
099    public Optional<Problem> getError() {
100        return getJSON().get(KEY_ERROR).map(it -> it.asProblem(getLocation()));
101    }
102
103    /**
104     * Prepares the response message for triggering the challenge. Subclasses can add
105     * fields to the {@link JSONBuilder} as required by the challenge. Implementations of
106     * subclasses should make sure that {@link #prepareResponse(JSONBuilder)} of the
107     * superclass is invoked.
108     *
109     * @param response
110     *         {@link JSONBuilder} to write the response to
111     */
112    protected void prepareResponse(JSONBuilder response) {
113        // Do nothing here...
114    }
115
116    /**
117     * Checks if the type is acceptable to this challenge. This generic class only checks
118     * if the type is not blank. Subclasses should instead check if the given type matches
119     * expected challenge type.
120     *
121     * @param type
122     *         Type to check
123     * @return {@code true} if acceptable, {@code false} if not
124     */
125    protected boolean acceptable(String type) {
126        return type != null && !type.trim().isEmpty();
127    }
128
129    @Override
130    protected void setJSON(JSON json) {
131        var type = json.get(KEY_TYPE).asString();
132
133        if (!acceptable(type)) {
134            throw new AcmeProtocolException("incompatible type " + type + " for this challenge");
135        }
136
137        var loc = json.get(KEY_URL).asString();
138        if (!loc.equals(getLocation().toString())) {
139            throw new AcmeProtocolException("challenge has changed its location");
140        }
141
142        super.setJSON(json);
143    }
144
145    /**
146     * Triggers this {@link Challenge}. The ACME server is requested to validate the
147     * response. Note that the validation is performed asynchronously by the ACME server.
148     * <p>
149     * After a challenge is triggered, it changes to {@link Status#PENDING}. As soon as
150     * validation takes place, it changes to {@link Status#PROCESSING}. After validation
151     * the status changes to {@link Status#VALID} or {@link Status#INVALID}, depending on
152     * the outcome of the validation.
153     * <p>
154     * If the challenge requires a resource to be set on your side (e.g. a DNS record or
155     * an HTTP file), it <em>must</em> be reachable from public before {@link #trigger()}
156     * is invoked, and <em>must not</em> be taken down until the challenge has reached
157     * {@link Status#VALID} or {@link Status#INVALID}.
158     * <p>
159     * If this method is invoked a second time, the ACME server is requested to retry the
160     * validation. This can be useful if the client state has changed, for example after a
161     * firewall rule has been updated.
162     *
163     * @see #waitForCompletion(Duration)
164     */
165    public void trigger() throws AcmeException {
166        LOG.debug("trigger");
167        try (var conn = getSession().connect()) {
168            var claims = new JSONBuilder();
169            prepareResponse(claims);
170
171            conn.sendSignedRequest(getLocation(), claims, getLogin());
172            setJSON(conn.readJsonResponse());
173        }
174    }
175
176    /**
177     * Waits until the challenge is completed.
178     * <p>
179     * Is is completed if it reaches either {@link Status#VALID} or
180     * {@link Status#INVALID}.
181     * <p>
182     * This method is synchronous and blocks the current thread.
183     *
184     * @param timeout
185     *         Timeout until a terminal status must have been reached
186     * @return Status that was reached
187     * @since 3.4.0
188     */
189    public Status waitForCompletion(Duration timeout)
190            throws AcmeException, InterruptedException {
191        return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);
192    }
193
194}