You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by kw...@apache.org on 2026/07/06 11:57:50 UTC
(maven-resolver) branch feature/http3 created (now bb05b5ceb)
This is an automated email from the ASF dual-hosted git repository.
kwin pushed a change to branch feature/http3
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
at bb05b5ceb Support HTTP/3 in Jetty and JRE HTTP Client
This branch includes the following new commits:
new bb05b5ceb Support HTTP/3 in Jetty and JRE HTTP Client
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(maven-resolver) 01/01: Support HTTP/3 in Jetty and JRE HTTP Client
Posted by kw...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
kwin pushed a commit to branch feature/http3
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
commit bb05b5cebd18268ecfbaf6b6bbdb7aecdb7621e9
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Mon Jul 6 13:57:39 2026 +0200
Support HTTP/3 in Jetty and JRE HTTP Client
Consolidate http version configuration among HTTP transporters
Default to HTTP/2 in all transporters except Apache HTTP Client 4.x
Still WIP
---
.../eclipse/aether/ConfigurationProperties.java | 33 +++++++++
maven-resolver-test-http/pom.xml | 8 +++
.../aether/internal/test/util/http/HttpServer.java | 74 ++++++++++++++-----
.../test/util/http/HttpTransporterTest.java | 45 +++++++++---
.../src/main/resources/configuration.md.vm | 1 +
.../aether/transport/jdk/JdkTransporter.java | 60 ++++++++++++++--
.../aether/transport/jdk/JdkTransporterTest.java | 18 +++++
maven-resolver-transport-jetty/pom.xml | 8 +++
.../aether/transport/jetty/JettyTransporter.java | 83 ++++++++++++----------
.../java/org/eclipse/aether/util/ConfigUtils.java | 45 ++++++++++++
.../transport/http/HttpTransporterUtils.java | 10 +++
.../org/eclipse/aether/util/ConfigUtilsTest.java | 25 +++++++
12 files changed, 336 insertions(+), 74 deletions(-)
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
index d9d9ce391..b2b7d910f 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
@@ -514,6 +514,39 @@ public final class ConfigurationProperties {
*/
public static final String HTTPS_SECURITY_MODE_INSECURE = "insecure";
+ public enum HttpVersion {
+ /**
+ * The default HTTP version supported by the respective transporter (the most stable one)
+ */
+ DEFAULT,
+ HTTP_1_1,
+ HTTP_2,
+ HTTP_3,
+ /**
+ * The maximum HTTP version supported by the respective transporter (may be unstable).
+ */
+ MAXIMUM;
+ }
+
+ /**
+ * The maximum and preferred HTTP version. It transparently falls back to lower version if remote server does not support it.
+ * Value must be a {@link HttpVersion} enum value or its String representation. Default is {@link #DEFAULT_HTTP_VERSION}.
+ *
+ * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
+ * @configurationType {@link ConfigurationProperties.HttpVersion}
+ * @configurationDefaultValue {@link #DEFAULT_HTTP_VERSION}
+ * @configurationRepoIdSuffix Yes
+ * @since NEXT
+ */
+ public static final String HTTP_VERSION = PREFIX_TRANSPORT_HTTP + "version";
+
+ /**
+ * Default value if {@link #HTTP_VERSION} is not set.
+ *
+ * @since NEXT
+ */
+ public static final HttpVersion DEFAULT_HTTP_VERSION = HttpVersion.DEFAULT;
+
/**
* A flag indicating which visitor should be used to "flatten" the dependency graph into list. In Maven 4
* the default is new "levelOrder", while Maven 3 used "preOrder". This property accepts values
diff --git a/maven-resolver-test-http/pom.xml b/maven-resolver-test-http/pom.xml
index 6845de444..6bafd11ba 100644
--- a/maven-resolver-test-http/pom.xml
+++ b/maven-resolver-test-http/pom.xml
@@ -77,6 +77,14 @@
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-server</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http3</groupId>
+ <artifactId>jetty-http3-server</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.quic</groupId>
+ <artifactId>jetty-quic-quiche-server</artifactId>
+ </dependency>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-server</artifactId>
diff --git a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java
index ecffdd118..d5fc9a46c 100644
--- a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java
+++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java
@@ -55,8 +55,12 @@ import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
+import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory;
+import org.eclipse.jetty.http3.server.HTTP3ServerQuicConfiguration;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.quic.quiche.server.QuicheServerConnector;
+import org.eclipse.jetty.quic.quiche.server.QuicheServerQuicConfiguration;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
@@ -188,6 +192,10 @@ public class HttpServer {
return httpsConnector != null ? httpsConnector.getLocalPort() : -1;
}
+ public int getHttp3Port() {
+ return http3Connector != null ? http3Connector.getLocalPort() : -1;
+ }
+
public String getHttpUrl() {
return "http://" + getHost() + ":" + getHttpPort();
}
@@ -196,6 +204,10 @@ public class HttpServer {
return "https://" + getHost() + ":" + getHttpsPort();
}
+ public String getHttp3Url() {
+ return "https://" + getHost() + ":" + getHttp3Port();
+ }
+
public HttpServer addSslConnector() {
return addSslConnector(true, true);
}
@@ -210,23 +222,7 @@ public class HttpServer {
private HttpServer addSslConnector(boolean needClientAuth, boolean needHttp11) {
if (httpsConnector == null) {
- SslContextFactory.Server ssl = new SslContextFactory.Server();
- ssl.setNeedClientAuth(needClientAuth);
- if (!needClientAuth) {
- ssl.setKeyStorePath(HttpTransporterTest.KEY_STORE_SELF_SIGNED_PATH
- .toAbsolutePath()
- .toString());
- ssl.setKeyStorePassword("server-pwd");
- ssl.setSniRequired(false);
- } else {
- ssl.setKeyStorePath(
- HttpTransporterTest.KEY_STORE_PATH.toAbsolutePath().toString());
- ssl.setKeyStorePassword("server-pwd");
- ssl.setTrustStorePath(
- HttpTransporterTest.TRUST_STORE_PATH.toAbsolutePath().toString());
- ssl.setTrustStorePassword("client-pwd");
- ssl.setSniRequired(false);
- }
+ SslContextFactory.Server ssl = createSslContextFactory(needClientAuth);
HttpConfiguration httpsConfig = new HttpConfiguration();
SecureRequestCustomizer customizer = new SecureRequestCustomizer();
@@ -259,6 +255,48 @@ public class HttpServer {
return this;
}
+ private SslContextFactory.Server createSslContextFactory(boolean needClientAuth) {
+ SslContextFactory.Server ssl = new SslContextFactory.Server();
+ ssl.setNeedClientAuth(needClientAuth);
+ if (!needClientAuth) {
+ ssl.setKeyStorePath(HttpTransporterTest.KEY_STORE_SELF_SIGNED_PATH
+ .toAbsolutePath()
+ .toString());
+ ssl.setKeyStorePassword("server-pwd");
+ ssl.setSniRequired(false);
+ } else {
+ ssl.setKeyStorePath(
+ HttpTransporterTest.KEY_STORE_PATH.toAbsolutePath().toString());
+ ssl.setKeyStorePassword("server-pwd");
+ ssl.setTrustStorePath(
+ HttpTransporterTest.TRUST_STORE_PATH.toAbsolutePath().toString());
+ ssl.setTrustStorePassword("client-pwd");
+ ssl.setSniRequired(false);
+ }
+ return ssl;
+ }
+
+ public HttpServer addHttp3Connector(boolean needClientAuth) {
+ if (http3Connector == null) {
+ QuicheServerQuicConfiguration serverQuicConfig = HTTP3ServerQuicConfiguration.configure(
+ new QuicheServerQuicConfiguration(HttpTransporterTest.PEM_QUICHE_SERVER_PATH));
+ http3Connector = new QuicheServerConnector(
+ server,
+ createSslContextFactory(needClientAuth),
+ serverQuicConfig,
+ new HTTP3ServerConnectionFactory());
+ // TODO: should share same port as https connector, so that the client can use the same port for both
+ // TCP/UDP
+ server.addConnector(http3Connector);
+ try {
+ http3Connector.start();
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return this;
+ }
+
public List<LogEntry> getLogEntries() {
return logEntries;
}
@@ -515,6 +553,8 @@ public class HttpServer {
private static final Pattern SIMPLE_RANGE = Pattern.compile("bytes=([0-9])+-");
+ private QuicheServerConnector http3Connector;
+
private class RepoHandler extends Handler.Abstract {
@Override
public boolean handle(Request req, Response response, Callback callback) throws Exception {
diff --git a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
index 00bbbc242..1ba37eed8 100644
--- a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
+++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
@@ -29,7 +29,6 @@ import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.URI;
-import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -100,6 +99,8 @@ public abstract class HttpTransporterTest {
protected static final Path TRUST_STORE_PATH = Paths.get("target/trustStore");
+ protected static final Path PEM_QUICHE_SERVER_PATH = Paths.get("target/pems");
+
protected static SSLContext defaultSslContext;
static {
@@ -110,18 +111,22 @@ public abstract class HttpTransporterTest {
@BeforeAll
protected static void beforeAll() throws NoSuchAlgorithmException {
// populate custom keystore and truststore
- URL keyStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store");
- URL keyStoreSelfSignedUrl =
- HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store-selfsigned");
- URL trustStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/client-store");
-
try {
- try (InputStream keyStoreStream = keyStoreUrl.openStream();
- InputStream keyStoreSelfSignedStream = keyStoreSelfSignedUrl.openStream();
- InputStream trustStoreStream = trustStoreUrl.openStream()) {
+ try (InputStream keyStoreStream =
+ HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/server-store");
+ InputStream keyStoreSelfSignedStream = HttpTransporterTest.class
+ .getClassLoader()
+ .getResourceAsStream("ssl/server-store-selfsigned");
+ InputStream trustStoreStream =
+ HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/client-store");
+ InputStream serverPemStream =
+ HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/server.pem");
+ InputStream serverSelfsignedPemStream =
+ HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/selfsigned.pem")) {
Files.copy(keyStoreStream, KEY_STORE_PATH, StandardCopyOption.REPLACE_EXISTING);
Files.copy(keyStoreSelfSignedStream, KEY_STORE_SELF_SIGNED_PATH, StandardCopyOption.REPLACE_EXISTING);
Files.copy(trustStoreStream, TRUST_STORE_PATH, StandardCopyOption.REPLACE_EXISTING);
+ Files.createDirectories(PEM_QUICHE_SERVER_PATH);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
@@ -805,8 +810,6 @@ public abstract class HttpTransporterTest {
@Test
protected void testGet_HTTPS_HTTP2Only_Insecure_SecurityMode() throws Exception {
- // here we use alternate server-store-selfigned key (as the key set it static initializer is probably already
- // used to init SSLContext/SSLSocketFactory/etc
enableHttp2Protocol();
session.setConfigProperty(
ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
@@ -825,6 +828,26 @@ public abstract class HttpTransporterTest {
protected void enableHttp2Protocol() {}
+ @Test
+ protected void testGet_HTTP3() throws Exception {
+ // accept self-signed server certificate
+ session.setConfigProperty(
+ ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
+ session.setConfigProperty(ConfigurationProperties.HTTP_VERSION, ConfigurationProperties.HttpVersion.HTTP_3);
+ httpServer.addHttp3Connector(false);
+ httpServer.start();
+ newTransporter(httpServer.getHttp3Url());
+ RecordingTransportListener listener = new RecordingTransportListener();
+ GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
+ transporter.get(task);
+ assertEquals("test", task.getDataString());
+ assertEquals(0L, listener.getDataOffset());
+ assertEquals(4L, listener.getDataLength());
+ assertEquals(1, listener.getStartedCount());
+ assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
+ assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
+ }
+
@Test
protected void testGet_Redirect() throws Exception {
httpServer.addSslConnector();
diff --git a/maven-resolver-tools/src/main/resources/configuration.md.vm b/maven-resolver-tools/src/main/resources/configuration.md.vm
index 2b5669e89..9efd0a5ae 100644
--- a/maven-resolver-tools/src/main/resources/configuration.md.vm
+++ b/maven-resolver-tools/src/main/resources/configuration.md.vm
@@ -68,6 +68,7 @@ From | To | With
`String` | `int` | [`Integer.parseInt(...)`](https://docs.oracle.com/javase/7/docs/api/java/lang/Integer.html#parseInt(java.lang.String))
`String` | `long` | [`Long.parseLong(...)`](https://docs.oracle.com/javase/7/docs/api/java/lang/Long.html#parseLong(java.lang.String))
`String` | `float` | [`Float.parseFloat(...)`](https://docs.oracle.com/javase/7/docs/api/java/lang/Float.html#parseFloat(java.lang.String))
+`String` | `T extends Enum<T>` | [`Enum.valueOf(...)`](https://docs.oracle.com/javase/7/docs/api/java/lang/Enum.html#valueOf(java.lang.Class,%20java.lang.String))
## Set Configuration from Apache Maven
diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
index 0cb866af9..e8986351b 100644
--- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
+++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
@@ -71,6 +71,7 @@ import com.github.mizosoft.methanol.Methanol;
import com.github.mizosoft.methanol.RetryInterceptor;
import com.github.mizosoft.methanol.RetryInterceptor.Context;
import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.ConfigurationProperties.HttpVersion;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.RemoteRepository;
@@ -105,7 +106,6 @@ import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.RANG
import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.USER_AGENT;
import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_HTTP_VERSION;
import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_MAX_CONCURRENT_REQUESTS;
-import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.DEFAULT_HTTP_VERSION;
import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.DEFAULT_MAX_CONCURRENT_REQUESTS;
/**
@@ -468,6 +468,53 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
}
}
+ HttpClient.Version getHttpVersion(RepositorySystemSession session, RemoteRepository repository) {
+ HttpVersion httpVersion = HttpTransporterUtils.getHttpVersion(session, repository);
+ if (httpVersion == ConfigurationProperties.DEFAULT_HTTP_VERSION) {
+ // Fall back to legacy JDK Transporter specific property when it is explicitly configured.
+ String configuredLegacyHttpVersion = ConfigUtils.getString(
+ session, null, CONFIG_PROP_HTTP_VERSION + "." + repository.getId(), CONFIG_PROP_HTTP_VERSION);
+ if (configuredLegacyHttpVersion != null) {
+ return resolveHttpVersion(configuredLegacyHttpVersion);
+ }
+ return getMaximumSupportedHttpVersion();
+ } else {
+ switch (httpVersion) {
+ case MAXIMUM:
+ return getMaximumSupportedHttpVersion();
+ case HTTP_1_1:
+ return HttpClient.Version.HTTP_1_1;
+ case HTTP_2:
+ case DEFAULT:
+ return HttpClient.Version.HTTP_2;
+ case HTTP_3:
+ return resolveHttpVersion("HTTP_3");
+ default:
+ throw new IllegalArgumentException("Unsupported HTTP version: " + httpVersion);
+ }
+ }
+ }
+
+ private HttpClient.Version resolveHttpVersion(String requestedVersion) {
+ HttpClient.Version maximumHttpVersion = getMaximumSupportedHttpVersion();
+ HttpClient.Version resolved;
+ try {
+ resolved = HttpClient.Version.valueOf(requestedVersion);
+ } catch (IllegalArgumentException e) {
+ LOGGER.warn(
+ "HTTP version '{}' is not supported by the running JRE, using '{}' instead",
+ requestedVersion,
+ maximumHttpVersion);
+ return maximumHttpVersion;
+ }
+ return resolved;
+ }
+
+ HttpClient.Version getMaximumSupportedHttpVersion() {
+ HttpClient.Version[] values = HttpClient.Version.values();
+ return values[values.length - 1];
+ }
+
private HttpClient createClient(RepositorySystemSession session, RemoteRepository repository, boolean insecure)
throws RuntimeException {
@@ -487,7 +534,10 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
if (sslContext == null) {
try {
if (insecure) {
- sslContext = SSLContext.getInstance("TLS");
+ sslContext = SSLContext.getDefault();
+ // custom trust manager not supported for HTTP/3 (Quic)
+ // (https://github.com/openjdk/jdk/blob/631b675d7949a0e6312d8d6f45e2515d53b12f05/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java#L529)
+ // https://openjdk.org/jeps/517
X509ExtendedTrustManager tm = new X509ExtendedTrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@@ -526,11 +576,7 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
}
Methanol.Builder builder = Methanol.newBuilder()
- .version(HttpClient.Version.valueOf(ConfigUtils.getString(
- session,
- DEFAULT_HTTP_VERSION,
- CONFIG_PROP_HTTP_VERSION + "." + repository.getId(),
- CONFIG_PROP_HTTP_VERSION)))
+ .version(getHttpVersion(session, repository))
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofMillis(connectTimeout))
// this only considers the time until the response header is received, see
diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
index c570fa5b0..fb6e5c62a 100644
--- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
+++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
@@ -20,8 +20,10 @@ package org.eclipse.aether.transport.jdk;
import java.net.ConnectException;
import java.net.URI;
+import java.net.http.HttpClient;
import java.util.stream.Stream;
+import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.internal.impl.DefaultPathProcessor;
import org.eclipse.aether.internal.test.util.TestUtils;
import org.eclipse.aether.internal.test.util.http.HttpTransporterTest;
@@ -126,4 +128,20 @@ class JdkTransporterTest extends HttpTransporterTest {
"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
JdkTransporter.getBasicAuthValue("Aladdin", "open sesame".toCharArray()));
}
+
+ @Test
+ void testMaximumHttpVersionAtRuntime() throws Exception {
+ RepositorySystemSession session = TestUtils.newSession();
+ RemoteRepository remoteRepository =
+ new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build();
+ JdkTransporterFactory factory = new JdkTransporterFactory(s -> null, new DefaultPathProcessor());
+
+ try (Transporter transporter = factory.newInstance(session, remoteRepository)) {
+ JdkTransporter jdkTransporter = (JdkTransporter) transporter;
+ HttpClient.Version[] versions = HttpClient.Version.values();
+ HttpClient.Version maximumSupported = versions[versions.length - 1];
+ assertEquals(maximumSupported, jdkTransporter.getMaximumSupportedHttpVersion());
+ assertEquals(maximumSupported, jdkTransporter.getHttpVersion(session, remoteRepository));
+ }
+ }
}
diff --git a/maven-resolver-transport-jetty/pom.xml b/maven-resolver-transport-jetty/pom.xml
index 87f845454..1e78e32ed 100644
--- a/maven-resolver-transport-jetty/pom.xml
+++ b/maven-resolver-transport-jetty/pom.xml
@@ -72,6 +72,14 @@
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-client-transport</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http3</groupId>
+ <artifactId>jetty-http3-client-transport</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.quic</groupId>
+ <artifactId>jetty-quic-quiche-client</artifactId>
+ </dependency>
<!-- supported compressions -->
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
index 2b9e8248d..de033774a 100644
--- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
+++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
@@ -19,7 +19,6 @@
package org.eclipse.aether.transport.jetty;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
@@ -28,7 +27,8 @@ import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
-import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
@@ -39,6 +39,7 @@ import java.util.function.Function;
import java.util.regex.Matcher;
import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.ConfigurationProperties.HttpVersion;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.RemoteRepository;
@@ -67,7 +68,13 @@ import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2;
+import org.eclipse.jetty.http3.client.HTTP3Client;
+import org.eclipse.jetty.http3.client.HTTP3ClientQuicConfiguration;
+import org.eclipse.jetty.http3.client.transport.ClientConnectionFactoryOverHTTP3;
+import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.quic.quiche.client.QuicheClientQuicConfiguration;
+import org.eclipse.jetty.quic.quiche.client.QuicheTransport;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.ACCEPT_ENCODING;
@@ -388,40 +395,19 @@ final class JettyTransporter extends AbstractTransporter implements HttpTranspor
}
}
- if (sslContext == null) {
- try {
- if (insecure) {
- sslContext = SSLContext.getInstance("TLS");
- X509TrustManager tm = new X509TrustManager() {
- @Override
- public void checkClientTrusted(X509Certificate[] chain, String authType) {}
-
- @Override
- public void checkServerTrusted(X509Certificate[] chain, String authType) {}
-
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[0];
- }
- };
- sslContext.init(null, new X509TrustManager[] {tm}, null);
- } else {
- sslContext = SSLContext.getDefault();
- }
- } catch (Exception e) {
- if (e instanceof RuntimeException) {
- throw (RuntimeException) e;
- } else {
- throw new IllegalStateException("SSL Context setup failure", e);
- }
- }
- }
-
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
- sslContextFactory.setSslContext(sslContext);
if (insecure) {
+ // this is also passed on to Quiche for HTTP/3
sslContextFactory.setEndpointIdentificationAlgorithm(null);
sslContextFactory.setHostnameVerifier((name, context) -> true);
+ sslContextFactory.setTrustAll(true);
+ }
+ if (sslContext != null) {
+ // if completely custom SSLContext is provided, try to use it
+ // not properly supported by Quiche for HTTP/3, but Jetty will use it for HTTP/2 and HTTP/1.1
+ sslContextFactory.setSslContext(sslContext);
+ // as then other sslContextFactory settings are not applied, we need to set the hostname verifier and endpoint identification algorithm manually
+
}
ClientConnector clientConnector = new ClientConnector();
@@ -430,16 +416,35 @@ final class JettyTransporter extends AbstractTransporter implements HttpTranspor
HTTP2Client http2Client = new HTTP2Client(clientConnector);
ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client);
- HttpClientTransportDynamic transport;
+ QuicheClientQuicConfiguration clientQuicConfig =
+ HTTP3ClientQuicConfiguration.configure(new QuicheClientQuicConfiguration());
+ HTTP3Client http3Client = new HTTP3Client(clientQuicConfig, clientConnector);
+ QuicheTransport transport = new QuicheTransport(clientQuicConfig);
+ ClientConnectionFactoryOverHTTP3.HTTP3 http3 =
+ new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client, transport);
+
+ Collection<ClientConnectionFactory.Info> connectors = new ArrayList<>();
if ("https".equalsIgnoreCase(repository.getProtocol())) {
- transport = new HttpClientTransportDynamic(
- clientConnector, http2, HttpClientConnectionFactory.HTTP11); // HTTPS, prefer H2
- } else {
- transport = new HttpClientTransportDynamic(
- clientConnector, HttpClientConnectionFactory.HTTP11, http2); // plaintext HTTP, H2 cannot be used
+ HttpVersion httpVersion = HttpTransporterUtils.getHttpVersion(session, repository);
+ switch (httpVersion) {
+ case MAXIMUM:
+ case HTTP_3:
+ connectors.add(http3);
+ break;
+ // always support HTTP/2 as fallback for HTTP/3
+ case HTTP_2:
+ case DEFAULT:
+ connectors.add(http2);
+ break;
+ default:
+ break;
+ }
}
+ connectors.add(HttpClientConnectionFactory.HTTP11); // HTTP/1.1, always supported but has least priority
- HttpClient httpClient = new HttpClient(transport);
+ HttpClientTransportDynamic dynamicTransport = new HttpClientTransportDynamic(
+ clientConnector, connectors.toArray(new ClientConnectionFactory.Info[0]));
+ HttpClient httpClient = new HttpClient(dynamicTransport);
httpClient.setConnectTimeout(connectTimeout);
httpClient.setIdleTimeout(requestTimeout);
httpClient.setFollowRedirects(ConfigUtils.getBoolean(
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
index 523ffba14..ab1e9ca37 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
@@ -368,4 +368,49 @@ public final class ConfigUtils {
public static List<String> parseCommaSeparatedUniqueNames(String commaSeparatedNames) {
return parseCommaSeparatedNames(commaSeparatedNames).stream().distinct().collect(toList());
}
+
+ /**
+ * Gets the specified configuration property.
+ * @param <T> the enum type
+ *
+ * @param properties the configuration properties to read, must not be {@code null}
+ * @param enumClass the enum class to read, must not be {@code null}
+ * @param defaultValue the default value to return in case none of the property keys is set to a boolean
+ * @param keys the property keys to read, must not be {@code null}. The specified keys are read one after one until
+ * a enum type {@code T} or a string (parsed using {@link Enum#valueOf(Class, String)} is found.
+ * @return the property value or {@code defaultValue} if none found
+ */
+ public static <T extends Enum<T>> T getEnum(
+ Map<?, ?> properties, Class<T> enumClass, T defaultValue, String... keys) {
+ for (String key : keys) {
+ Object value = properties.get(key);
+
+ if (value instanceof Enum) {
+ return (T) value;
+ } else if (value instanceof String) {
+ return Enum.valueOf(enumClass, (String) value);
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the specified configuration property.
+ *
+ * @param session the repository system session from which to read the configuration property, must not be
+ * {@code null}
+ * @param <T> the enum type
+ *
+ * @param session the repository system session from which to read the configuration property, must not be
+ * {@code null}
+ * @param enumClass the enum class to read, must not be {@code null}
+ * @param defaultValue the default value to return in case none of the property keys is set to a boolean
+ * @param keys the property keys to read, must not be {@code null}. The specified keys are read one after one until
+ * a enum type {@code T} or a string (parsed using {@link Enum#valueOf(Class, String)} is found.
+ * @return the property value or {@code defaultValue} if none found
+ */
+ public static <T extends Enum<T>> T getEnum(
+ RepositorySystemSession session, Class<T> enumClass, T defaultValue, String... keys) {
+ return getEnum(session.getConfigProperties(), enumClass, defaultValue, keys);
+ }
}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
index a8fe4efed..787d1ff3e 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
@@ -315,6 +315,16 @@ public final class HttpTransporterUtils {
return Optional.empty();
}
+ public static ConfigurationProperties.HttpVersion getHttpVersion(
+ RepositorySystemSession session, RemoteRepository repository) {
+ return ConfigUtils.getEnum(
+ session,
+ ConfigurationProperties.HttpVersion.class,
+ ConfigurationProperties.DEFAULT_HTTP_VERSION,
+ ConfigurationProperties.HTTP_VERSION + "." + repository.getId(),
+ ConfigurationProperties.HTTP_VERSION);
+ }
+
/**
* Shared code to create "base {@link URI}" for most common HTTP remote repositories and all HTTP transports.
* Note: this method just applies common validation and adjustments to URI, but it does not enforce protocol
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java
index d3797a314..480fbafc8 100644
--- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java
@@ -199,4 +199,29 @@ public class ConfigUtilsTest {
config.put("some-number", -1234f);
assertEquals(-1234f, ConfigUtils.getFloat(config, 0, "some-number"), 0.1f);
}
+
+ private enum TestEnum {
+ FIRST,
+ SECOND
+ }
+
+ @Test
+ void testGetEnum() {
+ config.put("some-enum", TestEnum.SECOND);
+ assertEquals(TestEnum.SECOND, ConfigUtils.getEnum(config, TestEnum.class, TestEnum.FIRST, "some-enum"));
+ }
+
+ @Test
+ void testGetEnum_StringConversion() {
+ config.put("some-enum", "SECOND");
+ assertEquals(TestEnum.SECOND, ConfigUtils.getEnum(config, TestEnum.class, TestEnum.FIRST, "some-enum"));
+ }
+
+ @Test
+ void testGetEnum_StringInvalidValue() {
+ config.put("some-enum", "second");
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ConfigUtils.getEnum(config, TestEnum.class, TestEnum.FIRST, "some-enum"));
+ }
}