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