mirror of https://github.com/shred/acme4j
Boulder needs a pre-draft-15 compatibility mode
parent
1297ca4de2
commit
bb35678c2d
|
@ -137,32 +137,7 @@ public class DefaultConnection implements Connection {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendRequest(URL url, Session session) throws AcmeException {
|
public void sendRequest(URL url, Session session) throws AcmeException {
|
||||||
Objects.requireNonNull(url, "url");
|
sendRequest(url, session, MIME_JSON);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -189,31 +164,6 @@ public class DefaultConnection implements Connection {
|
||||||
return sendSignedRequest(url, claims, session, keypair, null, MIME_JSON);
|
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
|
@Override
|
||||||
@CheckForNull
|
@CheckForNull
|
||||||
public JSON readJsonResponse() throws AcmeException {
|
public JSON readJsonResponse() throws AcmeException {
|
||||||
|
@ -320,6 +270,96 @@ public class DefaultConnection implements Connection {
|
||||||
conn = null;
|
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.
|
* Performs the POST request.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import java.net.URL;
|
||||||
|
|
||||||
import javax.annotation.ParametersAreNonnullByDefault;
|
import javax.annotation.ParametersAreNonnullByDefault;
|
||||||
|
|
||||||
|
import org.shredzone.acme4j.connector.Connection;
|
||||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||||
import org.shredzone.acme4j.provider.AbstractAcmeProvider;
|
import org.shredzone.acme4j.provider.AbstractAcmeProvider;
|
||||||
import org.shredzone.acme4j.provider.AcmeProvider;
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -648,7 +648,12 @@ public class DefaultConnectionTest {
|
||||||
public void testSendRequest() throws Exception {
|
public void testSendRequest() throws Exception {
|
||||||
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
|
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);
|
conn.sendRequest(requestUrl, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,6 +13,7 @@
|
||||||
*/
|
*/
|
||||||
package org.shredzone.acme4j.provider.letsencrypt;
|
package org.shredzone.acme4j.provider.letsencrypt;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
import static org.shredzone.acme4j.toolbox.TestUtils.url;
|
import static org.shredzone.acme4j.toolbox.TestUtils.url;
|
||||||
|
@ -21,6 +22,7 @@ import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.shredzone.acme4j.connector.Connection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link LetsEncryptAcmeProvider}.
|
* 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)));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue