mirror of https://github.com/shred/acme4j
Use the java.net.http client
With this patch, the old HttpURLConnection client is replaced with the new java.net.http client that was introduced in Java 11.pull/140/head
parent
d6296111f7
commit
783fdde013
|
@ -14,6 +14,7 @@
|
|||
|
||||
module org.shredzone.acme4j {
|
||||
requires static com.github.spotbugs.annotations;
|
||||
requires java.net.http;
|
||||
requires org.jose4j;
|
||||
requires org.slf4j;
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
*/
|
||||
package org.shredzone.acme4j;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.KeyPair;
|
||||
|
@ -187,7 +186,7 @@ public class Session {
|
|||
* @return {@link Connection}
|
||||
*/
|
||||
public Connection connect() {
|
||||
return provider.connect(getServerUri());
|
||||
return provider.connect(getServerUri(), networkSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
*/
|
||||
package org.shredzone.acme4j.connector;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
@ -47,8 +46,8 @@ public interface Connection extends AutoCloseable {
|
|||
/**
|
||||
* Sends a simple GET request.
|
||||
* <p>
|
||||
* If the response code was not {@link HttpURLConnection#HTTP_OK}, an
|
||||
* {@link AcmeException} matching the error is raised.
|
||||
* If the response code was not HTTP status 200, an {@link AcmeException} matching
|
||||
* the error is raised.
|
||||
*
|
||||
* @param url
|
||||
* {@link URL} to send the request to.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2015 Richard "Shred" Körber
|
||||
* Copyright (C) 2023 Richard "Shred" Körber
|
||||
* http://acme4j.shredzone.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -14,15 +14,17 @@
|
|||
package org.shredzone.acme4j.connector;
|
||||
|
||||
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static java.util.stream.Collectors.toUnmodifiableList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
|
@ -31,11 +33,13 @@ import java.time.Instant;
|
|||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.Nullable;
|
||||
|
@ -63,6 +67,11 @@ import org.slf4j.LoggerFactory;
|
|||
public class DefaultConnection implements Connection {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultConnection.class);
|
||||
|
||||
private static final int HTTP_OK = 200;
|
||||
private static final int HTTP_CREATED = 201;
|
||||
private static final int HTTP_NO_CONTENT = 204;
|
||||
private static final int HTTP_NOT_MODIFIED = 304;
|
||||
|
||||
private static final String ACCEPT_HEADER = "Accept";
|
||||
private static final String ACCEPT_CHARSET_HEADER = "Accept-Charset";
|
||||
private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language";
|
||||
|
@ -86,18 +95,21 @@ public class DefaultConnection implements Connection {
|
|||
|
||||
private static final Pattern NO_CACHE_PATTERN = Pattern.compile("(?:^|.*?,)\\s*no-(?:cache|store)\\s*(?:,.*|$)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern MAX_AGE_PATTERN = Pattern.compile("(?:^|.*?,)\\s*max-age=(\\d+)\\s*(?:,.*|$)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DIGITS_ONLY_PATTERN = Pattern.compile("^\\d+$");
|
||||
|
||||
protected final HttpConnector httpConnector;
|
||||
protected @Nullable HttpURLConnection conn;
|
||||
protected final HttpClient httpClient;
|
||||
protected @Nullable HttpResponse<InputStream> lastResponse;
|
||||
|
||||
/**
|
||||
* Creates a new {@link DefaultConnection}.
|
||||
*
|
||||
* @param httpConnector
|
||||
* {@link HttpConnector} to be used for HTTP connections
|
||||
* {@link HttpConnector} to be used for HTTP connections
|
||||
*/
|
||||
public DefaultConnection(HttpConnector httpConnector) {
|
||||
this.httpConnector = Objects.requireNonNull(httpConnector, "httpConnector");
|
||||
this.httpClient = httpConnector.createClientBuilder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -111,15 +123,13 @@ public class DefaultConnection implements Connection {
|
|||
|
||||
LOG.debug("HEAD {}", newNonceUrl);
|
||||
|
||||
conn = httpConnector.openConnection(newNonceUrl, session.networkSettings());
|
||||
conn.setRequestMethod("HEAD");
|
||||
conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag());
|
||||
conn.connect();
|
||||
sendRequest(session, newNonceUrl, b ->
|
||||
b.method("HEAD", HttpRequest.BodyPublishers.noBody()));
|
||||
|
||||
logHeaders();
|
||||
|
||||
var rc = conn.getResponseCode();
|
||||
if (rc != HttpURLConnection.HTTP_OK && rc != HttpURLConnection.HTTP_NO_CONTENT) {
|
||||
var rc = getResponse().statusCode();
|
||||
if (rc != HTTP_OK && rc != HTTP_NO_CONTENT) {
|
||||
throwAcmeException();
|
||||
}
|
||||
|
||||
|
@ -131,14 +141,43 @@ public class DefaultConnection implements Connection {
|
|||
} catch (IOException ex) {
|
||||
throw new AcmeNetworkException(ex);
|
||||
} finally {
|
||||
conn = null;
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sendRequest(URL url, Session session, @Nullable ZonedDateTime ifModifiedSince)
|
||||
throws AcmeException {
|
||||
return sendRequest(url, session, MIME_JSON, ifModifiedSince);
|
||||
Objects.requireNonNull(url, "url");
|
||||
Objects.requireNonNull(session, "session");
|
||||
assertConnectionIsClosed();
|
||||
|
||||
LOG.debug("GET {}", url);
|
||||
|
||||
try {
|
||||
sendRequest(session, url, builder -> {
|
||||
builder.GET();
|
||||
builder.header(ACCEPT_HEADER, MIME_JSON);
|
||||
if (ifModifiedSince != null) {
|
||||
builder.header(IF_MODIFIED_SINCE_HEADER, ifModifiedSince.format(RFC_1123_DATE_TIME));
|
||||
}
|
||||
});
|
||||
|
||||
logHeaders();
|
||||
|
||||
var nonce = getNonce();
|
||||
if (nonce != null) {
|
||||
session.setNonce(nonce);
|
||||
}
|
||||
|
||||
var rc = getResponse().statusCode();
|
||||
if (rc != HTTP_OK && rc != HTTP_CREATED && (rc != HTTP_NOT_MODIFIED || ifModifiedSince == null)) {
|
||||
throwAcmeException();
|
||||
}
|
||||
return rc;
|
||||
} catch (IOException ex) {
|
||||
throw new AcmeNetworkException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -161,25 +200,15 @@ public class DefaultConnection implements Connection {
|
|||
|
||||
@Override
|
||||
public int sendSignedRequest(URL url, JSONBuilder claims, Session session, KeyPair keypair)
|
||||
throws AcmeException {
|
||||
throws AcmeException {
|
||||
return sendSignedRequest(url, claims, session, keypair, null, MIME_JSON);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON readJsonResponse() throws AcmeException {
|
||||
assertConnectionIsOpen();
|
||||
expectContentType(Set.of(MIME_JSON, MIME_JSON_PROBLEM));
|
||||
|
||||
if (conn.getContentLength() == 0) {
|
||||
throw new AcmeProtocolException("Empty response");
|
||||
}
|
||||
|
||||
var contentType = AcmeUtils.getContentType(conn.getHeaderField(CONTENT_TYPE_HEADER));
|
||||
if (!(MIME_JSON.equals(contentType) || MIME_JSON_PROBLEM.equals(contentType))) {
|
||||
throw new AcmeProtocolException("Unexpected content type: " + contentType);
|
||||
}
|
||||
|
||||
try {
|
||||
var in = conn.getResponseCode() < 400 ? conn.getInputStream() : conn.getErrorStream();
|
||||
try (var in = getResponse().body()) {
|
||||
if (in == null) {
|
||||
throw new AcmeProtocolException("JSON response is empty");
|
||||
}
|
||||
|
@ -194,18 +223,19 @@ public class DefaultConnection implements Connection {
|
|||
|
||||
@Override
|
||||
public List<X509Certificate> readCertificates() throws AcmeException {
|
||||
assertConnectionIsOpen();
|
||||
expectContentType(Set.of(MIME_CERTIFICATE_CHAIN));
|
||||
|
||||
var contentType = AcmeUtils.getContentType(conn.getHeaderField(CONTENT_TYPE_HEADER));
|
||||
if (!(MIME_CERTIFICATE_CHAIN.equals(contentType))) {
|
||||
throw new AcmeProtocolException("Unexpected content type: " + contentType);
|
||||
}
|
||||
try (var in = getResponse().body()) {
|
||||
if (in == null) {
|
||||
throw new AcmeProtocolException("Certificate response is empty");
|
||||
}
|
||||
|
||||
try (var in = new TrimmingInputStream(conn.getInputStream())) {
|
||||
var cf = CertificateFactory.getInstance("X.509");
|
||||
return cf.generateCertificates(in).stream()
|
||||
.map(c -> (X509Certificate) c)
|
||||
.collect(toList());
|
||||
try (var ins = new TrimmingInputStream(in)) {
|
||||
var cf = CertificateFactory.getInstance("X.509");
|
||||
return cf.generateCertificates(ins).stream()
|
||||
.map(X509Certificate.class::cast)
|
||||
.collect(toUnmodifiableList());
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new AcmeNetworkException(ex);
|
||||
} catch (CertificateException ex) {
|
||||
|
@ -215,8 +245,6 @@ public class DefaultConnection implements Connection {
|
|||
|
||||
@Override
|
||||
public void handleRetryAfter(String message) throws AcmeException {
|
||||
assertConnectionIsOpen();
|
||||
|
||||
var retryAfter = getRetryAfterHeader();
|
||||
if (retryAfter.isPresent()) {
|
||||
throw new AcmeRetryAfterException(message, retryAfter.get());
|
||||
|
@ -226,13 +254,15 @@ public class DefaultConnection implements Connection {
|
|||
@Override
|
||||
@Nullable
|
||||
public String getNonce() {
|
||||
assertConnectionIsOpen();
|
||||
|
||||
var nonceHeader = conn.getHeaderField(REPLAY_NONCE_HEADER);
|
||||
if (nonceHeader == null || nonceHeader.trim().isEmpty()) {
|
||||
var nonceHeaderOpt = getResponse().headers()
|
||||
.firstValue(REPLAY_NONCE_HEADER)
|
||||
.map(String::trim)
|
||||
.filter(not(String::isEmpty));
|
||||
if (nonceHeaderOpt.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var nonceHeader = nonceHeaderOpt.get();
|
||||
if (!AcmeUtils.isValidBase64Url(nonceHeader)) {
|
||||
throw new AcmeProtocolException("Invalid replay nonce: " + nonceHeader);
|
||||
}
|
||||
|
@ -245,128 +275,92 @@ public class DefaultConnection implements Connection {
|
|||
@Override
|
||||
@Nullable
|
||||
public URL getLocation() {
|
||||
assertConnectionIsOpen();
|
||||
|
||||
var location = conn.getHeaderField(LOCATION_HEADER);
|
||||
if (location == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.debug("Location: {}", location);
|
||||
return resolveRelative(location);
|
||||
return getResponse().headers()
|
||||
.firstValue(LOCATION_HEADER)
|
||||
.map(l -> {
|
||||
LOG.debug("Location: {}", l);
|
||||
return l;
|
||||
})
|
||||
.map(this::resolveRelative)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ZonedDateTime> getLastModified() {
|
||||
assertConnectionIsOpen();
|
||||
|
||||
var header = conn.getHeaderField(LAST_MODIFIED_HEADER);
|
||||
if (header != null) {
|
||||
try {
|
||||
return Optional.of(ZonedDateTime.parse(header, RFC_1123_DATE_TIME));
|
||||
} catch (DateTimeParseException ex) {
|
||||
LOG.debug("Ignored invalid Last-Modified date: {}", header, ex);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
return getResponse().headers()
|
||||
.firstValue(LAST_MODIFIED_HEADER)
|
||||
.map(lm -> {
|
||||
try {
|
||||
return ZonedDateTime.parse(lm, RFC_1123_DATE_TIME);
|
||||
} catch (DateTimeParseException ex) {
|
||||
LOG.debug("Ignored invalid Last-Modified date: {}", lm, ex);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ZonedDateTime> getExpiration() {
|
||||
assertConnectionIsOpen();
|
||||
var cacheControlHeader = getResponse().headers()
|
||||
.firstValue(CACHE_CONTROL_HEADER)
|
||||
.filter(not(h -> NO_CACHE_PATTERN.matcher(h).matches()))
|
||||
.map(MAX_AGE_PATTERN::matcher)
|
||||
.filter(Matcher::matches)
|
||||
.map(m -> Integer.parseInt(m.group(1)))
|
||||
.filter(maxAge -> maxAge != 0)
|
||||
.map(maxAge -> ZonedDateTime.now(ZoneId.of("UTC")).plusSeconds(maxAge));
|
||||
|
||||
var cacheHeader = conn.getHeaderField(CACHE_CONTROL_HEADER);
|
||||
if (cacheHeader != null) {
|
||||
if (NO_CACHE_PATTERN.matcher(cacheHeader).matches()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
var m = MAX_AGE_PATTERN.matcher(cacheHeader);
|
||||
if (m.matches()) {
|
||||
var maxAge = Integer.parseInt(m.group(1));
|
||||
if (maxAge == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(ZonedDateTime.now(ZoneId.of("UTC")).plusSeconds(maxAge));
|
||||
}
|
||||
if (cacheControlHeader.isPresent()) {
|
||||
return cacheControlHeader;
|
||||
}
|
||||
|
||||
var expiresHeader = conn.getHeaderField(EXPIRES_HEADER);
|
||||
if (expiresHeader != null) {
|
||||
try {
|
||||
return Optional.of(ZonedDateTime.parse(expiresHeader, RFC_1123_DATE_TIME));
|
||||
} catch (DateTimeParseException ex) {
|
||||
LOG.debug("Ignored invalid Expires date: {}", expiresHeader, ex);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
return getResponse().headers()
|
||||
.firstValue(EXPIRES_HEADER)
|
||||
.flatMap(header -> {
|
||||
try {
|
||||
return Optional.of(ZonedDateTime.parse(header, RFC_1123_DATE_TIME));
|
||||
} catch (DateTimeParseException ex) {
|
||||
LOG.debug("Ignored invalid Expires date: {}", header, ex);
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<URL> getLinks(String relation) {
|
||||
return collectLinks(relation).stream()
|
||||
.map(this::resolveRelative)
|
||||
.collect(toList());
|
||||
.collect(toUnmodifiableList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
conn = null;
|
||||
lastResponse = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an unsigned GET request.
|
||||
* Sends a HTTP request via http client. This is the central method to be used for
|
||||
* sending. It will create a {@link HttpRequest} by using the request builder,
|
||||
* configure commnon headers, and then send the request via {@link HttpClient}.
|
||||
*
|
||||
* @param url
|
||||
* {@link URL} to send the request to.
|
||||
* @param session
|
||||
* {@link Session} instance to be used for signing and tracking
|
||||
* @param accept
|
||||
* Accept header
|
||||
* @param ifModifiedSince
|
||||
* Set an If-Modified-Since header with the given date. If set, an
|
||||
* NOT_MODIFIED response is accepted as valid.
|
||||
* @return HTTP 200 class status that was returned
|
||||
* {@link Session} to be used for sending
|
||||
* @param url
|
||||
* Target {@link URL}
|
||||
* @param body
|
||||
* Callback that completes the {@link HttpRequest.Builder} with the request
|
||||
* body (e.g. HTTP method, request body, more headers).
|
||||
*/
|
||||
protected int sendRequest(URL url, Session session, String accept,
|
||||
@Nullable ZonedDateTime ifModifiedSince) throws AcmeException {
|
||||
Objects.requireNonNull(url, "url");
|
||||
Objects.requireNonNull(session, "session");
|
||||
Objects.requireNonNull(accept, "accept");
|
||||
assertConnectionIsClosed();
|
||||
|
||||
LOG.debug("GET {}", url);
|
||||
|
||||
protected void sendRequest(Session session, URL url, Consumer<HttpRequest.Builder> body) throws IOException {
|
||||
try {
|
||||
conn = httpConnector.openConnection(url, session.networkSettings());
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty(ACCEPT_HEADER, accept);
|
||||
conn.setRequestProperty(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET);
|
||||
conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag());
|
||||
if (ifModifiedSince != null) {
|
||||
conn.setRequestProperty(IF_MODIFIED_SINCE_HEADER, ifModifiedSince.format(RFC_1123_DATE_TIME));
|
||||
}
|
||||
conn.setDoOutput(false);
|
||||
var builder = httpConnector.createRequestBuilder(url)
|
||||
.header(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET)
|
||||
.header(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag());
|
||||
body.accept(builder);
|
||||
|
||||
conn.connect();
|
||||
|
||||
logHeaders();
|
||||
|
||||
var nonce = getNonce();
|
||||
if (nonce != null) {
|
||||
session.setNonce(nonce);
|
||||
}
|
||||
|
||||
var rc = conn.getResponseCode();
|
||||
if (rc != HttpURLConnection.HTTP_OK && rc != HttpURLConnection.HTTP_CREATED
|
||||
&& (rc != HttpURLConnection.HTTP_NOT_MODIFIED || ifModifiedSince == null)) {
|
||||
throwAcmeException();
|
||||
}
|
||||
return rc;
|
||||
} catch (IOException ex) {
|
||||
throw new AcmeNetworkException(ex);
|
||||
lastResponse = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofInputStream());
|
||||
} catch (InterruptedException ex) {
|
||||
throw new IOException("Request was interrupted", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -374,23 +368,23 @@ public class DefaultConnection implements Connection {
|
|||
* Sends a signed POST request.
|
||||
*
|
||||
* @param url
|
||||
* {@link URL} to send the request to.
|
||||
* {@link URL} to send the request to.
|
||||
* @param claims
|
||||
* {@link JSONBuilder} containing claims. {@code null} for POST-as-GET
|
||||
* request.
|
||||
* {@link JSONBuilder} containing claims. {@code null} for POST-as-GET
|
||||
* request.
|
||||
* @param session
|
||||
* {@link Session} instance to be used for signing and tracking
|
||||
* {@link Session} instance to be used for signing and tracking
|
||||
* @param keypair
|
||||
* {@link KeyPair} to be used for signing
|
||||
* {@link KeyPair} to be used for signing
|
||||
* @param accountLocation
|
||||
* If set, the account location is set as "kid" header. If {@code null},
|
||||
* the public key is set as "jwk" header.
|
||||
* 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
|
||||
* Accept header
|
||||
* @return HTTP 200 class status that was returned
|
||||
*/
|
||||
protected int sendSignedRequest(URL url, @Nullable JSONBuilder claims, Session session,
|
||||
KeyPair keypair, @Nullable URL accountLocation, String accept) throws AcmeException {
|
||||
KeyPair keypair, @Nullable URL accountLocation, String accept) throws AcmeException {
|
||||
Objects.requireNonNull(url, "url");
|
||||
Objects.requireNonNull(session, "session");
|
||||
Objects.requireNonNull(keypair, "keypair");
|
||||
|
@ -418,37 +412,29 @@ public class DefaultConnection implements Connection {
|
|||
* Performs the POST request.
|
||||
*
|
||||
* @param url
|
||||
* {@link URL} to send the request to.
|
||||
* {@link URL} to send the request to.
|
||||
* @param claims
|
||||
* {@link JSONBuilder} containing claims. {@code null} for POST-as-GET
|
||||
* request.
|
||||
* {@link JSONBuilder} containing claims. {@code null} for POST-as-GET
|
||||
* request.
|
||||
* @param session
|
||||
* {@link Session} instance to be used for signing and tracking
|
||||
* {@link Session} instance to be used for signing and tracking
|
||||
* @param keypair
|
||||
* {@link KeyPair} to be used for signing
|
||||
* {@link KeyPair} to be used for signing
|
||||
* @param accountLocation
|
||||
* If set, the account location is set as "kid" header. If {@code null},
|
||||
* the public key is set as "jwk" header.
|
||||
* 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
|
||||
* Accept header
|
||||
* @return HTTP 200 class status that was returned
|
||||
*/
|
||||
private int performRequest(URL url, @Nullable JSONBuilder claims, Session session,
|
||||
KeyPair keypair, @Nullable URL accountLocation, String accept)
|
||||
throws AcmeException {
|
||||
KeyPair keypair, @Nullable URL accountLocation, String accept)
|
||||
throws AcmeException {
|
||||
try {
|
||||
if (session.getNonce() == null) {
|
||||
resetNonce(session);
|
||||
}
|
||||
|
||||
conn = httpConnector.openConnection(url, session.networkSettings());
|
||||
conn.setRequestMethod("POST");
|
||||
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");
|
||||
conn.setDoOutput(true);
|
||||
|
||||
var jose = JoseUtils.createJoseRequest(
|
||||
url,
|
||||
keypair,
|
||||
|
@ -457,21 +443,20 @@ public class DefaultConnection implements Connection {
|
|||
accountLocation != null ? accountLocation.toString() : null
|
||||
);
|
||||
|
||||
var outputData = jose.toString().getBytes(StandardCharsets.UTF_8);
|
||||
var outputData = jose.toString();
|
||||
|
||||
conn.setFixedLengthStreamingMode(outputData.length);
|
||||
conn.connect();
|
||||
|
||||
try (var out = conn.getOutputStream()) {
|
||||
out.write(outputData);
|
||||
}
|
||||
sendRequest(session, url, builder -> {
|
||||
builder.POST(HttpRequest.BodyPublishers.ofString(outputData));
|
||||
builder.header(ACCEPT_HEADER, accept);
|
||||
builder.header(CONTENT_TYPE_HEADER, "application/jose+json");
|
||||
});
|
||||
|
||||
logHeaders();
|
||||
|
||||
session.setNonce(getNonce());
|
||||
|
||||
var rc = conn.getResponseCode();
|
||||
if (rc != HttpURLConnection.HTTP_OK && rc != HttpURLConnection.HTTP_CREATED) {
|
||||
var rc = getResponse().statusCode();
|
||||
if (rc != HTTP_OK && rc != HTTP_CREATED) {
|
||||
throwAcmeException();
|
||||
}
|
||||
return rc;
|
||||
|
@ -484,28 +469,38 @@ public class DefaultConnection implements Connection {
|
|||
* Gets the instant sent with the Retry-After header.
|
||||
*/
|
||||
private Optional<Instant> getRetryAfterHeader() {
|
||||
return getResponse().headers()
|
||||
.firstValue(RETRY_AFTER_HEADER)
|
||||
.map(this::parseRetryAfterHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the content of a Retry-After header. The header can either contain a
|
||||
* relative or an absolute time.
|
||||
*
|
||||
* @param header
|
||||
* Retry-After header
|
||||
* @return Instant given in the header
|
||||
* @throws AcmeProtocolException
|
||||
* if the header content is invalid
|
||||
*/
|
||||
private Instant parseRetryAfterHeader(String header) {
|
||||
// See RFC 2616 section 14.37
|
||||
var header = conn.getHeaderField(RETRY_AFTER_HEADER);
|
||||
if (header != null) {
|
||||
try {
|
||||
// delta-seconds
|
||||
if (header.matches("^\\d+$")) {
|
||||
var delta = Integer.parseInt(header);
|
||||
var date = conn.getHeaderFieldDate(DATE_HEADER, System.currentTimeMillis());
|
||||
return Optional.of(Instant.ofEpochMilli(date).plusSeconds(delta));
|
||||
}
|
||||
|
||||
// HTTP-date
|
||||
var date = conn.getHeaderFieldDate(RETRY_AFTER_HEADER, 0L);
|
||||
if (date != 0) {
|
||||
return Optional.of(Instant.ofEpochMilli(date));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new AcmeProtocolException("Bad retry-after header value: " + header, ex);
|
||||
try {
|
||||
// delta-seconds
|
||||
if (DIGITS_ONLY_PATTERN.matcher(header).matches()) {
|
||||
var delta = Integer.parseInt(header);
|
||||
var date = getResponse().headers().firstValue(DATE_HEADER)
|
||||
.map(d -> ZonedDateTime.parse(d, RFC_1123_DATE_TIME).toInstant())
|
||||
.orElseGet(Instant::now);
|
||||
return date.plusSeconds(delta);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
// HTTP-date
|
||||
return ZonedDateTime.parse(header, RFC_1123_DATE_TIME).toInstant();
|
||||
} catch (RuntimeException ex) {
|
||||
throw new AcmeProtocolException("Bad retry-after header value: " + header, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -514,12 +509,15 @@ public class DefaultConnection implements Connection {
|
|||
*/
|
||||
private void throwAcmeException() throws AcmeException {
|
||||
try {
|
||||
var contentType = AcmeUtils.getContentType(conn.getHeaderField(CONTENT_TYPE_HEADER));
|
||||
if (!MIME_JSON_PROBLEM.equals(contentType)) {
|
||||
throw new AcmeException("HTTP " + conn.getResponseCode() + ": " + conn.getResponseMessage());
|
||||
if (getResponse().headers().firstValue(CONTENT_TYPE_HEADER)
|
||||
.map(AcmeUtils::getContentType)
|
||||
.filter(MIME_JSON_PROBLEM::equals)
|
||||
.isEmpty()) {
|
||||
// Generic HTTP error
|
||||
throw new AcmeException("HTTP " + getResponse().statusCode());
|
||||
}
|
||||
|
||||
var problem = new Problem(readJsonResponse(), conn.getURL());
|
||||
var problem = new Problem(readJsonResponse(), getResponse().request().uri().toURL());
|
||||
|
||||
var error = AcmeUtils.stripErrorPrefix(problem.getType().toString());
|
||||
|
||||
|
@ -548,19 +546,42 @@ public class DefaultConnection implements Connection {
|
|||
}
|
||||
|
||||
/**
|
||||
* Asserts that the connection is currently open. Throws an exception if not.
|
||||
* Checks if the returned content type is in the list of expected types.
|
||||
*
|
||||
* @param expectedTypes
|
||||
* content types that are accepted
|
||||
* @throws AcmeProtocolException
|
||||
* if the returned content type is different
|
||||
*/
|
||||
private void assertConnectionIsOpen() {
|
||||
if (conn == null) {
|
||||
private void expectContentType(Set<String> expectedTypes) {
|
||||
var contentType = getResponse().headers()
|
||||
.firstValue(CONTENT_TYPE_HEADER)
|
||||
.map(AcmeUtils::getContentType)
|
||||
.orElseThrow(() -> new AcmeProtocolException("No content type header found"));
|
||||
if (!expectedTypes.contains(contentType)) {
|
||||
throw new AcmeProtocolException("Unexpected content type: " + contentType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response of the last request. If there is no connection currently
|
||||
* open, an exception is thrown instead.
|
||||
* <p>
|
||||
* Note that the response provides an {@link InputStream} that can be read only
|
||||
* once.
|
||||
*/
|
||||
private HttpResponse<InputStream> getResponse() {
|
||||
if (lastResponse == null) {
|
||||
throw new IllegalStateException("Not connected.");
|
||||
}
|
||||
return lastResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the connection is currently closed. Throws an exception if not.
|
||||
*/
|
||||
private void assertConnectionIsClosed() {
|
||||
if (conn != null) {
|
||||
if (lastResponse != null) {
|
||||
throw new IllegalStateException("Previous connection is not closed.");
|
||||
}
|
||||
}
|
||||
|
@ -573,10 +594,10 @@ public class DefaultConnection implements Connection {
|
|||
return;
|
||||
}
|
||||
|
||||
conn.getHeaderFields().forEach((key, headers) ->
|
||||
headers.forEach(value ->
|
||||
LOG.debug("HEADER {}: {}", key, value)
|
||||
)
|
||||
getResponse().headers().map().forEach((key, headers) ->
|
||||
headers.forEach(value ->
|
||||
LOG.debug("HEADER {}: {}", key, value)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -584,48 +605,31 @@ public class DefaultConnection implements Connection {
|
|||
* Collects links of the given relation.
|
||||
*
|
||||
* @param relation
|
||||
* Link relation
|
||||
* Link relation
|
||||
* @return Collection of links, unconverted
|
||||
*/
|
||||
private Collection<String> collectLinks(String relation) {
|
||||
assertConnectionIsOpen();
|
||||
var p = Pattern.compile("<(.*?)>\\s*;\\s*rel=\"?" + Pattern.quote(relation) + "\"?");
|
||||
|
||||
var result = new ArrayList<String>();
|
||||
|
||||
var links = conn.getHeaderFields().get(LINK_HEADER);
|
||||
if (links != null) {
|
||||
var p = Pattern.compile("<(.*?)>\\s*;\\s*rel=\"?"+ Pattern.quote(relation) + "\"?");
|
||||
for (var link : links) {
|
||||
var m = p.matcher(link);
|
||||
if (m.matches()) {
|
||||
var location = m.group(1);
|
||||
LOG.debug("Link: {} -> {}", relation, location);
|
||||
result.add(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return getResponse().headers().allValues(LINK_HEADER)
|
||||
.stream()
|
||||
.map(p::matcher)
|
||||
.filter(Matcher::matches)
|
||||
.map(m -> m.group(1))
|
||||
.peek(location -> LOG.debug("Link: {} -> {}", relation, location))
|
||||
.collect(toUnmodifiableList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a relative link against the connection's last URL.
|
||||
*
|
||||
* @param link
|
||||
* Link to resolve. Absolute links are just converted to an URL. May be
|
||||
* {@code null}.
|
||||
* @return Absolute URL of the given link, or {@code null} if the link was
|
||||
* {@code null}.
|
||||
* Link to resolve. Absolute links are just converted to an URL.
|
||||
* @return Absolute URL of the given link
|
||||
*/
|
||||
@Nullable
|
||||
private URL resolveRelative(@Nullable String link) {
|
||||
if (link == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
assertConnectionIsOpen();
|
||||
private URL resolveRelative(String link) {
|
||||
try {
|
||||
return new URL(conn.getURL(), link);
|
||||
return resolveUri(link).toURL();
|
||||
} catch (MalformedURLException ex) {
|
||||
throw new AcmeProtocolException("Cannot resolve relative link: " + link, ex);
|
||||
}
|
||||
|
@ -635,21 +639,11 @@ public class DefaultConnection implements Connection {
|
|||
* Resolves a relative URI against the connection's last URL.
|
||||
*
|
||||
* @param uri
|
||||
* URI to resolve
|
||||
* @return Absolute URI of the given link, or {@code null} if the URI was
|
||||
* {@code null}.
|
||||
* URI to resolve
|
||||
* @return Absolute URI of the given link
|
||||
*/
|
||||
@Nullable
|
||||
private URI resolveUri(@Nullable String uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return conn.getURL().toURI().resolve(uri);
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new AcmeProtocolException("Invalid URI", ex);
|
||||
}
|
||||
private URI resolveUri(String uri) {
|
||||
return getResponse().request().uri().resolve(uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,24 +13,26 @@
|
|||
*/
|
||||
package org.shredzone.acme4j.connector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A generic HTTP connector. It connects to the given URL with a 10 seconds connection and
|
||||
* read timeout.
|
||||
* <p>
|
||||
* Subclasses may reconfigure the {@link HttpURLConnection} and pin it to a concrete SSL
|
||||
* certificate.
|
||||
* A generic HTTP connector. It creates {@link HttpClient.Builder} and
|
||||
* {@link HttpRequest.Builder} that can be individually customized according to the needs
|
||||
* of the CA.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class HttpConnector {
|
||||
|
||||
private static final String USER_AGENT;
|
||||
|
||||
private final NetworkSettings networkSettings;
|
||||
|
||||
static {
|
||||
var agent = new StringBuilder("acme4j");
|
||||
|
||||
|
@ -57,38 +59,51 @@ public class HttpConnector {
|
|||
}
|
||||
|
||||
/**
|
||||
* Opens a {@link HttpURLConnection} to the given {@link URL}.
|
||||
*
|
||||
* @param url
|
||||
* {@link URL} to connect to
|
||||
* @param settings
|
||||
* {@link NetworkSettings} to be used
|
||||
* @return {@link HttpURLConnection} connected to the {@link URL}
|
||||
* Creates a new {@link HttpConnector} that is using the given
|
||||
* {@link NetworkSettings}.
|
||||
*/
|
||||
public HttpURLConnection openConnection(URL url, NetworkSettings settings) throws IOException {
|
||||
var conn = (HttpURLConnection) url.openConnection(settings.getProxy());
|
||||
configure(conn, settings);
|
||||
return conn;
|
||||
public HttpConnector(NetworkSettings networkSettings) {
|
||||
this.networkSettings = networkSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the new {@link HttpURLConnection}.
|
||||
* <p>
|
||||
* The {@link HttpURLConnection} is already preconfigured with a reasonable timeout,
|
||||
* disabled caches and a User-Agent header. Subclasses can override this method to
|
||||
* change the configuration.
|
||||
* Creates a new {@link HttpRequest.Builder} that is preconfigured and bound to the
|
||||
* given URL. Subclasses can override this method to extend the configuration, or
|
||||
* create a different builder.
|
||||
*
|
||||
* @param conn
|
||||
* {@link HttpURLConnection} to configure.
|
||||
* @param settings
|
||||
* {@link NetworkSettings} with settings to be used
|
||||
* @param url
|
||||
* {@link URL} to connect to
|
||||
* @return {@link HttpRequest.Builder} connected to the {@link URL}
|
||||
*/
|
||||
protected void configure(HttpURLConnection conn, NetworkSettings settings) {
|
||||
var timeout = (int) settings.getTimeout().toMillis();
|
||||
conn.setConnectTimeout(timeout);
|
||||
conn.setReadTimeout(timeout);
|
||||
conn.setUseCaches(false);
|
||||
conn.setRequestProperty("User-Agent", USER_AGENT);
|
||||
public HttpRequest.Builder createRequestBuilder(URL url) {
|
||||
try {
|
||||
return HttpRequest.newBuilder(url.toURI())
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.timeout(networkSettings.getTimeout());
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new IllegalArgumentException("Invalid URL", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HttpClient.Builder}.
|
||||
* <p>
|
||||
* The {@link HttpClient.Builder} is already preconfigured with a reasonable timeout,
|
||||
* the proxy settings, authenticator, and that it follows normal redirects.
|
||||
* Subclasses can override this method to extend the configuration, or to create a
|
||||
* different builder.
|
||||
*/
|
||||
public HttpClient.Builder createClientBuilder() {
|
||||
var builder = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(networkSettings.getTimeout())
|
||||
.proxy(networkSettings.getProxySelector());
|
||||
|
||||
if (networkSettings.getAuthenticator() != null) {
|
||||
builder.authenticator(networkSettings.getAuthenticator());
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
*/
|
||||
package org.shredzone.acme4j.connector;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.net.Authenticator;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.Nullable;
|
||||
|
@ -25,22 +27,47 @@ import edu.umd.cs.findbugs.annotations.Nullable;
|
|||
*/
|
||||
public class NetworkSettings {
|
||||
|
||||
private Proxy proxy = Proxy.NO_PROXY;
|
||||
private ProxySelector proxySelector = HttpClient.Builder.NO_PROXY;
|
||||
private Duration timeout = Duration.ofSeconds(10);
|
||||
private @Nullable Authenticator authenticator = null;
|
||||
|
||||
/**
|
||||
* Gets the {@link Proxy} to be used for connections.
|
||||
* Gets the {@link ProxySelector} to be used for connections.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public Proxy getProxy() {
|
||||
return proxy;
|
||||
public ProxySelector getProxySelector() {
|
||||
return proxySelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a {@link Proxy} that is to be used for all connections. If {@code null},
|
||||
* {@link Proxy#NO_PROXY} is used, which is also the default.
|
||||
* Sets a {@link ProxySelector} that is to be used for all connections. If
|
||||
* {@code null}, {@link HttpClient.Builder#NO_PROXY} is used, which is also the
|
||||
* default.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public void setProxy(@Nullable Proxy proxy) {
|
||||
this.proxy = proxy != null ? proxy : Proxy.NO_PROXY;
|
||||
public void setProxySelector(@Nullable ProxySelector proxySelector) {
|
||||
this.proxySelector = proxySelector != null ? proxySelector : HttpClient.Builder.NO_PROXY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Authenticator} to be used, or {@code null} if none is to be set.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public @Nullable Authenticator getAuthenticator() {
|
||||
return authenticator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an {@link Authenticator} to be used if HTTP authentication is needed (e.g.
|
||||
* by a proxy). {@code null} means that no authenticator shall be set.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public void setAuthenticator(@Nullable Authenticator authenticator) {
|
||||
this.authenticator = authenticator;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,9 +87,6 @@ public class NetworkSettings {
|
|||
if (timeout == null || timeout.isNegative() || timeout.isZero()) {
|
||||
throw new IllegalArgumentException("Timeout must be positive");
|
||||
}
|
||||
if (timeout.toMillis() > Integer.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("Timeout is out of range");
|
||||
}
|
||||
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
*/
|
||||
package org.shredzone.acme4j.provider;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
|
@ -32,6 +31,7 @@ import org.shredzone.acme4j.challenge.TokenChallenge;
|
|||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.DefaultConnection;
|
||||
import org.shredzone.acme4j.connector.HttpConnector;
|
||||
import org.shredzone.acme4j.connector.NetworkSettings;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.toolbox.JSON;
|
||||
|
||||
|
@ -43,12 +43,13 @@ import org.shredzone.acme4j.toolbox.JSON;
|
|||
* and {@link AbstractAcmeProvider#resolve(URI)}.
|
||||
*/
|
||||
public abstract class AbstractAcmeProvider implements AcmeProvider {
|
||||
private static final int HTTP_NOT_MODIFIED = 304;
|
||||
|
||||
private static final Map<String, ChallengeProvider> CHALLENGES = challengeMap();
|
||||
|
||||
@Override
|
||||
public Connection connect(URI serverUri) {
|
||||
return new DefaultConnection(createHttpConnector());
|
||||
public Connection connect(URI serverUri, NetworkSettings networkSettings) {
|
||||
return new DefaultConnection(createHttpConnector(networkSettings));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -59,10 +60,10 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
try (var conn = connect(serverUri)) {
|
||||
try (var conn = connect(serverUri, session.networkSettings())) {
|
||||
var lastModified = session.getDirectoryLastModified();
|
||||
var rc = conn.sendRequest(resolve(serverUri), session, lastModified);
|
||||
if (lastModified != null && rc == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
if (lastModified != null && rc == HTTP_NOT_MODIFIED) {
|
||||
// The server has not been modified since
|
||||
return null;
|
||||
}
|
||||
|
@ -146,8 +147,8 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
|
|||
* <p>
|
||||
* Subclasses may override this method to configure the {@link HttpConnector}.
|
||||
*/
|
||||
protected HttpConnector createHttpConnector() {
|
||||
return new HttpConnector();
|
||||
protected HttpConnector createHttpConnector(NetworkSettings settings) {
|
||||
return new HttpConnector(settings);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.shredzone.acme4j.Login;
|
|||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.NetworkSettings;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.toolbox.JSON;
|
||||
|
||||
|
@ -58,10 +59,12 @@ public interface AcmeProvider {
|
|||
* Creates a {@link Connection} for communication with the ACME server.
|
||||
*
|
||||
* @param serverUri
|
||||
* Server {@link URI}
|
||||
* Server {@link URI}
|
||||
* @param networkSettings
|
||||
* {@link NetworkSettings} to be used for the connection
|
||||
* @return {@link Connection} that was generated
|
||||
*/
|
||||
Connection connect(URI serverUri);
|
||||
Connection connect(URI serverUri, NetworkSettings networkSettings);
|
||||
|
||||
/**
|
||||
* Returns the provider's directory. The structure must contain resource URLs, and may
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.net.URL;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
import org.shredzone.acme4j.connector.HttpConnector;
|
||||
import org.shredzone.acme4j.connector.NetworkSettings;
|
||||
import org.shredzone.acme4j.provider.AbstractAcmeProvider;
|
||||
import org.shredzone.acme4j.provider.AcmeProvider;
|
||||
|
||||
|
@ -81,8 +82,8 @@ public class PebbleAcmeProvider extends AbstractAcmeProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected HttpConnector createHttpConnector() {
|
||||
return new PebbleHttpConnector();
|
||||
protected HttpConnector createHttpConnector(NetworkSettings settings) {
|
||||
return new PebbleHttpConnector(settings);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,8 +14,7 @@
|
|||
package org.shredzone.acme4j.provider.pebble;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.http.HttpClient;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
|
@ -23,9 +22,7 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.cert.CertificateException;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.Nullable;
|
||||
|
@ -33,29 +30,29 @@ import org.shredzone.acme4j.connector.HttpConnector;
|
|||
import org.shredzone.acme4j.connector.NetworkSettings;
|
||||
|
||||
/**
|
||||
* {@link HttpConnector} to be used for Pebble. Pebble uses a static, self signed SSL
|
||||
* {@link HttpConnector} to be used for Pebble. Pebble uses a static, self-signed SSL
|
||||
* certificate.
|
||||
*/
|
||||
public class PebbleHttpConnector extends HttpConnector {
|
||||
private static @Nullable SSLSocketFactory sslSocketFactory = null;
|
||||
private static @Nullable SSLContext sslContext = null;
|
||||
|
||||
public PebbleHttpConnector(NetworkSettings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpURLConnection openConnection(URL url, NetworkSettings settings) throws IOException {
|
||||
var conn = super.openConnection(url, settings);
|
||||
if (conn instanceof HttpsURLConnection) {
|
||||
var conns = (HttpsURLConnection) conn;
|
||||
conns.setSSLSocketFactory(createSocketFactory());
|
||||
conns.setHostnameVerifier((h, s) -> true);
|
||||
}
|
||||
return conn;
|
||||
public HttpClient.Builder createClientBuilder() {
|
||||
var builder = super.createClientBuilder();
|
||||
builder.sslContext(createSSLContext());
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily creates an {@link SSLSocketFactory} that exclusively accepts the Pebble
|
||||
* Lazily creates an {@link SSLContext} that exclusively accepts the Pebble
|
||||
* certificate.
|
||||
*/
|
||||
protected synchronized SSLSocketFactory createSocketFactory() throws IOException {
|
||||
if (sslSocketFactory == null) {
|
||||
protected synchronized SSLContext createSSLContext() {
|
||||
if (sslContext == null) {
|
||||
try (var in = getClass().getResourceAsStream("/org/shredzone/acme4j/provider/pebble/pebble.truststore")) {
|
||||
var keystore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keystore.load(in, "acme4j".toCharArray());
|
||||
|
@ -63,16 +60,14 @@ public class PebbleHttpConnector extends HttpConnector {
|
|||
var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(keystore);
|
||||
|
||||
var ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(null, tmf.getTrustManagers(), null);
|
||||
|
||||
sslSocketFactory = ctx.getSocketFactory();
|
||||
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException
|
||||
| KeyManagementException ex) {
|
||||
throw new IOException("Could not create truststore", ex);
|
||||
sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, tmf.getTrustManagers(), null);
|
||||
} catch (IOException | KeyStoreException | CertificateException
|
||||
| NoSuchAlgorithmException | KeyManagementException ex) {
|
||||
throw new RuntimeException("Could not create truststore", ex);
|
||||
}
|
||||
}
|
||||
return Objects.requireNonNull(sslSocketFactory);
|
||||
return Objects.requireNonNull(sslContext);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import static org.shredzone.acme4j.toolbox.TestUtils.getResourceAsByteArray;
|
|||
import static org.shredzone.acme4j.toolbox.TestUtils.url;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
|
@ -70,9 +69,9 @@ public class DefaultConnectionTest {
|
|||
private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding();
|
||||
private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC);
|
||||
public static final String DIRECTORY_PATH = "/dir";
|
||||
public static final String NEW_NONCE_PATH = "/newNonce";
|
||||
public static final String REQUEST_PATH = "/test/test";
|
||||
private static final String DIRECTORY_PATH = "/dir";
|
||||
private static final String NEW_NONCE_PATH = "/newNonce";
|
||||
private static final String REQUEST_PATH = "/test/test";
|
||||
|
||||
private final URL accountUrl = TestUtils.url(TestUtils.ACCOUNT_URL);
|
||||
private Session session;
|
||||
|
@ -549,7 +548,7 @@ public class DefaultConnectionTest {
|
|||
conn.sendSignedRequest(requestUrl, new JSONBuilder(), login);
|
||||
}
|
||||
});
|
||||
assertThat(ex.getMessage()).isEqualTo("HTTP 500: Infernal Server Error");
|
||||
assertThat(ex.getMessage()).isEqualTo("HTTP 500");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,14 +15,12 @@ package org.shredzone.acme4j.connector;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Authenticator;
|
||||
import java.net.URL;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
|
@ -31,41 +29,58 @@ import org.junit.jupiter.api.Test;
|
|||
public class HttpConnectorTest {
|
||||
|
||||
/**
|
||||
* Test if a HTTP connection can be opened.
|
||||
* <p>
|
||||
* This is just a mock to check that the parameters are properly set.
|
||||
* Test if a {@link java.net.http.HttpClient.Builder} can be created and has proper
|
||||
* default values.
|
||||
*/
|
||||
@Test
|
||||
public void testMockOpenConnection() {
|
||||
public void testClientBuilderDefaultValues() {
|
||||
var settings = new NetworkSettings();
|
||||
settings.setTimeout(Duration.ofSeconds(50));
|
||||
|
||||
var conn = mock(HttpURLConnection.class);
|
||||
var connector = new HttpConnector(settings);
|
||||
var client = connector.createClientBuilder().build();
|
||||
|
||||
var connector = new HttpConnector();
|
||||
connector.configure(conn, settings);
|
||||
|
||||
verify(conn).setConnectTimeout(50000);
|
||||
verify(conn).setReadTimeout(50000);
|
||||
verify(conn).setUseCaches(false);
|
||||
verify(conn).setRequestProperty("User-Agent", HttpConnector.defaultUserAgent());
|
||||
assertThat(client.connectTimeout().orElseThrow()).isEqualTo(settings.getTimeout());
|
||||
assertThat(client.followRedirects()).isEqualTo(HttpClient.Redirect.NORMAL);
|
||||
assertThat(client.authenticator()).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a HTTP connection can be opened.
|
||||
* <p>
|
||||
* This test requires a network connection. It should be excluded from automated
|
||||
* builds.
|
||||
* Test if a {@link java.net.http.HttpClient.Builder} can be created and if it is
|
||||
* preconfigured properly.
|
||||
*/
|
||||
@Test
|
||||
@Tag("requires-network")
|
||||
public void testOpenConnection() throws IOException {
|
||||
public void testClientBuilder() {
|
||||
var timeout = Duration.ofSeconds(50);
|
||||
var authenticator = mock(Authenticator.class);
|
||||
|
||||
var settings = new NetworkSettings();
|
||||
var connector = new HttpConnector();
|
||||
var conn = connector.openConnection(new URL("http://example.com"), settings);
|
||||
assertThat(conn).isNotNull();
|
||||
conn.connect();
|
||||
assertThat(conn.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK);
|
||||
settings.setTimeout(timeout);
|
||||
settings.setAuthenticator(authenticator);
|
||||
|
||||
var connector = new HttpConnector(settings);
|
||||
var client = connector.createClientBuilder().build();
|
||||
|
||||
assertThat(client.connectTimeout().orElseThrow()).isEqualTo(timeout);
|
||||
assertThat(client.followRedirects()).isEqualTo(HttpClient.Redirect.NORMAL);
|
||||
assertThat(client.authenticator().orElseThrow()).isSameAs(authenticator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a {@link java.net.http.HttpRequest.Builder} can be created and has proper
|
||||
* default values.
|
||||
*/
|
||||
@Test
|
||||
public void testRequestBuilderDefaultValues() throws Exception {
|
||||
var url = new URL("http://example.org:123/foo");
|
||||
var settings = new NetworkSettings();
|
||||
|
||||
var connector = new HttpConnector(settings);
|
||||
var request = connector.createRequestBuilder(url).build();
|
||||
|
||||
assertThat(request.uri().toString()).isEqualTo(url.toExternalForm());
|
||||
assertThat(request.timeout().orElseThrow()).isEqualTo(settings.getTimeout());
|
||||
assertThat(request.headers().firstValue("User-Agent").orElseThrow())
|
||||
.isEqualTo(HttpConnector.defaultUserAgent());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,8 +16,12 @@ package org.shredzone.acme4j.connector;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.net.Authenticator;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -34,16 +38,23 @@ public class NetworkSettingsTest {
|
|||
public void testGettersAndSetters() {
|
||||
var settings = new NetworkSettings();
|
||||
|
||||
assertThat(settings.getProxy()).isEqualTo(Proxy.NO_PROXY);
|
||||
var proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.0.0.1", 8080));
|
||||
settings.setProxy(proxy);
|
||||
assertThat(settings.getProxy()).isEqualTo(proxy);
|
||||
settings.setProxy(null);
|
||||
assertThat(settings.getProxy()).isEqualTo(Proxy.NO_PROXY);
|
||||
var proxyAddress = new InetSocketAddress("10.0.0.1", 8080);
|
||||
var proxySelector = ProxySelector.of(proxyAddress);
|
||||
|
||||
assertThat(settings.getProxySelector()).isSameAs(HttpClient.Builder.NO_PROXY);
|
||||
settings.setProxySelector(proxySelector);
|
||||
assertThat(settings.getProxySelector()).isSameAs(proxySelector);
|
||||
settings.setProxySelector(null);
|
||||
assertThat(settings.getProxySelector()).isEqualTo(HttpClient.Builder.NO_PROXY);
|
||||
|
||||
assertThat(settings.getTimeout()).isEqualTo(Duration.ofSeconds(10));
|
||||
settings.setTimeout(Duration.ofMillis(5120));
|
||||
assertThat(settings.getTimeout()).isEqualTo(Duration.ofMillis(5120));
|
||||
|
||||
var defaultAuthenticator = Authenticator.getDefault();
|
||||
assertThat(settings.getAuthenticator()).isNull();
|
||||
settings.setAuthenticator(defaultAuthenticator);
|
||||
assertThat(settings.getAuthenticator()).isSameAs(defaultAuthenticator);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -59,9 +70,6 @@ public class NetworkSettingsTest {
|
|||
assertThrows(IllegalArgumentException.class,
|
||||
() -> settings.setTimeout(Duration.ofSeconds(20).negated()),
|
||||
"timeout accepted negative duration");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> settings.setTimeout(Duration.ofMillis(Integer.MAX_VALUE + 1L)),
|
||||
"timeout accepted out of range value");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ public class SessionProviderTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Connection connect(URI serverUri) {
|
||||
public Connection connect(URI serverUri, NetworkSettings networkSettings) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ public class SessionProviderTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Connection connect(URI serverUri) {
|
||||
public Connection connect(URI serverUri, NetworkSettings networkSettings) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||
import static org.mockito.Mockito.*;
|
||||
import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
@ -39,6 +38,7 @@ import org.shredzone.acme4j.challenge.TokenChallenge;
|
|||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.DefaultConnection;
|
||||
import org.shredzone.acme4j.connector.HttpConnector;
|
||||
import org.shredzone.acme4j.connector.NetworkSettings;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||
|
@ -51,6 +51,7 @@ public class AbstractAcmeProviderTest {
|
|||
|
||||
private static final URI SERVER_URI = URI.create("http://example.com/acme");
|
||||
private static final URL RESOLVED_URL = TestUtils.url("http://example.com/acme/directory");
|
||||
private static final NetworkSettings NETWORK_SETTINGS = new NetworkSettings();
|
||||
|
||||
/**
|
||||
* Test that connect returns a connection.
|
||||
|
@ -61,13 +62,14 @@ public class AbstractAcmeProviderTest {
|
|||
|
||||
var provider = new TestAbstractAcmeProvider() {
|
||||
@Override
|
||||
protected HttpConnector createHttpConnector() {
|
||||
protected HttpConnector createHttpConnector(NetworkSettings settings) {
|
||||
assertThat(settings).isSameAs(NETWORK_SETTINGS);
|
||||
invoked.set(true);
|
||||
return super.createHttpConnector();
|
||||
return super.createHttpConnector(settings);
|
||||
}
|
||||
};
|
||||
|
||||
var connection = provider.connect(SERVER_URI);
|
||||
var connection = provider.connect(SERVER_URI, NETWORK_SETTINGS);
|
||||
assertThat(connection).isNotNull();
|
||||
assertThat(connection).isInstanceOf(DefaultConnection.class);
|
||||
assertThat(invoked).isTrue();
|
||||
|
@ -123,6 +125,7 @@ public class AbstractAcmeProviderTest {
|
|||
verify(session).setDirectoryExpires(eq(expiryDate));
|
||||
verify(session).getDirectoryExpires();
|
||||
verify(session).getDirectoryLastModified();
|
||||
verify(session).networkSettings();
|
||||
verifyNoMoreInteractions(session);
|
||||
|
||||
verify(connection).sendRequest(RESOLVED_URL, session, null);
|
||||
|
@ -182,6 +185,7 @@ public class AbstractAcmeProviderTest {
|
|||
verify(session).setDirectoryLastModified(eq(null));
|
||||
verify(session).getDirectoryExpires();
|
||||
verify(session).getDirectoryLastModified();
|
||||
verify(session).networkSettings();
|
||||
verifyNoMoreInteractions(session);
|
||||
|
||||
verify(connection).sendRequest(RESOLVED_URL, session, null);
|
||||
|
@ -215,6 +219,7 @@ public class AbstractAcmeProviderTest {
|
|||
|
||||
verify(session).getDirectoryExpires();
|
||||
verify(session).getDirectoryLastModified();
|
||||
verify(session).networkSettings();
|
||||
verifyNoMoreInteractions(session);
|
||||
|
||||
verify(connection).sendRequest(RESOLVED_URL, session, modifiedSinceDate);
|
||||
|
@ -222,25 +227,6 @@ public class AbstractAcmeProviderTest {
|
|||
verifyNoMoreInteractions(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that HTTP errors are handled correctly.
|
||||
*/
|
||||
@Test
|
||||
public void testResourcesHttpError() throws IOException {
|
||||
var conn = mock(HttpURLConnection.class);
|
||||
var connector = mock(HttpConnector.class);
|
||||
var connection = new DefaultConnection(connector);
|
||||
|
||||
when(connector.openConnection(any(), any())).thenReturn(conn);
|
||||
when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR);
|
||||
when(conn.getResponseMessage()).thenReturn("Internal error");
|
||||
|
||||
var provider = new TestAbstractAcmeProvider(connection);
|
||||
var session = TestUtils.session(provider);
|
||||
|
||||
assertThrows(AcmeException.class, () -> provider.directory(session, SERVER_URI));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that challenges are generated properly.
|
||||
*/
|
||||
|
@ -316,9 +302,9 @@ public class AbstractAcmeProviderTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Connection connect(URI serverUri) {
|
||||
public Connection connect(URI serverUri, NetworkSettings networkSettings) {
|
||||
assertThat(serverUri).isEqualTo(SERVER_URI);
|
||||
return connection != null ? connection : super.connect(serverUri);
|
||||
return connection != null ? connection : super.connect(serverUri, networkSettings);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
package org.shredzone.acme4j.provider;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.shredzone.acme4j.toolbox.TestUtils.DEFAULT_NETWORK_SETTINGS;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
@ -50,7 +51,7 @@ public class GenericAcmeProviderTest {
|
|||
var resolvedUrl = provider.resolve(serverUri);
|
||||
assertThat(resolvedUrl.toString()).isEqualTo(serverUri.toString());
|
||||
|
||||
var connection = provider.connect(serverUri);
|
||||
var connection = provider.connect(serverUri, DEFAULT_NETWORK_SETTINGS);
|
||||
assertThat(connection).isInstanceOf(DefaultConnection.class);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.shredzone.acme4j.Session;
|
|||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.DummyConnection;
|
||||
import org.shredzone.acme4j.connector.NetworkSettings;
|
||||
import org.shredzone.acme4j.connector.Resource;
|
||||
import org.shredzone.acme4j.toolbox.JSON;
|
||||
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||
|
@ -32,7 +33,7 @@ import org.shredzone.acme4j.toolbox.TestUtils;
|
|||
|
||||
/**
|
||||
* Test implementation of {@link AcmeProvider}. It also implements a dummy implementation
|
||||
* of {@link Connection} that is always returned on {@link #connect(URI)}.
|
||||
* of {@link Connection} that is always returned on {@link #connect(URI, NetworkSettings)}.
|
||||
*/
|
||||
public class TestableConnectionProvider extends DummyConnection implements AcmeProvider {
|
||||
private final Map<String, BiFunction<Login, JSON, Challenge>> creatorMap = new HashMap<>();
|
||||
|
@ -119,7 +120,7 @@ public class TestableConnectionProvider extends DummyConnection implements AcmeP
|
|||
}
|
||||
|
||||
@Override
|
||||
public Connection connect(URI serverUri) {
|
||||
public Connection connect(URI serverUri, NetworkSettings networkSettings) {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ import org.shredzone.acme4j.Login;
|
|||
import org.shredzone.acme4j.Problem;
|
||||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.NetworkSettings;
|
||||
import org.shredzone.acme4j.provider.AcmeProvider;
|
||||
|
||||
/**
|
||||
|
@ -72,6 +73,7 @@ public final class TestUtils {
|
|||
|
||||
public static final String DUMMY_NONCE = Base64.getUrlEncoder().withoutPadding().encodeToString("foo-nonce-foo".getBytes());
|
||||
|
||||
public static final NetworkSettings DEFAULT_NETWORK_SETTINGS = new NetworkSettings();
|
||||
|
||||
private TestUtils() {
|
||||
// utility class without constructor
|
||||
|
@ -161,7 +163,7 @@ public final class TestUtils {
|
|||
|
||||
@Override
|
||||
public Connection connect() {
|
||||
return provider.connect(getServerUri());
|
||||
return provider.connect(getServerUri(), DEFAULT_NETWORK_SETTINGS);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -220,7 +220,7 @@ public class OrderIT extends PebbleITBase {
|
|||
Certificate cert2 = login2.bindCertificate(certificate.getLocation());
|
||||
cert2.download();
|
||||
}, "Could download revoked cert");
|
||||
assertThat(ex.getMessage()).isEqualTo("HTTP 404: Not Found");
|
||||
assertThat(ex.getMessage()).isEqualTo("HTTP 404");
|
||||
|
||||
// Try to revoke again
|
||||
var ex2 = assertThrows(AcmeServerException.class,
|
||||
|
|
|
@ -49,7 +49,6 @@ If there is no cached copy of the CA's directory, it is fetched now.
|
|||
|
||||
```log
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - GET https://localhost:14000/dir
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER null: HTTP/1.1 200 OK
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Length: 406
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Date: Wed, 27 Apr 2022 17:42:43 GMT
|
||||
|
@ -63,7 +62,6 @@ If _acme4j_ has no current nonce, it will fetch a new one from the `newNonce` en
|
|||
|
||||
```log
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEAD https://localhost:14000/nonce-plz
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER null: HTTP/1.1 200 OK
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Replay-Nonce: Os_sBjfWzVZenwwjvLrwXA
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Date: Wed, 27 Apr 2022 17:42:43 GMT
|
||||
|
@ -84,7 +82,6 @@ Now _acme4j_ sends a `POST` request to the `newAccount` endpoint. As `Payload`,
|
|||
This is a possible response of the server:
|
||||
|
||||
```log
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER null: HTTP/1.1 201 Created
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Replay-Nonce: mmnKF6lBuisPWhj9kkFMRA
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Length: 491
|
||||
|
@ -114,7 +111,6 @@ Again, we see the `POST` request to the `newAccount` endpoint. It uses the nonce
|
|||
The server responds with a `400 Bad Request` and an `application/problem+json` document:
|
||||
|
||||
```log
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER null: HTTP/1.1 400 Bad Request
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Replay-Nonce: LDDZAGcBuKYpuNlFTCxPYw
|
||||
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Length: 147
|
||||
|
|
|
@ -5,6 +5,7 @@ This document will help you migrate your code to the latest _acme4j_ version.
|
|||
## Migration to Version 3.0.0
|
||||
|
||||
- Starting with _acme4j_ v3, we will require the smallest Java SE LTS version that is still receiving premier support according to the [Oracle Java SE Support Roadmap](https://www.oracle.com/java/technologies/java-se-support-roadmap.html). At the moment of writing, these are Java 11 and Java 17, so _acme4j_ requires Java 11 starting from now. With the prospected release of Java 21 (LTS) in September 2023, we will start to require Java 17, and so on. If you still need Java 8, you can use _acme4j_ v2, which will receive bugfixes until September 2023.
|
||||
- Changed to `java.net.http` client. Due to limitations of the API, HTTP errors are only thrown with the error code, but not with the error message. If you checked the message in unit tests, be prepared that the error message might have changed.
|
||||
- All deprecated methods have been removed.
|
||||
|
||||
## Migration to Version 2.16
|
||||
|
|
|
@ -43,9 +43,8 @@ By default, the system's default locale is used.
|
|||
|
||||
## Network Settings
|
||||
|
||||
_acme4j_ uses a standard `HttpURLConnection` for HTTP connections. You can use `Session.networkSettings()` to change some network parameters for the session.
|
||||
You can use `Session.networkSettings()` to change some network parameters for the session.
|
||||
|
||||
* If a proxy must be used for internet connections, you can set a `Proxy` instance via `setProxy()`. An alternative is to use the system properties `https.proxyHost` and `https.proxyPort` to globally set a proxy for the Java process.
|
||||
* If a proxy must be used for internet connections, you can set a `ProxySelector` instance via `setProxySelector()`.
|
||||
* To change network timeouts, use `setTimeout()`. The default timeout is 10 seconds. You can either increase the timeout on poor network connections, or reduce it to fail early on network errors.
|
||||
|
||||
If the proxy needs authentication, you need to set a default `Authenticator`. Be careful: Most code snippets I have found on the internet will send out the proxy credentials to anyone who is asking. See [this blog article](https://rolandtapken.de/blog/2012-04/java-process-httpproxyuser-and-httpproxypassword) for a good way to implement a proxy `Authenticator`.
|
||||
* If you need authentication (e.g. for the proxy), you can set an `Authenticator` via `setAuthenticator()`. Be careful here! Most code snippets I have found on the internet will send out the proxy credentials to anyone who is asking. You should check `Authenticator.getRequestorType()` and make sure it is `RequestorType.PROXY` before sending the proxy credentials.
|
||||
|
|
Loading…
Reference in New Issue