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}