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