mirror of https://github.com/shred/acme4j
Use new-nonce resource for fetching initial nonce
parent
be6b511085
commit
7aeb439a62
|
@ -28,6 +28,14 @@ import org.shredzone.acme4j.util.JSONBuilder;
|
||||||
*/
|
*/
|
||||||
public interface Connection extends AutoCloseable {
|
public interface Connection extends AutoCloseable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the session nonce, by fetching a new one.
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* {@link Session} instance to fetch a nonce for
|
||||||
|
*/
|
||||||
|
void resetNonce(Session session) throws AcmeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a simple GET request.
|
* Sends a simple GET request.
|
||||||
*
|
*
|
||||||
|
|
|
@ -90,6 +90,38 @@ public class DefaultConnection implements Connection {
|
||||||
this.httpConnector = Objects.requireNonNull(httpConnector, "httpConnector");
|
this.httpConnector = Objects.requireNonNull(httpConnector, "httpConnector");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resetNonce(Session session) throws AcmeException {
|
||||||
|
assertConnectionIsClosed();
|
||||||
|
|
||||||
|
try {
|
||||||
|
session.setNonce(null);
|
||||||
|
|
||||||
|
URI newNonceUri = session.resourceUri(Resource.NEW_NONCE);
|
||||||
|
|
||||||
|
conn = httpConnector.openConnection(newNonceUri);
|
||||||
|
conn.setRequestMethod("HEAD");
|
||||||
|
conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag());
|
||||||
|
conn.connect();
|
||||||
|
|
||||||
|
int rc = conn.getResponseCode();
|
||||||
|
if (rc != HttpURLConnection.HTTP_OK && rc != HttpURLConnection.HTTP_NO_CONTENT) {
|
||||||
|
throw new AcmeProtocolException("Fetching a nonce returned " + rc + " "
|
||||||
|
+ conn.getResponseMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSession(session);
|
||||||
|
|
||||||
|
if (session.getNonce() == null) {
|
||||||
|
throw new AcmeProtocolException("Server did not provide a nonce");
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AcmeNetworkException(ex);
|
||||||
|
} finally {
|
||||||
|
conn = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendRequest(URI uri, Session session) throws AcmeException {
|
public void sendRequest(URI uri, Session session) throws AcmeException {
|
||||||
Objects.requireNonNull(uri, "uri");
|
Objects.requireNonNull(uri, "uri");
|
||||||
|
@ -124,17 +156,7 @@ public class DefaultConnection implements Connection {
|
||||||
KeyPair keypair = session.getKeyPair();
|
KeyPair keypair = session.getKeyPair();
|
||||||
|
|
||||||
if (session.getNonce() == null) {
|
if (session.getNonce() == null) {
|
||||||
LOG.debug("Getting initial nonce, HEAD {}", uri);
|
resetNonce(session);
|
||||||
conn = httpConnector.openConnection(uri);
|
|
||||||
conn.setRequestMethod("HEAD");
|
|
||||||
conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag());
|
|
||||||
conn.connect();
|
|
||||||
updateSession(session);
|
|
||||||
conn = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.getNonce() == null) {
|
|
||||||
throw new AcmeProtocolException("Server did not provide a nonce");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.debug("POST {} with claims: {}", uri, claims);
|
LOG.debug("POST {} with claims: {}", uri, claims);
|
||||||
|
|
|
@ -22,6 +22,7 @@ public enum Resource {
|
||||||
NEW_REG("new-reg"),
|
NEW_REG("new-reg"),
|
||||||
NEW_AUTHZ("new-authz"),
|
NEW_AUTHZ("new-authz"),
|
||||||
NEW_CERT("new-cert"),
|
NEW_CERT("new-cert"),
|
||||||
|
NEW_NONCE("new-nonce"),
|
||||||
REVOKE_CERT("revoke-cert");
|
REVOKE_CERT("revoke-cert");
|
||||||
|
|
||||||
private final String path;
|
private final String path;
|
||||||
|
|
|
@ -46,6 +46,7 @@ import org.shredzone.acme4j.exception.AcmeNetworkException;
|
||||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||||
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
||||||
import org.shredzone.acme4j.exception.AcmeServerException;
|
import org.shredzone.acme4j.exception.AcmeServerException;
|
||||||
|
import org.shredzone.acme4j.provider.AcmeProvider;
|
||||||
import org.shredzone.acme4j.util.JSON;
|
import org.shredzone.acme4j.util.JSON;
|
||||||
import org.shredzone.acme4j.util.JSONBuilder;
|
import org.shredzone.acme4j.util.JSONBuilder;
|
||||||
import org.shredzone.acme4j.util.TestUtils;
|
import org.shredzone.acme4j.util.TestUtils;
|
||||||
|
@ -61,13 +62,19 @@ public class DefaultConnectionTest {
|
||||||
private Session session;
|
private Session session;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() throws IOException {
|
public void setup() throws AcmeException, IOException {
|
||||||
mockUrlConnection = mock(HttpURLConnection.class);
|
mockUrlConnection = mock(HttpURLConnection.class);
|
||||||
|
|
||||||
mockHttpConnection = mock(HttpConnector.class);
|
mockHttpConnection = mock(HttpConnector.class);
|
||||||
when(mockHttpConnection.openConnection(requestUri)).thenReturn(mockUrlConnection);
|
when(mockHttpConnection.openConnection(requestUri)).thenReturn(mockUrlConnection);
|
||||||
|
|
||||||
session = TestUtils.session();
|
final AcmeProvider mockProvider = mock(AcmeProvider.class);
|
||||||
|
when(mockProvider.directory(
|
||||||
|
ArgumentMatchers.any(Session.class),
|
||||||
|
ArgumentMatchers.eq(URI.create(TestUtils.ACME_SERVER_URI))))
|
||||||
|
.thenReturn(TestUtils.getJsonAsObject("directory"));
|
||||||
|
|
||||||
|
session = TestUtils.session(mockProvider);
|
||||||
session.setLocale(Locale.JAPAN);
|
session.setLocale(Locale.JAPAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,6 +140,47 @@ public class DefaultConnectionTest {
|
||||||
verifyNoMoreInteractions(mockUrlConnection);
|
verifyNoMoreInteractions(mockUrlConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that {@link DefaultConnection#resetNonce(Session)} fetches a new nonce via
|
||||||
|
* new-nonce resource and a HEAD request.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testResetNonce() throws AcmeException, IOException {
|
||||||
|
byte[] nonce = "foo-nonce-foo".getBytes();
|
||||||
|
|
||||||
|
when(mockHttpConnection.openConnection(URI.create("https://example.com/acme/new-nonce")))
|
||||||
|
.thenReturn(mockUrlConnection);
|
||||||
|
when(mockUrlConnection.getResponseCode())
|
||||||
|
.thenReturn(HttpURLConnection.HTTP_NO_CONTENT);
|
||||||
|
|
||||||
|
assertThat(session.getNonce(), is(nullValue()));
|
||||||
|
|
||||||
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
|
conn.resetNonce(session);
|
||||||
|
fail("missing Replay-Nonce header not detected");
|
||||||
|
} catch (AcmeProtocolException ex) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(session.getNonce(), is(nullValue()));
|
||||||
|
|
||||||
|
when(mockUrlConnection.getHeaderField("Replay-Nonce"))
|
||||||
|
.thenReturn(Base64Url.encode(nonce));
|
||||||
|
|
||||||
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
|
conn.resetNonce(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(session.getNonce(), is(nonce));
|
||||||
|
|
||||||
|
verify(mockUrlConnection, atLeastOnce()).setRequestMethod("HEAD");
|
||||||
|
verify(mockUrlConnection, atLeastOnce()).setRequestProperty("Accept-Language", "ja-JP");
|
||||||
|
verify(mockUrlConnection, atLeastOnce()).connect();
|
||||||
|
verify(mockUrlConnection, atLeastOnce()).getResponseCode();
|
||||||
|
verify(mockUrlConnection, atLeastOnce()).getHeaderField("Replay-Nonce");
|
||||||
|
verifyNoMoreInteractions(mockUrlConnection);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that an absolute Location header is evaluated.
|
* Test that an absolute Location header is evaluated.
|
||||||
*/
|
*/
|
||||||
|
@ -512,11 +560,19 @@ public class DefaultConnectionTest {
|
||||||
|
|
||||||
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
|
||||||
@Override
|
@Override
|
||||||
public void updateSession(Session session) {
|
public void resetNonce(Session session) throws AcmeException {
|
||||||
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
|
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
|
||||||
if (session.getNonce() == null) {
|
if (session.getNonce() == null) {
|
||||||
session.setNonce(nonce1);
|
session.setNonce(nonce1);
|
||||||
} else if (session.getNonce() == nonce1) {
|
} else {
|
||||||
|
fail("unknown nonce");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateSession(Session session) {
|
||||||
|
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
|
||||||
|
if (session.getNonce() == nonce1) {
|
||||||
session.setNonce(nonce2);
|
session.setNonce(nonce2);
|
||||||
} else {
|
} else {
|
||||||
fail("unknown nonce");
|
fail("unknown nonce");
|
||||||
|
@ -528,14 +584,12 @@ public class DefaultConnectionTest {
|
||||||
conn.sendSignedRequest(requestUri, cb, DefaultConnectionTest.this.session);
|
conn.sendSignedRequest(requestUri, cb, DefaultConnectionTest.this.session);
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(mockUrlConnection).setRequestMethod("HEAD");
|
|
||||||
verify(mockUrlConnection, times(2)).setRequestProperty("Accept-Language", "ja-JP");
|
|
||||||
verify(mockUrlConnection, times(2)).connect();
|
|
||||||
|
|
||||||
verify(mockUrlConnection).setRequestMethod("POST");
|
verify(mockUrlConnection).setRequestMethod("POST");
|
||||||
verify(mockUrlConnection).setRequestProperty("Accept", "application/json");
|
verify(mockUrlConnection).setRequestProperty("Accept", "application/json");
|
||||||
verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8");
|
verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8");
|
||||||
|
verify(mockUrlConnection).setRequestProperty("Accept-Language", "ja-JP");
|
||||||
verify(mockUrlConnection).setRequestProperty("Content-Type", "application/jose+json");
|
verify(mockUrlConnection).setRequestProperty("Content-Type", "application/jose+json");
|
||||||
|
verify(mockUrlConnection).connect();
|
||||||
verify(mockUrlConnection).setDoOutput(true);
|
verify(mockUrlConnection).setDoOutput(true);
|
||||||
verify(mockUrlConnection).setFixedLengthStreamingMode(outputStream.toByteArray().length);
|
verify(mockUrlConnection).setFixedLengthStreamingMode(outputStream.toByteArray().length);
|
||||||
verify(mockUrlConnection).getOutputStream();
|
verify(mockUrlConnection).getOutputStream();
|
||||||
|
@ -574,6 +628,11 @@ public class DefaultConnectionTest {
|
||||||
*/
|
*/
|
||||||
@Test(expected = AcmeProtocolException.class)
|
@Test(expected = AcmeProtocolException.class)
|
||||||
public void testSendSignedRequestNoNonce() throws Exception {
|
public void testSendSignedRequestNoNonce() throws Exception {
|
||||||
|
when(mockHttpConnection.openConnection(URI.create("https://example.com/acme/new-nonce")))
|
||||||
|
.thenReturn(mockUrlConnection);
|
||||||
|
when(mockUrlConnection.getResponseCode())
|
||||||
|
.thenReturn(HttpURLConnection.HTTP_NOT_FOUND);
|
||||||
|
|
||||||
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
JSONBuilder cb = new JSONBuilder();
|
JSONBuilder cb = new JSONBuilder();
|
||||||
conn.sendSignedRequest(requestUri, cb, DefaultConnectionTest.this.session);
|
conn.sendSignedRequest(requestUri, cb, DefaultConnectionTest.this.session);
|
||||||
|
|
|
@ -28,6 +28,11 @@ import org.shredzone.acme4j.util.JSONBuilder;
|
||||||
*/
|
*/
|
||||||
public class DummyConnection implements Connection {
|
public class DummyConnection implements Connection {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resetNonce(Session session) throws AcmeException {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendRequest(URI uri, Session session) {
|
public void sendRequest(URI uri, Session session) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
|
|
@ -31,11 +31,12 @@ public class ResourceTest {
|
||||||
assertThat(Resource.KEY_CHANGE.path(), is("key-change"));
|
assertThat(Resource.KEY_CHANGE.path(), is("key-change"));
|
||||||
assertThat(Resource.NEW_AUTHZ.path(), is("new-authz"));
|
assertThat(Resource.NEW_AUTHZ.path(), is("new-authz"));
|
||||||
assertThat(Resource.NEW_CERT.path(), is("new-cert"));
|
assertThat(Resource.NEW_CERT.path(), is("new-cert"));
|
||||||
|
assertThat(Resource.NEW_NONCE.path(), is("new-nonce"));
|
||||||
assertThat(Resource.NEW_REG.path(), is("new-reg"));
|
assertThat(Resource.NEW_REG.path(), is("new-reg"));
|
||||||
assertThat(Resource.REVOKE_CERT.path(), is("revoke-cert"));
|
assertThat(Resource.REVOKE_CERT.path(), is("revoke-cert"));
|
||||||
|
|
||||||
// fails if there are untested future Resource values
|
// fails if there are untested future Resource values
|
||||||
assertThat(Resource.values().length, is(5));
|
assertThat(Resource.values().length, is(6));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ directory = \
|
||||||
"new-reg": "https://example.com/acme/new-reg",\
|
"new-reg": "https://example.com/acme/new-reg",\
|
||||||
"new-authz": "https://example.com/acme/new-authz",\
|
"new-authz": "https://example.com/acme/new-authz",\
|
||||||
"new-cert": "https://example.com/acme/new-cert",\
|
"new-cert": "https://example.com/acme/new-cert",\
|
||||||
|
"new-nonce": "https://example.com/acme/new-nonce",\
|
||||||
"meta": {\
|
"meta": {\
|
||||||
"terms-of-service": "https://example.com/acme/terms",\
|
"terms-of-service": "https://example.com/acme/terms",\
|
||||||
"website": "https://www.example.com/",\
|
"website": "https://www.example.com/",\
|
||||||
|
|
Loading…
Reference in New Issue