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 {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
@ -90,6 +90,38 @@ public class DefaultConnection implements Connection {
|
|||
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
|
||||
public void sendRequest(URI uri, Session session) throws AcmeException {
|
||||
Objects.requireNonNull(uri, "uri");
|
||||
|
@ -124,17 +156,7 @@ public class DefaultConnection implements Connection {
|
|||
KeyPair keypair = session.getKeyPair();
|
||||
|
||||
if (session.getNonce() == null) {
|
||||
LOG.debug("Getting initial nonce, HEAD {}", uri);
|
||||
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");
|
||||
resetNonce(session);
|
||||
}
|
||||
|
||||
LOG.debug("POST {} with claims: {}", uri, claims);
|
||||
|
|
|
@ -22,6 +22,7 @@ public enum Resource {
|
|||
NEW_REG("new-reg"),
|
||||
NEW_AUTHZ("new-authz"),
|
||||
NEW_CERT("new-cert"),
|
||||
NEW_NONCE("new-nonce"),
|
||||
REVOKE_CERT("revoke-cert");
|
||||
|
||||
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.AcmeRetryAfterException;
|
||||
import org.shredzone.acme4j.exception.AcmeServerException;
|
||||
import org.shredzone.acme4j.provider.AcmeProvider;
|
||||
import org.shredzone.acme4j.util.JSON;
|
||||
import org.shredzone.acme4j.util.JSONBuilder;
|
||||
import org.shredzone.acme4j.util.TestUtils;
|
||||
|
@ -61,13 +62,19 @@ public class DefaultConnectionTest {
|
|||
private Session session;
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
public void setup() throws AcmeException, IOException {
|
||||
mockUrlConnection = mock(HttpURLConnection.class);
|
||||
|
||||
mockHttpConnection = mock(HttpConnector.class);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -133,6 +140,47 @@ public class DefaultConnectionTest {
|
|||
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.
|
||||
*/
|
||||
|
@ -512,11 +560,19 @@ public class DefaultConnectionTest {
|
|||
|
||||
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
|
||||
@Override
|
||||
public void updateSession(Session session) {
|
||||
public void resetNonce(Session session) throws AcmeException {
|
||||
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
|
||||
if (session.getNonce() == null) {
|
||||
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);
|
||||
} else {
|
||||
fail("unknown nonce");
|
||||
|
@ -528,14 +584,12 @@ public class DefaultConnectionTest {
|
|||
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).setRequestProperty("Accept", "application/json");
|
||||
verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8");
|
||||
verify(mockUrlConnection).setRequestProperty("Accept-Language", "ja-JP");
|
||||
verify(mockUrlConnection).setRequestProperty("Content-Type", "application/jose+json");
|
||||
verify(mockUrlConnection).connect();
|
||||
verify(mockUrlConnection).setDoOutput(true);
|
||||
verify(mockUrlConnection).setFixedLengthStreamingMode(outputStream.toByteArray().length);
|
||||
verify(mockUrlConnection).getOutputStream();
|
||||
|
@ -574,6 +628,11 @@ public class DefaultConnectionTest {
|
|||
*/
|
||||
@Test(expected = AcmeProtocolException.class)
|
||||
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)) {
|
||||
JSONBuilder cb = new JSONBuilder();
|
||||
conn.sendSignedRequest(requestUri, cb, DefaultConnectionTest.this.session);
|
||||
|
|
|
@ -28,6 +28,11 @@ import org.shredzone.acme4j.util.JSONBuilder;
|
|||
*/
|
||||
public class DummyConnection implements Connection {
|
||||
|
||||
@Override
|
||||
public void resetNonce(Session session) throws AcmeException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendRequest(URI uri, Session session) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
|
|
@ -31,11 +31,12 @@ public class ResourceTest {
|
|||
assertThat(Resource.KEY_CHANGE.path(), is("key-change"));
|
||||
assertThat(Resource.NEW_AUTHZ.path(), is("new-authz"));
|
||||
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.REVOKE_CERT.path(), is("revoke-cert"));
|
||||
|
||||
// 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-authz": "https://example.com/acme/new-authz",\
|
||||
"new-cert": "https://example.com/acme/new-cert",\
|
||||
"new-nonce": "https://example.com/acme/new-nonce",\
|
||||
"meta": {\
|
||||
"terms-of-service": "https://example.com/acme/terms",\
|
||||
"website": "https://www.example.com/",\
|
||||
|
|
Loading…
Reference in New Issue