Boulder needs a pre-draft-15 compatibility mode

pull/81/head
Richard Körber 2018-09-26 19:27:41 +02:00
parent 1297ca4de2
commit bb35678c2d
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
6 changed files with 291 additions and 52 deletions

View File

@ -137,32 +137,7 @@ public class DefaultConnection implements Connection {
@Override
public void sendRequest(URL url, Session session) throws AcmeException {
Objects.requireNonNull(url, "url");
Objects.requireNonNull(session, "session");
assertConnectionIsClosed();
LOG.debug("GET {}", url);
try {
conn = httpConnector.openConnection(url, session.getProxy());
conn.setRequestMethod("GET");
conn.setRequestProperty(ACCEPT_HEADER, MIME_JSON);
conn.setRequestProperty(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET);
conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag());
conn.setDoOutput(false);
conn.connect();
logHeaders();
int rc = conn.getResponseCode();
if (rc != HttpURLConnection.HTTP_OK) {
throwAcmeException();
}
} catch (IOException ex) {
throw new AcmeNetworkException(ex);
}
sendRequest(url, session, MIME_JSON);
}
@Override
@ -189,31 +164,6 @@ public class DefaultConnection implements Connection {
return sendSignedRequest(url, claims, session, keypair, null, MIME_JSON);
}
private int sendSignedRequest(URL url, @Nullable JSONBuilder claims, Session session,
KeyPair keypair, @Nullable URL accountLocation, String accept) throws AcmeException {
Objects.requireNonNull(url, "url");
Objects.requireNonNull(session, "session");
Objects.requireNonNull(keypair, "keypair");
Objects.requireNonNull(accept, "accept");
assertConnectionIsClosed();
AcmeException lastException = null;
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
return performRequest(url, claims, session, keypair, accountLocation, accept);
} catch (AcmeServerException ex) {
if (!BAD_NONCE_ERROR.equals(ex.getType())) {
throw ex;
}
lastException = ex;
LOG.info("Bad Replay Nonce, trying again (attempt {}/{})", attempt, MAX_ATTEMPTS);
}
}
throw new AcmeException("Too many reattempts", lastException);
}
@Override
@CheckForNull
public JSON readJsonResponse() throws AcmeException {
@ -320,6 +270,96 @@ public class DefaultConnection implements Connection {
conn = null;
}
/**
* Sends an unsigned GET request.
*
* @param url
* {@link URL} to send the request to.
* @param session
* {@link Session} instance to be used for signing and tracking
* @param accept
* Accept header
* @return HTTP 200 class status that was returned
*/
protected int sendRequest(URL url, Session session, String accept) throws AcmeException {
Objects.requireNonNull(url, "url");
Objects.requireNonNull(session, "session");
Objects.requireNonNull(accept, "accept");
assertConnectionIsClosed();
LOG.debug("GET {}", url);
try {
conn = httpConnector.openConnection(url, session.getProxy());
conn.setRequestMethod("GET");
conn.setRequestProperty(ACCEPT_HEADER, accept);
conn.setRequestProperty(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET);
conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag());
conn.setDoOutput(false);
conn.connect();
logHeaders();
String nonce = getNonce();
if (nonce != null) {
session.setNonce(nonce);
}
int rc = conn.getResponseCode();
if (rc != HttpURLConnection.HTTP_OK && rc != HttpURLConnection.HTTP_CREATED) {
throwAcmeException();
}
return rc;
} catch (IOException ex) {
throw new AcmeNetworkException(ex);
}
}
/**
* Sends a signed POST request.
*
* @param url
* {@link URL} to send the request to.
* @param claims
* {@link JSONBuilder} containing claims. {@code null} for POST-as-GET
* request.
* @param session
* {@link Session} instance to be used for signing and tracking
* @param keypair
* {@link KeyPair} to be used for signing
* @param accountLocation
* If set, the account location is set as "kid" header. If {@code null},
* the public key is set as "jwk" header.
* @param accept
* Accept header
* @return HTTP 200 class status that was returned
*/
protected int sendSignedRequest(URL url, @Nullable JSONBuilder claims, Session session,
KeyPair keypair, @Nullable URL accountLocation, String accept) throws AcmeException {
Objects.requireNonNull(url, "url");
Objects.requireNonNull(session, "session");
Objects.requireNonNull(keypair, "keypair");
Objects.requireNonNull(accept, "accept");
assertConnectionIsClosed();
AcmeException lastException = null;
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
return performRequest(url, claims, session, keypair, accountLocation, accept);
} catch (AcmeServerException ex) {
if (!BAD_NONCE_ERROR.equals(ex.getType())) {
throw ex;
}
lastException = ex;
LOG.info("Bad Replay Nonce, trying again (attempt {}/{})", attempt, MAX_ATTEMPTS);
}
}
throw new AcmeException("Too many reattempts", lastException);
}
/**
* Performs the POST request.
*

View File

@ -0,0 +1,49 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2018 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.net.URL;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.exception.AcmeException;
/**
* This {@link Connection} is used for servers that do not implement the POST-as-GET
* feature that was introduced in ACME draft-15.
*
* @since 2.4
* @deprecated Only meant for compatibility purposes. If your server needs this
* connection, it should be fixed soon.
*/
@Deprecated
public class PreDraft15Connection extends DefaultConnection {
private static final String MIME_JSON = "application/json";
private static final String MIME_CERTIFICATE_CHAIN = "application/pem-certificate-chain";
public PreDraft15Connection(HttpConnector httpConnector) {
super(httpConnector);
}
@Override
public int sendCertificateRequest(URL url, Login login) throws AcmeException {
return sendRequest(url, login.getSession(), MIME_CERTIFICATE_CHAIN);
}
@Override
public int sendSignedPostAsGetRequest(URL url, Login login) throws AcmeException {
return sendRequest(url, login.getSession(), MIME_JSON);
}
}

