Skip to content

Commit

Permalink
feat: use HTTP/1.1 on HTTP/3 fail
Browse files Browse the repository at this point in the history
  • Loading branch information
olenagerasimova committed Dec 1, 2023
1 parent 57d6d4f commit 4ad378c
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 37 deletions.
21 changes: 20 additions & 1 deletion mvn-resolver-transport-http3/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,21 @@

<name>Artipie Maven Artifact Resolver Transport HTTP3</name>
<description>A transport implementation for repositories using HTTP3.</description>

<url>https://github.com/artipie/maven-resolver-http3-plugin</url>
<inceptionYear>2023</inceptionYear>
<licenses>
<license>
<name>MIT</name>
<url>https://github.com/artipie/maven-resolver-http3-plugin/blob/master/LICENSE.txt</url>
</license>
</licenses>
<issueManagement>
<system>GitHub</system>
<url>https://github.com/artipie/maven-resolver-http3-plugin/issues</url>
</issueManagement>
<scm>
<url>https://github.com/artipie/maven-resolver-http3-plugin</url>
</scm>
<properties>
<Automatic-Module-Name>com.artipie.maven.resolver.transport.http3</Automatic-Module-Name>
<Bundle-SymbolicName>${Automatic-Module-Name}</Bundle-SymbolicName>
Expand Down Expand Up @@ -132,6 +146,11 @@
<artifactId>jetty-io</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
import static java.util.Objects.requireNonNull;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.TimeUnit;
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;
Expand Down Expand Up @@ -69,12 +71,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<String> CENTRAL = Set.of(
"repo.maven.apache.org",
"oss.sonatype.org",
"packages.atlassian.com"
);

private final Map<String, ChecksumExtractor> checksumExtractors;

private final AuthenticationContext repoAuthContext;
Expand All @@ -83,8 +92,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;

Expand Down Expand Up @@ -123,7 +135,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(),
Expand All @@ -135,16 +147,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
Expand All @@ -157,12 +196,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<InputStream, HttpFields> response = this.makeRequest(HttpMethod.GET, task, null);
final Pair<InputStream, HttpFields> response =
this.makeRequest(HttpMethod.GET, task, null, this.chooseClient());
final boolean resume = false;
final File dataFile = task.getDataFile();
long length = Long.parseLong(
Expand Down Expand Up @@ -202,16 +242,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));
}
Expand All @@ -220,17 +265,20 @@ protected void implClose() {
}

private Pair<InputStream, HttpFields> 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());
Expand All @@ -245,22 +293,26 @@ private Pair<InputStream, HttpFields> 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) {
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);
}
}

Expand All @@ -274,6 +326,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Expand All @@ -79,15 +79,15 @@ 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);
}

@Test
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);
}

Expand Down Expand Up @@ -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))
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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();
}

Expand All @@ -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();
}
}
Loading

0 comments on commit 4ad378c

Please sign in to comment.