All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.shredzone.acme4j.Authorization Maven / Gradle / Ivy

/*
 * acme4j - Java ACME client
 *
 * Copyright (C) 2015 Richard "Shred" Körber
 *   http://acme4j.shredzone.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 */
package org.shredzone.acme4j;

import static java.util.stream.Collectors.toList;
import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp;

import java.net.HttpURLConnection;
import java.net.URI;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents an authorization request at the ACME server.
 */
public class Authorization extends AcmeResource {
    private static final long serialVersionUID = -3116928998379417741L;
    private static final Logger LOG = LoggerFactory.getLogger(Authorization.class);

    private String domain;
    private Status status;
    private Instant expires;
    private List challenges;
    private List> combinations;
    private boolean loaded = false;

    protected Authorization(Session session, URI location) {
        super(session);
        setLocation(location);
    }

    /**
     * Creates a new instance of {@link Authorization} and binds it to the
     * {@link Session}.
     *
     * @param session
     *            {@link Session} to be used
     * @param location
     *            Location of the Authorization
     * @return {@link Authorization} bound to the session and location
     */
    public static Authorization bind(Session session, URI location) {
        return new Authorization(session, location);
    }

    /**
     * Gets the domain name to be authorized.
     */
    public String getDomain() {
        load();
        return domain;
    }

    /**
     * Gets the authorization status.
     */
    public Status getStatus() {
        load();
        return status;
    }

    /**
     * Gets the expiry date of the authorization, if set by the server.
     */
    public Instant getExpires() {
        load();
        return expires;
    }

    /**
     * Gets a list of all challenges offered by the server.
     */
    public List getChallenges() {
        load();
        return challenges;
    }

    /**
     * Gets all combinations of challenges supported by the server.
     */
    public List> getCombinations() {
        load();
        return combinations;
    }

    /**
     * Finds a single {@link Challenge} of the given type. Responding to this
     * {@link Challenge} is sufficient for authorization. This is a convenience call to
     * {@link #findCombination(String...)}.
     *
     * @param type
     *            Challenge name (e.g. "http-01")
     * @return {@link Challenge} matching that name, or {@code null} if there is no such
     *         challenge, or if the challenge alone is not sufficient for authorization.
     * @throws ClassCastException
     *             if the type does not match the expected Challenge class type
     */
    @SuppressWarnings("unchecked")
    public  T findChallenge(String type) {
        return (T) findCombination(type).stream().findFirst().orElse(null);
    }

    /**
     * Finds a combination of {@link Challenge} types that the client supports. The client
     * has to respond to all of the {@link Challenge}s returned. However, this
     * method attempts to find the combination with the smallest number of
     * {@link Challenge}s.
     *
     * @param types
     *            Challenge name or names (e.g. "http-01"), in no particular order.
     *            Basically this is a collection of all challenge types supported by your
     *            implementation.
     * @return Matching {@link Challenge} combination, or an empty collection if the ACME
     *         server does not support any of your challenges. The challenges are returned
     *         in no particular order. The result may be a subset of the types you have
     *         provided, if fewer challenges are actually required for a successful
     *         validation.
     */
    public Collection findCombination(String... types) {
        Collection available = Arrays.asList(types);
        Collection combinationTypes = new ArrayList<>();

        Collection result = Collections.emptyList();

        for (List combination : getCombinations()) {
            combinationTypes.clear();
            for (Challenge c : combination) {
                combinationTypes.add(c.getType());
            }

            if (available.containsAll(combinationTypes) &&
                    (result.isEmpty() || result.size() > combination.size())) {
                result = combination;
            }
        }

        return Collections.unmodifiableCollection(result);
    }

    /**
     * Updates the {@link Authorization}. After invocation, the {@link Authorization}
     * reflects the current state at the ACME server.
     *
     * @throws AcmeRetryAfterException
     *             the auhtorization is still being validated, and the server returned an
     *             estimated date when the validation will be completed. If you are
     *             polling for the authorization to complete, you should wait for the date
     *             given in {@link AcmeRetryAfterException#getRetryAfter()}. Note that the
     *             authorization status is updated even if this exception was thrown.
     */
    public void update() throws AcmeException {
        LOG.debug("update");
        try (Connection conn = getSession().provider().connect()) {
            conn.sendRequest(getLocation(), getSession());
            conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED);

            unmarshalAuthorization(conn.readJsonResponse());

            conn.handleRetryAfter("authorization is not completed yet");
        }
    }

    /**
     * Permanently deactivates the {@link Authorization}.
     */
    public void deactivate() throws AcmeException {
        LOG.debug("deactivate");
        try (Connection conn = getSession().provider().connect()) {
            JSONBuilder claims = new JSONBuilder();
            claims.putResource("authz");
            claims.put("status", "deactivated");

            conn.sendSignedRequest(getLocation(), claims, getSession());
            conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED);
        }
    }

    /**
     * Lazily updates the object's state when one of the getters is invoked.
     */
    protected void load() {
        if (!loaded) {
            try {
                update();
            } catch (AcmeRetryAfterException ex) {
                // ignore... The object was still updated.
                LOG.debug("Retry-After", ex);
            } catch (AcmeException ex) {
                throw new AcmeProtocolException("Could not load lazily", ex);
            }
        }
    }

    /**
     * Sets the properties according to the given JSON data.
     *
     * @param json
     *            JSON data
     */
    protected void unmarshalAuthorization(JSON json) {
        this.status = Status.parse(json.get("status").asString(), Status.PENDING);

        String jsonExpires = json.get("expires").asString();
        if (jsonExpires != null) {
            expires = parseTimestamp(jsonExpires);
        }

        JSON jsonIdentifier = json.get("identifier").asObject();
        if (jsonIdentifier != null) {
            String type = jsonIdentifier.get("type").asString();
            if (type != null && !"dns".equals(type)) {
                throw new AcmeProtocolException("Unknown authorization type: " + type);
            }
            domain = jsonIdentifier.get("value").asString();
        }

        challenges = fetchChallenges(json);
        combinations = fetchCombinations(json, challenges);

        loaded = true;
    }

    /**
     * Fetches all {@link Challenge} that are defined in the JSON.
     *
     * @param json
     *            {@link JSON} to read
     * @return List of {@link Challenge}
     */
    private List fetchChallenges(JSON json) {
        Session session = getSession();

        return Collections.unmodifiableList(json.get("challenges").asArray().stream()
                .map(JSON.Value::asObject)
                .map(session::createChallenge)
                .collect(toList()));
    }

    /**
     * Fetches all possible combination of {@link Challenge} that are defined in the JSON.
     *
     * @param json
     *            {@link JSON} to read
     * @param challenges
     *            List of available {@link Challenge}
     * @return List of {@link Challenge} combinations
     */
    private List> fetchCombinations(JSON json, List challenges) {
        JSON.Array jsonCombinations = json.get("combinations").asArray();
        if (jsonCombinations == null) {
            return Arrays.asList(challenges);
        }

        return Collections.unmodifiableList(jsonCombinations.stream()
                .map(JSON.Value::asArray)
                .map(this::findChallenges)
                .collect(toList()));
    }

    /**
     * Converts an array of challenge indexes to a list of matching {@link Challenge}.
     *
     * @param combination
     *            {@link Array} of the challenge indexes
     * @return List of matching {@link Challenge}
     */
    private List findChallenges(JSON.Array combination) {
        return combination.stream()
               .mapToInt(JSON.Value::asInt)
               .mapToObj(challenges::get)
               .collect(toList());
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy