From 0f4d5e114dcaa669c3e15e140ad47f57d8bf8325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Sun, 13 Dec 2015 19:37:27 +0100 Subject: [PATCH] Refactor, use new HttpConnector for connecting to server --- .../acme4j/connector/Connection.java | 14 ++-- .../acme4j/connector/HttpConnector.java | 49 +++++++++++ .../acme4j/impl/AbstractAcmeClient.java | 16 ++-- .../acme4j/impl/GenericAcmeClient.java | 6 +- .../provider/AbstractAcmeClientProvider.java | 23 ++--- .../acme4j/provider/AcmeClientProvider.java | 12 +-- .../acme4j/AcmeClientFactoryTest.java | 7 +- .../acme4j/connector/ConnectionTest.java | 52 ++++-------- .../acme4j/connector/HttpConnectorTest.java | 50 +++++++++++ .../AbstractAcmeClientProviderTest.java | 30 ------- .../LetsEncryptAcmeClientProvider.java | 48 +---------- .../provider/LetsEncryptHttpConnector.java | 77 +++++++++++++++++ .../LetsEncryptAcmeClientProviderTest.java | 55 +----------- .../LetsEncryptHttpConnectorTest.java | 84 +++++++++++++++++++ 14 files changed, 317 insertions(+), 206 deletions(-) create mode 100644 acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java create mode 100644 acme4j-letsencrypt/src/main/java/org/shredzone/acme4j/provider/LetsEncryptHttpConnector.java create mode 100644 acme4j-letsencrypt/src/test/java/org/shredzone/acme4j/provider/LetsEncryptHttpConnectorTest.java diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java index bd80ebcf..e0420a4c 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java @@ -38,7 +38,6 @@ import org.jose4j.lang.JoseException; import org.shredzone.acme4j.Account; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeServerException; -import org.shredzone.acme4j.provider.AcmeClientProvider; import org.shredzone.acme4j.util.ClaimBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,16 +48,15 @@ import org.slf4j.LoggerFactory; * @author Richard "Shred" Körber */ public class Connection implements AutoCloseable { - private static final Logger LOG = LoggerFactory.getLogger(Connection.class); private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]+"); - private final AcmeClientProvider provider; + protected final HttpConnector httpConnector; protected HttpURLConnection conn; - public Connection(AcmeClientProvider provider) { - this.provider = provider; + public Connection(HttpConnector httpConnector) { + this.httpConnector = httpConnector; } @Override @@ -78,7 +76,7 @@ public class Connection implements AutoCloseable { public void startSession(URI uri, Session session) throws AcmeException { try { LOG.debug("Initial replay nonce from {}", uri); - HttpURLConnection localConn = provider.openConnection(uri); + HttpURLConnection localConn = httpConnector.openConnection(uri); localConn.setRequestMethod("HEAD"); localConn.connect(); @@ -99,7 +97,7 @@ public class Connection implements AutoCloseable { try { LOG.debug("GET {}", uri); - conn = provider.openConnection(uri); + conn = httpConnector.openConnection(uri); conn.setRequestMethod("GET"); conn.setRequestProperty("Accept-Charset", "utf-8"); conn.setDoOutput(false); @@ -140,7 +138,7 @@ public class Connection implements AutoCloseable { LOG.debug("POST {} with claims: {}", uri, claims); - conn = provider.openConnection(uri); + conn = httpConnector.openConnection(uri); conn.setRequestMethod("POST"); conn.setRequestProperty("Accept", "application/json"); conn.setRequestProperty("Accept-Charset", "utf-8"); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java new file mode 100644 index 00000000..4a5b36f9 --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java @@ -0,0 +1,49 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2015 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.connector; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; + +/** + * A generic HTTP connector. It connects to the given URI with a 10 seconds connection and + * read timeout. + *

