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