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}