+ * Subclasses may reconfigure the {@link HttpURLConnection} and pin it to a concrete SSL + * certificate. + * + * @author Richard "Shred" Körber + */ +public class HttpConnector { + + private static final int TIMEOUT = 10000; + + /** + * Opens a {@link HttpURLConnection} to the given {@link URI}. + * + * @param uri + * {@link URI} to connect to + * @return {@link HttpURLConnection} connected to the {@link URI} + */ + public HttpURLConnection openConnection(URI uri) throws IOException { + HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); + conn.setConnectTimeout(TIMEOUT); + conn.setReadTimeout(TIMEOUT); + conn.setUseCaches(false); + conn.setRequestProperty("User-Agent", "acme4j"); + return conn; + } + +} diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java index d9f99de6..aa9c7d7f 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java @@ -66,11 +66,11 @@ public abstract class AbstractAcmeClient implements AcmeClient { * * @return {@link Connection} instance */ - protected abstract Connection connect(); + protected abstract Connection createConnection(); @Override public void newRegistration(Account account, Registration registration) throws AcmeException { - try (Connection conn = connect()) { + try (Connection conn = createConnection()) { ClaimBuilder claims = new ClaimBuilder(); claims.putResource(Resource.NEW_REG); if (!registration.getContacts().isEmpty()) { @@ -98,7 +98,7 @@ public abstract class AbstractAcmeClient implements AcmeClient { throw new IllegalArgumentException("location must be set. Use newRegistration() if not known."); } - try (Connection conn = connect()) { + try (Connection conn = createConnection()) { ClaimBuilder claims = new ClaimBuilder(); claims.putResource("reg"); if (!registration.getContacts().isEmpty()) { @@ -116,7 +116,7 @@ public abstract class AbstractAcmeClient implements AcmeClient { @Override public void newAuthorization(Account account, Authorization auth) throws AcmeException { - try (Connection conn = connect()) { + try (Connection conn = createConnection()) { ClaimBuilder claims = new ClaimBuilder(); claims.putResource(Resource.NEW_AUTHZ); claims.object("identifier") @@ -163,7 +163,7 @@ public abstract class AbstractAcmeClient implements AcmeClient { @Override public void triggerChallenge(Account account, Challenge challenge) throws AcmeException { - try (Connection conn = connect()) { + try (Connection conn = createConnection()) { ClaimBuilder claims = new ClaimBuilder(); claims.putResource("challenge"); challenge.marshall(claims); @@ -176,7 +176,7 @@ public abstract class AbstractAcmeClient implements AcmeClient { @Override public void updateChallenge(Account account, Challenge challenge) throws AcmeException { - try (Connection conn = connect()) { + try (Connection conn = createConnection()) { conn.sendRequest(challenge.getUri()); challenge.unmarshall(conn.readJsonResponse()); } @@ -184,7 +184,7 @@ public abstract class AbstractAcmeClient implements AcmeClient { @Override public URI requestCertificate(Account account, byte[] csr) throws AcmeException { - try (Connection conn = connect()) { + try (Connection conn = createConnection()) { ClaimBuilder claims = new ClaimBuilder(); claims.putResource(Resource.NEW_CERT); claims.putBase64("csr", csr); @@ -200,7 +200,7 @@ public abstract class AbstractAcmeClient implements AcmeClient { @Override public X509Certificate downloadCertificate(URI certUri) throws AcmeException { - try (Connection conn = connect()) { + try (Connection conn = createConnection()) { conn.sendRequest(certUri); return conn.readCertificate(); } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/GenericAcmeClient.java b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/GenericAcmeClient.java index 357ca195..bc6ce559 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/GenericAcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/GenericAcmeClient.java @@ -55,14 +55,14 @@ public class GenericAcmeClient extends AbstractAcmeClient { } @Override - protected Connection connect() { - return new Connection(provider); + protected Connection createConnection() { + return provider.createConnection(); } @Override protected URI resourceUri(Resource resource) throws AcmeException { if (directoryMap.isEmpty()) { - try (Connection conn = connect()) { + try (Connection conn = createConnection()) { conn.sendRequest(directoryUri); directoryMap.putAll(conn.readDirectory()); } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeClientProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeClientProvider.java index 616c0490..203b4ee6 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeClientProvider.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeClientProvider.java @@ -13,8 +13,6 @@ */ package org.shredzone.acme4j.provider; -import java.io.IOException; -import java.net.HttpURLConnection; import java.net.URI; import java.util.Collection; import java.util.Collections; @@ -28,6 +26,8 @@ import org.shredzone.acme4j.challenge.GenericChallenge; import org.shredzone.acme4j.challenge.HttpChallenge; import org.shredzone.acme4j.challenge.ProofOfPossessionChallenge; import org.shredzone.acme4j.challenge.TlsSniChallenge; +import org.shredzone.acme4j.connector.Connection; +import org.shredzone.acme4j.connector.HttpConnector; import org.shredzone.acme4j.impl.GenericAcmeClient; /** @@ -41,8 +41,6 @@ import org.shredzone.acme4j.impl.GenericAcmeClient; */ public abstract class AbstractAcmeClientProvider implements AcmeClientProvider { - private static final int TIMEOUT = 10000; - private final Map> challenges = new HashMap<>(); public AbstractAcmeClientProvider() { @@ -86,13 +84,16 @@ public abstract class AbstractAcmeClientProvider implements AcmeClientProvider { } @Override - public HttpURLConnection openConnection(URI uri) throws IOException { - HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); - conn.setConnectTimeout(TIMEOUT); - conn.setReadTimeout(TIMEOUT); - conn.setUseCaches(false); - conn.setRequestProperty("User-Agent", "acme4j"); - return conn; + public Connection createConnection() { + return new Connection(createHttpConnector()); + } + + /** + * Creates a {@link HttpConnector}. Subclasses may override this method to + * configure the {@link HttpConnector}. + */ + protected HttpConnector createHttpConnector() { + return new HttpConnector(); } /** diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeClientProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeClientProvider.java index 744399a7..c0ebf4b8 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeClientProvider.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeClientProvider.java @@ -13,14 +13,13 @@ */ package org.shredzone.acme4j.provider; -import java.io.IOException; -import java.net.HttpURLConnection; import java.net.URI; import java.util.ServiceLoader; import org.shredzone.acme4j.AcmeClient; import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.GenericChallenge; +import org.shredzone.acme4j.connector.Connection; /** * An {@link AcmeClientProvider} creates {@link AcmeClient} instances to be used for @@ -70,13 +69,10 @@ public interface AcmeClientProvider { T createChallenge(String type); /** - * Opens a {@link HttpURLConnection} to the given {@link URI}. Implementations may - * configure the connection, e.g. pin it to a concrete SSL certificate. + * Creates a {@link Connection} for communication with the ACME server. * - * @param uri - * {@link URI} to connect to - * @return {@link HttpURLConnection} connected to the {@link URI} + * @return {@link Connection} that was generated */ - HttpURLConnection openConnection(URI uri) throws IOException; + Connection createConnection(); } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/AcmeClientFactoryTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/AcmeClientFactoryTest.java index 72a26feb..3012711e 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/AcmeClientFactoryTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/AcmeClientFactoryTest.java @@ -17,14 +17,13 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; -import java.io.IOException; -import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.util.ServiceLoader; import org.junit.Test; import org.shredzone.acme4j.challenge.Challenge; +import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.provider.AcmeClientProvider; @@ -88,7 +87,7 @@ public class AcmeClientFactoryTest { } @Override - public HttpURLConnection openConnection(URI uri) throws IOException { + public Connection createConnection() { fail("not supposed to be invoked"); return null; } @@ -114,7 +113,7 @@ public class AcmeClientFactoryTest { } @Override - public HttpURLConnection openConnection(URI uri) throws IOException { + public Connection createConnection() { fail("not supposed to be invoked"); return null; } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ConnectionTest.java index a73c01e8..0b9832e5 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ConnectionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ConnectionTest.java @@ -41,7 +41,6 @@ import org.junit.Test; import org.shredzone.acme4j.Account; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeServerException; -import org.shredzone.acme4j.provider.AcmeClientProvider; import org.shredzone.acme4j.util.ClaimBuilder; import org.shredzone.acme4j.util.TestUtils; @@ -53,8 +52,8 @@ import org.shredzone.acme4j.util.TestUtils; public class ConnectionTest { private URI requestUri; - private AcmeClientProvider mockProvider; private HttpURLConnection mockUrlConnection; + private HttpConnector mockHttpConnection; @Before public void setup() throws IOException, URISyntaxException { @@ -62,8 +61,8 @@ public class ConnectionTest { mockUrlConnection = mock(HttpURLConnection.class); - mockProvider = mock(AcmeClientProvider.class); - when(mockProvider.openConnection(requestUri)).thenReturn(mockUrlConnection); + mockHttpConnection = mock(HttpConnector.class); + when(mockHttpConnection.openConnection(requestUri)).thenReturn(mockUrlConnection); } /** @@ -74,7 +73,7 @@ public class ConnectionTest { public void testNoNonceFromHeader() throws AcmeException { when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(null); - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.getNonceFromHeader(mockUrlConnection); fail("Expected to fail"); } catch (AcmeException ex) { @@ -83,7 +82,6 @@ public class ConnectionTest { verify(mockUrlConnection).getHeaderField("Replay-Nonce"); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -97,14 +95,13 @@ public class ConnectionTest { when(mockUrlConnection.getHeaderField("Replay-Nonce")) .thenReturn(Base64Url.encode(nonce)); - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { byte[] nonceFromHeader = conn.getNonceFromHeader(mockUrlConnection); assertThat(nonceFromHeader, is(nonce)); } verify(mockUrlConnection).getHeaderField("Replay-Nonce"); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -117,7 +114,7 @@ public class ConnectionTest { when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(badNonce); - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.getNonceFromHeader(mockUrlConnection); fail("Expected to fail"); } catch (AcmeException ex) { @@ -126,7 +123,6 @@ public class ConnectionTest { verify(mockUrlConnection).getHeaderField("Replay-Nonce"); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -136,7 +132,7 @@ public class ConnectionTest { public void testGetLocation() throws Exception { when(mockUrlConnection.getHeaderField("Location")).thenReturn("http://example.com/otherlocation"); - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.conn = mockUrlConnection; URI location = conn.getLocation(); assertThat(location, is(new URI("http://example.com/otherlocation"))); @@ -144,7 +140,6 @@ public class ConnectionTest { verify(mockUrlConnection).getHeaderField("Location"); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -152,7 +147,7 @@ public class ConnectionTest { */ @Test public void testNoLocation() throws Exception { - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.conn = mockUrlConnection; URI location = conn.getLocation(); assertThat(location, is(nullValue())); @@ -160,7 +155,6 @@ public class ConnectionTest { verify(mockUrlConnection).getHeaderField("Location"); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -170,14 +164,13 @@ public class ConnectionTest { public void testNoThrowException() throws AcmeException { when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json"); - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.conn = mockUrlConnection; conn.throwException(); } verify(mockUrlConnection).getHeaderField("Content-Type"); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -191,7 +184,7 @@ public class ConnectionTest { when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.conn = mockUrlConnection; conn.throwException(); fail("Expected to fail"); @@ -207,7 +200,6 @@ public class ConnectionTest { verify(mockUrlConnection).getResponseCode(); verify(mockUrlConnection).getErrorStream(); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -218,7 +210,7 @@ public class ConnectionTest { when(mockUrlConnection.getHeaderField("Content-Type")) .thenReturn("application/problem+json"); - try (Connection conn = new Connection(mockProvider) { + try (Connection conn = new Connection(mockHttpConnection) { @Override public Map readJsonResponse() throws AcmeException { Map result = new HashMap(); @@ -240,7 +232,6 @@ public class ConnectionTest { verify(mockUrlConnection).getHeaderField("Content-Type"); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -254,7 +245,7 @@ public class ConnectionTest { .thenReturn(Base64Url.encode(nonce)); Session session = new Session(); - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.startSession(requestUri, session); } assertThat(session.getNonce(), is(nonce)); @@ -262,9 +253,7 @@ public class ConnectionTest { verify(mockUrlConnection).setRequestMethod("HEAD"); verify(mockUrlConnection).connect(); verify(mockUrlConnection).getHeaderField("Replay-Nonce"); - verify(mockProvider).openConnection(requestUri); verifyNoMoreInteractions(mockUrlConnection); - verifyNoMoreInteractions(mockProvider); } /** @@ -274,7 +263,7 @@ public class ConnectionTest { public void testSendRequest() throws Exception { final Set invoked = new HashSet<>(); - try (Connection conn = new Connection(mockProvider) { + try (Connection conn = new Connection(mockHttpConnection) { @Override protected void throwException() throws AcmeException { invoked.add("throwException"); @@ -288,9 +277,7 @@ public class ConnectionTest { verify(mockUrlConnection).setDoOutput(false); verify(mockUrlConnection).connect(); verify(mockUrlConnection).getResponseCode(); - verify(mockProvider).openConnection(requestUri); verifyNoMoreInteractions(mockUrlConnection); - verifyNoMoreInteractions(mockProvider); assertThat(invoked, hasItem("throwException")); } @@ -308,7 +295,7 @@ public class ConnectionTest { when(mockUrlConnection.getOutputStream()).thenReturn(outputStream); when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(Base64Url.encode(nonce2)); - try (Connection conn = new Connection(mockProvider) { + try (Connection conn = new Connection(mockHttpConnection) { @Override protected void throwException() throws AcmeException { invoked.add("throwException"); @@ -342,9 +329,7 @@ public class ConnectionTest { verify(mockUrlConnection, atLeastOnce()).getHeaderField(anyString()); verify(mockUrlConnection).getOutputStream(); verify(mockUrlConnection).getResponseCode(); - verify(mockProvider).openConnection(requestUri); verifyNoMoreInteractions(mockUrlConnection); - verifyNoMoreInteractions(mockProvider); assertThat(invoked, hasItems("throwException", "startSession")); String[] written = CompactSerializer.deserialize(new String(outputStream.toByteArray(), "utf-8")); @@ -378,7 +363,7 @@ public class ConnectionTest { when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.conn = mockUrlConnection; Map result = conn.readJsonResponse(); assertThat(result.keySet(), hasSize(2)); @@ -390,7 +375,6 @@ public class ConnectionTest { verify(mockUrlConnection).getResponseCode(); verify(mockUrlConnection).getInputStream(); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -408,7 +392,7 @@ public class ConnectionTest { when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(original.getEncoded())); X509Certificate downloaded; - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.conn = mockUrlConnection; downloaded = conn.readCertificate(); } @@ -420,7 +404,6 @@ public class ConnectionTest { verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection).getInputStream(); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } /** @@ -438,7 +421,7 @@ public class ConnectionTest { when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json"); when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.toString().getBytes("utf-8"))); - try (Connection conn = new Connection(mockProvider)) { + try (Connection conn = new Connection(mockHttpConnection)) { conn.conn = mockUrlConnection; Map result = conn.readDirectory(); assertThat(result.keySet(), hasSize(2)); @@ -450,7 +433,6 @@ public class ConnectionTest { verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection).getInputStream(); verifyNoMoreInteractions(mockUrlConnection); - verifyZeroInteractions(mockProvider); } } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java new file mode 100644 index 00000000..e0c8f3f5 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java @@ -0,0 +1,50 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2015 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.connector; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +/** + * Unit tests for {@link HttpConnector}. + * + * @author Richard "Shred" Körber + */ +public class HttpConnectorTest { + + /** + * Test if a HTTP connection can be opened. + *

+ * This test requires a network connection. It should be excluded from automated + * builds. + */ + @Test + @Category(HttpURLConnection.class) + public void testOpenConnection() throws IOException, URISyntaxException { + HttpConnector connector = new HttpConnector(); + HttpURLConnection conn = connector.openConnection(new URI("http://example.com")); + assertThat(conn, not(nullValue())); + conn.connect(); + assertThat(conn.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeClientProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeClientProviderTest.java index ba38cdab..37399ebe 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeClientProviderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeClientProviderTest.java @@ -16,13 +16,10 @@ package org.shredzone.acme4j.provider; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import java.io.IOException; -import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import org.junit.Test; -import org.junit.experimental.categories.Category; import org.shredzone.acme4j.AcmeClient; import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.DnsChallenge; @@ -76,33 +73,6 @@ public class AbstractAcmeClientProviderTest { } } - /** - * Test if a HTTP connection can be opened. - *

- * This test requires a network connection. It should be excluded from automated - * builds. - */ - @Test - @Category(HttpURLConnection.class) - public void testOpenConnection() throws IOException, URISyntaxException { - AbstractAcmeClientProvider provider = new AbstractAcmeClientProvider() { - @Override - public boolean accepts(URI serverUri) { - throw new UnsupportedOperationException(); - } - - @Override - protected URI resolve(URI serverUri) { - throw new UnsupportedOperationException(); - } - }; - - HttpURLConnection conn = provider.openConnection(new URI("http://example.com")); - assertThat(conn, not(nullValue())); - conn.connect(); - assertThat(conn.getResponseCode(), is(HttpURLConnection.HTTP_OK)); - } - /** * Test that all base challenges are registered on initialization, and that additional * challenges are properly registered. diff --git a/acme4j-letsencrypt/src/main/java/org/shredzone/acme4j/provider/LetsEncryptAcmeClientProvider.java b/acme4j-letsencrypt/src/main/java/org/shredzone/acme4j/provider/LetsEncryptAcmeClientProvider.java index ac543600..bdc62e46 100644 --- a/acme4j-letsencrypt/src/main/java/org/shredzone/acme4j/provider/LetsEncryptAcmeClientProvider.java +++ b/acme4j-letsencrypt/src/main/java/org/shredzone/acme4j/provider/LetsEncryptAcmeClientProvider.java @@ -13,20 +13,10 @@ */ package org.shredzone.acme4j.provider; -import java.io.IOException; -import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManagerFactory; +import org.shredzone.acme4j.connector.HttpConnector; /** * An {@link AcmeClientProvider} for Let's Encrypt. @@ -45,8 +35,6 @@ public class LetsEncryptAcmeClientProvider extends AbstractAcmeClientProvider { private static final String V01_DIRECTORY_URI = "https://acme-v01.api.letsencrypt.org/directory"; private static final String STAGING_DIRECTORY_URI = "https://acme-staging.api.letsencrypt.org/directory"; - private SSLSocketFactory sslSocketFactory; - @Override public boolean accepts(URI serverUri) { return "acme".equals(serverUri.getScheme()) @@ -73,38 +61,8 @@ public class LetsEncryptAcmeClientProvider extends AbstractAcmeClientProvider { } @Override - public HttpURLConnection openConnection(URI uri) throws IOException { - HttpURLConnection conn = super.openConnection(uri); - if (conn instanceof HttpsURLConnection) { - ((HttpsURLConnection) conn).setSSLSocketFactory(createSocketFactory()); - } - return conn; - } - - /** - * Lazily creates an {@link SSLSocketFactory} that exclusively accepts the Let's - * Encrypt certificate. - */ - protected SSLSocketFactory createSocketFactory() throws IOException { - if (sslSocketFactory == null) { - try { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(getClass().getResourceAsStream("/org/shredzone/acme4j/letsencrypt.truststore"), - "acme4j".toCharArray()); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(keystore); - - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(null, tmf.getTrustManagers(), null); - - sslSocketFactory = ctx.getSocketFactory(); - } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException - | KeyManagementException ex) { - throw new IOException("Could not create truststore", ex); - } - } - return sslSocketFactory; + protected HttpConnector createHttpConnector() { + return new LetsEncryptHttpConnector(); } } diff --git a/acme4j-letsencrypt/src/main/java/org/shredzone/acme4j/provider/LetsEncryptHttpConnector.java b/acme4j-letsencrypt/src/main/java/org/shredzone/acme4j/provider/LetsEncryptHttpConnector.java new file mode 100644 index 00000000..eaa5d710 --- /dev/null +++ b/acme4j-letsencrypt/src/main/java/org/shredzone/acme4j/provider/LetsEncryptHttpConnector.java @@ -0,0 +1,77 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2015 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.provider; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import org.shredzone.acme4j.connector.HttpConnector; + +/** + * {@link HttpConnector} to be used for Let's Encrypt. It is pinned to the Let's Encrypt + * server certificate. + * + * @author Richard "Shred" Körber + */ +public class LetsEncryptHttpConnector extends HttpConnector { + + private SSLSocketFactory sslSocketFactory; + + @Override + public HttpURLConnection openConnection(URI uri) throws IOException { + HttpURLConnection conn = super.openConnection(uri); + if (conn instanceof HttpsURLConnection) { + ((HttpsURLConnection) conn).setSSLSocketFactory(createSocketFactory()); + } + return conn; + } + + /** + * Lazily creates an {@link SSLSocketFactory} that exclusively accepts the Let's + * Encrypt certificate. + */ + protected SSLSocketFactory createSocketFactory() throws IOException { + if (sslSocketFactory == null) { + try { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(getClass().getResourceAsStream("/org/shredzone/acme4j/letsencrypt.truststore"), + "acme4j".toCharArray()); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keystore); + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, tmf.getTrustManagers(), null); + + sslSocketFactory = ctx.getSocketFactory(); + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException + | KeyManagementException ex) { + throw new IOException("Could not create truststore", ex); + } + } + return sslSocketFactory; + } + +} diff --git a/acme4j-letsencrypt/src/test/java/org/shredzone/acme4j/provider/LetsEncryptAcmeClientProviderTest.java b/acme4j-letsencrypt/src/test/java/org/shredzone/acme4j/provider/LetsEncryptAcmeClientProviderTest.java index fbb44593..894f5786 100644 --- a/acme4j-letsencrypt/src/test/java/org/shredzone/acme4j/provider/LetsEncryptAcmeClientProviderTest.java +++ b/acme4j-letsencrypt/src/test/java/org/shredzone/acme4j/provider/LetsEncryptAcmeClientProviderTest.java @@ -13,20 +13,13 @@ */ package org.shredzone.acme4j.provider; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; -import java.io.IOException; -import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLSocketFactory; - import org.junit.Test; -import org.junit.experimental.categories.Category; /** * Unit tests for {@link LetsEncryptAcmeClientProvider}. @@ -74,50 +67,4 @@ public class LetsEncryptAcmeClientProviderTest { } } - /** - * Test if the {@link LetsEncryptAcmeClientProvider#openConnection(URI)} accepts only - * the Let's Encrypt certificate. - *

- * This test requires a network connection. It should be excluded from automated - * builds. - */ - @Test - @Category(HttpURLConnection.class) - public void testCertificate() throws IOException, URISyntaxException { - LetsEncryptAcmeClientProvider provider = new LetsEncryptAcmeClientProvider(); - - try { - HttpURLConnection goodConn = provider.openConnection( - new URI("https://acme-staging.api.letsencrypt.org/directory")); - assertThat(goodConn, is(instanceOf(HttpsURLConnection.class))); - goodConn.connect(); - } catch (SSLHandshakeException ex) { - fail("Connection does not accept Let's Encrypt certificate"); - } - - try { - HttpURLConnection badConn = provider.openConnection( - new URI("https://www.google.com")); - assertThat(badConn, is(instanceOf(HttpsURLConnection.class))); - badConn.connect(); - fail("Connection accepts foreign certificate"); - } catch (SSLHandshakeException ex) { - // expected - } - } - - /** - * Test that the {@link SSLSocketFactory} can be instantiated and is cached. - */ - @Test - public void testCreateSocketFactory() throws IOException { - LetsEncryptAcmeClientProvider provider = new LetsEncryptAcmeClientProvider(); - - SSLSocketFactory factory1 = provider.createSocketFactory(); - assertThat(factory1, is(notNullValue())); - - SSLSocketFactory factory2 = provider.createSocketFactory(); - assertThat(factory1, is(sameInstance(factory2))); - } - } diff --git a/acme4j-letsencrypt/src/test/java/org/shredzone/acme4j/provider/LetsEncryptHttpConnectorTest.java b/acme4j-letsencrypt/src/test/java/org/shredzone/acme4j/provider/LetsEncryptHttpConnectorTest.java new file mode 100644 index 00000000..e16575d3 --- /dev/null +++ b/acme4j-letsencrypt/src/test/java/org/shredzone/acme4j/provider/LetsEncryptHttpConnectorTest.java @@ -0,0 +1,84 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2015 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.provider; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocketFactory; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +/** + * Unit test for {@link LetsEncryptHttpConnector}. + * + * @author Richard "Shred" Körber + */ +public class LetsEncryptHttpConnectorTest { + + /** + * Test if the {@link LetsEncryptAcmeClientProvider#openConnection(URI)} accepts only + * the Let's Encrypt certificate. + *

+ * This test requires a network connection. It should be excluded from automated + * builds. + */ + @Test + @Category(HttpURLConnection.class) + public void testCertificate() throws IOException, URISyntaxException { + LetsEncryptHttpConnector connector = new LetsEncryptHttpConnector(); + + try { + HttpURLConnection goodConn = connector.openConnection( + new URI("https://acme-staging.api.letsencrypt.org/directory")); + assertThat(goodConn, is(instanceOf(HttpsURLConnection.class))); + goodConn.connect(); + } catch (SSLHandshakeException ex) { + fail("Connection does not accept Let's Encrypt certificate"); + } + + try { + HttpURLConnection badConn = connector.openConnection( + new URI("https://www.google.com")); + assertThat(badConn, is(instanceOf(HttpsURLConnection.class))); + badConn.connect(); + fail("Connection accepts foreign certificate"); + } catch (SSLHandshakeException ex) { + // expected + } + } + + /** + * Test that the {@link SSLSocketFactory} can be instantiated and is cached. + */ + @Test + public void testCreateSocketFactory() throws IOException { + LetsEncryptHttpConnector connector = new LetsEncryptHttpConnector(); + + SSLSocketFactory factory1 = connector.createSocketFactory(); + assertThat(factory1, is(notNullValue())); + + SSLSocketFactory factory2 = connector.createSocketFactory(); + assertThat(factory1, is(sameInstance(factory2))); + } + +}