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 java.net.Proxy;
017import java.net.URI;
018import java.net.URL;
019import java.security.KeyPair;
020import java.time.ZonedDateTime;
021import java.util.EnumMap;
022import java.util.Locale;
023import java.util.Map;
024import java.util.Objects;
025import java.util.ServiceLoader;
026import java.util.concurrent.atomic.AtomicReference;
027import java.util.stream.StreamSupport;
028
029import edu.umd.cs.findbugs.annotations.Nullable;
030import org.shredzone.acme4j.connector.Connection;
031import org.shredzone.acme4j.connector.NetworkSettings;
032import org.shredzone.acme4j.connector.Resource;
033import org.shredzone.acme4j.exception.AcmeException;
034import org.shredzone.acme4j.provider.AcmeProvider;
035import org.shredzone.acme4j.provider.GenericAcmeProvider;
036import org.shredzone.acme4j.toolbox.JSON;
037import org.shredzone.acme4j.toolbox.JSON.Value;
038
039/**
040 * A session stores the ACME server URI. It also tracks communication parameters.
041 */
042public class Session {
043
044    private static final GenericAcmeProvider GENERIC_PROVIDER = new GenericAcmeProvider();
045
046    private final AtomicReference<Map<Resource, URL>> resourceMap = new AtomicReference<>();
047    private final AtomicReference<Metadata> metadata = new AtomicReference<>();
048    private final NetworkSettings networkSettings = new NetworkSettings();
049    private final URI serverUri;
050    private final AcmeProvider provider;
051
052    private @Nullable String nonce;
053    private Locale locale = Locale.getDefault();
054    protected @Nullable ZonedDateTime directoryLastModified;
055    protected @Nullable ZonedDateTime directoryExpires;
056
057    /**
058     * Creates a new {@link Session}.
059     *
060     * @param serverUri
061     *            URI string of the ACME server
062     */
063    public Session(String serverUri) {
064        this(URI.create(serverUri));
065    }
066
067    /**
068     * Creates a new {@link Session}.
069     *
070     * @param serverUri
071     *            {@link URI} of the ACME server
072     * @throws IllegalArgumentException
073     *             if no ACME provider was found for the server URI.
074     */
075    public Session(URI serverUri) {
076        this.serverUri = Objects.requireNonNull(serverUri, "serverUri");
077
078        if (GENERIC_PROVIDER.accepts(serverUri)) {
079            provider = GENERIC_PROVIDER;
080            return;
081        }
082
083        final URI localServerUri = serverUri;
084
085        Iterable<AcmeProvider> providers = ServiceLoader.load(AcmeProvider.class);
086        provider = StreamSupport.stream(providers.spliterator(), false)
087            .filter(p -> p.accepts(localServerUri))
088            .reduce((a, b) -> {
089                    throw new IllegalArgumentException("Both ACME providers "
090                        + a.getClass().getSimpleName() + " and "
091                        + b.getClass().getSimpleName() + " accept "
092                        + localServerUri + ". Please check your classpath.");
093                })
094            .orElseThrow(() -> new IllegalArgumentException("No ACME provider found for " + localServerUri));
095    }
096
097    /**
098     * Creates a new {@link Session} using the given {@link AcmeProvider}.
099     * <p>
100     * This constructor should only be used for testing purposes.
101     *
102     * @param serverUri
103     *         {@link URI} of the ACME server
104     * @param provider
105     *         {@link AcmeProvider} to be used
106     * @since 2.8
107     */
108    public Session(URI serverUri, AcmeProvider provider) {
109        this.serverUri = Objects.requireNonNull(serverUri, "serverUri");
110        this.provider = Objects.requireNonNull(provider, "provider");
111
112        if (!provider.accepts(serverUri)) {
113            throw new IllegalArgumentException("Provider does not accept " + serverUri);
114        }
115    }
116
117    /**
118     * Logs into an existing account.
119     *
120     * @param accountLocation
121     *            Location {@link URL} of the account
122     * @param accountKeyPair
123     *            Account {@link KeyPair}
124     * @return {@link Login} to this account
125     */
126    public Login login(URL accountLocation, KeyPair accountKeyPair) {
127        return new Login(accountLocation, accountKeyPair, this);
128    }
129
130    /**
131     * Gets the ACME server {@link URI} of this session.
132     */
133    public URI getServerUri() {
134        return serverUri;
135    }
136
137    /**
138     * Gets the last base64 encoded nonce, or {@code null} if the session is new.
139     */
140    @Nullable
141    public String getNonce() {
142        return nonce;
143    }
144
145    /**
146     * Sets the base64 encoded nonce received by the server.
147     */
148    public void setNonce(@Nullable String nonce) {
149        this.nonce = nonce;
150    }
151
152    /**
153     * Gets the current locale of this session.
154     */
155    public Locale getLocale() {
156        return locale;
157    }
158
159    /**
160     * Sets the locale used in this session. The locale is passed to the server as
161     * Accept-Language header. The server <em>may</em> respond with localized messages.
162     */
163    public void setLocale(@Nullable Locale locale) {
164        this.locale = locale != null ? locale : Locale.getDefault();
165    }
166
167    /**
168     * Gets the {@link Proxy} to be used for connections.
169     *
170     * @deprecated Use {@code networkSettings().getProxy()}
171     */
172    @Deprecated
173    public Proxy getProxy() {
174        return networkSettings.getProxy();
175    }
176
177    /**
178     * Sets a {@link Proxy} that is to be used for all connections. If {@code null},
179     * {@link Proxy#NO_PROXY} is used, which is also the default.
180     *
181     * @deprecated Use {@code networkSettings().setProxy(Proxy)}
182     */
183    @Deprecated
184    public void setProxy(@Nullable Proxy proxy) {
185        networkSettings.setProxy(proxy);
186    }
187
188    /**
189     * Returns the current {@link NetworkSettings}.
190     *
191     * @return {@link NetworkSettings}
192     * @since 2.8
193     */
194    public NetworkSettings networkSettings() {
195        return networkSettings;
196    }
197
198    /**
199     * Returns the {@link AcmeProvider} that is used for this session.
200     *
201     * @return {@link AcmeProvider}
202     */
203    public AcmeProvider provider() {
204        return provider;
205    }
206
207    /**
208     * Returns a new {@link Connection} to the ACME server.
209     *
210     * @return {@link Connection}
211     */
212    public Connection connect() {
213        return provider.connect(getServerUri());
214    }
215
216    /**
217     * Gets the {@link URL} of the given {@link Resource}. This may involve connecting to
218     * the server and getting a directory. The result is cached.
219     *
220     * @param resource
221     *            {@link Resource} to get the {@link URL} of
222     * @return {@link URL} of the resource
223     * @throws AcmeException
224     *             if the server does not offer the {@link Resource}
225     */
226    public URL resourceUrl(Resource resource) throws AcmeException {
227        readDirectory();
228        URL result = resourceMap.get().get(Objects.requireNonNull(resource, "resource"));
229        if (result == null) {
230            throw new AcmeException("Server does not offer " + resource.path());
231        }
232        return result;
233    }
234
235    /**
236     * Gets the metadata of the provider's directory. This may involve connecting to the
237     * server and getting a directory. The result is cached.
238     *
239     * @return {@link Metadata}. May contain no data, but is never {@code null}.
240     */
241    public Metadata getMetadata() throws AcmeException {
242        readDirectory();
243        return metadata.get();
244    }
245
246    /**
247     * Returns the date when the directory has been modified the last time.
248     *
249     * @return The last modification date of the directory, or {@code null} if unknown
250     * (directory has not been read yet or did not provide this information).
251     * @since 2.10
252     */
253    @Nullable
254    public ZonedDateTime getDirectoryLastModified() {
255        return directoryLastModified;
256    }
257
258    /**
259     * Sets the date when the directory has been modified the last time. Should only be
260     * invoked by {@link AcmeProvider} implementations.
261     *
262     * @param directoryLastModified
263     *         The last modification date of the directory, or {@code null} if unknown
264     *         (directory has not been read yet or did not provide this information).
265     * @since 2.10
266     */
267    public void setDirectoryLastModified(@Nullable ZonedDateTime directoryLastModified) {
268        this.directoryLastModified = directoryLastModified;
269    }
270
271    /**
272     * Returns the date when the current directory records will expire. A fresh copy of
273     * the directory will be fetched automatically after that instant.
274     *
275     * @return The expiration date, or {@code null} if the server did not provide this
276     * information.
277     * @since 2.10
278     */
279    @Nullable
280    public ZonedDateTime getDirectoryExpires() {
281        return directoryExpires;
282    }
283
284    /**
285     * Sets the date when the current directory will expire. Should only be invoked by
286     * {@link AcmeProvider} implementations.
287     *
288     * @param directoryExpires
289     *         Expiration date, or {@code null} if the server did not provide this
290     *         information.
291     * @since 2.10
292     */
293    public void setDirectoryExpires(@Nullable ZonedDateTime directoryExpires) {
294        this.directoryExpires = directoryExpires;
295    }
296
297    /**
298     * Returns {@code true} if a directory is available. Should only be invoked by {@link
299     * AcmeProvider} implementations.
300     *
301     * @return {@code true} if a directory is available.
302     * @since 2.10
303     */
304    public boolean hasDirectory() {
305        return resourceMap.get() != null;
306    }
307
308    /**
309     * Reads the provider's directory, then rebuild the resource map. The resource map
310     * is unchanged if the {@link AcmeProvider} returns that the directory has not been
311     * changed on the remote side.
312     */
313    private void readDirectory() throws AcmeException {
314        JSON directoryJson = provider().directory(this, getServerUri());
315        if (directoryJson == null) {
316            if (!hasDirectory()) {
317                throw new AcmeException("AcmeProvider did not provide a directory");
318            }
319            return;
320        }
321
322        Value meta = directoryJson.get("meta");
323        if (meta.isPresent()) {
324            metadata.set(new Metadata(meta.asObject()));
325        } else {
326            metadata.set(new Metadata(JSON.empty()));
327        }
328
329        Map<Resource, URL> map = new EnumMap<>(Resource.class);
330        for (Resource res : Resource.values()) {
331            directoryJson.get(res.path())
332                    .map(Value::asURL)
333                    .ifPresent(url -> map.put(res, url));
334        }
335
336        resourceMap.set(map);
337    }
338
339}