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     * Gets a list of all challenges offered by the server, in no specific order.
088     */
089    public List<Challenge> getChallenges() {
090        var login = getLogin();
091
092        return getJSON().get("challenges")
093                .asArray()
094                .stream()
095                .map(Value::asObject)
096                .map(login::createChallenge)
097                .collect(toUnmodifiableList());
098    }
099
100    /**
101     * Finds a {@link Challenge} of the given type. Responding to this {@link Challenge}
102     * is sufficient for authorization.
103     * <p>
104     * {@link Authorization#findChallenge(Class)} should be preferred, as this variant
105     * is not type safe.
106     *
107     * @param type
108     *            Challenge name (e.g. "http-01")
109     * @return {@link Challenge} matching that name, or empty if there is no such
110     *         challenge, or if the challenge alone is not sufficient for authorization.
111     * @throws ClassCastException
112     *             if the type does not match the expected Challenge class type
113     */
114    @SuppressWarnings("unchecked")
115    public <T extends Challenge> Optional<T> findChallenge(final String type) {
116        return (Optional<T>) getChallenges().stream()
117                .filter(ch -> type.equals(ch.getType()))
118                .reduce((a, b) -> {
119                    throw new AcmeProtocolException("Found more than one challenge of type " + type);
120                });
121    }
122
123    /**
124     * Finds a {@link Challenge} of the given class type. Responding to this {@link
125     * Challenge} is sufficient for authorization.
126     *
127     * @param type
128     *         Challenge type (e.g. "Http01Challenge.class")
129     * @return {@link Challenge} of that type, or empty if there is no such
130     * challenge, or if the challenge alone is not sufficient for authorization.
131     * @since 2.8
132     */
133    public <T extends Challenge> Optional<T> findChallenge(Class<T> type) {
134        return getChallenges().stream()
135                .filter(type::isInstance)
136                .map(type::cast)
137                .reduce((a, b) -> {
138                    throw new AcmeProtocolException("Found more than one challenge of type " + type.getName());
139                });
140    }
141
142    /**
143     * Permanently deactivates the {@link Authorization}.
144     */
145    public void deactivate() throws AcmeException {
146        LOG.debug("deactivate");
147        try (var conn = getSession().connect()) {
148            var claims = new JSONBuilder();
149            claims.put("status", "deactivated");
150
151            conn.sendSignedRequest(getLocation(), claims, getLogin());
152            setJSON(conn.readJsonResponse());
153        }
154    }
155
156}