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