providers = ServiceLoader.load(AcmeProvider.class);
+ provider = StreamSupport.stream(providers.spliterator(), false)
+ .filter(p -> p.accepts(localServerUri))
+ .reduce((a, b) -> {
+ throw new IllegalArgumentException("Both ACME providers "
+ + a.getClass().getSimpleName() + " and "
+ + b.getClass().getSimpleName() + " accept "
+ + localServerUri + ". Please check your classpath.");
+ })
+ .orElseThrow(() -> new IllegalArgumentException("No ACME provider found for " + localServerUri));
}
/**
@@ -143,32 +158,10 @@ public class Session {
/**
* Returns the {@link AcmeProvider} that is used for this session.
- *
- * The {@link AcmeProvider} instance is lazily created and cached.
*
* @return {@link AcmeProvider}
*/
public AcmeProvider provider() {
- synchronized (this) {
- if (provider == null) {
- List candidates = new ArrayList<>();
- for (AcmeProvider acp : ServiceLoader.load(AcmeProvider.class)) {
- if (acp.accepts(serverUri)) {
- candidates.add(acp);
- }
- }
-
- if (candidates.isEmpty()) {
- throw new IllegalArgumentException("No ACME provider found for " + serverUri);
- } else if (candidates.size() > 1) {
- throw new IllegalStateException("There are " + candidates.size() + " "
- + AcmeProvider.class.getSimpleName() + " accepting " + serverUri
- + ". Please check your classpath.");
- } else {
- provider = candidates.get(0);
- }
- }
- }
return provider;
}
@@ -206,7 +199,7 @@ public class Session {
*/
public URI resourceUri(Resource resource) throws AcmeException {
readDirectory();
- return resourceMap.get(Objects.requireNonNull(resource, "resource"));
+ return resourceMap.get().get(Objects.requireNonNull(resource, "resource"));
}
/**
@@ -217,7 +210,7 @@ public class Session {
*/
public Metadata getMetadata() throws AcmeException {
readDirectory();
- return metadata;
+ return metadata.get();
}
/**
@@ -227,26 +220,28 @@ public class Session {
private void readDirectory() throws AcmeException {
synchronized (this) {
Instant now = Instant.now();
- if (directoryJson == null || !directoryCacheExpiry.isAfter(now)) {
- directoryJson = provider().directory(this, getServerUri());
- directoryCacheExpiry = now.plus(Duration.ofHours(1));
+ if (directoryJson != null && directoryCacheExpiry.isAfter(now)) {
+ return;
+ }
+ directoryJson = provider().directory(this, getServerUri());
+ directoryCacheExpiry = now.plus(Duration.ofHours(1));
+ }
- JSON meta = directoryJson.get("meta").asObject();
- if (meta != null) {
- metadata = new Metadata(meta);
- } else {
- metadata = new Metadata(JSON.empty());
- }
+ JSON meta = directoryJson.get("meta").asObject();
+ if (meta != null) {
+ metadata.set(new Metadata(meta));
+ } else {
+ metadata.set(new Metadata(JSON.empty()));
+ }
- resourceMap.clear();
- for (Resource res : Resource.values()) {
- URI uri = directoryJson.get(res.path()).asURI();
- if (uri != null) {
- resourceMap.put(res, uri);
- }
- }
+ Map map = new EnumMap<>(Resource.class);
+ for (Resource res : Resource.values()) {
+ URI uri = directoryJson.get(res.path()).asURI();
+ if (uri != null) {
+ map.put(res, uri);
}
}
+ resourceMap.set(map);
}
}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java
index 1c4c689f..11effc4b 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java
@@ -39,6 +39,7 @@ import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
+import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.provider.AcmeProvider;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.util.JSON;
@@ -399,6 +400,62 @@ public class RegistrationTest {
provider.close();
}
+ /**
+ * Test that an unparseable certificate can be requested, and at least its location
+ * is made available.
+ */
+ @Test
+ public void testRequestCertificateBrokenSync() throws AcmeException, IOException {
+ TestableConnectionProvider provider = new TestableConnectionProvider() {
+ @Override
+ public void sendSignedRequest(URI uri, JSONBuilder claims, Session session) {
+ assertThat(uri, is(resourceUri));
+ assertThat(claims.toString(), sameJSONAs(getJson("requestCertificateRequestWithDate")));
+ assertThat(session, is(notNullValue()));
+ }
+
+ @Override
+ public int accept(int... httpStatus) throws AcmeException {
+ assertThat(httpStatus, isIntArrayContainingInAnyOrder(
+ HttpURLConnection.HTTP_CREATED, HttpURLConnection.HTTP_ACCEPTED));
+ return HttpURLConnection.HTTP_CREATED;
+ }
+
+ @Override
+ public X509Certificate readCertificate() {
+ throw new AcmeProtocolException("Failed to read certificate");
+ }
+
+ @Override
+ public URI getLink(String relation) {
+ switch(relation) {
+ case "up": return chainUri;
+ default: return null;
+ }
+ }
+
+ @Override
+ public URI getLocation() {
+ return locationUri;
+ }
+ };
+
+ provider.putTestResource(Resource.NEW_CERT, resourceUri);
+
+ byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
+ ZoneId utc = ZoneId.of("UTC");
+ Instant notBefore = LocalDate.of(2016, 1, 1).atStartOfDay(utc).toInstant();
+ Instant notAfter = LocalDate.of(2016, 1, 8).atStartOfDay(utc).toInstant();
+
+ Registration registration = new Registration(provider.createSession(), locationUri);
+ Certificate cert = registration.requestCertificate(csr, notBefore, notAfter);
+
+ assertThat(cert.getLocation(), is(locationUri));
+ assertThat(cert.getChainLocation(), is(chainUri));
+
+ provider.close();
+ }
+
/**
* Test that the account key can be changed.
*/
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java
index cc9d892d..2981a4bd 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java
@@ -775,4 +775,22 @@ public class DefaultConnectionTest {
verifyNoMoreInteractions(mockUrlConnection);
}
+ /**
+ * Test that a bad certificate throws an exception.
+ */
+ @Test(expected = AcmeProtocolException.class)
+ public void testReadBadCertificate() throws Exception {
+ X509Certificate original = TestUtils.createCertificate();
+ byte[] badCert = original.getEncoded();
+ Arrays.sort(badCert); // break it
+
+ when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/pkix-cert");
+ when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(badCert));
+
+ try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
+ conn.conn = mockUrlConnection;
+ conn.readCertificate();
+ }
+ }
+
}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java
index 73a8e2ec..ae66b5d2 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java
@@ -71,9 +71,9 @@ public class SessionProviderTest {
/**
* There are two testing providers accepting {@code acme://example.net}. Test that
- * connecting to this URI will result in an {@link IllegalStateException}.
+ * connecting to this URI will result in an {@link IllegalArgumentException}.
*/
- @Test(expected = IllegalStateException.class)
+ @Test(expected = IllegalArgumentException.class)
public void testDuplicate() throws Exception {
new Session(new URI("acme://example.net"), keyPair).provider();
}
diff --git a/acme4j-example/pom.xml b/acme4j-example/pom.xml
index 21b95136..e86e0f0e 100644
--- a/acme4j-example/pom.xml
+++ b/acme4j-example/pom.xml
@@ -20,7 +20,7 @@
org.shredzone.acme4j
acme4j
- 0.10-SNAPSHOT
+ 0.11-SNAPSHOT
acme4j-example
diff --git a/acme4j-utils/pom.xml b/acme4j-utils/pom.xml
index 41bba567..7dea129c 100644
--- a/acme4j-utils/pom.xml
+++ b/acme4j-utils/pom.xml
@@ -20,7 +20,7 @@
org.shredzone.acme4j
acme4j
- 0.10-SNAPSHOT
+ 0.11-SNAPSHOT
acme4j-utils
diff --git a/pom.xml b/pom.xml
index 59e099bb..dd65fb53 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
org.shredzone.acme4j
acme4j
- 0.10-SNAPSHOT
+ 0.11-SNAPSHOT
pom
acme4j
@@ -51,9 +51,9 @@
- 1.55
- 0.5.3
- 1.7.21
+ 1.56
+ 0.5.5
+ 1.7.25
utf-8
true
@@ -134,6 +134,19 @@
true
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.0.1
+
+
+ attach-sources
+
+ jar
+
+
+
+
diff --git a/src/site/markdown/migration.md b/src/site/markdown/migration.md
index 30dd0fab..ea06ee79 100644
--- a/src/site/markdown/migration.md
+++ b/src/site/markdown/migration.md
@@ -2,6 +2,12 @@
This document will help you migrate your code to the latest _acme4j_ version.
+## Migration to Version 0.10
+
+Starting with version 0.10, _acme4j_ requires Java 8 or higher. This is also reflected in the API.
+
+The most noticeable change is that the old `java.util.Date` has been replaced by the new `java.time` API in the entire project. If you don't want to migrate your code to the new API, you can use `Date.from()` and `Date.toInstant()` to convert between the different date objects, where necessary.
+
## Migration to Version 0.9
Version 0.9 brought many changes to the internal API. However, this is only relevant if you run your own CA and make own extensions to _acme4j_ (e.g. if you implement a proprietary `Challenge`). If you use _acme4j_ only for retrieving certificates, you should not notice any changes.