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.Duration;
021import java.time.Instant;
022import java.util.EnumMap;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Objects;
026import java.util.ServiceLoader;
027import java.util.concurrent.atomic.AtomicReference;
028import java.util.stream.StreamSupport;
029
030import javax.annotation.CheckForNull;
031import javax.annotation.Nullable;
032import javax.annotation.ParametersAreNonnullByDefault;
033import javax.annotation.concurrent.ThreadSafe;
034
035import org.shredzone.acme4j.connector.Connection;
036import org.shredzone.acme4j.connector.Resource;
037import org.shredzone.acme4j.exception.AcmeException;
038import org.shredzone.acme4j.provider.AcmeProvider;
039import org.shredzone.acme4j.provider.GenericAcmeProvider;
040import org.shredzone.acme4j.toolbox.JSON;
041import org.shredzone.acme4j.toolbox.JSON.Value;
042
043/**
044 * A session stores the ACME server URI. It also tracks communication parameters.
045 */
046@ParametersAreNonnullByDefault
047@ThreadSafe
048public class Session {
049
050    private static final GenericAcmeProvider GENERIC_PROVIDER = new GenericAcmeProvider();
051
052    private final AtomicReference<Map<Resource, URL>> resourceMap = new AtomicReference<>();
053    private final AtomicReference<Metadata> metadata = new AtomicReference<>();
054    private final URI serverUri;
055    private final AcmeProvider provider;
056
057    private String nonce;
058    private Locale locale = Locale.getDefault();
059    private Proxy proxy = Proxy.NO_PROXY;
060    protected Instant directoryCacheExpiry;
061
062    /**
063     * Creates a new {@link Session}.
064     *
065     * @param serverUri
066     *            URI string of the ACME server
067     */
068    public Session(String serverUri) {
069        this(URI.create(serverUri));
070    }
071
072    /**
073     * Creates a new {@link Session}.
074     *
075     * @param serverUri
076     *            {@link URI} of the ACME server
077     * @throws IllegalArgumentException
078     *             if no ACME provider was found for the server URI.
079     */
080    public Session(URI serverUri) {
081        this.serverUri = Objects.requireNonNull(serverUri, "serverUri");
082
083        if (GENERIC_PROVIDER.accepts(serverUri)) {
084            provider = GENERIC_PROVIDER;
085            return;
086        }
087
088        final URI localServerUri = serverUri;
089
090        Iterable<AcmeProvider> providers = ServiceLoader.load(AcmeProvider.class);
091        provider = StreamSupport.stream(providers.spliterator(), false)
092            .filter(p -> p.accepts(localServerUri))
093            .reduce((a, b) -> {
094                    throw new IllegalArgumentException("Both ACME providers "
095                        + a.getClass().getSimpleName() + " and "
096                        + b.getClass().getSimpleName() + " accept "
097                        + localServerUri + ". Please check your classpath.");
098                })
099            .orElseThrow(() -> new IllegalArgumentException("No ACME provider found for " + localServerUri));
100    }
101
102    /**
103     * Logs into an existing account.
104     *
105     * @param accountLocation
106     *            Location {@link URL} of the account
107     * @param accountKeyPair
108     *            Account {@link KeyPair}
109     * @return {@link Login} to this account
110     */
111    public Login login(URL accountLocation, KeyPair accountKeyPair) {
112        return new Login(accountLocation, accountKeyPair, this);
113    }
114
115    /**
116     * Gets the ACME server {@link URI} of this session.
117     */
118    public URI getServerUri() {
119        return serverUri;
120    }
121
122    /**
123     * Gets the last base64 encoded nonce, or {@code null} if the session is new.
124     */
125    @CheckForNull
126    public String getNonce() {
127        return nonce;
128    }
129
130    /**
131     * Sets the base64 encoded nonce received by the server.
132     */
133    public void setNonce(@Nullable String nonce) {
134        this.nonce = nonce;
135    }
136
137    /**
138     * Gets the current locale of this session.
139     */
140    public Locale getLocale() {
141        return locale;
142    }
143
144    /**
145     * Sets the locale used in this session. The locale is passed to the server as
146     * Accept-Language header. The server <em>may</em> respond with localized messages.
147     */
148    public void setLocale(@Nullable Locale locale) {
149        this.locale = locale != null ? locale : Locale.getDefault();
150    }
151
152    /**
153     * Gets the {@link Proxy} to be used for connections.
154     */
155    public Proxy getProxy() {
156        return proxy;
157    }
158
159    /**
160     * Sets a {@link Proxy} that is to be used for all connections. If {@code null},
161     * {@link Proxy#NO_PROXY} is used, which is also the default.
162     */
163    public void setProxy(@Nullable Proxy proxy) {
164        this.proxy = proxy != null ? proxy : Proxy.NO_PROXY;
165    }
166
167    /**
168     * Returns the {@link AcmeProvider} that is used for this session.
169     *
170     * @return {@link AcmeProvider}
171     */
172    public AcmeProvider provider() {
173        return provider;
174    }
175
176    /**
177     * Returns a new {@link Connection} to the ACME server.
178     *
179     * @return {@link Connection}
180     */
181    public Connection connect() {
182        return provider.connect(getServerUri());
183    }
184
185    /**
186     * Gets the {@link URL} of the given {@link Resource}. This may involve connecting to
187     * the server and getting a directory. The result is cached.
188     *
189     * @param resource
190     *            {@link Resource} to get the {@link URL} of
191     * @return {@link URL} of the resource
192     * @throws AcmeException
193     *             if the server does not offer the {@link Resource}
194     */
195    public URL resourceUrl(Resource resource) throws AcmeException {
196        readDirectory();
197        URL result = resourceMap.get().get(Objects.requireNonNull(resource, "resource"));
198        if (result == null) {
199            throw new AcmeException("Server does not offer " + resource.path());
200        }
201        return result;
202    }
203
204    /**
205     * Gets the metadata of the provider's directory. This may involve connecting to the
206     * server and getting a directory. The result is cached.
207     *
208     * @return {@link Metadata}. May contain no data, but is never {@code null}.
209     */
210    public Metadata getMetadata() throws AcmeException {
211        readDirectory();
212        return metadata.get();
213    }
214
215    /**
216     * Reads the provider's directory, then rebuild the resource map. The response is
217     * cached.
218     */
219    private void readDirectory() throws AcmeException {
220        synchronized (this) {
221            Instant now = Instant.now();
222            if (directoryCacheExpiry != null && directoryCacheExpiry.isAfter(now)) {
223                return;
224            }
225            directoryCacheExpiry = now.plus(Duration.ofHours(1));
226        }
227
228        JSON directoryJson = provider().directory(this, getServerUri());
229
230        Value meta = directoryJson.get("meta");
231        if (meta.isPresent()) {
232            metadata.set(new Metadata(meta.asObject()));
233        } else {
234            metadata.set(new Metadata(JSON.empty()));
235        }
236
237        Map<Resource, URL> map = new EnumMap<>(Resource.class);
238        for (Resource res : Resource.values()) {
239            directoryJson.get(res.path())
240                    .map(Value::asURL)
241                    .ifPresent(url -> map.put(res, url));
242        }
243
244        resourceMap.set(map);
245    }
246
247}