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}