Refactor, use new HttpConnector for connecting to server

pull/17/merge
Richard Körber 2015-12-13 19:37:27 +01:00
parent b12ee4a28a
commit 0f4d5e114d
14 changed files with 317 additions and 206 deletions

View File

@ -38,7 +38,6 @@ import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.Account; import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeServerException; import org.shredzone.acme4j.exception.AcmeServerException;
import org.shredzone.acme4j.provider.AcmeClientProvider;
import org.shredzone.acme4j.util.ClaimBuilder; import org.shredzone.acme4j.util.ClaimBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -49,16 +48,15 @@ import org.slf4j.LoggerFactory;
* @author Richard "Shred" Körber * @author Richard "Shred" Körber
*/ */
public class Connection implements AutoCloseable { public class Connection implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(Connection.class); private static final Logger LOG = LoggerFactory.getLogger(Connection.class);
private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]+"); private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]+");
private final AcmeClientProvider provider; protected final HttpConnector httpConnector;
protected HttpURLConnection conn; protected HttpURLConnection conn;
public Connection(AcmeClientProvider provider) { public Connection(HttpConnector httpConnector) {
this.provider = provider; this.httpConnector = httpConnector;
} }
@Override @Override
@ -78,7 +76,7 @@ public class Connection implements AutoCloseable {
public void startSession(URI uri, Session session) throws AcmeException { public void startSession(URI uri, Session session) throws AcmeException {
try { try {
LOG.debug("Initial replay nonce from {}", uri); LOG.debug("Initial replay nonce from {}", uri);
HttpURLConnection localConn = provider.openConnection(uri); HttpURLConnection localConn = httpConnector.openConnection(uri);
localConn.setRequestMethod("HEAD"); localConn.setRequestMethod("HEAD");
localConn.connect(); localConn.connect();
@ -99,7 +97,7 @@ public class Connection implements AutoCloseable {
try { try {
LOG.debug("GET {}", uri); LOG.debug("GET {}", uri);
conn = provider.openConnection(uri); conn = httpConnector.openConnection(uri);
conn.setRequestMethod("GET"); conn.setRequestMethod("GET");
conn.setRequestProperty("Accept-Charset", "utf-8"); conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setDoOutput(false); conn.setDoOutput(false);
@ -140,7 +138,7 @@ public class Connection implements AutoCloseable {
LOG.debug("POST {} with claims: {}", uri, claims); LOG.debug("POST {} with claims: {}", uri, claims);
conn = provider.openConnection(uri); conn = httpConnector.openConnection(uri);
conn.setRequestMethod("POST"); conn.setRequestMethod("POST");
conn.setRequestProperty("Accept", "application/json"); conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Accept-Charset", "utf-8"); conn.setRequestProperty("Accept-Charset", "utf-8");

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -66,11 +66,11 @@ public abstract class AbstractAcmeClient implements AcmeClient {
* *
* @return {@link Connection} instance * @return {@link Connection} instance
*/ */
protected abstract Connection connect(); protected abstract Connection createConnection();
@Override @Override
public void newRegistration(Account account, Registration registration) throws AcmeException { public void newRegistration(Account account, Registration registration) throws AcmeException {
try (Connection conn = connect()) { try (Connection conn = createConnection()) {
ClaimBuilder claims = new ClaimBuilder(); ClaimBuilder claims = new ClaimBuilder();
claims.putResource(Resource.NEW_REG); claims.putResource(Resource.NEW_REG);
if (!registration.getContacts().isEmpty()) { 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."); throw new IllegalArgumentException("location must be set. Use newRegistration() if not known.");
} }
try (Connection conn = connect()) { try (Connection conn = createConnection()) {
ClaimBuilder claims = new ClaimBuilder(); ClaimBuilder claims = new ClaimBuilder();
claims.putResource("reg"); claims.putResource("reg");
if (!registration.getContacts().isEmpty()) { if (!registration.getContacts().isEmpty()) {
@ -116,7 +116,7 @@ public abstract class AbstractAcmeClient implements AcmeClient {
@Override @Override
public void newAuthorization(Account account, Authorization auth) throws AcmeException { public void newAuthorization(Account account, Authorization auth) throws AcmeException {
try (Connection conn = connect()) { try (Connection conn = createConnection()) {
ClaimBuilder claims = new ClaimBuilder(); ClaimBuilder claims = new ClaimBuilder();
claims.putResource(Resource.NEW_AUTHZ); claims.putResource(Resource.NEW_AUTHZ);
claims.object("identifier") claims.object("identifier")
@ -163,7 +163,7 @@ public abstract class AbstractAcmeClient implements AcmeClient {
@Override @Override
public void triggerChallenge(Account account, Challenge challenge) throws AcmeException { public void triggerChallenge(Account account, Challenge challenge) throws AcmeException {
try (Connection conn = connect()) { try (Connection conn = createConnection()) {
ClaimBuilder claims = new ClaimBuilder(); ClaimBuilder claims = new ClaimBuilder();
claims.putResource("challenge"); claims.putResource("challenge");
challenge.marshall(claims); challenge.marshall(claims);
@ -176,7 +176,7 @@ public abstract class AbstractAcmeClient implements AcmeClient {
@Override @Override
public void updateChallenge(Account account, Challenge challenge) throws AcmeException { public void updateChallenge(Account account, Challenge challenge) throws AcmeException {
try (Connection conn = connect()) { try (Connection conn = createConnection()) {
conn.sendRequest(challenge.getUri()); conn.sendRequest(challenge.getUri());
challenge.unmarshall(conn.readJsonResponse()); challenge.unmarshall(conn.readJsonResponse());
} }
@ -184,7 +184,7 @@ public abstract class AbstractAcmeClient implements AcmeClient {
@Override @Override
public URI requestCertificate(Account account, byte[] csr) throws AcmeException { public URI requestCertificate(Account account, byte[] csr) throws AcmeException {
try (Connection conn = connect()) { try (Connection conn = createConnection()) {
ClaimBuilder claims = new ClaimBuilder(); ClaimBuilder claims = new ClaimBuilder();
claims.putResource(Resource.NEW_CERT); claims.putResource(Resource.NEW_CERT);
claims.putBase64("csr", csr); claims.putBase64("csr", csr);
@ -200,7 +200,7 @@ public abstract class AbstractAcmeClient implements AcmeClient {
@Override @Override
public X509Certificate downloadCertificate(URI certUri) throws AcmeException { public X509Certificate downloadCertificate(URI certUri) throws AcmeException {
try (Connection conn = connect()) { try (Connection conn = createConnection()) {
conn.sendRequest(certUri); conn.sendRequest(certUri);
return conn.readCertificate(); return conn.readCertificate();
} }

View File

@ -55,14 +55,14 @@ public class GenericAcmeClient extends AbstractAcmeClient {
} }
@Override @Override
protected Connection connect() { protected Connection createConnection() {
return new Connection(provider); return provider.createConnection();
} }
@Override @Override
protected URI resourceUri(Resource resource) throws AcmeException { protected URI resourceUri(Resource resource) throws AcmeException {
if (directoryMap.isEmpty()) { if (directoryMap.isEmpty()) {
try (Connection conn = connect()) { try (Connection conn = createConnection()) {
conn.sendRequest(directoryUri); conn.sendRequest(directoryUri);
directoryMap.putAll(conn.readDirectory()); directoryMap.putAll(conn.readDirectory());
} }

View File

@ -13,8 +13,6 @@
*/ */
package org.shredzone.acme4j.provider; package org.shredzone.acme4j.provider;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; 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.HttpChallenge;
import org.shredzone.acme4j.challenge.ProofOfPossessionChallenge; import org.shredzone.acme4j.challenge.ProofOfPossessionChallenge;
import org.shredzone.acme4j.challenge.TlsSniChallenge; import org.shredzone.acme4j.challenge.TlsSniChallenge;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.HttpConnector;
import org.shredzone.acme4j.impl.GenericAcmeClient; import org.shredzone.acme4j.impl.GenericAcmeClient;
/** /**
@ -41,8 +41,6 @@ import org.shredzone.acme4j.impl.GenericAcmeClient;
*/ */
public abstract class AbstractAcmeClientProvider implements AcmeClientProvider { public abstract class AbstractAcmeClientProvider implements AcmeClientProvider {
private static final int TIMEOUT = 10000;
private final Map<String, Class<? extends Challenge>> challenges = new HashMap<>(); private final Map<String, Class<? extends Challenge>> challenges = new HashMap<>();
public AbstractAcmeClientProvider() { public AbstractAcmeClientProvider() {
@ -86,13 +84,16 @@ public abstract class AbstractAcmeClientProvider implements AcmeClientProvider {
} }
@Override @Override
public HttpURLConnection openConnection(URI uri) throws IOException { public Connection createConnection() {
HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); return new Connection(createHttpConnector());
conn.setConnectTimeout(TIMEOUT); }
conn.setReadTimeout(TIMEOUT);
conn.setUseCaches(false); /**
conn.setRequestProperty("User-Agent", "acme4j"); * Creates a {@link HttpConnector}. Subclasses may override this method to
return conn; * configure the {@link HttpConnector}.
*/
protected HttpConnector createHttpConnector() {
return new HttpConnector();
} }
/** /**

View File

@ -13,14 +13,13 @@
*/ */
package org.shredzone.acme4j.provider; package org.shredzone.acme4j.provider;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import org.shredzone.acme4j.AcmeClient; import org.shredzone.acme4j.AcmeClient;
import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.GenericChallenge; import org.shredzone.acme4j.challenge.GenericChallenge;
import org.shredzone.acme4j.connector.Connection;
/** /**
* An {@link AcmeClientProvider} creates {@link AcmeClient} instances to be used for * An {@link AcmeClientProvider} creates {@link AcmeClient} instances to be used for
@ -70,13 +69,10 @@ public interface AcmeClientProvider {
<T extends Challenge> T createChallenge(String type); <T extends Challenge> T createChallenge(String type);
/** /**
* Opens a {@link HttpURLConnection} to the given {@link URI}. Implementations may * Creates a {@link Connection} for communication with the ACME server.
* configure the connection, e.g. pin it to a concrete SSL certificate.
* *
* @param uri * @return {@link Connection} that was generated
* {@link URI} to connect to
* @return {@link HttpURLConnection} connected to the {@link URI}
*/ */
HttpURLConnection openConnection(URI uri) throws IOException; Connection createConnection();
} }

View File

@ -17,14 +17,13 @@ import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import org.junit.Test; import org.junit.Test;
import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.AcmeClientProvider; import org.shredzone.acme4j.provider.AcmeClientProvider;
@ -88,7 +87,7 @@ public class AcmeClientFactoryTest {
} }
@Override @Override
public HttpURLConnection openConnection(URI uri) throws IOException { public Connection createConnection() {
fail("not supposed to be invoked"); fail("not supposed to be invoked");
return null; return null;
} }
@ -114,7 +113,7 @@ public class AcmeClientFactoryTest {
} }
@Override @Override
public HttpURLConnection openConnection(URI uri) throws IOException { public Connection createConnection() {
fail("not supposed to be invoked"); fail("not supposed to be invoked");
return null; return null;
} }

View File

@ -41,7 +41,6 @@ import org.junit.Test;
import org.shredzone.acme4j.Account; import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeServerException; import org.shredzone.acme4j.exception.AcmeServerException;
import org.shredzone.acme4j.provider.AcmeClientProvider;
import org.shredzone.acme4j.util.ClaimBuilder; import org.shredzone.acme4j.util.ClaimBuilder;
import org.shredzone.acme4j.util.TestUtils; import org.shredzone.acme4j.util.TestUtils;
@ -53,8 +52,8 @@ import org.shredzone.acme4j.util.TestUtils;
public class ConnectionTest { public class ConnectionTest {
private URI requestUri; private URI requestUri;
private AcmeClientProvider mockProvider;
private HttpURLConnection mockUrlConnection; private HttpURLConnection mockUrlConnection;
private HttpConnector mockHttpConnection;
@Before @Before
public void setup() throws IOException, URISyntaxException { public void setup() throws IOException, URISyntaxException {
@ -62,8 +61,8 @@ public class ConnectionTest {
mockUrlConnection = mock(HttpURLConnection.class); mockUrlConnection = mock(HttpURLConnection.class);
mockProvider = mock(AcmeClientProvider.class); mockHttpConnection = mock(HttpConnector.class);
when(mockProvider.openConnection(requestUri)).thenReturn(mockUrlConnection); when(mockHttpConnection.openConnection(requestUri)).thenReturn(mockUrlConnection);
} }
/** /**
@ -74,7 +73,7 @@ public class ConnectionTest {
public void testNoNonceFromHeader() throws AcmeException { public void testNoNonceFromHeader() throws AcmeException {
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(null); when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(null);
try (Connection conn = new Connection(mockProvider)) { try (Connection conn = new Connection(mockHttpConnection)) {
conn.getNonceFromHeader(mockUrlConnection); conn.getNonceFromHeader(mockUrlConnection);
fail("Expected to fail"); fail("Expected to fail");
} catch (AcmeException ex) { } catch (AcmeException ex) {
@ -83,7 +82,6 @@ public class ConnectionTest {
verify(mockUrlConnection).getHeaderField("Replay-Nonce"); verify(mockUrlConnection).getHeaderField("Replay-Nonce");
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -97,14 +95,13 @@ public class ConnectionTest {
when(mockUrlConnection.getHeaderField("Replay-Nonce")) when(mockUrlConnection.getHeaderField("Replay-Nonce"))
.thenReturn(Base64Url.encode(nonce)); .thenReturn(Base64Url.encode(nonce));
try (Connection conn = new Connection(mockProvider)) { try (Connection conn = new Connection(mockHttpConnection)) {
byte[] nonceFromHeader = conn.getNonceFromHeader(mockUrlConnection); byte[] nonceFromHeader = conn.getNonceFromHeader(mockUrlConnection);
assertThat(nonceFromHeader, is(nonce)); assertThat(nonceFromHeader, is(nonce));
} }
verify(mockUrlConnection).getHeaderField("Replay-Nonce"); verify(mockUrlConnection).getHeaderField("Replay-Nonce");
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -117,7 +114,7 @@ public class ConnectionTest {
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(badNonce); when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(badNonce);
try (Connection conn = new Connection(mockProvider)) { try (Connection conn = new Connection(mockHttpConnection)) {
conn.getNonceFromHeader(mockUrlConnection); conn.getNonceFromHeader(mockUrlConnection);
fail("Expected to fail"); fail("Expected to fail");
} catch (AcmeException ex) { } catch (AcmeException ex) {
@ -126,7 +123,6 @@ public class ConnectionTest {
verify(mockUrlConnection).getHeaderField("Replay-Nonce"); verify(mockUrlConnection).getHeaderField("Replay-Nonce");
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -136,7 +132,7 @@ public class ConnectionTest {
public void testGetLocation() throws Exception { public void testGetLocation() throws Exception {
when(mockUrlConnection.getHeaderField("Location")).thenReturn("http://example.com/otherlocation"); when(mockUrlConnection.getHeaderField("Location")).thenReturn("http://example.com/otherlocation");
try (Connection conn = new Connection(mockProvider)) { try (Connection conn = new Connection(mockHttpConnection)) {
conn.conn = mockUrlConnection; conn.conn = mockUrlConnection;
URI location = conn.getLocation(); URI location = conn.getLocation();
assertThat(location, is(new URI("http://example.com/otherlocation"))); assertThat(location, is(new URI("http://example.com/otherlocation")));
@ -144,7 +140,6 @@ public class ConnectionTest {
verify(mockUrlConnection).getHeaderField("Location"); verify(mockUrlConnection).getHeaderField("Location");
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -152,7 +147,7 @@ public class ConnectionTest {
*/ */
@Test @Test
public void testNoLocation() throws Exception { public void testNoLocation() throws Exception {
try (Connection conn = new Connection(mockProvider)) { try (Connection conn = new Connection(mockHttpConnection)) {
conn.conn = mockUrlConnection; conn.conn = mockUrlConnection;
URI location = conn.getLocation(); URI location = conn.getLocation();
assertThat(location, is(nullValue())); assertThat(location, is(nullValue()));
@ -160,7 +155,6 @@ public class ConnectionTest {
verify(mockUrlConnection).getHeaderField("Location"); verify(mockUrlConnection).getHeaderField("Location");
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -170,14 +164,13 @@ public class ConnectionTest {
public void testNoThrowException() throws AcmeException { public void testNoThrowException() throws AcmeException {
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json"); when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
try (Connection conn = new Connection(mockProvider)) { try (Connection conn = new Connection(mockHttpConnection)) {
conn.conn = mockUrlConnection; conn.conn = mockUrlConnection;
conn.throwException(); conn.throwException();
} }
verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection).getHeaderField("Content-Type");
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -191,7 +184,7 @@ public class ConnectionTest {
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN);
when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); 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.conn = mockUrlConnection;
conn.throwException(); conn.throwException();
fail("Expected to fail"); fail("Expected to fail");
@ -207,7 +200,6 @@ public class ConnectionTest {
verify(mockUrlConnection).getResponseCode(); verify(mockUrlConnection).getResponseCode();
verify(mockUrlConnection).getErrorStream(); verify(mockUrlConnection).getErrorStream();
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -218,7 +210,7 @@ public class ConnectionTest {
when(mockUrlConnection.getHeaderField("Content-Type")) when(mockUrlConnection.getHeaderField("Content-Type"))
.thenReturn("application/problem+json"); .thenReturn("application/problem+json");
try (Connection conn = new Connection(mockProvider) { try (Connection conn = new Connection(mockHttpConnection) {
@Override @Override
public Map<String,Object> readJsonResponse() throws AcmeException { public Map<String,Object> readJsonResponse() throws AcmeException {
Map<String, Object> result = new HashMap<String, Object>(); Map<String, Object> result = new HashMap<String, Object>();
@ -240,7 +232,6 @@ public class ConnectionTest {
verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection).getHeaderField("Content-Type");
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -254,7 +245,7 @@ public class ConnectionTest {
.thenReturn(Base64Url.encode(nonce)); .thenReturn(Base64Url.encode(nonce));
Session session = new Session(); Session session = new Session();
try (Connection conn = new Connection(mockProvider)) { try (Connection conn = new Connection(mockHttpConnection)) {
conn.startSession(requestUri, session); conn.startSession(requestUri, session);
} }
assertThat(session.getNonce(), is(nonce)); assertThat(session.getNonce(), is(nonce));
@ -262,9 +253,7 @@ public class ConnectionTest {
verify(mockUrlConnection).setRequestMethod("HEAD"); verify(mockUrlConnection).setRequestMethod("HEAD");
verify(mockUrlConnection).connect(); verify(mockUrlConnection).connect();
verify(mockUrlConnection).getHeaderField("Replay-Nonce"); verify(mockUrlConnection).getHeaderField("Replay-Nonce");
verify(mockProvider).openConnection(requestUri);
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyNoMoreInteractions(mockProvider);
} }
/** /**
@ -274,7 +263,7 @@ public class ConnectionTest {
public void testSendRequest() throws Exception { public void testSendRequest() throws Exception {
final Set<String> invoked = new HashSet<>(); final Set<String> invoked = new HashSet<>();
try (Connection conn = new Connection(mockProvider) { try (Connection conn = new Connection(mockHttpConnection) {
@Override @Override
protected void throwException() throws AcmeException { protected void throwException() throws AcmeException {
invoked.add("throwException"); invoked.add("throwException");
@ -288,9 +277,7 @@ public class ConnectionTest {
verify(mockUrlConnection).setDoOutput(false); verify(mockUrlConnection).setDoOutput(false);
verify(mockUrlConnection).connect(); verify(mockUrlConnection).connect();
verify(mockUrlConnection).getResponseCode(); verify(mockUrlConnection).getResponseCode();
verify(mockProvider).openConnection(requestUri);
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyNoMoreInteractions(mockProvider);
assertThat(invoked, hasItem("throwException")); assertThat(invoked, hasItem("throwException"));
} }
@ -308,7 +295,7 @@ public class ConnectionTest {
when(mockUrlConnection.getOutputStream()).thenReturn(outputStream); when(mockUrlConnection.getOutputStream()).thenReturn(outputStream);
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(Base64Url.encode(nonce2)); when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(Base64Url.encode(nonce2));
try (Connection conn = new Connection(mockProvider) { try (Connection conn = new Connection(mockHttpConnection) {
@Override @Override
protected void throwException() throws AcmeException { protected void throwException() throws AcmeException {
invoked.add("throwException"); invoked.add("throwException");
@ -342,9 +329,7 @@ public class ConnectionTest {
verify(mockUrlConnection, atLeastOnce()).getHeaderField(anyString()); verify(mockUrlConnection, atLeastOnce()).getHeaderField(anyString());
verify(mockUrlConnection).getOutputStream(); verify(mockUrlConnection).getOutputStream();
verify(mockUrlConnection).getResponseCode(); verify(mockUrlConnection).getResponseCode();
verify(mockProvider).openConnection(requestUri);
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyNoMoreInteractions(mockProvider);
assertThat(invoked, hasItems("throwException", "startSession")); assertThat(invoked, hasItems("throwException", "startSession"));
String[] written = CompactSerializer.deserialize(new String(outputStream.toByteArray(), "utf-8")); 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.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); 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; conn.conn = mockUrlConnection;
Map<String, Object> result = conn.readJsonResponse(); Map<String, Object> result = conn.readJsonResponse();
assertThat(result.keySet(), hasSize(2)); assertThat(result.keySet(), hasSize(2));
@ -390,7 +375,6 @@ public class ConnectionTest {
verify(mockUrlConnection).getResponseCode(); verify(mockUrlConnection).getResponseCode();
verify(mockUrlConnection).getInputStream(); verify(mockUrlConnection).getInputStream();
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -408,7 +392,7 @@ public class ConnectionTest {
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(original.getEncoded())); when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(original.getEncoded()));
X509Certificate downloaded; X509Certificate downloaded;
try (Connection conn = new Connection(mockProvider)) { try (Connection conn = new Connection(mockHttpConnection)) {
conn.conn = mockUrlConnection; conn.conn = mockUrlConnection;
downloaded = conn.readCertificate(); downloaded = conn.readCertificate();
} }
@ -420,7 +404,6 @@ public class ConnectionTest {
verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection).getHeaderField("Content-Type");
verify(mockUrlConnection).getInputStream(); verify(mockUrlConnection).getInputStream();
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
/** /**
@ -438,7 +421,7 @@ public class ConnectionTest {
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json"); when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.toString().getBytes("utf-8"))); 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; conn.conn = mockUrlConnection;
Map<Resource, URI> result = conn.readDirectory(); Map<Resource, URI> result = conn.readDirectory();
assertThat(result.keySet(), hasSize(2)); assertThat(result.keySet(), hasSize(2));
@ -450,7 +433,6 @@ public class ConnectionTest {
verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection).getHeaderField("Content-Type");
verify(mockUrlConnection).getInputStream(); verify(mockUrlConnection).getInputStream();
verifyNoMoreInteractions(mockUrlConnection); verifyNoMoreInteractions(mockUrlConnection);
verifyZeroInteractions(mockProvider);
} }
} }

View File

@ -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.
* <p>
* 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));
}
}

View File

@ -16,13 +16,10 @@ package org.shredzone.acme4j.provider;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import org.junit.Test; import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.shredzone.acme4j.AcmeClient; import org.shredzone.acme4j.AcmeClient;
import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.DnsChallenge; import org.shredzone.acme4j.challenge.DnsChallenge;
@ -76,33 +73,6 @@ public class AbstractAcmeClientProviderTest {
} }
} }
/**
* Test if a HTTP connection can be opened.
* <p>
* 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 * Test that all base challenges are registered on initialization, and that additional
* challenges are properly registered. * challenges are properly registered.

View File

@ -13,20 +13,10 @@
*/ */
package org.shredzone.acme4j.provider; package org.shredzone.acme4j.provider;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; 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 org.shredzone.acme4j.connector.HttpConnector;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
/** /**
* An {@link AcmeClientProvider} for <em>Let's Encrypt</em>. * An {@link AcmeClientProvider} for <em>Let's Encrypt</em>.
@ -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 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 static final String STAGING_DIRECTORY_URI = "https://acme-staging.api.letsencrypt.org/directory";
private SSLSocketFactory sslSocketFactory;
@Override @Override
public boolean accepts(URI serverUri) { public boolean accepts(URI serverUri) {
return "acme".equals(serverUri.getScheme()) return "acme".equals(serverUri.getScheme())
@ -73,38 +61,8 @@ public class LetsEncryptAcmeClientProvider extends AbstractAcmeClientProvider {
} }
@Override @Override
public HttpURLConnection openConnection(URI uri) throws IOException { protected HttpConnector createHttpConnector() {
HttpURLConnection conn = super.openConnection(uri); return new LetsEncryptHttpConnector();
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;
} }
} }

View File

@ -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;
}
}

View File

@ -13,20 +13,13 @@
*/ */
package org.shredzone.acme4j.provider; package org.shredzone.acme4j.provider;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; 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.Test;
import org.junit.experimental.categories.Category;
/** /**
* Unit tests for {@link LetsEncryptAcmeClientProvider}. * 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.
* <p>
* 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)));
}
} }

View File

@ -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.
* <p>
* 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)));
}
}