001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2017 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.Objects.requireNonNull; 017import static java.util.stream.Collectors.toList; 018 019import java.net.URL; 020import java.time.Duration; 021import java.time.Instant; 022import java.util.Collection; 023import java.util.LinkedHashSet; 024import java.util.Set; 025 026import edu.umd.cs.findbugs.annotations.Nullable; 027import org.shredzone.acme4j.connector.Connection; 028import org.shredzone.acme4j.connector.Resource; 029import org.shredzone.acme4j.exception.AcmeException; 030import org.shredzone.acme4j.exception.AcmeProtocolException; 031import org.shredzone.acme4j.toolbox.JSONBuilder; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035/** 036 * A builder for a new {@link Order} object. 037 */ 038public class OrderBuilder { 039 private static final Logger LOG = LoggerFactory.getLogger(OrderBuilder.class); 040 041 private final Login login; 042 043 private final Set<Identifier> identifierSet = new LinkedHashSet<>(); 044 private @Nullable Instant notBefore; 045 private @Nullable Instant notAfter; 046 private boolean autoRenewal; 047 private @Nullable Instant autoRenewalStart; 048 private @Nullable Instant autoRenewalEnd; 049 private @Nullable Duration autoRenewalLifetime; 050 private @Nullable Duration autoRenewalLifetimeAdjust; 051 private boolean autoRenewalGet; 052 053 /** 054 * Create a new {@link OrderBuilder}. 055 * 056 * @param login 057 * {@link Login} to bind with 058 */ 059 protected OrderBuilder(Login login) { 060 this.login = login; 061 } 062 063 /** 064 * Adds a domain name to the order. 065 * 066 * @param domain 067 * Name of a domain to be ordered. May be a wildcard domain if supported by 068 * the CA. IDN names are accepted and will be ACE encoded automatically. 069 * @return itself 070 */ 071 public OrderBuilder domain(String domain) { 072 return identifier(Identifier.dns(domain)); 073 } 074 075 /** 076 * Adds domain names to the order. 077 * 078 * @param domains 079 * Collection of domain names to be ordered. May be wildcard domains if 080 * supported by the CA. IDN names are accepted and will be ACE encoded 081 * automatically. 082 * @return itself 083 */ 084 public OrderBuilder domains(String... domains) { 085 for (String domain : requireNonNull(domains, "domains")) { 086 domain(domain); 087 } 088 return this; 089 } 090 091 /** 092 * Adds a collection of domain names to the order. 093 * 094 * @param domains 095 * Collection of domain names to be ordered. May be wildcard domains if 096 * supported by the CA. IDN names are accepted and will be ACE encoded 097 * automatically. 098 * @return itself 099 */ 100 public OrderBuilder domains(Collection<String> domains) { 101 requireNonNull(domains, "domains").forEach(this::domain); 102 return this; 103 } 104 105 /** 106 * Adds an {@link Identifier} to the order. 107 * 108 * @param identifier 109 * {@link Identifier} to be added to the order. 110 * @return itself 111 * @since 2.3 112 */ 113 public OrderBuilder identifier(Identifier identifier) { 114 identifierSet.add(requireNonNull(identifier, "identifier")); 115 return this; 116 } 117 118 /** 119 * Adds a collection of {@link Identifier} to the order. 120 * 121 * @param identifiers 122 * Collection of {@link Identifier} to be added to the order. 123 * @return itself 124 * @since 2.3 125 */ 126 public OrderBuilder identifiers(Collection<Identifier> identifiers) { 127 requireNonNull(identifiers, "identifiers").forEach(this::identifier); 128 return this; 129 } 130 131 /** 132 * Sets a "not before" date in the certificate. May be ignored by the CA. 133 * 134 * @param notBefore "not before" date 135 * @return itself 136 */ 137 public OrderBuilder notBefore(Instant notBefore) { 138 if (autoRenewal) { 139 throw new IllegalArgumentException("cannot combine notBefore with autoRenew"); 140 } 141 this.notBefore = requireNonNull(notBefore, "notBefore"); 142 return this; 143 } 144 145 /** 146 * Sets a "not after" date in the certificate. May be ignored by the CA. 147 * 148 * @param notAfter "not after" date 149 * @return itself 150 */ 151 public OrderBuilder notAfter(Instant notAfter) { 152 if (autoRenewal) { 153 throw new IllegalArgumentException("cannot combine notAfter with autoRenew"); 154 } 155 this.notAfter = requireNonNull(notAfter, "notAfter"); 156 return this; 157 } 158 159 /** 160 * Enables short-term automatic renewal of the certificate. Must be supported by the 161 * CA. 162 * <p> 163 * Automatic renewals cannot be combined with {@link #notBefore(Instant)} or {@link 164 * #notAfter(Instant)}. 165 * 166 * @return itself 167 * @since 2.3 168 */ 169 public OrderBuilder autoRenewal() { 170 if (notBefore != null || notAfter != null) { 171 throw new IllegalArgumentException("cannot combine notBefore/notAfter with autoRenewalOr"); 172 } 173 this.autoRenewal = true; 174 return this; 175 } 176 177 /** 178 * Sets the earliest date of validity of the first issued certificate. If not set, 179 * the start date is the earliest possible date. 180 * <p> 181 * Implies {@link #autoRenewal()}. 182 * 183 * @param start 184 * Start date of validity 185 * @return itself 186 * @since 2.3 187 */ 188 public OrderBuilder autoRenewalStart(Instant start) { 189 autoRenewal(); 190 this.autoRenewalStart = requireNonNull(start, "start"); 191 return this; 192 } 193 194 /** 195 * Sets the latest date of validity of the last issued certificate. If not set, the 196 * CA's default is used. 197 * <p> 198 * Implies {@link #autoRenewal()}. 199 * 200 * @param end 201 * End date of validity 202 * @return itself 203 * @see Metadata#getAutoRenewalMaxDuration() 204 * @since 2.3 205 */ 206 public OrderBuilder autoRenewalEnd(Instant end) { 207 autoRenewal(); 208 this.autoRenewalEnd = requireNonNull(end, "end"); 209 return this; 210 } 211 212 /** 213 * Sets the maximum validity period of each certificate. If not set, the CA's 214 * default is used. 215 * <p> 216 * Implies {@link #autoRenewal()}. 217 * 218 * @param duration 219 * Duration of validity of each certificate 220 * @return itself 221 * @see Metadata#getAutoRenewalMinLifetime() 222 * @since 2.3 223 */ 224 public OrderBuilder autoRenewalLifetime(Duration duration) { 225 autoRenewal(); 226 this.autoRenewalLifetime = requireNonNull(duration, "duration"); 227 return this; 228 } 229 230 /** 231 * Sets the amount of pre-dating each certificate. If not set, the CA's 232 * default (0) is used. 233 * <p> 234 * Implies {@link #autoRenewal()}. 235 * 236 * @param duration 237 * Duration of certificate pre-dating 238 * @return itself 239 * @since 2.7 240 */ 241 public OrderBuilder autoRenewalLifetimeAdjust(Duration duration) { 242 autoRenewal(); 243 this.autoRenewalLifetimeAdjust = requireNonNull(duration, "duration"); 244 return this; 245 } 246 247 /** 248 * Announces that the client wishes to fetch the auto-renewed certificate via GET 249 * request. If not used, the STAR certificate can only be fetched via POST-as-GET 250 * request. {@link Metadata#isAutoRenewalGetAllowed()} must return {@code true} in 251 * order for this option to work. 252 * <p> 253 * This option is only needed if you plan to fetch the STAR certificate via other 254 * means than by using acme4j. 255 * <p> 256 * Implies {@link #autoRenewal()}. 257 * 258 * @return itself 259 * @since 2.6 260 */ 261 public OrderBuilder autoRenewalEnableGet() { 262 autoRenewal(); 263 this.autoRenewalGet = true; 264 return this; 265 } 266 267 /** 268 * Sends a new order to the server, and returns an {@link Order} object. 269 * 270 * @return {@link Order} that was created 271 */ 272 public Order create() throws AcmeException { 273 if (identifierSet.isEmpty()) { 274 throw new IllegalArgumentException("At least one identifer is required"); 275 } 276 277 Session session = login.getSession(); 278 279 if (autoRenewal && !session.getMetadata().isAutoRenewalEnabled()) { 280 throw new AcmeException("CA does not support short-term automatic renewals"); 281 } 282 283 LOG.debug("create"); 284 try (Connection conn = session.connect()) { 285 JSONBuilder claims = new JSONBuilder(); 286 claims.array("identifiers", identifierSet.stream().map(Identifier::toMap).collect(toList())); 287 288 if (notBefore != null) { 289 claims.put("notBefore", notBefore); 290 } 291 if (notAfter != null) { 292 claims.put("notAfter", notAfter); 293 } 294 295 if (autoRenewal) { 296 JSONBuilder arClaims = claims.object("auto-renewal"); 297 if (autoRenewalStart != null) { 298 arClaims.put("start-date", autoRenewalStart); 299 } 300 if (autoRenewalStart != null) { 301 arClaims.put("end-date", autoRenewalEnd); 302 } 303 if (autoRenewalLifetime != null) { 304 arClaims.put("lifetime", autoRenewalLifetime); 305 } 306 if (autoRenewalLifetimeAdjust != null) { 307 arClaims.put("lifetime-adjust", autoRenewalLifetimeAdjust); 308 } 309 if (autoRenewalGet) { 310 arClaims.put("allow-certificate-get", autoRenewalGet); 311 } 312 } 313 314 conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login); 315 316 URL orderLocation = conn.getLocation(); 317 if (orderLocation == null) { 318 throw new AcmeProtocolException("Server did not provide an order location"); 319 } 320 321 Order order = new Order(login, orderLocation); 322 order.setJSON(conn.readJsonResponse()); 323 return order; 324 } 325 } 326 327}