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}