Use new-nonce resource for fetching initial nonce

pull/55/head
Richard Körber 2017-02-15 01:07:19 +01:00
parent be6b511085
commit 7aeb439a62
7 changed files with 117 additions and 20 deletions

View File

@ -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.
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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/",\