diff --git a/mvn-resolver-transport-http3/pom.xml b/mvn-resolver-transport-http3/pom.xml
index ee19dff..d79f975 100644
--- a/mvn-resolver-transport-http3/pom.xml
+++ b/mvn-resolver-transport-http3/pom.xml
@@ -25,7 +25,21 @@
Artipie Maven Artifact Resolver Transport HTTP3
A transport implementation for repositories using HTTP3.
-
+ https://github.com/artipie/maven-resolver-http3-plugin
+ 2023
+
+
+ MIT
+ https://github.com/artipie/maven-resolver-http3-plugin/blob/master/LICENSE.txt
+
+
+
+ GitHub
+ https://github.com/artipie/maven-resolver-http3-plugin/issues
+
+
+ https://github.com/artipie/maven-resolver-http3-plugin
+
com.artipie.maven.resolver.transport.http3
${Automatic-Module-Name}
@@ -132,6 +146,11 @@
jetty-io
${jettyVersion}
+
+ org.eclipse.jetty
+ jetty-server
+ ${jettyVersion}
+
javax.servlet
javax.servlet-api
diff --git a/mvn-resolver-transport-http3/src/main/java/com/artipie/aether/transport/http3/HttpTransporter.java b/mvn-resolver-transport-http3/src/main/java/com/artipie/aether/transport/http3/HttpTransporter.java
index b78f119..1722f1f 100644
--- a/mvn-resolver-transport-http3/src/main/java/com/artipie/aether/transport/http3/HttpTransporter.java
+++ b/mvn-resolver-transport-http3/src/main/java/com/artipie/aether/transport/http3/HttpTransporter.java
@@ -38,7 +38,10 @@
import static java.util.Objects.requireNonNull;
import java.util.Optional;
import java.util.ServiceLoader;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.apache.commons.lang3.exception.UncheckedException;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.aether.ConfigurationProperties;
@@ -69,12 +72,19 @@
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http3.client.HTTP3Client;
import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
/**
* A transporter for HTTP/HTTPS.
*/
final class HttpTransporter extends AbstractTransporter {
+ private final static Set CENTRAL = Set.of(
+ "repo.maven.apache.org",
+ "oss.sonatype.org",
+ "packages.atlassian.com"
+ );
+
private final Map checksumExtractors;
private final AuthenticationContext repoAuthContext;
@@ -83,8 +93,11 @@ final class HttpTransporter extends AbstractTransporter {
private final URI baseUri;
- private final HttpClient client;
+ private HttpClient http3Client;
+ private HttpClient httpClient = null;
+
private final int connectTimeout;
+ private final String httpsSecurityMode;
private String[] authInfo = null;
@@ -123,7 +136,7 @@ final class HttpTransporter extends AbstractTransporter {
};
}
- String httpsSecurityMode = ConfigUtils.getString(
+ httpsSecurityMode = ConfigUtils.getString(
session,
ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT,
ConfigurationProperties.HTTPS_SECURITY_MODE + "." + repository.getId(),
@@ -135,16 +148,43 @@ final class HttpTransporter extends AbstractTransporter {
ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(),
ConfigurationProperties.CONNECT_TIMEOUT
);
+ this.chooseClient();
+ }
- HTTP3Client h3Client = new HTTP3Client();
- HttpClientTransportOverHTTP3 transport = new HttpClientTransportOverHTTP3(h3Client);
- this.client = new HttpClient(transport);
- this.client.setFollowRedirects(true);
- this.client.setConnectTimeout(connectTimeout);
- this.client.start();
- h3Client.getClientConnector().getSslContextFactory().setTrustAll(
- httpsSecurityMode.equals(ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE)
- );
+ private HttpClient initOrGetHttpClient() {
+ if (this.httpClient == null) {
+ this.httpClient = new HttpClient();
+ this.httpClient.setFollowRedirects(true);
+ this.httpClient.setConnectTimeout(connectTimeout);
+ SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+ sslContextFactory.setTrustAll(httpsSecurityMode.equals(ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE));
+ httpClient.setSslContextFactory(sslContextFactory);
+ try {
+ this.httpClient.start();
+ } catch (Exception e) {
+ throw new UncheckedException(e);
+ }
+ }
+ return this.httpClient;
+ }
+
+ private HttpClient initOrGetHttp3Client() {
+ if (this.http3Client == null) {
+ HTTP3Client h3Client = new HTTP3Client();
+ HttpClientTransportOverHTTP3 transport = new HttpClientTransportOverHTTP3(h3Client);
+ this.http3Client = new HttpClient(transport);
+ this.http3Client.setFollowRedirects(true);
+ this.http3Client.setConnectTimeout(connectTimeout);
+ try {
+ this.http3Client.start();
+ h3Client.getClientConnector().getSslContextFactory().setTrustAll(
+ httpsSecurityMode.equals(ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE)
+ );
+ } catch (Exception e) {
+ throw new UncheckedException(e);
+ }
+ }
+ return this.http3Client;
}
@Override
@@ -157,12 +197,13 @@ public int classify(Throwable error) {
@Override
protected void implPeek(PeekTask task) throws Exception {
- this.makeRequest(HttpMethod.HEAD, task, null);
+ this.makeRequest(HttpMethod.HEAD, task, null, this.chooseClient());
}
@Override
protected void implGet(GetTask task) throws Exception {
- final Pair response = this.makeRequest(HttpMethod.GET, task, null);
+ final Pair response =
+ this.makeRequest(HttpMethod.GET, task, null, this.chooseClient());
final boolean resume = false;
final File dataFile = task.getDataFile();
long length = Long.parseLong(
@@ -202,16 +243,21 @@ protected void implGet(GetTask task) throws Exception {
protected void implPut(PutTask task) throws Exception {
try (final InputStream stream = task.newInputStream()) {
this.makeRequest(HttpMethod.PUT, task,
- new InputStreamRequestContent(stream)
- );
+ new InputStreamRequestContent(stream), this.chooseClient());
}
}
@Override
protected void implClose() {
try {
- client.stop();
- client.destroy();
+ if (this.http3Client != null) {
+ http3Client.stop();
+ http3Client.destroy();
+ }
+ if (this.httpClient != null) {
+ this.httpClient.stop();
+ this.httpClient.destroy();
+ }
} catch (Exception e) {
throw new UncheckedIOException(new IOException(e));
}
@@ -220,17 +266,20 @@ protected void implClose() {
}
private Pair makeRequest(
- HttpMethod method, TransportTask task, Request.Content bodyContent
+ HttpMethod method, TransportTask task, Request.Content bodyContent, HttpClient client
) {
final String url = this.baseUri.resolve(task.getLocation()).toString();
if (this.authInfo != null) {
- this.client.getAuthenticationStore().addAuthenticationResult(
+ client.getAuthenticationStore().addAuthenticationResult(
new BasicAuthentication.BasicResult(this.baseUri, this.authInfo[0], this.authInfo[1])
);
}
+ Request request = null;
+ final HttpVersion version = this.httpVersion(client);
try {
InputStreamResponseListener listener = new InputStreamResponseListener();
- this.client.newRequest(url).method(method).headers(
+ request = client.newRequest(url);
+ request.method(method).headers(
httpFields -> {
if (bodyContent != null) {
httpFields.add(HttpHeader.CONTENT_TYPE, bodyContent.getContentType());
@@ -245,22 +294,26 @@ private Pair makeRequest(
final Response response = listener.get(this.connectTimeout, TimeUnit.MILLISECONDS);
if (response.getStatus() >= 300) {
System.err.printf(
- "Request over HTTP3 error status %s, method=%s, url=%s%n",
- response.getStatus(), method, url
+ "Request over %s error status %s, method=%s, url=%s%n",
+ version, response.getStatus(), method, url
);
throw new HttpResponseException(Integer.toString(response.getStatus()), response);
}
System.err.printf(
- "Request over HTTP3 done, method=%s, resp status=%s, url=%s%n",
- method, response.getStatus(), url
+ "Request over %s done, method=%s, resp status=%s, url=%s%n",
+ version, method, response.getStatus(), url
);
return new ImmutablePair<>(listener.getInputStream(), response.getHeaders());
} catch (Exception ex) {
System.err.printf(
- "Request over HTTP3 error=%s: %s, method=%s, url=%s%n",
+ "Request over %s error=%s: %s, method=%s, url=%s%n", version,
ex.getClass(), ex.getMessage(), method, url
);
- throw new HttpRequestException(ex.getMessage(), this.client.newRequest(url));
+ if (version == HttpVersion.HTTP_3 && ex instanceof TimeoutException) {
+ System.err.printf("Repeat request over HTTP/1.1 method=%s, url=%s%n", method, url);
+ return this.makeRequest(method, task, bodyContent, this.initOrGetHttpClient());
+ }
+ throw new HttpRequestException(ex.getMessage(), request);
}
}
@@ -274,6 +327,24 @@ private void extractChecksums(HttpFields response, GetTask task) {
}
}
+ /**
+ * Choose http client to initialize and perform request with: if host is present in known
+ * central's hosts {@link HttpTransporter#CENTRAL}, http 1.1 client is used, otherwise we use http3 client.
+ */
+ private HttpClient chooseClient() {
+ final HttpClient res;
+ if (CENTRAL.contains(this.baseUri.getHost())) {
+ res = Optional.ofNullable(this.httpClient).orElseGet(this::initOrGetHttpClient);
+ } else {
+ res = Optional.ofNullable(this.http3Client).orElseGet(this::initOrGetHttp3Client);
+ }
+ return res;
+ }
+
+ private HttpVersion httpVersion(final HttpClient client) {
+ return client.getTransport() instanceof HttpClientTransportOverHTTP3 ? HttpVersion.HTTP_3 : HttpVersion.HTTP_1_1;
+ }
+
/**
* TOOD: For unknown reason when running inside Maven, HttpFieldPreEncoder for HTTP3 is missing.
* It is not available in Jetty static initializer when that library is loaded by Maven.
diff --git a/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/ArtipieHTTP3IT.java b/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/ArtipieHTTP3IT.java
index 5911352..d6e0797 100644
--- a/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/ArtipieHTTP3IT.java
+++ b/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/ArtipieHTTP3IT.java
@@ -82,8 +82,8 @@ void resolvesDependencies() throws IOException, InterruptedException {
res,
Matchers.stringContainsInOrder(
"BUILD SUCCESS",
- "Request over HTTP3 done, method=GET, resp status=200, url=https://artipie:8091/my-maven-proxy/args4j/args4j/2.33/args4j-2.33.jar",
- "Request over HTTP3 done, method=GET, resp status=200, url=https://artipie:8091/my-maven-proxy/org/springframework/spring-web/6.1.0/spring-web-6.1.0.jar"
+ "Request over HTTP/3.0 done, method=GET, resp status=200, url=https://artipie:8091/my-maven-proxy/args4j/args4j/2.33/args4j-2.33.jar",
+ "Request over HTTP/3.0 done, method=GET, resp status=200, url=https://artipie:8091/my-maven-proxy/org/springframework/spring-web/6.1.0/spring-web-6.1.0.jar"
)
);
}
diff --git a/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/MavenResolverIT.java b/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/MavenResolverIT.java
index 2a34efd..13afc54 100644
--- a/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/MavenResolverIT.java
+++ b/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/MavenResolverIT.java
@@ -43,8 +43,8 @@
* Testing transport via containerized Caddy http3 server in proxy mode.
*/
public class MavenResolverIT {
- private static final String REMOTE_PATH = "commons-cli/commons-cli/1.4/commons-cli-1.4.jar";
- private static final String LOCAL_PATH = "commons-cli-1.4.jar";
+ static final String REMOTE_PATH = "commons-cli/commons-cli/1.4/commons-cli-1.4.jar";
+ static final String LOCAL_PATH = "commons-cli-1.4.jar";
private static GenericContainer> caddyProxy;
private static File tempFile;
@@ -53,7 +53,7 @@ public class MavenResolverIT {
public void testTransporterAuth() throws Exception {
final byte[] data = testTransporter("https://demo:demo@localhost:7444/maven2");
assertNotEquals(null, data);
- final byte[] local = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
+ final byte[] local = getCommonsJar();
assertArrayEquals(local, data);
}
@@ -79,7 +79,7 @@ public void testTransporterAnonAuthFail() {
public void testTransporterAnon() throws Exception {
final byte[] data = testTransporter("https://localhost:7443/maven2");
assertNotEquals(null, data);
- final byte[] local = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
+ final byte[] local = getCommonsJar();
assertArrayEquals(local, data);
}
@@ -87,7 +87,7 @@ public void testTransporterAnon() throws Exception {
public void testAnonTransporterSuccess() throws Exception {
final byte[] data = testTransporter("https://demo:demo@localhost:7443/maven2");
assertNotNull(data);
- final byte[] local = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
+ final byte[] local = getCommonsJar();
assertArrayEquals(local, data);
}
@@ -132,7 +132,7 @@ public void testJettyLocalhostPut() throws Exception {
final HttpClient client = new HttpClient(transport);
client.start();
h3Client.getClientConnector().getSslContextFactory().setTrustAll(true);
- final byte[] srcData = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
+ final byte[] srcData = getCommonsJar();
final ContentResponse response = client.newRequest("https://localhost:7445/test1").
method(HttpMethod.PUT).body(
new InputStreamRequestContent(new ByteArrayInputStream(srcData))
@@ -150,7 +150,7 @@ public void testTransporterPutAnon() throws Exception {
resetPutServer();
final HttpTransporterFactory factory = new HttpTransporterFactory();
final PutTask task = new PutTask(URI.create("test1")).setListener(new TransportListener() {});
- final byte[] srcData = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
+ final byte[] srcData = getCommonsJar();
task.setDataBytes(srcData);
try (final Transporter transporter = factory.newInstance(newSession(), newRepo(repo))) {
transporter.put(task);
@@ -166,7 +166,7 @@ public void testTransporterPutAuth() throws Exception {
resetPutServer();
final HttpTransporterFactory factory = new HttpTransporterFactory();
final PutTask task = new PutTask(URI.create("test1")).setListener(new TransportListener() {});
- final byte[] srcData = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
+ final byte[] srcData = getCommonsJar();
task.setDataBytes(srcData);
try (final Transporter transporter = factory.newInstance(newSession(), newRepo(repo))) {
transporter.put(task);
@@ -209,14 +209,14 @@ public static void finish() {
tempFile.delete();
}
- private static DefaultRepositorySystemSession newSession() {
+ static DefaultRepositorySystemSession newSession() {
DefaultRepositorySystemSession session = new DefaultRepositorySystemSession();
session.setLocalRepositoryManager(new TestLocalRepositoryManager());
session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
return session;
}
- private RemoteRepository newRepo(final String url) {
+ static RemoteRepository newRepo(final String url) {
return new RemoteRepository.Builder("test", "default", url).build();
}
@@ -226,4 +226,8 @@ private static void resetPutServer() throws InterruptedException, IOException {
assertEquals(0, result.getExitCode());
assertTrue(deleted);
}
+
+ byte[] getCommonsJar() throws IOException {
+ return getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
+ }
}
diff --git a/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/TransporterTest.java b/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/TransporterTest.java
new file mode 100644
index 0000000..d3e0c46
--- /dev/null
+++ b/mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/TransporterTest.java
@@ -0,0 +1,126 @@
+package com.artipie.aether.transport.http3;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.TransportListener;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.Callback;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.core.IsEqual;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class TransporterTest {
+
+ private int port;
+
+ private Server server;
+
+ private CountDownLatch latch;
+
+ @BeforeEach
+ void init() throws Exception {
+ this.server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+ latch = new CountDownLatch(1);
+ this.server.setHandler(new Handler.Abstract() {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+ response.setStatus(200);
+ if ("GET".equals(request.getMethod())) {
+ response.write(true, ByteBuffer.wrap(getCommonsJar()), Callback.NOOP);
+ } else {
+ response.write(true, ByteBuffer.allocate(0), Callback.NOOP);
+ }
+ Content.Chunk chunk = request.read();
+ if (chunk.hasRemaining()) {
+ MatcherAssert.assertThat(
+ chunk.getByteBuffer().array(), new IsEqual<>(getCommonsJar())
+ );
+ }
+ latch.countDown();
+ return false;
+ }
+ });
+ this.server.start();
+ this.port = connector.getLocalPort();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "https://repo.maven.apache.org/maven2/",
+ "https://oss.sonatype.org/content/repositories/releases/"
+ })
+ void performsRequestToCentral(final String url) throws Exception {
+ final byte[] data = this.getResource(url);
+ MatcherAssert.assertThat(data, new IsEqual<>(this.getCommonsJar()));
+ }
+
+ @Test
+ void getsResourceFromLocalhostViaHttp1() throws Exception {
+ final byte[] data = this.getResource(String.format("http://localhost:%d", this.port));
+ MatcherAssert.assertThat(data, new IsEqual<>(this.getCommonsJar()));
+ }
+
+ @Test
+ void performsHeadRequest() throws Exception {
+ final PeekTask task = new PeekTask(URI.create(MavenResolverIT.REMOTE_PATH));
+ try (final Transporter transporter = new HttpTransporterFactory().newInstance(
+ MavenResolverIT.newSession(),
+ MavenResolverIT.newRepo(String.format("http://localhost:%d", this.port))
+ )) {
+ transporter.peek(task);
+ }
+ MatcherAssert.assertThat("Head performed", latch.await(1, TimeUnit.MINUTES));
+ }
+
+ @Test
+ void performsPutRequest() throws Exception {
+ final PutTask task = new PutTask(URI.create(MavenResolverIT.REMOTE_PATH))
+ .setListener(new TransportListener() {});
+ try (final Transporter transporter = new HttpTransporterFactory().newInstance(
+ MavenResolverIT.newSession(),
+ MavenResolverIT.newRepo(String.format("http://localhost:%d", this.port))
+ )) {
+ transporter.put(task);
+ }
+ MatcherAssert.assertThat("Put performed", latch.await(1, TimeUnit.MINUTES));
+ }
+
+ @AfterEach
+ void close() throws Exception {
+ this.server.stop();
+ this.server.destroy();
+ }
+
+ private byte[] getResource(final String repo) throws Exception {
+ final GetTask task = new GetTask(URI.create(MavenResolverIT.REMOTE_PATH))
+ .setListener(new TransportListener() {});
+ try (final Transporter transporter = new HttpTransporterFactory()
+ .newInstance(MavenResolverIT.newSession(), MavenResolverIT.newRepo(repo))) {
+ transporter.get(task);
+ }
+ return task.getDataBytes();
+ }
+
+ byte[] getCommonsJar() throws IOException {
+ return getClass().getClassLoader().getResourceAsStream(MavenResolverIT.LOCAL_PATH).readAllBytes();
+ }
+
+}