Skip to content

Commit 603ca01

Browse files
authored
Merge pull request #15 from olenagerasimova/14-add-http1
feat: use HTTP/1.1 on HTTP/3 fail
2 parents 57d6d4f + 95c92c0 commit 603ca01

File tree

5 files changed

+259
-39
lines changed

5 files changed

+259
-39
lines changed

mvn-resolver-transport-http3/pom.xml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,21 @@
2525

2626
<name>Artipie Maven Artifact Resolver Transport HTTP3</name>
2727
<description>A transport implementation for repositories using HTTP3.</description>
28-
28+
<url>https://github.com/artipie/maven-resolver-http3-plugin</url>
29+
<inceptionYear>2023</inceptionYear>
30+
<licenses>
31+
<license>
32+
<name>MIT</name>
33+
<url>https://github.com/artipie/maven-resolver-http3-plugin/blob/master/LICENSE.txt</url>
34+
</license>
35+
</licenses>
36+
<issueManagement>
37+
<system>GitHub</system>
38+
<url>https://github.com/artipie/maven-resolver-http3-plugin/issues</url>
39+
</issueManagement>
40+
<scm>
41+
<url>https://github.com/artipie/maven-resolver-http3-plugin</url>
42+
</scm>
2943
<properties>
3044
<Automatic-Module-Name>com.artipie.maven.resolver.transport.http3</Automatic-Module-Name>
3145
<Bundle-SymbolicName>${Automatic-Module-Name}</Bundle-SymbolicName>
@@ -132,6 +146,11 @@
132146
<artifactId>jetty-io</artifactId>
133147
<version>${jettyVersion}</version>
134148
</dependency>
149+
<dependency>
150+
<groupId>org.eclipse.jetty</groupId>
151+
<artifactId>jetty-server</artifactId>
152+
<version>${jettyVersion}</version>
153+
</dependency>
135154
<dependency>
136155
<groupId>javax.servlet</groupId>
137156
<artifactId>javax.servlet-api</artifactId>

mvn-resolver-transport-http3/src/main/java/com/artipie/aether/transport/http3/HttpTransporter.java

Lines changed: 97 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
import static java.util.Objects.requireNonNull;
3939
import java.util.Optional;
4040
import java.util.ServiceLoader;
41+
import java.util.Set;
4142
import java.util.concurrent.TimeUnit;
43+
import java.util.concurrent.TimeoutException;
44+
import org.apache.commons.lang3.exception.UncheckedException;
4245
import org.apache.commons.lang3.tuple.ImmutablePair;
4346
import org.apache.commons.lang3.tuple.Pair;
4447
import org.eclipse.aether.ConfigurationProperties;
@@ -69,12 +72,19 @@
6972
import org.eclipse.jetty.http.PreEncodedHttpField;
7073
import org.eclipse.jetty.http3.client.HTTP3Client;
7174
import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3;
75+
import org.eclipse.jetty.util.ssl.SslContextFactory;
7276

