mirror of https://github.com/shred/acme4j
Give access to directory metadata
parent
0195e5b16c
commit
8f2ac7c4c7
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2016 Richard "Shred" Körber
|
||||
* http://acme4j.shredzone.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
package org.shredzone.acme4j;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
|
||||
/**
|
||||
* Contains metadata related to the provider.
|
||||
*
|
||||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
public class Metadata {
|
||||
|
||||
private final Map<String, Object> meta;
|
||||
|
||||
/**
|
||||
* Creates an empty new {@link Metadata} instance.
|
||||
*/
|
||||
public Metadata() {
|
||||
this(new HashMap<String, Object>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Metadata} instance.
|
||||
*
|
||||
* @param meta
|
||||
* JSON map of metadata
|
||||
*/
|
||||
public Metadata(Map<String, Object> meta) {
|
||||
this.meta = meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link URI} to the current terms of service, or {@code null} if not
|
||||
* available.
|
||||
*/
|
||||
public URI getTermsOfService() {
|
||||
return getUri("terms-of-service");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link URI} to a website providing more information about the ACME
|
||||
* server. {@code null} if not available.
|
||||
*/
|
||||
public URI getWebsite() {
|
||||
return getUri("website");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of hostnames, which the ACME server recognises as referring to
|
||||
* itself for the purposes of CAA record validation. {@code null} if not available.
|
||||
*/
|
||||
public String[] getCaaIdentities() {
|
||||
return getStringArray("caa-identities");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a custom metadata value, as {@link String}.
|
||||
*
|
||||
* @param key
|
||||
* Key of the meta value
|
||||
* @return Value as {@link String}, or {@code null} if there is no such key in the
|
||||
* directory metadata.
|
||||
*/
|
||||
public String get(String key) {
|
||||
Object value = meta.get(key);
|
||||
return (value != null ? value.toString() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a custom metadata value, as {@link URI}.
|
||||
*
|
||||
* @param key
|
||||
* Key of the meta value
|
||||
* @return Value as {@link URI}, or {@code null} if there is no such key in the
|
||||
* directory metadata.
|
||||
* @throws AcmeProtocolException
|
||||
* if the value is not an {@link URI}
|
||||
*/
|
||||
public URI getUri(String key) {
|
||||
Object uri = meta.get(key);
|
||||
try {
|
||||
return (uri != null ? new URI(uri.toString()) : null);
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new AcmeProtocolException("Bad URI: " + uri, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a custom metadata value, as array of {@link String}.
|
||||
*
|
||||
* @param key
|
||||
* Key of the meta value
|
||||
* @return {@link String} array, or {@code null} if there is no such key in the
|
||||
* directory metadata.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public String[] getStringArray(String key) {
|
||||
Object value = meta.get(key);
|
||||
if (value != null && value instanceof Collection) {
|
||||
Collection<String> data = (Collection<String>) value;
|
||||
return data.toArray(new String[data.size()]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the metadata as raw JSON map.
|
||||
* <p>
|
||||
* Do not modify the map or its contents. Changes will have a session-wide effect.
|
||||
*/
|
||||
public Map<String, Object> getJsonData() {
|
||||
return meta;
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
package org.shredzone.acme4j;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.KeyPair;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
@ -26,6 +27,7 @@ import org.shredzone.acme4j.challenge.Challenge;
|
|||
import org.shredzone.acme4j.challenge.TokenChallenge;
|
||||
import org.shredzone.acme4j.connector.Resource;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.provider.AcmeProvider;
|
||||
|
||||
/**
|
||||
|
@ -38,12 +40,14 @@ import org.shredzone.acme4j.provider.AcmeProvider;
|
|||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
public class Session {
|
||||
private final Map<Resource, URI> directoryMap = new EnumMap<>(Resource.class);
|
||||
private final Map<Resource, URI> resourceMap = new EnumMap<>(Resource.class);
|
||||
private final URI serverUri;
|
||||
|
||||
private KeyPair keyPair;
|
||||
private AcmeProvider provider;
|
||||
private byte[] nonce;
|
||||
private Map<String, Object> directoryMap;
|
||||
private Metadata metadata;
|
||||
protected Date directoryCacheExpiry;
|
||||
|
||||
/**
|
||||
|
@ -184,21 +188,53 @@ public class Session {
|
|||
if (resource == null) {
|
||||
throw new NullPointerException("resource must not be null");
|
||||
}
|
||||
readDirectory();
|
||||
return resourceMap.get(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the metadata of the provider's directory. This may involve connecting to the
|
||||
* server and getting a directory. The result is cached.
|
||||
*
|
||||
* @return {@link Metadata}. May contain no data, but is never {@code null}.
|
||||
*/
|
||||
public Metadata getMetadata() throws AcmeException {
|
||||
readDirectory();
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the provider's directory, then rebuild the resource map. The response is
|
||||
* cached.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void readDirectory() throws AcmeException {
|
||||
synchronized (this) {
|
||||
Date now = new Date();
|
||||
|
||||
if (directoryMap.isEmpty() || !directoryCacheExpiry.after(now)) {
|
||||
Map<Resource, URI> newMap = provider().resources(this, getServerUri());
|
||||
|
||||
// only reached when readDirectory did not throw an exception
|
||||
directoryMap.clear();
|
||||
directoryMap.putAll(newMap);
|
||||
if (directoryMap == null || !directoryCacheExpiry.after(now)) {
|
||||
directoryMap = provider().directory(this, getServerUri());
|
||||
directoryCacheExpiry = new Date(now.getTime() + 60 * 60 * 1000L);
|
||||
|
||||
Object meta = directoryMap.get("meta");
|
||||
if (meta != null && meta instanceof Map) {
|
||||
metadata = new Metadata((Map<String, Object>) meta);
|
||||
} else {
|
||||
metadata = new Metadata();
|
||||
}
|
||||
|
||||
resourceMap.clear();
|
||||
for (Map.Entry<String, Object> entry : directoryMap.entrySet()) {
|
||||
Resource res = Resource.parse(entry.getKey());
|
||||
if (res != null) {
|
||||
try {
|
||||
resourceMap.put(res, new URI(entry.getValue().toString()));
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new AcmeProtocolException("Illegal URI for resource " + res, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return directoryMap.get(resource);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -67,13 +67,6 @@ public interface Connection extends AutoCloseable {
|
|||
*/
|
||||
X509Certificate readCertificate() throws IOException;
|
||||
|
||||
/**
|
||||
* Reads a resource directory.
|
||||
*
|
||||
* @return Map of {@link Resource} and the respective {@link URI} to invoke
|
||||
*/
|
||||
Map<Resource, URI> readDirectory() throws IOException;
|
||||
|
||||
/**
|
||||
* Updates a {@link Session} by evaluating the HTTP response header.
|
||||
*
|
||||
|
|
|
@ -28,7 +28,6 @@ import java.security.cert.CertificateException;
|
|||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
|
@ -217,38 +216,6 @@ public class DefaultConnection implements Connection {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Resource, URI> readDirectory() throws IOException {
|
||||
assertConnectionIsOpen();
|
||||
|
||||
String contentType = conn.getHeaderField("Content-Type");
|
||||
if (!("application/json".equals(contentType))) {
|
||||
throw new AcmeProtocolException("Unexpected content type: " + contentType);
|
||||
}
|
||||
|
||||
EnumMap<Resource, URI> resourceMap = new EnumMap<>(Resource.class);
|
||||
String response = "";
|
||||
|
||||
try {
|
||||
response = readStream(conn.getInputStream());
|
||||
|
||||
Map<String, Object> result = JsonUtil.parseJson(response);
|
||||
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
||||
Resource res = Resource.parse(entry.getKey());
|
||||
if (res != null) {
|
||||
URI uri = new URI(entry.getValue().toString());
|
||||
resourceMap.put(res, uri);
|
||||
}
|
||||
}
|
||||
|
||||
LOG.debug("Resource directory: {}", resourceMap);
|
||||
} catch (JoseException | URISyntaxException ex) {
|
||||
throw new AcmeProtocolException("Failed to read directory: " + response, ex);
|
||||
}
|
||||
|
||||
return resourceMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSession(Session session) {
|
||||
assertConnectionIsOpen();
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
|||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.DefaultConnection;
|
||||
import org.shredzone.acme4j.connector.HttpConnector;
|
||||
import org.shredzone.acme4j.connector.Resource;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeNetworkException;
|
||||
|
||||
|
@ -48,7 +47,7 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<Resource, URI> resources(Session session, URI serverUri) throws AcmeException {
|
||||
public Map<String, Object> directory(Session session, URI serverUri) throws AcmeException {
|
||||
try (Connection conn = connect()) {
|
||||
int rc = conn.sendRequest(resolve(serverUri));
|
||||
if (rc != HttpURLConnection.HTTP_OK) {
|
||||
|
@ -58,7 +57,7 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
|
|||
// use nonce header if there is one, saves a HEAD request...
|
||||
conn.updateSession(session);
|
||||
|
||||
return conn.readDirectory();
|
||||
return conn.readJsonResponse();
|
||||
} catch (IOException ex) {
|
||||
throw new AcmeNetworkException(ex);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import java.util.ServiceLoader;
|
|||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.Resource;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
|
||||
/**
|
||||
|
@ -62,7 +61,8 @@ public interface AcmeProvider {
|
|||
Connection connect();
|
||||
|
||||
/**
|
||||
* Returns a map of all known {@link Resource} {@link URI} of this provider.
|
||||
* Returns the provider's directory. The map must contain resource URIs, and may
|
||||
* optionally contain metadata.
|
||||
* <p>
|
||||
* The default implementation resolves the server URI and fetches the directory via
|
||||
* HTTP request. Subclasses may override this method, e.g. if the directory is static.
|
||||
|
@ -71,9 +71,9 @@ public interface AcmeProvider {
|
|||
* {@link Session} to be used
|
||||
* @param serverUri
|
||||
* Server {@link URI}
|
||||
* @return Map of resource URIs
|
||||
* @return Map of directory data
|
||||
*/
|
||||
Map<Resource, URI> resources(Session session, URI serverUri) throws AcmeException;
|
||||
Map<String, Object> directory(Session session, URI serverUri) throws AcmeException;
|
||||
|
||||
/**
|
||||
* Creates a {@link Challenge} instance for the given challenge type.
|
||||
|
|
|
@ -19,10 +19,8 @@ import static org.mockito.Mockito.*;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
@ -46,9 +44,9 @@ public class SessionTest {
|
|||
* Test constructor
|
||||
*/
|
||||
@Test
|
||||
public void testConstructor() throws Exception {
|
||||
public void testConstructor() throws IOException {
|
||||
KeyPair keyPair = TestUtils.createKeyPair();
|
||||
URI serverUri = new URI(TestUtils.ACME_SERVER_URI);
|
||||
URI serverUri = URI.create(TestUtils.ACME_SERVER_URI);
|
||||
|
||||
try {
|
||||
new Session((URI) null, null);
|
||||
|
@ -93,10 +91,10 @@ public class SessionTest {
|
|||
* Test getters and setters.
|
||||
*/
|
||||
@Test
|
||||
public void testGettersAndSetters() throws Exception {
|
||||
public void testGettersAndSetters() throws IOException {
|
||||
KeyPair kp1 = TestUtils.createKeyPair();
|
||||
KeyPair kp2 = TestUtils.createDomainKeyPair();
|
||||
URI serverUri = new URI(TestUtils.ACME_SERVER_URI);
|
||||
URI serverUri = URI.create(TestUtils.ACME_SERVER_URI);
|
||||
|
||||
Session session = new Session(serverUri, kp1);
|
||||
|
||||
|
@ -116,9 +114,9 @@ public class SessionTest {
|
|||
* Test if challenges are correctly created via provider.
|
||||
*/
|
||||
@Test
|
||||
public void testCreateChallenge() throws IOException, URISyntaxException {
|
||||
public void testCreateChallenge() throws IOException {
|
||||
KeyPair keyPair = TestUtils.createKeyPair();
|
||||
URI serverUri = new URI(TestUtils.ACME_SERVER_URI);
|
||||
URI serverUri = URI.create(TestUtils.ACME_SERVER_URI);
|
||||
String challengeType = Http01Challenge.TYPE;
|
||||
|
||||
Map<String, Object> data = new ClaimBuilder()
|
||||
|
@ -151,19 +149,15 @@ public class SessionTest {
|
|||
* Test that the directory is properly read and cached.
|
||||
*/
|
||||
@Test
|
||||
public void testResourceUri() throws AcmeException, IOException, URISyntaxException {
|
||||
public void testDirectory() throws AcmeException, IOException {
|
||||
KeyPair keyPair = TestUtils.createKeyPair();
|
||||
URI serverUri = new URI(TestUtils.ACME_SERVER_URI);
|
||||
|
||||
Map<Resource, URI> directoryMap = new HashMap<>();
|
||||
directoryMap.put(Resource.NEW_AUTHZ, new URI("http://example.com/acme/new-authz"));
|
||||
directoryMap.put(Resource.NEW_CERT, new URI("http://example.com/acme/new-cert"));
|
||||
URI serverUri = URI.create(TestUtils.ACME_SERVER_URI);
|
||||
|
||||
final AcmeProvider mockProvider = mock(AcmeProvider.class);
|
||||
when(mockProvider.resources(
|
||||
when(mockProvider.directory(
|
||||
ArgumentMatchers.any(Session.class),
|
||||
ArgumentMatchers.eq(serverUri)))
|
||||
.thenReturn(directoryMap);
|
||||
.thenReturn(TestUtils.getJsonAsMap("directory"));
|
||||
|
||||
Session session = new Session(serverUri, keyPair) {
|
||||
@Override
|
||||
|
@ -172,15 +166,10 @@ public class SessionTest {
|
|||
};
|
||||
};
|
||||
|
||||
assertThat(session.resourceUri(Resource.NEW_AUTHZ),
|
||||
is(new URI("http://example.com/acme/new-authz")));
|
||||
assertThat(session.resourceUri(Resource.NEW_CERT),
|
||||
is(new URI("http://example.com/acme/new-cert")));
|
||||
assertThat(session.resourceUri(Resource.NEW_REG),
|
||||
is(nullValue()));
|
||||
assertSession(session);
|
||||
|
||||
// Make sure directory is only read once!
|
||||
verify(mockProvider, times(1)).resources(
|
||||
verify(mockProvider, times(1)).directory(
|
||||
ArgumentMatchers.any(Session.class),
|
||||
ArgumentMatchers.any(URI.class));
|
||||
|
||||
|
@ -188,15 +177,74 @@ public class SessionTest {
|
|||
session.directoryCacheExpiry = new Date();
|
||||
|
||||
// Make sure directory is read once again
|
||||
assertThat(session.resourceUri(Resource.NEW_AUTHZ),
|
||||
is(new URI("http://example.com/acme/new-authz")));
|
||||
assertThat(session.resourceUri(Resource.NEW_CERT),
|
||||
is(new URI("http://example.com/acme/new-cert")));
|
||||
assertThat(session.resourceUri(Resource.NEW_REG),
|
||||
is(nullValue()));
|
||||
verify(mockProvider, times(2)).resources(
|
||||
assertSession(session);
|
||||
verify(mockProvider, times(2)).directory(
|
||||
ArgumentMatchers.any(Session.class),
|
||||
ArgumentMatchers.any(URI.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the directory is properly read even if there are no metadata.
|
||||
*/
|
||||
@Test
|
||||
public void testNoMeta() throws AcmeException, IOException {
|
||||
KeyPair keyPair = TestUtils.createKeyPair();
|
||||
URI serverUri = URI.create(TestUtils.ACME_SERVER_URI);
|
||||
|
||||
final AcmeProvider mockProvider = mock(AcmeProvider.class);
|
||||
when(mockProvider.directory(
|
||||
org.mockito.Matchers.any(Session.class),
|
||||
org.mockito.Matchers.eq(serverUri)))
|
||||
.thenReturn(TestUtils.getJsonAsMap("directoryNoMeta"));
|
||||
|
||||
Session session = new Session(serverUri, keyPair) {
|
||||
@Override
|
||||
public AcmeProvider provider() {
|
||||
return mockProvider;
|
||||
};
|
||||
};
|
||||
|
||||
assertThat(session.resourceUri(Resource.NEW_REG),
|
||||
is(URI.create("https://example.com/acme/new-reg")));
|
||||
assertThat(session.resourceUri(Resource.NEW_AUTHZ),
|
||||
is(URI.create("https://example.com/acme/new-authz")));
|
||||
assertThat(session.resourceUri(Resource.NEW_CERT),
|
||||
is(URI.create("https://example.com/acme/new-cert")));
|
||||
assertThat(session.resourceUri(Resource.REVOKE_CERT),
|
||||
is(nullValue()));
|
||||
|
||||
Metadata meta = session.getMetadata();
|
||||
assertThat(meta, not(nullValue()));
|
||||
assertThat(meta.getTermsOfService(), is(nullValue()));
|
||||
assertThat(meta.getWebsite(), is(nullValue()));
|
||||
assertThat(meta.getCaaIdentities(), is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the {@link Session} returns correct
|
||||
* {@link Session#resourceUri(Resource)} and {@link Session#getMetadata()}.
|
||||
*
|
||||
* @param session
|
||||
* {@link Session} to assert
|
||||
*/
|
||||
private void assertSession(Session session) throws AcmeException {
|
||||
assertThat(session.resourceUri(Resource.NEW_REG),
|
||||
is(URI.create("https://example.com/acme/new-reg")));
|
||||
assertThat(session.resourceUri(Resource.NEW_AUTHZ),
|
||||
is(URI.create("https://example.com/acme/new-authz")));
|
||||
assertThat(session.resourceUri(Resource.NEW_CERT),
|
||||
is(URI.create("https://example.com/acme/new-cert")));
|
||||
assertThat(session.resourceUri(Resource.REVOKE_CERT),
|
||||
is(nullValue()));
|
||||
|
||||
Metadata meta = session.getMetadata();
|
||||
assertThat(meta, not(nullValue()));
|
||||
assertThat(meta.getTermsOfService(), is(URI.create("https://example.com/acme/terms")));
|
||||
assertThat(meta.getWebsite(), is(URI.create("https://www.example.com/")));
|
||||
assertThat(meta.getCaaIdentities(), is(arrayContaining("example.com")));
|
||||
assertThat(meta.get("x-test-string"), is("foobar"));
|
||||
assertThat(meta.getUri("x-test-uri"), is(URI.create("https://www.example.org")));
|
||||
assertThat(meta.getStringArray("x-test-array"), is(arrayContaining("foo", "bar", "barfoo")));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -470,33 +470,4 @@ public class DefaultConnectionTest {
|
|||
verifyNoMoreInteractions(mockUrlConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a resource directory is read correctly.
|
||||
*/
|
||||
@Test
|
||||
public void testReadDirectory() throws Exception {
|
||||
StringBuilder jsonData = new StringBuilder();
|
||||
jsonData.append("{\n");
|
||||
jsonData.append("\"new-reg\":\"http://example.com/acme/newreg\",\n");
|
||||
jsonData.append("\"new-authz\":\"http://example.com/acme/newauthz\",\n");
|
||||
jsonData.append("\"old-foo\":\"http://example.com/acme/oldfoo\"\n");
|
||||
jsonData.append("}\n");
|
||||
|
||||
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
|
||||
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.toString().getBytes("utf-8")));
|
||||
|
||||
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||
conn.conn = mockUrlConnection;
|
||||
Map<Resource, URI> result = conn.readDirectory();
|
||||
assertThat(result.keySet(), hasSize(2));
|
||||
assertThat(result, hasEntry(Resource.NEW_REG, new URI("http://example.com/acme/newreg")));
|
||||
assertThat(result, hasEntry(Resource.NEW_AUTHZ, new URI("http://example.com/acme/newauthz")));
|
||||
// "old-foo" resource is unknown and thus not available in the map
|
||||
}
|
||||
|
||||
verify(mockUrlConnection).getHeaderField("Content-Type");
|
||||
verify(mockUrlConnection).getInputStream();
|
||||
verifyNoMoreInteractions(mockUrlConnection);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -50,11 +50,6 @@ public class DummyConnection implements Connection {
|
|||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Resource, URI> readDirectory() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSession(Session session) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
|
|
@ -99,7 +99,7 @@ public class SessionProviderTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<Resource, URI> resources(Session session, URI serverUri) throws AcmeException {
|
||||
public Map<String, Object> directory(Session session, URI serverUri) throws AcmeException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,7 @@ public class SessionProviderTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<Resource, URI> resources(Session session, URI serverUri) throws AcmeException {
|
||||
public Map<String, Object> directory(Session session, URI serverUri) throws AcmeException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
|
|
@ -17,13 +17,14 @@ import static org.hamcrest.Matchers.*;
|
|||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.jose4j.json.JsonUtil;
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
|
@ -34,7 +35,7 @@ import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
|||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.DefaultConnection;
|
||||
import org.shredzone.acme4j.connector.HttpConnector;
|
||||
import org.shredzone.acme4j.connector.Resource;
|
||||
import org.shredzone.acme4j.util.TestUtils;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AbstractAcmeProvider}.
|
||||
|
@ -83,12 +84,9 @@ public class AbstractAcmeProviderTest {
|
|||
final URI testResolvedUri = new URI("http://example.com/acme/directory");
|
||||
final Connection connection = mock(Connection.class);
|
||||
final Session session = mock(Session.class);
|
||||
final Map<Resource, URI> resourceMap = new EnumMap<>(Resource.class);
|
||||
resourceMap.put(Resource.NEW_REG, new URI("http://example.com/acme/new-reg"));
|
||||
resourceMap.put(Resource.NEW_CERT, new URI("http://example.com/acme/new-cert"));
|
||||
|
||||
when(connection.sendRequest(testResolvedUri)).thenReturn(HttpURLConnection.HTTP_OK);
|
||||
when(connection.readDirectory()).thenReturn(resourceMap);
|
||||
when(connection.readJsonResponse()).thenReturn(TestUtils.getJsonAsMap("directory"));
|
||||
|
||||
AbstractAcmeProvider provider = new AbstractAcmeProvider() {
|
||||
@Override
|
||||
|
@ -109,12 +107,12 @@ public class AbstractAcmeProviderTest {
|
|||
}
|
||||
};
|
||||
|
||||
Map<Resource, URI> map = provider.resources(session, testServerUri);
|
||||
assertThat(map, is(resourceMap));
|
||||
Map<String, Object> map = provider.directory(session, testServerUri);
|
||||
assertThat(JsonUtil.toJson(map), sameJSONAs(TestUtils.getJson("directory")));
|
||||
|
||||
verify(connection).sendRequest(testResolvedUri);
|
||||
verify(connection).updateSession(any(Session.class));
|
||||
verify(connection).readDirectory();
|
||||
verify(connection).readJsonResponse();
|
||||
verify(connection).close();
|
||||
verifyNoMoreInteractions(connection);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ package org.shredzone.acme4j.provider;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -25,6 +24,7 @@ import org.shredzone.acme4j.connector.Connection;
|
|||
import org.shredzone.acme4j.connector.DummyConnection;
|
||||
import org.shredzone.acme4j.connector.Resource;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
import org.shredzone.acme4j.util.TestUtils;
|
||||
|
||||
/**
|
||||
|
@ -34,8 +34,8 @@ import org.shredzone.acme4j.util.TestUtils;
|
|||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
public class TestableConnectionProvider extends DummyConnection implements AcmeProvider {
|
||||
private final Map<Resource, URI> resourceMap = new HashMap<>();
|
||||
private final Map<String, Challenge> challengeMap = new HashMap<>();
|
||||
private final ClaimBuilder directory = new ClaimBuilder();
|
||||
|
||||
/**
|
||||
* Register a {@link Resource} mapping.
|
||||
|
@ -46,7 +46,7 @@ public class TestableConnectionProvider extends DummyConnection implements AcmeP
|
|||
* {@link URI} to be returned
|
||||
*/
|
||||
public void putTestResource(Resource r, URI u) {
|
||||
resourceMap.put(r, u);
|
||||
directory.put(r.path(), u);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,11 +86,12 @@ public class TestableConnectionProvider extends DummyConnection implements AcmeP
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<Resource, URI> resources(Session session, URI serverUri) throws AcmeException {
|
||||
if (resourceMap.isEmpty()) {
|
||||
public Map<String, Object> directory(Session session, URI serverUri) throws AcmeException {
|
||||
Map<String, Object> result = directory.toMap();
|
||||
if (result.isEmpty()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
return Collections.unmodifiableMap(resourceMap);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -12,6 +12,29 @@
|
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
|
||||
directory = \
|
||||
{\
|
||||
"new-reg": "https://example.com/acme/new-reg",\
|
||||
"new-authz": "https://example.com/acme/new-authz",\
|
||||
"new-cert": "https://example.com/acme/new-cert",\
|
||||
"meta": {\
|
||||
"terms-of-service": "https://example.com/acme/terms",\
|
||||
"website": "https://www.example.com/",\
|
||||
"caa-identities": ["example.com"],\
|
||||
"x-test-string": "foobar",\
|
||||
"x-test-uri": "https://www.example.org",\
|
||||
"x-test-array": ["foo", "bar", "barfoo"]\
|
||||
}\
|
||||
}
|
||||
|
||||
directoryNoMeta = \
|
||||
{\
|
||||
"new-reg": "https://example.com/acme/new-reg",\
|
||||
"new-authz": "https://example.com/acme/new-authz",\
|
||||
"new-cert": "https://example.com/acme/new-cert"\
|
||||
}
|
||||
|
||||
|
||||
newRegistration = \
|
||||
{"resource":"new-reg",\
|
||||
"contact":["mailto:foo@example.com"]}
|
||||
|
|
|
@ -18,6 +18,17 @@ Session session =
|
|||
|
||||
Connecting via `acme` URI should always be preferred over using the directory URL.
|
||||
|
||||
## Metadata
|
||||
|
||||
Some CAs provide metadata related to their ACME server. This information can be retrieved via the `Session` object:
|
||||
|
||||
```java
|
||||
Metadata meta = session.getMetadata();
|
||||
URI website = meta.getWebsite();
|
||||
```
|
||||
|
||||
`meta` is never `null`, even if the server did not provide any metadata. All of the `Metadata` getters are optional though, and may return `null` if the respective information was not provided by the server.
|
||||
|
||||
## Available Providers
|
||||
|
||||
In _acme4j_ these providers are available:
|
||||
|
|
Loading…
Reference in New Issue