View File

@ -19,6 +19,7 @@ import java.net.URL;
import javax.annotation.ParametersAreNonnullByDefault;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.provider.AbstractAcmeProvider;
import org.shredzone.acme4j.provider.AcmeProvider;
@ -64,4 +65,10 @@ public class LetsEncryptAcmeProvider extends AbstractAcmeProvider {
}
}
@Override
@SuppressWarnings("deprecation")
public Connection connect() {
return new org.shredzone.acme4j.connector.PreDraft15Connection(createHttpConnector());
}
}

View File

@ -648,7 +648,12 @@ public class DefaultConnectionTest {
public void testSendRequest() throws Exception {
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
@Override
public String getNonce() {
return null;
}
}) {
conn.sendRequest(requestUrl, session);
}

View File

@ -0,0 +1,125 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2018 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.mockito.Mockito.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair;
import java.util.Locale;
import org.jose4j.base64url.Base64Url;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.AcmeProvider;
import org.shredzone.acme4j.toolbox.TestUtils;
/**
* Unit tests for {@link PreDraft15Connection}.
*/
@SuppressWarnings("deprecation")
public class PreDraft15ConnectionTest {
private URL requestUrl = TestUtils.url("http://example.com/acme/");
private URL accountUrl = TestUtils.url(TestUtils.ACCOUNT_URL);
private HttpURLConnection mockUrlConnection;
private HttpConnector mockHttpConnection;
private Session session;
private Login login;
private KeyPair keyPair;
@Before
public void setup() throws AcmeException, IOException {
mockUrlConnection = mock(HttpURLConnection.class);
mockHttpConnection = mock(HttpConnector.class);
when(mockHttpConnection.openConnection(requestUrl, Proxy.NO_PROXY)).thenReturn(mockUrlConnection);
final AcmeProvider mockProvider = mock(AcmeProvider.class);
when(mockProvider.directory(
ArgumentMatchers.any(Session.class),
ArgumentMatchers.eq(URI.create(TestUtils.ACME_SERVER_URI))))
.thenReturn(TestUtils.getJSON("directory"));
session = TestUtils.session(mockProvider);
session.setLocale(Locale.JAPAN);
keyPair = TestUtils.createKeyPair();
login = session.login(accountUrl, keyPair);
}
/**
* Test signed POST-as-GET requests in compatibility mode.
*/
@Test
public void testSendSignedPostAsGetRequest() throws Exception {
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
try (PreDraft15Connection conn = new PreDraft15Connection(mockHttpConnection) {
@Override
public String getNonce() {
return Base64Url.encode("foo-nonce-1-foo".getBytes());
}
}) {
conn.sendSignedPostAsGetRequest(requestUrl, login);
}
verify(mockUrlConnection).setRequestMethod("GET");
verify(mockUrlConnection).setRequestProperty("Accept", "application/json");
verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8");
verify(mockUrlConnection).setRequestProperty("Accept-Language", "ja-JP");
verify(mockUrlConnection).setDoOutput(false);
verify(mockUrlConnection).connect();
verify(mockUrlConnection).getResponseCode();
verify(mockUrlConnection, atLeast(0)).getHeaderFields();
verifyNoMoreInteractions(mockUrlConnection);
}
/**
* Test certificate POST-as-GET requests in compatibility mode.
*/
@Test
public void testSendCertificateRequest() throws Exception {
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
try (PreDraft15Connection conn = new PreDraft15Connection(mockHttpConnection) {
@Override
public String getNonce() {
return Base64Url.encode("foo-nonce-1-foo".getBytes());
}
}) {
conn.sendCertificateRequest(requestUrl, login);
}
verify(mockUrlConnection).setRequestMethod("GET");
verify(mockUrlConnection).setRequestProperty("Accept", "application/pem-certificate-chain");
verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8");
verify(mockUrlConnection).setRequestProperty("Accept-Language", "ja-JP");
verify(mockUrlConnection).setDoOutput(false);
verify(mockUrlConnection).connect();
verify(mockUrlConnection).getResponseCode();
verify(mockUrlConnection, atLeast(0)).getHeaderFields();
verifyNoMoreInteractions(mockUrlConnection);
}
}

View File

@ -13,6 +13,7 @@
*/
package org.shredzone.acme4j.provider.letsencrypt;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;
import static org.shredzone.acme4j.toolbox.TestUtils.url;
@ -21,6 +22,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import org.junit.Test;
import org.shredzone.acme4j.connector.Connection;
/**
* Unit tests for {@link LetsEncryptAcmeProvider}.
@ -66,4 +68,15 @@ public class LetsEncryptAcmeProviderTest {
}
}
/**
* Test that Boulder is still having pre draft-15 connections.
*/
@Test
@SuppressWarnings("deprecation")
public void testConnect() {
LetsEncryptAcmeProvider provider = new LetsEncryptAcmeProvider();
Connection connection = provider.connect();
assertThat(connection, is(instanceOf(org.shredzone.acme4j.connector.PreDraft15Connection.class)));
}
}