7377
/**
7478
* A transporter for HTTP/HTTPS.
7579
*/
7680
final class HttpTransporter extends AbstractTransporter {
7781

82+
private final static Set<String> CENTRAL = Set.of(
83+
"repo.maven.apache.org",
84+
"oss.sonatype.org",
85+
"packages.atlassian.com"
86+
);
87+
7888
private final Map<String, ChecksumExtractor> checksumExtractors;
7989

8090
private final AuthenticationContext repoAuthContext;
@@ -83,8 +93,11 @@ final class HttpTransporter extends AbstractTransporter {
8393

8494
private final URI baseUri;
8595

86-
private final HttpClient client;
96+
private HttpClient http3Client;
97+
private HttpClient httpClient = null;
98+
8799
private final int connectTimeout;
100+
private final String httpsSecurityMode;
88101

89102
private String[] authInfo = null;
90103

@@ -123,7 +136,7 @@ final class HttpTransporter extends AbstractTransporter {
123136
};
124137
}
125138

126-
String httpsSecurityMode = ConfigUtils.getString(
139+
httpsSecurityMode = ConfigUtils.getString(
127140
session,
128141
ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT,
129142
ConfigurationProperties.HTTPS_SECURITY_MODE + "." + repository.getId(),
@@ -135,16 +148,43 @@ final class HttpTransporter extends AbstractTransporter {
135148
ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(),
136149
ConfigurationProperties.CONNECT_TIMEOUT
137150
);
151+
this.chooseClient();
152+
}
138153

139-
HTTP3Client h3Client = new HTTP3Client();
140-
HttpClientTransportOverHTTP3 transport = new HttpClientTransportOverHTTP3(h3Client);
141-
this.client = new HttpClient(transport);
142-
this.client.setFollowRedirects(true);
143-
this.client.setConnectTimeout(connectTimeout);
144-
this.client.start();
145-
h3Client.getClientConnector().getSslContextFactory().setTrustAll(
146-
httpsSecurityMode.equals(ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE)
147-
);
154+
private HttpClient initOrGetHttpClient() {
155+
if (this.httpClient == null) {
156+
this.httpClient = new HttpClient();
157+
this.httpClient.setFollowRedirects(true);
158+
this.httpClient.setConnectTimeout(connectTimeout);
159+
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
160+
sslContextFactory.setTrustAll(httpsSecurityMode.equals(ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE));
161+
httpClient.setSslContextFactory(sslContextFactory);
162+
try {
163+
this.httpClient.start();
164+
} catch (Exception e) {
165+
throw new UncheckedException(e);
166+
}
167+
}
168+
return this.httpClient;
169+
}
170+
171+
private HttpClient initOrGetHttp3Client() {
172+
if (this.http3Client == null) {
173+
HTTP3Client h3Client = new HTTP3Client();
174+
HttpClientTransportOverHTTP3 transport = new HttpClientTransportOverHTTP3(h3Client);
175+
this.http3Client = new HttpClient(transport);
176+
this.http3Client.setFollowRedirects(true);
177+
this.http3Client.setConnectTimeout(connectTimeout);
178+
try {
179+
this.http3Client.start();
180+
h3Client.getClientConnector().getSslContextFactory().setTrustAll(
181+
httpsSecurityMode.equals(ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE)
182+
);
183+
} catch (Exception e) {
184+
throw new UncheckedException(e);
185+
}
186+
}
187+
return this.http3Client;
148188
}
149189

150190
@Override
@@ -157,12 +197,13 @@ public int classify(Throwable error) {
157197

158198
@Override
159199
protected void implPeek(PeekTask task) throws Exception {
160-
this.makeRequest(HttpMethod.HEAD, task, null);
200+
this.makeRequest(HttpMethod.HEAD, task, null, this.chooseClient());
161201
}
162202

163203
@Override
164204
protected void implGet(GetTask task) throws Exception {
165-
final Pair<InputStream, HttpFields> response = this.makeRequest(HttpMethod.GET, task, null);
205+
final Pair<InputStream, HttpFields> response =
206+
this.makeRequest(HttpMethod.GET, task, null, this.chooseClient());
166207
final boolean resume = false;
167208
final File dataFile = task.getDataFile();
168209
long length = Long.parseLong(
@@ -202,16 +243,21 @@ protected void implGet(GetTask task) throws Exception {
202243
protected void implPut(PutTask task) throws Exception {
203244
try (final InputStream stream = task.newInputStream()) {
204245
this.makeRequest(HttpMethod.PUT, task,
205-
new InputStreamRequestContent(stream)
206-
);
246+
new InputStreamRequestContent(stream), this.chooseClient());
207247
}
208248
}
209249

210250
@Override
211251
protected void implClose() {
212252
try {
213-
client.stop();
214-
client.destroy();
253+
if (this.http3Client != null) {
254+
http3Client.stop();
255+
http3Client.destroy();
256+
}
257+
if (this.httpClient != null) {
258+
this.httpClient.stop();
259+
this.httpClient.destroy();
260+
}
215261
} catch (Exception e) {
216262
throw new UncheckedIOException(new IOException(e));
217263
}
@@ -220,17 +266,20 @@ protected void implClose() {
220266
}
221267

222268
private Pair<InputStream, HttpFields> makeRequest(
223-
HttpMethod method, TransportTask task, Request.Content bodyContent
269+
HttpMethod method, TransportTask task, Request.Content bodyContent, HttpClient client
224270
) {
225271
final String url = this.baseUri.resolve(task.getLocation()).toString();
226272
if (this.authInfo != null) {
227-
this.client.getAuthenticationStore().addAuthenticationResult(
273+
client.getAuthenticationStore().addAuthenticationResult(
228274
new BasicAuthentication.BasicResult(this.baseUri, this.authInfo[0], this.authInfo[1])
229275
);
230276
}
277+
Request request = null;
278+
final HttpVersion version = this.httpVersion(client);
231279
try {
232280
InputStreamResponseListener listener = new InputStreamResponseListener();
233-
this.client.newRequest(url).method(method).headers(
281+
request = client.newRequest(url);
282+
request.method(method).headers(
234283
httpFields -> {
235284
if (bodyContent != null) {
236285
httpFields.add(HttpHeader.CONTENT_TYPE, bodyContent.getContentType());
@@ -245,22 +294,26 @@ private Pair<InputStream, HttpFields> makeRequest(
245294
final Response response = listener.get(this.connectTimeout, TimeUnit.MILLISECONDS);
246295
if (response.getStatus() >= 300) {
247296
System.err.printf(
248-
"Request over HTTP3 error status %s, method=%s, url=%s%n",
249-
response.getStatus(), method, url
297+
"Request over %s error status %s, method=%s, url=%s%n",
298+
version, response.getStatus(), method, url
250299
);
251300
throw new HttpResponseException(Integer.toString(response.getStatus()), response);
252301
}
253302
System.err.printf(
254-
"Request over HTTP3 done, method=%s, resp status=%s, url=%s%n",
255-
method, response.getStatus(), url
303+
"Request over %s done, method=%s, resp status=%s, url=%s%n",
304+
version, method, response.getStatus(), url
256305
);
257306
return new ImmutablePair<>(listener.getInputStream(), response.getHeaders());
258307
} catch (Exception ex) {
259308
System.err.printf(
260-
"Request over HTTP3 error=%s: %s, method=%s, url=%s%n",
309+
"Request over %s error=%s: %s, method=%s, url=%s%n", version,
261310
ex.getClass(), ex.getMessage(), method, url
262311
);
263-
throw new HttpRequestException(ex.getMessage(), this.client.newRequest(url));
312+
if (version == HttpVersion.HTTP_3 && ex instanceof TimeoutException) {
313+
System.err.printf("Repeat request over HTTP/1.1 method=%s, url=%s%n", method, url);
314+
return this.makeRequest(method, task, bodyContent, this.initOrGetHttpClient());
315+
}
316+
throw new HttpRequestException(ex.getMessage(), request);
264317
}
265318
}
266319

@@ -274,6 +327,24 @@ private void extractChecksums(HttpFields response, GetTask task) {
274327
}
275328
}
276329

330+
/**
331+
* Choose http client to initialize and perform request with: if host is present in known
332+
* central's hosts {@link HttpTransporter#CENTRAL}, http 1.1 client is used, otherwise we use http3 client.
333+
*/
334+
private HttpClient chooseClient() {
335+
final HttpClient res;
336+
if (CENTRAL.contains(this.baseUri.getHost())) {
337+
res = Optional.ofNullable(this.httpClient).orElseGet(this::initOrGetHttpClient);
338+
} else {
339+
res = Optional.ofNullable(this.http3Client).orElseGet(this::initOrGetHttp3Client);
340+
}
341+
return res;
342+
}
343+
344+
private HttpVersion httpVersion(final HttpClient client) {
345+
return client.getTransport() instanceof HttpClientTransportOverHTTP3 ? HttpVersion.HTTP_3 : HttpVersion.HTTP_1_1;
346+
}
347+
277348
/**
278349
* TOOD: For unknown reason when running inside Maven, HttpFieldPreEncoder for HTTP3 is missing.
279350
* It is not available in Jetty static initializer when that library is loaded by Maven.

mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/ArtipieHTTP3IT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ void resolvesDependencies() throws IOException, InterruptedException {
8282
res,
8383
Matchers.stringContainsInOrder(
8484
"BUILD SUCCESS",
85-
"Request over HTTP3 done, method=GET, resp status=200, url=https://artipie:8091/my-maven-proxy/args4j/args4j/2.33/args4j-2.33.jar",
86-
"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"
85+
"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",
86+
"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"
8787
)
8888
);
8989
}

mvn-resolver-transport-http3/src/test/java/com/artipie/aether/transport/http3/MavenResolverIT.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
* Testing transport via containerized Caddy http3 server in proxy mode.
4444
*/
4545
public class MavenResolverIT {
46-
private static final String REMOTE_PATH = "commons-cli/commons-cli/1.4/commons-cli-1.4.jar";
47-
private static final String LOCAL_PATH = "commons-cli-1.4.jar";
46+
static final String REMOTE_PATH = "commons-cli/commons-cli/1.4/commons-cli-1.4.jar";
47+
static final String LOCAL_PATH = "commons-cli-1.4.jar";
4848

4949
private static GenericContainer<?> caddyProxy;
5050
private static File tempFile;
@@ -53,7 +53,7 @@ public class MavenResolverIT {
5353
public void testTransporterAuth() throws Exception {
5454
final byte[] data = testTransporter("https://demo:demo@localhost:7444/maven2");
5555
assertNotEquals(null, data);
56-
final byte[] local = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
56+
final byte[] local = getCommonsJar();
5757
assertArrayEquals(local, data);
5858
}
5959

@@ -79,15 +79,15 @@ public void testTransporterAnonAuthFail() {
7979
public void testTransporterAnon() throws Exception {
8080
final byte[] data = testTransporter("https://localhost:7443/maven2");
8181
assertNotEquals(null, data);
82-
final byte[] local = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
82+
final byte[] local = getCommonsJar();
8383
assertArrayEquals(local, data);
8484
}
8585

8686
@Test
8787
public void testAnonTransporterSuccess() throws Exception {
8888
final byte[] data = testTransporter("https://demo:demo@localhost:7443/maven2");
8989
assertNotNull(data);
90-
final byte[] local = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
90+
final byte[] local = getCommonsJar();
9191
assertArrayEquals(local, data);
9292
}
9393

@@ -132,7 +132,7 @@ public void testJettyLocalhostPut() throws Exception {
132132
final HttpClient client = new HttpClient(transport);
133133
client.start();
134134
h3Client.getClientConnector().getSslContextFactory().setTrustAll(true);
135-
final byte[] srcData = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
135+
final byte[] srcData = getCommonsJar();
136136
final ContentResponse response = client.newRequest("https://localhost:7445/test1").
137137
method(HttpMethod.PUT).body(
138138
new InputStreamRequestContent(new ByteArrayInputStream(srcData))
@@ -150,7 +150,7 @@ public void testTransporterPutAnon() throws Exception {
150150
resetPutServer();
151151
final HttpTransporterFactory factory = new HttpTransporterFactory();
152152
final PutTask task = new PutTask(URI.create("test1")).setListener(new TransportListener() {});
153-
final byte[] srcData = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
153+
final byte[] srcData = getCommonsJar();
154154
task.setDataBytes(srcData);
155155
try (final Transporter transporter = factory.newInstance(newSession(), newRepo(repo))) {
156156
transporter.put(task);
@@ -166,7 +166,7 @@ public void testTransporterPutAuth() throws Exception {
166166
resetPutServer();
167167
final HttpTransporterFactory factory = new HttpTransporterFactory();
168168
final PutTask task = new PutTask(URI.create("test1")).setListener(new TransportListener() {});
169-
final byte[] srcData = getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
169+
final byte[] srcData = getCommonsJar();
170170
task.setDataBytes(srcData);
171171
try (final Transporter transporter = factory.newInstance(newSession(), newRepo(repo))) {
172172
transporter.put(task);
@@ -209,14 +209,14 @@ public static void finish() {
209209
tempFile.delete();
210210
}
211211

212-
private static DefaultRepositorySystemSession newSession() {
212+
static DefaultRepositorySystemSession newSession() {
213213
DefaultRepositorySystemSession session = new DefaultRepositorySystemSession();
214214
session.setLocalRepositoryManager(new TestLocalRepositoryManager());
215215
session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
216216
return session;
217217
}
218218

219-
private RemoteRepository newRepo(final String url) {
219+
static RemoteRepository newRepo(final String url) {
220220
return new RemoteRepository.Builder("test", "default", url).build();
221221
}
222222

@@ -226,4 +226,8 @@ private static void resetPutServer() throws InterruptedException, IOException {
226226
assertEquals(0, result.getExitCode());
227227
assertTrue(deleted);
228228
}
229+
230+
byte[] getCommonsJar() throws IOException {
231+
return getClass().getClassLoader().getResourceAsStream(LOCAL_PATH).readAllBytes();
232+
}
229233
}

0 commit comments

Comments
 (0)