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