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 <[email protected]>
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"));
+    }
 }


Reply via email to