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.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");

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
*/
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();
}

View File

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

View File

@ -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<String, Class<? extends Challenge>> 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();
}
/**

View File

@ -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 extends Challenge> 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();
}

View File

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

View File

@ -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<String,Object> readJsonResponse() throws AcmeException {
Map<String, Object> result = new HashMap<String, Object>();
@ -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<String> 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<String, Object> 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<Resource, URI> 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);
}
}

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.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.
* <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
* challenges are properly registered.

View File

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

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