diff --git a/acme4j-client/src/main/java/module-info.java b/acme4j-client/src/main/java/module-info.java index 761b249f..1d3df187 100644 --- a/acme4j-client/src/main/java/module-info.java +++ b/acme4j-client/src/main/java/module-info.java @@ -14,6 +14,7 @@ module org.shredzone.acme4j { requires static com.github.spotbugs.annotations; + requires java.net.http; requires org.jose4j; requires org.slf4j; diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java index 97efb3e7..6d2db56c 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java @@ -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); } /** diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java index fe1a2f81..93c15875 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java @@ -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. *
- * 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.
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java
index 403aa4c3..fb0f4ba1 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java
@@ -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
+ * Note that the response provides an {@link InputStream} that can be read only
+ * once.
+ */
+ private HttpResponse
- * 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}.
- *
- * 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}.
+ *
+ * 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;
}
}
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/NetworkSettings.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/NetworkSettings.java
index e504f3ea..2c9d7d64 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/NetworkSettings.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/NetworkSettings.java
@@ -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;
}
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java
index e1bf1a11..21af7dce 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java
@@ -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
* 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);
}
}
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeProvider.java
index 41cd93fe..048a0b53 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeProvider.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeProvider.java
@@ -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
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProvider.java
index 4db21294..9b10123d 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProvider.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProvider.java
@@ -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);
}
}
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java
index e228a225..15f2b11f 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java
@@ -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);
}
}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java
index 54c0df3c..80a84472 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java
@@ -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");
}
/**
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java
index 2c12315b..74ee67bd 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/HttpConnectorTest.java
@@ -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.
- *
- * 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.
- *
- * 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());
}
/**
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/NetworkSettingsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/NetworkSettingsTest.java
index ebb91254..6988b566 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/NetworkSettingsTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/NetworkSettingsTest.java
@@ -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");
}
}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java
index f541260f..6b5f7c29 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java
@@ -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();
}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeProviderTest.java
index b658ae68..0b2daa63 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeProviderTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeProviderTest.java
@@ -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);
}
}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/GenericAcmeProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/GenericAcmeProviderTest.java
index 90269dbf..6bdb9cf9 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/GenericAcmeProviderTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/GenericAcmeProviderTest.java
@@ -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);
}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/TestableConnectionProvider.java b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/TestableConnectionProvider.java
index dfb1701f..1203e009 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/TestableConnectionProvider.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/TestableConnectionProvider.java
@@ -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