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