Send POST-as-GET requests

pull/81/head
Richard Körber 2018-09-26 19:24:20 +02:00
parent 5098b5364b
commit 1297ca4de2
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
13 changed files with 226 additions and 75 deletions

View File

@ -116,7 +116,7 @@ public abstract class AcmeJsonResource extends AcmeResource {
String resourceType = getClass().getSimpleName();
LOG.debug("update {}", resourceType);
try (Connection conn = connect()) {
conn.sendRequest(getLocation(), getSession());
conn.sendSignedPostAsGetRequest(getLocation(), getLogin());
JSON json = conn.readJsonResponse();
if (json != null) {
setJSON(json);

View File

@ -71,7 +71,7 @@ public class Certificate extends AcmeResource {
if (certChain == null) {
LOG.debug("download");
try (Connection conn = connect()) {
conn.sendCertificateRequest(getLocation(), getSession());
conn.sendCertificateRequest(getLocation(), getLogin());
alternates = new ArrayList<>(conn.getLinks("alternate"));
certChain = new ArrayList<>(conn.readCertificates());
}

View File

@ -58,17 +58,36 @@ public interface Connection extends AutoCloseable {
void sendRequest(URL url, Session session) throws AcmeException;
/**
* Sends a request for a certificate resource.
* Sends a signed POST-as-GET request for a certificate resource. Requires a
* {@link Login} for the session and {@link KeyPair}. The {@link Login} account
* location is sent in a "kid" protected header.
* <p>
* If the response code was not {@link HttpURLConnection#HTTP_OK}, an
* {@link AcmeException} matching the error is raised.
* If the server does not return a 200 class status code, an {@link AcmeException} is
* raised matching the error.
*
* @param url
* {@link URL} to send the request to.
* @param session
* {@link Session} instance to be used for tracking
* @param login
* {@link Login} instance to be used for signing and tracking.
* @return HTTP 200 class status that was returned
*/
void sendCertificateRequest(URL url, Session session) throws AcmeException;
int sendCertificateRequest(URL url, Login login) throws AcmeException;
/**
* Sends a signed POST-as-GET request. Requires a {@link Login} for the session and
* {@link KeyPair}. The {@link Login} account location is sent in a "kid" protected
* header.
* <p>
* If the server does not return a 200 class status code, an {@link AcmeException} is
* raised matching the error.
*
* @param url
* {@link URL} to send the request to.
* @param login
* {@link Login} instance to be used for signing and tracking.
* @return HTTP 200 class status that was returned
*/
int sendSignedPostAsGetRequest(URL url, Login login) throws AcmeException;
/**
* Sends a signed POST request. Requires a {@link Login} for the session and

View File

@ -137,15 +137,6 @@ public class DefaultConnection implements Connection {
@Override
public void sendRequest(URL url, Session session) throws AcmeException {
sendRequest(url, session, MIME_JSON);
}
@Override
public void sendCertificateRequest(URL url, Session session) throws AcmeException {
sendRequest(url, session, MIME_CERTIFICATE_CHAIN);
}
private void sendRequest(URL url, Session session, String accept) throws AcmeException {
Objects.requireNonNull(url, "url");
Objects.requireNonNull(session, "session");
assertConnectionIsClosed();
@ -155,7 +146,7 @@ public class DefaultConnection implements Connection {
try {
conn = httpConnector.openConnection(url, session.getProxy());
conn.setRequestMethod("GET");
conn.setRequestProperty(ACCEPT_HEADER, accept);
conn.setRequestProperty(ACCEPT_HEADER, MIME_JSON);
conn.setRequestProperty(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET);
conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag());
conn.setDoOutput(false);
@ -174,30 +165,43 @@ public class DefaultConnection implements Connection {
}
}
@Override
public int sendCertificateRequest(URL url, Login login) throws AcmeException {
return sendSignedRequest(url, null, login.getSession(), login.getKeyPair(),
login.getAccountLocation(), MIME_CERTIFICATE_CHAIN);
}
@Override
public int sendSignedPostAsGetRequest(URL url, Login login) throws AcmeException {
return sendSignedRequest(url, null, login.getSession(), login.getKeyPair(),
login.getAccountLocation(), MIME_JSON);
}
@Override
public int sendSignedRequest(URL url, JSONBuilder claims, Login login) throws AcmeException {
return sendSignedRequest(url, claims, login.getSession(), login.getKeyPair(), login.getAccountLocation());
return sendSignedRequest(url, claims, login.getSession(), login.getKeyPair(),
login.getAccountLocation(), MIME_JSON);
}
@Override
public int sendSignedRequest(URL url, JSONBuilder claims, Session session, KeyPair keypair)
throws AcmeException {
return sendSignedRequest(url, claims, session, keypair, null);
return sendSignedRequest(url, claims, session, keypair, null, MIME_JSON);
}
private int sendSignedRequest(URL url, JSONBuilder claims, Session session, KeyPair keypair, @Nullable URL accountLocation)
throws AcmeException {
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(claims, "claims");
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);
return performRequest(url, claims, session, keypair, accountLocation, accept);
} catch (AcmeServerException ex) {
if (!BAD_NONCE_ERROR.equals(ex.getType())) {
throw ex;
@ -322,7 +326,8 @@ public class DefaultConnection implements Connection {
* @param url
* {@link URL} to send the request to.
* @param claims
* {@link JSONBuilder} containing claims. Must not be {@code null}.
* {@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
@ -330,19 +335,23 @@ public class DefaultConnection implements Connection {
* @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
*/
private int performRequest(URL url, JSONBuilder claims, Session session, KeyPair keypair,
@Nullable URL accountLocation)
private int performRequest(URL url, @Nullable JSONBuilder claims, Session session,
KeyPair keypair, @Nullable URL accountLocation, String accept)
throws AcmeException {
try {
if (session.getNonce() == null) {
resetNonce(session);
}
String claimJson = claims != null ? claims.toString() : "";
conn = httpConnector.openConnection(url, session.getProxy());
conn.setRequestMethod("POST");
conn.setRequestProperty(ACCEPT_HEADER, MIME_JSON);
conn.setRequestProperty(ACCEPT_HEADER, accept);
conn.setRequestProperty(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET);
conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag());
conn.setRequestProperty(CONTENT_TYPE_HEADER, "application/jose+json");
@ -350,7 +359,7 @@ public class DefaultConnection implements Connection {
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(keypair.getPublic());
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toString());
jws.setPayload(claimJson);
jws.getHeaders().setObjectHeaderValue("nonce", session.getNonce());
jws.getHeaders().setObjectHeaderValue("url", url);
if (accountLocation == null) {
@ -364,8 +373,10 @@ public class DefaultConnection implements Connection {
jws.sign();
if (LOG.isDebugEnabled()) {
LOG.debug("POST {}", url);
LOG.debug(" Payload: {}", claims.toString());
LOG.debug("{} {}", claims != null ? "POST" : "POST-as-GET", url);
if (claims != null) {
LOG.debug(" Payload: {}", claimJson);
}
LOG.debug(" JWS Header: {}", jws.getHeaders().getFullHeaderAsJsonString());
}

View File

@ -145,7 +145,7 @@ public class ResourceIterator<T extends AcmeResource> implements Iterator<T> {
private void readAndQueue() throws AcmeException {
Session session = login.getSession();
try (Connection conn = session.provider().connect()) {
conn.sendRequest(nextUrl, session);
conn.sendSignedPostAsGetRequest(nextUrl, login);
JSON json = conn.readJsonResponse();
if (json != null) {

View File

@ -72,12 +72,13 @@ public class AccountTest {
}
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
if (url("https://example.com/acme/acct/1/orders").equals(url)) {
jsonResponse = new JSONBuilder()
.array("orders", Arrays.asList("https://example.com/acme/order/1"))
.toJSON();
}
return HttpURLConnection.HTTP_OK;
}
@Override

View File

@ -92,8 +92,9 @@ public class AuthorizationTest {
public void testUpdate() throws Exception {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override
@ -138,8 +139,9 @@ public class AuthorizationTest {
public void testWildcard() throws Exception {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override
@ -181,9 +183,10 @@ public class AuthorizationTest {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
requestWasSent.set(true);
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override
@ -230,8 +233,9 @@ public class AuthorizationTest {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override

View File

@ -55,9 +55,10 @@ public class CertificateTest {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendCertificateRequest(URL url, Session session) {
public int sendCertificateRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
assertThat(session, is(notNullValue()));
assertThat(login, is(notNullValue()));
return HttpURLConnection.HTTP_OK;
}
@Override
@ -124,10 +125,11 @@ public class CertificateTest {
private boolean certRequested = false;
@Override
public void sendCertificateRequest(URL url, Session session) {
public int sendCertificateRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
assertThat(session, is(notNullValue()));
assertThat(login, is(notNullValue()));
certRequested = true;
return HttpURLConnection.HTTP_OK;
}
@Override
@ -171,10 +173,11 @@ public class CertificateTest {
private boolean certRequested = false;
@Override
public void sendCertificateRequest(URL url, Session session) {
public int sendCertificateRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
assertThat(session, is(notNullValue()));
assertThat(login, is(notNullValue()));
certRequested = true;
return HttpURLConnection.HTTP_OK;
}
@Override

View File

@ -48,8 +48,9 @@ public class OrderTest {
public void testUpdate() throws Exception {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override
@ -109,9 +110,10 @@ public class OrderTest {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
requestWasSent.set(true);
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override
@ -156,8 +158,9 @@ public class OrderTest {
private boolean isFinalized = false;
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override
@ -215,8 +218,9 @@ public class OrderTest {
public void testRecurrentUpdate() throws Exception {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override

View File

@ -28,7 +28,6 @@ import java.time.Instant;
import org.junit.Test;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
@ -126,8 +125,9 @@ public class ChallengeTest {
public void testUpdate() throws Exception {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override
@ -162,8 +162,9 @@ public class ChallengeTest {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
assertThat(url, is(locationUrl));
return HttpURLConnection.HTTP_OK;
}
@Override

View File

@ -663,28 +663,6 @@ public class DefaultConnectionTest {
verifyNoMoreInteractions(mockUrlConnection);
}
/**
* Test certificate GET requests.
*/
@Test
public void testSendCertificateRequest() throws Exception {
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
conn.sendCertificateRequest(requestUrl, session);
}
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);
}
/**
* Test signed POST requests.
*/
@ -760,6 +738,130 @@ public class DefaultConnectionTest {
assertThat(jws.verifySignature(), is(true));
}
/**
* Test signed POST-as-GET requests.
*/
@Test
public void testSendSignedPostAsGetRequest() throws Exception {
final String nonce1 = Base64Url.encode("foo-nonce-1-foo".getBytes());
final String nonce2 = Base64Url.encode("foo-nonce-2-foo".getBytes());
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
when(mockUrlConnection.getOutputStream()).thenReturn(outputStream);
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
@Override
public void resetNonce(Session session) {
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
if (session.getNonce() == null) {
session.setNonce(nonce1);
} else {
fail("unknown nonce");
}
}
@Override
public String getNonce() {
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
if (session.getNonce() == nonce1) {
return nonce2;
} else {
fail("unknown nonce");
return null;
}
}
}) {
conn.sendSignedPostAsGetRequest(requestUrl, login);
}
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).getResponseCode();
verify(mockUrlConnection).getOutputStream();
verify(mockUrlConnection, atLeast(0)).getHeaderFields();
verifyNoMoreInteractions(mockUrlConnection);
JSON data = JSON.parse(new String(outputStream.toByteArray(), "utf-8"));
String encodedHeader = data.get("protected").asString();
String encodedSignature = data.get("signature").asString();
String encodedPayload = data.get("payload").asString();
StringBuilder expectedHeader = new StringBuilder();
expectedHeader.append('{');
expectedHeader.append("\"nonce\":\"").append(nonce1).append("\",");
expectedHeader.append("\"url\":\"").append(requestUrl).append("\",");
expectedHeader.append("\"alg\":\"RS256\",");
expectedHeader.append("\"kid\":\"").append(accountUrl).append('"');
expectedHeader.append('}');
assertThat(Base64Url.decodeToUtf8String(encodedHeader), sameJSONAs(expectedHeader.toString()));
assertThat(Base64Url.decodeToUtf8String(encodedPayload), is(""));
assertThat(encodedSignature, not(isEmptyOrNullString()));
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));
jws.setKey(login.getKeyPair().getPublic());
assertThat(jws.verifySignature(), is(true));
}
/**
* Test certificate POST-as-GET requests.
*/
@Test
public void testSendCertificateRequest() throws Exception {
final String nonce1 = Base64Url.encode("foo-nonce-1-foo".getBytes());
final String nonce2 = Base64Url.encode("foo-nonce-2-foo".getBytes());
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
when(mockUrlConnection.getOutputStream()).thenReturn(outputStream);
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
@Override
public void resetNonce(Session session) {
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
if (session.getNonce() == null) {
session.setNonce(nonce1);
} else {
fail("unknown nonce");
}
}
@Override
public String getNonce() {
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
if (session.getNonce() == nonce1) {
return nonce2;
} else {
fail("unknown nonce");
return null;
}
}
}) {
conn.sendCertificateRequest(requestUrl, login);
}
verify(mockUrlConnection).setRequestMethod("POST");
verify(mockUrlConnection).setRequestProperty("Accept", "application/pem-certificate-chain");
verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8");
verify(mockUrlConnection).setRequestProperty("Accept-Language", "ja-JP");
verify(mockUrlConnection).setRequestProperty("Content-Type", "application/jose+json");
verify(mockUrlConnection).setDoOutput(true);
verify(mockUrlConnection).connect();
verify(mockUrlConnection).setFixedLengthStreamingMode(outputStream.toByteArray().length);
verify(mockUrlConnection).getResponseCode();
verify(mockUrlConnection).getOutputStream();
verify(mockUrlConnection, atLeast(0)).getHeaderFields();
verifyNoMoreInteractions(mockUrlConnection);
}
/**
* Test signed POST requests without KeyIdentifier.
*/

View File

@ -42,7 +42,12 @@ public class DummyConnection implements Connection {
}
@Override
public void sendCertificateRequest(URL url, Session session) throws AcmeException {
public int sendCertificateRequest(URL url, Login login) throws AcmeException {
throw new UnsupportedOperationException();
}
@Override
public int sendSignedPostAsGetRequest(URL url, Login login) throws AcmeException {
throw new UnsupportedOperationException();
}

View File

@ -18,6 +18,7 @@ import static org.junit.Assert.assertThat;
import static org.shredzone.acme4j.toolbox.TestUtils.url;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
@ -31,7 +32,6 @@ import org.junit.Before;
import org.junit.Test;
import org.shredzone.acme4j.Authorization;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
@ -134,9 +134,10 @@ public class ResourceIteratorTest {
private int ix;
@Override
public void sendRequest(URL url, Session session) {
public int sendSignedPostAsGetRequest(URL url, Login login) {
ix = pageURLs.indexOf(url);
assertThat(ix, is(greaterThanOrEqualTo(0)));
return HttpURLConnection.HTTP_OK;
}
@Override