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