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")); + } }
