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;
015
016import static java.util.stream.Collectors.toUnmodifiableList;
017
018import java.net.URL;
019import java.time.Duration;
020import java.time.Instant;
021import java.util.EnumSet;
022import java.util.List;
023import java.util.Optional;
024
025import org.shredzone.acme4j.challenge.Challenge;
026import org.shredzone.acme4j.exception.AcmeException;
027import org.shredzone.acme4j.exception.AcmeProtocolException;
028import org.shredzone.acme4j.toolbox.AcmeUtils;
029import org.shredzone.acme4j.toolbox.JSON.Value;
030import org.shredzone.acme4j.toolbox.JSONBuilder;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * Represents an authorization request at the ACME server.
036 */
037public class Authorization extends AcmeJsonResource implements PollableResource {
038    private static final long serialVersionUID = -3116928998379417741L;
039    private static final Logger LOG = LoggerFactory.getLogger(Authorization.class);
040
041    protected Authorization(Login login, URL location) {
042        super(login, location);
043    }
044
045    /**
046     * Gets the {@link Identifier} to be authorized.
047     * <p>
048     * For wildcard domain orders, the domain itself (without wildcard prefix) is returned
049     * here. To find out if this {@link Authorization} is related to a wildcard domain
050     * order, check the {@link #isWildcard()} method.
051     *
052     * @since 2.3
053     */
054    public Identifier getIdentifier() {
055        return getJSON().get("identifier").asIdentifier();
056    }
057
058    /**
059     * Gets the authorization status.
060     * <p>
061     * Possible values are: {@link Status#PENDING}, {@link Status#VALID},
062     * {@link Status#INVALID}, {@link Status#DEACTIVATED}, {@link Status#EXPIRED},
063     * {@link Status#REVOKED}.
064     */
065    @Override
066    public Status getStatus() {
067        return getJSON().get("status").asStatus();
068    }
069
070    /**
071     * Gets the expiry date of the authorization, if set by the server.
072     */
073    public Optional<Instant> getExpires() {
074        return getJSON().get("expires")
075                    .map(Value::asString)
076                    .map(AcmeUtils::parseTimestamp);
077    }
078
079    /**
080     * Returns {@code true} if this {@link Authorization} is related to a wildcard domain,
081     * {@code false} otherwise.
082     */
083    public boolean isWildcard() {
084        return getJSON().get("wildcard")
085                    .map(Value::asBoolean)
086                    .orElse(false);
087    }
088
089    /**
090     * Returns {@code true} if certificates for subdomains can be issued according to
091     * RFC9444.
092     *
093     * @since 3.3.0
094     */
095    public boolean isSubdomainAuthAllowed() {
096        return getJSON().get("subdomainAuthAllowed")
097                .map(Value::asBoolean)
098                .orElse(false);
099    }
100
101    /**
102     * Gets a list of all challenges offered by the server, in no specific order.
103     */
104    public List<Challenge> getChallenges() {
105        var login = getLogin();
106
107        return getJSON().get("challenges")
108                .asArray()
109                .stream()
110                .map(Value::asObject)
111                .map(login::createChallenge)
112                .collect(toUnmodifiableList());
113    }
114
115    /**
116     * Finds a {@link Challenge} of the given type. Responding to this {@link Challenge}
117     * is sufficient for authorization.
118     * <p>
119     * {@link Authorization#findChallenge(Class)} should be preferred, as this variant
120     * is not type safe.
121     *
122     * @param type
123     *            Challenge name (e.g. "http-01")
124     * @return {@link Challenge} matching that name, or empty if there is no such
125     *         challenge, or if the challenge alone is not sufficient for authorization.
126     * @throws ClassCastException
127     *             if the type does not match the expected Challenge class type
128     */
129    @SuppressWarnings("unchecked")
130    public <T extends Challenge> Optional<T> findChallenge(final String type) {
131        return (Optional<T>) getChallenges().stream()
132                .filter(ch -> type.equals(ch.getType()))
133                .reduce((a, b) -> {
134                    throw new AcmeProtocolException("Found more than one challenge of type " + type);
135                });
136    }
137
138    /**
139     * Finds a {@link Challenge} of the given class type. Responding to this {@link
140     * Challenge} is sufficient for authorization.
141     *
142     * @param type
143     *         Challenge type (e.g. "Http01Challenge.class")
144     * @return {@link Challenge} of that type, or empty if there is no such
145     * challenge, or if the challenge alone is not sufficient for authorization.
146     * @since 2.8
147     */
148    public <T extends Challenge> Optional<T> findChallenge(Class<T> type) {
149        return getChallenges().stream()
150                .filter(type::isInstance)
151                .map(type::cast)
152                .reduce((a, b) -> {
153                    throw new AcmeProtocolException("Found more than one challenge of type " + type.getName());
154                });
155    }
156
157    /**
158     * Waits until the authorization is completed.
159     * <p>
160     * Is is completed if it reaches either {@link Status#VALID} or
161     * {@link Status#INVALID}.
162     * <p>
163     * This method is synchronous and blocks the current thread.
164     *
165     * @param timeout
166     *         Timeout until a terminal status must have been reached
167     * @return Status that was reached
168     * @since 3.4.0
169     */
170    public Status waitForCompletion(Duration timeout)
171            throws AcmeException, InterruptedException {
172        return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);
173    }
174
175    /**
176     * Permanently deactivates the {@link Authorization}.
177     */
178    public void deactivate() throws AcmeException {
179        LOG.debug("deactivate");
180        try (var conn = getSession().connect()) {
181            var claims = new JSONBuilder();
182            claims.put("status", "deactivated");
183
184            conn.sendSignedRequest(getLocation(), claims, getLogin());
185            setJSON(conn.readJsonResponse());
186        }
187    }
188
189}