Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use HTTP/1.1 on HTTP/3 fail #15

Merged
merged 3 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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;
Expand Down Expand Up @@ -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<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 +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;

Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand All @@ -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<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 +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));
}
Expand All @@ -220,17 +266,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 +294,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 && 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);
}
}

Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
);
}
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
Loading