View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.internal.test.util.http;
20  
21  import javax.net.ssl.KeyManagerFactory;
22  import javax.net.ssl.SSLContext;
23  import javax.net.ssl.TrustManagerFactory;
24  
25  import java.io.File;
26  import java.io.FileNotFoundException;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.UncheckedIOException;
30  import java.net.ServerSocket;
31  import java.net.URI;
32  import java.nio.charset.StandardCharsets;
33  import java.nio.file.Files;
34  import java.nio.file.Path;
35  import java.nio.file.Paths;
36  import java.nio.file.StandardCopyOption;
37  import java.security.KeyStore;
38  import java.security.NoSuchAlgorithmException;
39  import java.time.Duration;
40  import java.util.Enumeration;
41  import java.util.HashMap;
42  import java.util.Map;
43  import java.util.concurrent.TimeUnit;
44  import java.util.concurrent.atomic.AtomicReference;
45  import java.util.function.Supplier;
46  import java.util.stream.Stream;
47  
48  import org.awaitility.Awaitility;
49  import org.eclipse.aether.ConfigurationProperties;
50  import org.eclipse.aether.DefaultRepositoryCache;
51  import org.eclipse.aether.DefaultRepositorySystemSession;
52  import org.eclipse.aether.DefaultSessionData;
53  import org.eclipse.aether.internal.impl.transport.http.DefaultChecksumExtractor;
54  import org.eclipse.aether.internal.impl.transport.http.Nx2ChecksumExtractor;
55  import org.eclipse.aether.internal.impl.transport.http.XChecksumExtractor;
56  import org.eclipse.aether.internal.test.util.TestFileUtils;
57  import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager;
58  import org.eclipse.aether.repository.Authentication;
59  import org.eclipse.aether.repository.Proxy;
60  import org.eclipse.aether.repository.RemoteRepository;
61  import org.eclipse.aether.spi.connector.transport.GetTask;
62  import org.eclipse.aether.spi.connector.transport.PeekTask;
63  import org.eclipse.aether.spi.connector.transport.PutTask;
64  import org.eclipse.aether.spi.connector.transport.Transporter;
65  import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
66  import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractorStrategy;
67  import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
68  import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
69  import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory;
70  import org.eclipse.aether.spi.connector.transport.http.RFC9457.HttpRFC9457Exception;
71  import org.eclipse.aether.transfer.HttpTransportProperty;
72  import org.eclipse.aether.transfer.NoTransporterException;
73  import org.eclipse.aether.transfer.TransferCancelledException;
74  import org.eclipse.aether.util.repository.AuthenticationBuilder;
75  import org.eclipse.jetty.http.HttpVersion;
76  import org.junit.jupiter.api.AfterAll;
77  import org.junit.jupiter.api.AfterEach;
78  import org.junit.jupiter.api.BeforeAll;
79  import org.junit.jupiter.api.BeforeEach;
80  import org.junit.jupiter.api.MethodOrderer;
81  import org.junit.jupiter.api.Test;
82  import org.junit.jupiter.api.TestInfo;
83  import org.junit.jupiter.api.TestMethodOrder;
84  import org.junit.jupiter.api.Timeout;
85  import org.junit.jupiter.params.ParameterizedTest;
86  import org.junit.jupiter.params.provider.ValueSource;
87  
88  import static java.util.Objects.requireNonNull;
89  import static org.junit.jupiter.api.Assertions.assertEquals;
90  import static org.junit.jupiter.api.Assertions.assertNotNull;
91  import static org.junit.jupiter.api.Assertions.assertNull;
92  import static org.junit.jupiter.api.Assertions.assertThrows;
93  import static org.junit.jupiter.api.Assertions.assertTrue;
94  import static org.junit.jupiter.api.Assertions.fail;
95  import static org.junit.jupiter.api.Assumptions.assumeFalse;
96  import static org.junit.jupiter.api.Assumptions.assumeTrue;
97  
98  /**
99   * Common set of tests against Http transporter.
100  */
101 @SuppressWarnings({"checkstyle:MethodName"})
102 @TestMethodOrder(MethodOrderer.MethodName.class)
103 public abstract class HttpTransporterTest {
104 
105     protected static final Path SERVER_STORE_PATH = Paths.get("target/server-store");
106 
107     protected static final Path CLIENT_STORE_PATH = Paths.get("target/client-store");
108 
109     protected static final Path PEM_QUICHE_SERVER_PATH = Paths.get("target/pems");
110 
111     protected static SSLContext defaultSslContext;
112 
113     static {
114         // uncomment to enable SSL debugging for easier troubleshooting of SSL related test failures
115         // System.setProperty("javax.net.debug", "all");
116     }
117 
118     @BeforeAll
119     protected static void beforeAll() throws NoSuchAlgorithmException {
120         // populate custom keystore and truststore
121         try {
122             try (InputStream keyStoreStream =
123                             HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/server-store");
124                     InputStream trustStoreStream =
125                             HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/client-store"); ) {
126                 Files.copy(keyStoreStream, SERVER_STORE_PATH, StandardCopyOption.REPLACE_EXISTING);
127                 Files.copy(trustStoreStream, CLIENT_STORE_PATH, StandardCopyOption.REPLACE_EXISTING);
128                 Files.createDirectories(PEM_QUICHE_SERVER_PATH);
129             }
130         } catch (IOException e) {
131             throw new UncheckedIOException(e);
132         }
133         // override default SSLContext to include our custom keystore and truststore (which are "cross connected" with
134         // HttpServer)
135         defaultSslContext = SSLContext.getDefault();
136         SSLContext.setDefault(createClientSSLContext());
137     }
138 
139     @AfterAll
140     protected static void afterAll() {
141         if (defaultSslContext != null) {
142             SSLContext.setDefault(defaultSslContext);
143         }
144     }
145 
146     /**
147      * Creates an {@link SSLContext} for the client that extends the default keystore and truststore with the entries
148      * from {@link #SERVER_STORE_PATH} (password {@code "server-pwd"}) and {@link #CLIENT_STORE_PATH}
149      * (password {@code "client-pwd"}).
150      *
151      * @return an {@link SSLContext} combining default and custom key/trust material
152      */
153     protected static SSLContext createClientSSLContext() {
154         try {
155             // Load custom key store (KEY_STORE_PATH acts as truststore in "cross connected" setup)
156             KeyStore customTrustStore = KeyStore.getInstance("pkcs12");
157             try (InputStream is = Files.newInputStream(SERVER_STORE_PATH)) {
158                 customTrustStore.load(is, "server-pwd".toCharArray());
159             }
160 
161             // Load custom trust store (TRUST_STORE_PATH acts as keystore in "cross connected" setup)
162             KeyStore customKeyStore = KeyStore.getInstance("pkcs12");
163             try (InputStream is = Files.newInputStream(CLIENT_STORE_PATH)) {
164                 customKeyStore.load(is, "client-pwd".toCharArray());
165             }
166 
167             // Load default truststore and merge custom entries
168             KeyStore defaultTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
169             Path defaultTrustStorePath = Path.of(System.getProperty("java.home"), "lib", "security", "cacerts");
170             if (Files.exists(defaultTrustStorePath)) {
171                 try (InputStream is = Files.newInputStream(defaultTrustStorePath)) {
172                     defaultTrustStore.load(is, "changeit".toCharArray());
173                 }
174             } else {
175                 defaultTrustStore.load(null, null);
176             }
177             Enumeration<String> aliases = customTrustStore.aliases();
178             while (aliases.hasMoreElements()) {
179                 String alias = aliases.nextElement();
180                 defaultTrustStore.setCertificateEntry("custom-trust-" + alias, customTrustStore.getCertificate(alias));
181             }
182 
183             // Load default keystore and merge custom entries
184             KeyStore mergedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
185             mergedKeyStore.load(null, null);
186             Enumeration<String> keyAliases = customKeyStore.aliases();
187             while (keyAliases.hasMoreElements()) {
188                 String alias = keyAliases.nextElement();
189                 if (customKeyStore.isKeyEntry(alias)) {
190                     mergedKeyStore.setKeyEntry(
191                             alias,
192                             customKeyStore.getKey(alias, "client-pwd".toCharArray()),
193                             "client-pwd".toCharArray(),
194                             customKeyStore.getCertificateChain(alias));
195                 } else {
196                     mergedKeyStore.setCertificateEntry(alias, customKeyStore.getCertificate(alias));
197                 }
198             }
199 
200             TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
201             tmf.init(defaultTrustStore);
202 
203             KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
204             kmf.init(mergedKeyStore, "client-pwd".toCharArray());
205 
206             SSLContext sslContext = SSLContext.getInstance("TLS");
207             sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
208             return sslContext;
209         } catch (Exception e) {
210             throw new RuntimeException("Failed to create SSLContext", e);
211         }
212     }
213 
214     private final Supplier<HttpTransporterFactory> transporterFactorySupplier;
215 
216     protected DefaultRepositorySystemSession session;
217 
218     protected HttpTransporterFactory factory;
219 
220     protected HttpTransporter transporter;
221 
222     protected Runnable closer;
223 
224     protected File repoDir;
225 
226     protected HttpServer httpServer;
227 
228     protected Authentication auth;
229 
230     protected Proxy proxy;
231 
232     protected HttpTransporterTest(Supplier<HttpTransporterFactory> transporterFactorySupplier) {
233         this.transporterFactorySupplier = requireNonNull(transporterFactorySupplier);
234     }
235 
236     protected static ChecksumExtractor standardChecksumExtractor() {
237         HashMap<String, ChecksumExtractorStrategy> strategies = new HashMap<>();
238         strategies.put("1", new Nx2ChecksumExtractor());
239         strategies.put("2", new XChecksumExtractor());
240         return new DefaultChecksumExtractor(strategies);
241     }
242 
243     protected RemoteRepository newRepo(String url) {
244         return new RemoteRepository.Builder("test", "default", url)
245                 .setAuthentication(auth)
246                 .setProxy(proxy)
247                 .build();
248     }
249 
250     protected void newTransporter(String url) throws Exception {
251         if (transporter != null) {
252             transporter.close();
253             transporter = null;
254         }
255         if (closer != null) {
256             closer.run();
257             closer = null;
258         }
259         session = new DefaultRepositorySystemSession(session);
260         session.setData(new DefaultSessionData());
261         transporter = factory.newInstance(session, newRepo(url));
262     }
263 
264     protected static final long OLD_FILE_TIMESTAMP = 160660800000L;
265 
266     /** HTTP status code for "Too Many Requests". */
267     private static final int SC_TOO_MANY_REQUESTS = 429;
268 
269     @BeforeEach
270     protected void setUp(TestInfo testInfo) throws Exception {
271         System.out.println("=== " + testInfo.getDisplayName() + " ===");
272         session = new DefaultRepositorySystemSession(h -> {
273             this.closer = h;
274             return true;
275         });
276         session.setLocalRepositoryManager(new TestLocalRepositoryManager());
277         factory = transporterFactorySupplier.get();
278         repoDir = TestFileUtils.createTempDir();
279         TestFileUtils.writeString(new File(repoDir, "file.txt"), "test");
280         TestFileUtils.writeString(new File(repoDir, "artifact.pom"), "<xml>pom</xml>");
281         TestFileUtils.writeString(new File(repoDir, "dir/file.txt"), "test");
282         TestFileUtils.writeString(new File(repoDir, "dir/oldFile.txt"), "oldTest", OLD_FILE_TIMESTAMP);
283         TestFileUtils.writeString(new File(repoDir, "empty.txt"), "");
284         TestFileUtils.writeString(new File(repoDir, "some space.txt"), "space");
285         try (InputStream is = getCompressibleFileStream()) {
286             Files.copy(is, repoDir.toPath().resolve("compressible-file.xml"));
287         }
288         File resumable = new File(repoDir, "resume.txt");
289         TestFileUtils.writeString(resumable, "resumable");
290         resumable.setLastModified(System.currentTimeMillis() - 90 * 1000);
291         httpServer = new HttpServer().setRepoDir(repoDir).start();
292         // always create a transporter connecting to the Http URL
293         newTransporter(httpServer.getHttpUrl());
294     }
295 
296     private static InputStream getCompressibleFileStream() {
297         return HttpTransporterTest.class.getClassLoader().getResourceAsStream("compressible-file.xml");
298     }
299 
300     @AfterEach
301     protected void tearDown() throws Exception {
302         if (transporter != null) {
303             transporter.close();
304             transporter = null;
305         }
306         if (closer != null) {
307             closer.run();
308             closer = null;
309         }
310         if (httpServer != null) {
311             if (closesAllConnectionsOnTransporterClose()) {
312                 // check for leaked connections (e.g., due to not closing response body streams)
313                 Awaitility.await().atMost(3, TimeUnit.SECONDS).until(() -> httpServer.getNumConnectedEndPoints() == 0);
314             }
315             httpServer.stop();
316             httpServer = null;
317         }
318         factory = null;
319         session = null;
320     }
321 
322     /**
323      * Indicates whether the transporter implementation closes all connections when the transporter is closed.
324      * @return {@code true} if all connections are closed on transporter close, {@code false} otherwise.
325      */
326     protected boolean closesAllConnectionsOnTransporterClose() {
327         return true;
328     }
329 
330     /**
331      * Indicates whether the transporter implementation supports preemptive authentication (i.e., sending credentials with the first request).
332      * @return {@code true} if preemptive authentication is supported, {@code false} otherwise.
333      */
334     protected boolean supportsPreemptiveAuth() {
335         return true;
336     }
337 
338     /**
339      * Indicates whether the transporter implementation supports HTTP/3.
340      * @return {@code true} if HTTP/3 is supported, {@code false} otherwise.
341      */
342     protected boolean supportsHttp3() {
343         // skip on ASF Jenkins due to incompatible GLIBC version
344         // (https://github.com/jetty-project/jetty-quiche-native/issues/180 and
345         // https://issues.apache.org/jira/browse/INFRA-28128)
346         // identified via property "os.version" exposed in https://ci-maven.apache.org/computer/maven6/systemInfo
347         assumeFalse(
348                 System.getProperty("os.version").equals("5.15.0-1089-azure"),
349                 "Skipping HTTP/3 tests on ASF Jenkins Linux Nodes");
350         return true;
351     }
352 
353     /**
354      * Indicates whether the transporter implementation supports HTTP/2.
355      * @return {@code true} if HTTP/2 is supported, {@code false} otherwise.
356      */
357     protected boolean supportsHttp2() {
358         return true;
359     }
360 
361     @Test
362     protected void testClassify() {
363         assertEquals(Transporter.ERROR_OTHER, transporter.classify(new FileNotFoundException()));
364         assertEquals(Transporter.ERROR_OTHER, transporter.classify(new HttpTransporterException(403)));
365         assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(new HttpTransporterException(404)));
366         assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(new HttpTransporterException(410)));
367     }
368 
369     @Test
370     protected void testPeek() throws Exception {
371         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
372     }
373 
374     @Test
375     protected void testPeek_DoesNotAcceptRfc9457() throws Exception {
376         // peek is HEAD request, therefore cannot support RFC 9457
377         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
378         String accept = httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept");
379         assertNull(accept, "No accept header expected for HEAD request, but was: " + accept);
380     }
381 
382     @Test
383     protected void testRetryHandler_defaultCount_positive() throws Exception {
384         httpServer.setConnectionsToClose(3);
385         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
386     }
387 
388     @Test
389     protected void testRetryHandler_defaultCount_negative() throws Exception {
390         httpServer.setConnectionsToClose(4);
391         try {
392             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
393             fail("Expected error");
394         } catch (Exception expected) {
395         }
396     }
397 
398     @Test
399     protected void testRetryHandler_tooManyRequests_explicitCount_positive() throws Exception {
400         // set low retry count as this involves back off delays
401         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 1);
402         int retryIntervalMs = 500;
403         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL, retryIntervalMs);
404         newTransporter(httpServer.getHttpUrl());
405         httpServer.setServerErrorsBeforeWorks(1, SC_TOO_MANY_REQUESTS);
406         long startTime = System.currentTimeMillis();
407         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
408         assertTrue(
409                 System.currentTimeMillis() - startTime >= retryIntervalMs,
410                 "Expected back off delay of at least " + retryIntervalMs);
411     }
412 
413     @Test
414     protected void testRetryHandler_tooManyRequests_withRetryAfterHeader_explicitCount_positive() throws Exception {
415         // set low retry count as this involves back off delays
416         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 1);
417         int retryIntervalMs = 5000;
418         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL, retryIntervalMs);
419         newTransporter(httpServer.getHttpUrl());
420         Duration retryAfterDuration = Duration.ofSeconds(1);
421         httpServer.setServerErrorsBeforeWorks(
422                 1, SC_TOO_MANY_REQUESTS, Map.of("Retry-After", String.valueOf(retryAfterDuration.getSeconds())));
423         long startTime = System.currentTimeMillis();
424         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
425         long elapsedTimeMs = System.currentTimeMillis() - startTime;
426         assertTrue(
427                 elapsedTimeMs >= retryAfterDuration.toMillis(),
428                 "Expected back off delay of at least " + retryAfterDuration.toMillis() + " ms");
429         assertTrue(elapsedTimeMs < retryIntervalMs, "Expected back off delay of less than " + retryIntervalMs + " ms");
430     }
431 
432     @Test
433     protected void testRetryHandler_tooManyRequests_explicitCount_negative() throws Exception {
434         // set low retry count as this involves back off delays
435         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 3);
436         int retryIntervalMs = 100;
437         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL, retryIntervalMs);
438         newTransporter(httpServer.getHttpUrl());
439         httpServer.setServerErrorsBeforeWorks(4, SC_TOO_MANY_REQUESTS);
440         long startTime = System.currentTimeMillis();
441         try {
442             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
443             fail("Expected error");
444         } catch (Exception expected) {
445         }
446         // linear backoff: 1x + 2x + 3x
447         long expectedMinimumDuration = retryIntervalMs * (1 + 2 + 3);
448         assertTrue(
449                 System.currentTimeMillis() - startTime >= expectedMinimumDuration,
450                 "Expected back off delay of at least " + expectedMinimumDuration);
451     }
452 
453     @Test
454     protected void testRetryHandler_explicitCount_positive() throws Exception {
455         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 10);
456         newTransporter(httpServer.getHttpUrl());
457         httpServer.setConnectionsToClose(10);
458         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
459     }
460 
461     @Test
462     protected void testRetryHandler_disabled() throws Exception {
463         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 0);
464         newTransporter(httpServer.getHttpUrl());
465         httpServer.setConnectionsToClose(1);
466         try {
467             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
468         } catch (Exception expected) {
469         }
470     }
471 
472     @Test
473     protected void testPeek_NotFound() throws Exception {
474         try {
475             transporter.peek(new PeekTask(URI.create("repo/missing.txt")));
476             fail("Expected error");
477         } catch (HttpTransporterException e) {
478             assertEquals(404, e.getStatusCode());
479             assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e));
480         }
481     }
482 
483     @Test
484     protected void testPeek_Closed() throws Exception {
485         transporter.close();
486         try {
487             transporter.peek(new PeekTask(URI.create("repo/missing.txt")));
488             fail("Expected error");
489         } catch (IllegalStateException e) {
490             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
491         }
492     }
493 
494     @Test
495     protected void testPeek_Authenticated() throws Exception {
496         httpServer.setAuthentication("testuser", "testpass");
497         auth = new AuthenticationBuilder()
498                 .addUsername("testuser")
499                 .addPassword("testpass")
500                 .build();
501         newTransporter(httpServer.getHttpUrl());
502         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
503     }
504 
505     @Test
506     protected void testPeek_Unauthenticated() throws Exception {
507         httpServer.setAuthentication("testuser", "testpass");
508         try {
509             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
510             fail("Expected error");
511         } catch (HttpTransporterException e) {
512             assertEquals(401, e.getStatusCode());
513             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
514         }
515     }
516 
517     @Test
518     protected void testPeek_ProxyAuthenticated() throws Exception {
519         httpServer.setProxyAuthentication("testuser", "testpass");
520         auth = new AuthenticationBuilder()
521                 .addUsername("testuser")
522                 .addPassword("testpass")
523                 .build();
524         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
525         newTransporter("http://bad.localhost:1/");
526         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
527     }
528 
529     @Test
530     protected void testPeek_ProxyUnauthenticated() throws Exception {
531         httpServer.setProxyAuthentication("testuser", "testpass");
532         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
533         newTransporter("http://bad.localhost:1/");
534         try {
535             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
536             fail("Expected error");
537         } catch (HttpTransporterException e) {
538             assertEquals(407, e.getStatusCode());
539             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
540         }
541     }
542 
543     @Test
544     protected void testPeek_SSL() throws Exception {
545         httpServer.addHttp2ConnectorWithMutualTLS();
546         newTransporter(httpServer.getHttpsUrl());
547         RecordingTransportListener listener = new RecordingTransportListener();
548         PeekTask task = new PeekTask(URI.create("repo/file.txt")).setListener(listener);
549         transporter.peek(task);
550         if (exposeContentCodingInTransportProperties()) {
551             assertEquals(
552                     HttpTransportProperty.SslProtocol.TLS_1_3,
553                     listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
554         }
555     }
556 
557     @Test
558     protected void testPeek_Redirect() throws Exception {
559         httpServer.addHttp2ConnectorWithMutualTLS();
560         transporter.peek(new PeekTask(URI.create("redirect/file.txt")));
561         transporter.peek(new PeekTask(URI.create("redirect/file.txt?scheme=https")));
562     }
563 
564     @Test
565     protected void testGet_ToMemory() throws Exception {
566         RecordingTransportListener listener = new RecordingTransportListener();
567         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
568         transporter.get(task);
569         assertEquals("test", task.getDataString());
570         assertEquals(0L, listener.getDataOffset());
571         assertEquals(4L, listener.getDataLength());
572         assertEquals(1, listener.getStartedCount());
573         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
574         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
575     }
576 
577     @Test
578     protected void testGet_ToFile() throws Exception {
579         File file = TestFileUtils.createTempFile("failure");
580         RecordingTransportListener listener = new RecordingTransportListener();
581         GetTask task = new GetTask(URI.create("repo/file.txt"))
582                 .setDataPath(file.toPath())
583                 .setListener(listener);
584         transporter.get(task);
585         assertEquals("test", TestFileUtils.readString(file));
586         assertEquals(0L, listener.getDataOffset());
587         assertEquals(4L, listener.getDataLength());
588         assertEquals(1, listener.getStartedCount());
589         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
590         assertEquals("test", listener.getBaos().toString(StandardCharsets.UTF_8));
591     }
592 
593     @Test
594     protected void testGet_ToFileTimestamp() throws Exception {
595         File file = TestFileUtils.createTempFile("failure");
596         RecordingTransportListener listener = new RecordingTransportListener();
597         GetTask task = new GetTask(URI.create("repo/dir/oldFile.txt"))
598                 .setDataPath(file.toPath())
599                 .setListener(listener);
600         transporter.get(task);
601         assertEquals("oldTest", TestFileUtils.readString(file));
602         assertEquals(0L, listener.getDataOffset());
603         assertEquals(7L, listener.getDataLength());
604         assertEquals(1, listener.getStartedCount());
605         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
606         assertEquals("oldTest", listener.getBaos().toString(StandardCharsets.UTF_8));
607         assertEquals(OLD_FILE_TIMESTAMP, file.lastModified());
608     }
609 
610     @Test
611     protected void testGet_AcceptsRfc9457() throws Exception {
612         GetTask task = new GetTask(URI.create("repo/file.txt"));
613         transporter.get(task);
614         String accept = httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept");
615         assertNotNull(accept, "Missing Accept header when retrieving artifact");
616         assertTrue(
617                 accept.contains("application/problem+json"),
618                 "Expected Accept header to contain application/problem+json, but was: " + accept);
619     }
620 
621     @Test
622     protected void testGet_ParseRfc9457() throws Exception {
623         // use Maven Central (Cloudflare CDN) as endpoints that return RFC 9457 responses
624         newTransporter("https://repo.maven.apache.org");
625         try {
626             // https://blog.cloudflare.com/rfc-9457-agent-error-pages/#how-to-use-it
627             GetTask task = new GetTask(URI.create("cdn-cgi/error/1020"));
628             transporter.get(task);
629             fail("Should have throw HttpRFC9457Exception");
630         } catch (HttpRFC9457Exception e) {
631             // Expected exception, verify the content of the RFC 9457 message.
632             assertEquals(403, e.getStatusCode());
633             assertEquals("Error 1020: Access denied", e.getPayload().getTitle());
634             assertEquals(
635                     "The request was blocked by a Cloudflare firewall rule configured by the site owner.",
636                     e.getPayload().getDetail());
637         }
638     }
639 
640     /**
641      * Provides compression algorithms supported by the transporter implementation.
642      * This should be the string value passed in the {@code Accept-Encoding} header.
643      *
644      * @return stream of supported compression algorithm names
645      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding#directives">Accept-Encoding directives</a>
646      */
647     protected abstract Stream<String> supportedCompressionAlgorithms();
648 
649     protected boolean exposeContentCodingInTransportProperties() {
650         return true;
651     }
652 
653     @ParameterizedTest
654     // DEFLATE isn't supported by Jetty server (https://github.com/jetty/jetty.project/issues/280)
655     @ValueSource(strings = {"br", "gzip", "zstd"})
656     protected void testGet_WithCompression(String encoding) throws Exception {
657         assumeTrue(
658                 supportedCompressionAlgorithms().anyMatch(supported -> supported.equals(encoding)),
659                 () -> "Transporter does not support compression algorithm: " + encoding);
660         RecordingTransportListener listener = new RecordingTransportListener();
661         // requires a file with at least 48/50 bytes (otherwise compression is disabled,
662         // https://github.com/jetty/jetty.project/blob/2264d3d9f9586f3e5e9040fba779ed72e931cb46/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/BrotliCompression.java#L61)
663         GetTask task = new GetTask(URI.create(encoding + "/repo/compressible-file.xml")).setListener(listener);
664         transporter.get(task);
665         String acceptEncoding =
666                 httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept-Encoding");
667         assertNotNull(acceptEncoding, "Missing Accept-Encoding header when retrieving pom");
668         assertTrue(acceptEncoding.contains(encoding));
669         // check original response header sent by server (client transparently handles compression and removes it)
670         // see https://issues.apache.org/jira/browse/HTTPCORE-792
671         // and https://github.com/mizosoft/methanol/issues/182
672         for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
673             assertEquals(encoding, log.getResponseHeaders().get("Content-Encoding"));
674         }
675         String expectedResourceData;
676         try (InputStream is = getCompressibleFileStream()) {
677             expectedResourceData = new String(is.readAllBytes(), StandardCharsets.UTF_8);
678         }
679         assertEquals(expectedResourceData, task.getDataString());
680         assertEquals(0L, listener.getDataOffset());
681         // data length is unknown as chunked transfer encoding is used with compression
682         assertEquals(-1, listener.getDataLength());
683         assertEquals(1, listener.getStartedCount());
684         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
685         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
686         if (exposeContentCodingInTransportProperties()) {
687             assertEquals(encoding, listener.getTransportProperties().get(HttpTransportProperty.Key.CONTENT_CODING));
688         }
689     }
690 
691     @Test
692     protected void testGet_EmptyResource() throws Exception {
693         File file = TestFileUtils.createTempFile("failure");
694         RecordingTransportListener listener = new RecordingTransportListener();
695         GetTask task = new GetTask(URI.create("repo/empty.txt"))
696                 .setDataPath(file.toPath())
697                 .setListener(listener);
698         transporter.get(task);
699         assertEquals("", TestFileUtils.readString(file));
700         assertEquals(0L, listener.getDataOffset());
701         assertEquals(0L, listener.getDataLength());
702         assertEquals(1, listener.getStartedCount());
703         assertEquals(0, listener.getProgressedCount());
704         assertEquals("", listener.getBaos().toString(StandardCharsets.UTF_8));
705     }
706 
707     @Test
708     protected void testGet_EncodedResourcePath() throws Exception {
709         GetTask task = new GetTask(URI.create("repo/some%20space.txt"));
710         transporter.get(task);
711         assertEquals("space", task.getDataString());
712     }
713 
714     @Test
715     protected void testGet_Authenticated() throws Exception {
716         httpServer.setAuthentication("testuser", "testpass");
717         auth = new AuthenticationBuilder()
718                 .addUsername("testuser")
719                 .addPassword("testpass")
720                 .build();
721         newTransporter(httpServer.getHttpUrl());
722         RecordingTransportListener listener = new RecordingTransportListener();
723         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
724         transporter.get(task);
725         assertEquals("test", task.getDataString());
726         assertEquals(0L, listener.getDataOffset());
727         assertEquals(4L, listener.getDataLength());
728         assertEquals(1, listener.getStartedCount());
729         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
730         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
731     }
732 
733     @Test
734     protected void testGet_Unauthenticated() throws Exception {
735         httpServer.setAuthentication("testuser", "testpass");
736         try {
737             transporter.get(new GetTask(URI.create("repo/file.txt")));
738             fail("Expected error");
739         } catch (HttpTransporterException e) {
740             assertEquals(401, e.getStatusCode());
741             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
742         }
743     }
744 
745     @Test
746     protected void testGet_ProxyAuthenticated() throws Exception {
747         httpServer.setProxyAuthentication("testuser", "testpass");
748         Authentication auth = new AuthenticationBuilder()
749                 .addUsername("testuser")
750                 .addPassword("testpass")
751                 .build();
752         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
753         newTransporter("http://bad.localhost:1/");
754         RecordingTransportListener listener = new RecordingTransportListener();
755         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
756         transporter.get(task);
757         assertEquals("test", task.getDataString());
758         assertEquals(0L, listener.getDataOffset());
759         assertEquals(4L, listener.getDataLength());
760         assertEquals(1, listener.getStartedCount());
761         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
762         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
763     }
764 
765     @Test
766     protected void testGet_ProxyUnauthenticated() throws Exception {
767         httpServer.setProxyAuthentication("testuser", "testpass");
768         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
769         newTransporter("http://bad.localhost:1/");
770         try {
771             transporter.get(new GetTask(URI.create("repo/file.txt")));
772             fail("Expected error");
773         } catch (HttpTransporterException e) {
774             assertEquals(407, e.getStatusCode());
775             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
776         }
777     }
778 
779     @Test
780     protected void testGet_RFC9457Response() throws Exception {
781         try {
782             transporter.get(new GetTask(URI.create("rfc9457/file.txt")));
783             fail("Expected error");
784         } catch (HttpRFC9457Exception e) {
785             assertEquals(403, e.getStatusCode());
786             assertEquals(e.getPayload().getType(), URI.create("https://example.com/probs/out-of-credit"));
787             assertEquals(403, e.getPayload().getStatus());
788             assertEquals("You do not have enough credit.", e.getPayload().getTitle());
789             assertEquals(
790                     "Your current balance is 30, but that costs 50.",
791                     e.getPayload().getDetail());
792             assertEquals(URI.create("/account/12345/msgs/abc"), e.getPayload().getInstance());
793         }
794     }
795 
796     @Test
797     protected void testGet_RFC9457Response_with_missing_fields() throws Exception {
798         try {
799             transporter.get(new GetTask(URI.create("rfc9457/missing_fields.txt")));
800             fail("Expected error");
801         } catch (HttpRFC9457Exception e) {
802             assertEquals(403, e.getStatusCode());
803             assertEquals(e.getPayload().getType(), URI.create("about:blank"));
804             assertNull(e.getPayload().getStatus());
805             assertNull(e.getPayload().getTitle());
806             assertNull(e.getPayload().getDetail());
807             assertNull(e.getPayload().getInstance());
808         }
809     }
810 
811     @Test
812     protected void testGet_SSL() throws Exception {
813         httpServer.addHttp2ConnectorWithMutualTLS();
814         newTransporter(httpServer.getHttpsUrl());
815         RecordingTransportListener listener = new RecordingTransportListener();
816         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
817         transporter.get(task);
818         assertEquals("test", task.getDataString());
819         assertEquals(0L, listener.getDataOffset());
820         assertEquals(4L, listener.getDataLength());
821         assertEquals(1, listener.getStartedCount());
822         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
823         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
824         if (exposeContentCodingInTransportProperties()) {
825             assertEquals(
826                     HttpTransportProperty.SslProtocol.TLS_1_3,
827                     listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
828             assertEquals(
829                     "TLS_AES_256_GCM_SHA384",
830                     listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_CIPHER_SUITE));
831         }
832     }
833 
834     @Test
835     protected void testGet_SSL_WithServerErrors() throws Exception {
836         httpServer.setServerErrorsBeforeWorks(1);
837         httpServer.addHttp2ConnectorWithMutualTLS();
838         newTransporter(httpServer.getHttpsUrl());
839         for (int i = 1; i < 3; i++) {
840             try {
841                 RecordingTransportListener listener = new RecordingTransportListener();
842                 GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
843                 transporter.get(task);
844                 assertEquals("test", task.getDataString());
845                 assertEquals(0L, listener.getDataOffset());
846                 assertEquals(4L, listener.getDataLength());
847                 assertEquals(1, listener.getStartedCount());
848                 assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
849                 assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
850             } catch (HttpTransporterException e) {
851                 assertEquals(500, e.getStatusCode());
852             }
853         }
854     }
855 
856     @Test
857     protected void testGet_HTTPS_Unknown_SecurityMode() throws Exception {
858         session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE, "unknown");
859         httpServer.addHttp2Connector();
860         try {
861             newTransporter(httpServer.getHttpsUrl());
862             fail("Unsupported security mode");
863         } catch (IllegalArgumentException a) {
864             // good
865         }
866     }
867 
868     @Test
869     protected void testGet_HTTPS_Insecure_SecurityMode() throws Exception {
870         // we have to reset the default ssl context to avoid the default truststore being used, which would make the
871         // test pass even if the security mode is not set to insecure
872         SSLContext.setDefault(defaultSslContext);
873         try {
874             session.setConfigProperty(
875                     ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
876             httpServer.addHttp2Connector();
877             newTransporter(httpServer.getHttpsUrl());
878             RecordingTransportListener listener = new RecordingTransportListener();
879             GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
880             transporter.get(task);
881             assertEquals("test", task.getDataString());
882             assertEquals(0L, listener.getDataOffset());
883             assertEquals(4L, listener.getDataLength());
884             assertEquals(1, listener.getStartedCount());
885             assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
886             assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
887         } finally {
888             // restore the default SSL context used for all other tests
889             SSLContext.setDefault(createClientSSLContext());
890         }
891     }
892 
893     @Test
894     protected void testGet_HTTPS_HTTP2Only_Insecure_SecurityMode() throws Exception {
895         assumeTrue(supportsHttp2(), "Transporter does not support HTTP/2");
896         // we have to reset the default ssl context to avoid the default truststore being used, which would make the
897         // test pass even if the security mode is not set to insecure
898         SSLContext.setDefault(defaultSslContext);
899         try {
900             session.setConfigProperty(ConfigurationProperties.HTTP_VERSION, ConfigurationProperties.HttpVersion.HTTP_2);
901             session.setConfigProperty(
902                     ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
903             httpServer.addHttp2OnlyConnector();
904             newTransporter(httpServer.getHttpsUrl());
905             RecordingTransportListener listener = new RecordingTransportListener();
906             GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
907             transporter.get(task);
908             assertEquals("test", task.getDataString());
909             assertEquals(0L, listener.getDataOffset());
910             assertEquals(4L, listener.getDataLength());
911             assertEquals(1, listener.getStartedCount());
912             assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
913             assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
914             httpServer.getLogEntries().forEach(log -> {
915                 assertEquals(HttpVersion.HTTP_2, log.getVersion());
916             });
917         } finally {
918             // restore the default SSL context used for all other tests
919             SSLContext.setDefault(createClientSSLContext());
920         }
921     }
922 
923     @Test
924     protected void testGet_HTTP3Only() throws Exception {
925         assumeTrue(supportsHttp3(), "Transporter does not support HTTP/3");
926         session.setConfigProperty(ConfigurationProperties.HTTP_VERSION, ConfigurationProperties.HttpVersion.HTTP_3);
927         httpServer.addHttp3Connector(false);
928         httpServer.start();
929         newTransporter(httpServer.getHttp3Url());
930         RecordingTransportListener listener = new RecordingTransportListener();
931         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
932         transporter.get(task);
933         assertEquals("test", task.getDataString());
934         assertEquals(0L, listener.getDataOffset());
935         assertEquals(4L, listener.getDataLength());
936         assertEquals(1, listener.getStartedCount());
937         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
938         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
939         httpServer.getLogEntries().forEach(log -> {
940             assertEquals(HttpVersion.HTTP_3, log.getVersion());
941         });
942     }
943 
944     @Test
945     protected void testGet_HTTP3() throws Exception {
946         assumeTrue(supportsHttp3(), "Transporter does not support HTTP/3");
947         session.setConfigProperty(ConfigurationProperties.HTTP_VERSION, ConfigurationProperties.HttpVersion.HTTP_3);
948         // both HTTP2 and HTTP3 endpoints are available at the same port
949         // reserve a port free for both TCP and UDP, as the same-numbered UDP port of an OS-assigned TCP port may be
950         // taken (https://github.com/apache/maven-resolver/issues/2036)
951         int port = httpServer.findFreeTcpAndUdpPort();
952         httpServer.addHttp2OnlyConnectorWithMutualTLS(port);
953         httpServer.addHttp3Connector(false, port);
954         // alt-svc header should point to http/3
955         httpServer.start();
956         newTransporter(httpServer.getHttp3Url());
957         RecordingTransportListener listener = new RecordingTransportListener();
958         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
959         // issue 2 requests to ensure that the second request is HTTP/3 (the first one may be HTTP/2 if the TCP
960         // connection is faster)
961         transporter.get(task);
962         transporter.get(task);
963         assertEquals("test", task.getDataString());
964         assertEquals(0L, listener.getDataOffset());
965         assertEquals(4L, listener.getDataLength());
966         assertEquals(2, listener.getStartedCount());
967         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
968         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
969         // the last request should be HTTP/3 finally
970         assertEquals(
971                 HttpVersion.HTTP_3,
972                 httpServer
973                         .getLogEntries()
974                         .get(httpServer.getLogEntries().size() - 1)
975                         .getVersion());
976     }
977 
978     @Test
979     protected void testGet_HTTP3FallbackToHTTP2() throws Exception {
980         assumeTrue(supportsHttp3(), "Transporter does not support HTTP/3");
981         session.setConfigProperty(ConfigurationProperties.HTTP_VERSION, ConfigurationProperties.HttpVersion.HTTP_3);
982         httpServer.addHttp2OnlyConnectorWithMutualTLS();
983         httpServer.start();
984         newTransporter(httpServer.getHttpsUrl());
985         RecordingTransportListener listener = new RecordingTransportListener();
986         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
987         transporter.get(task);
988         assertEquals("test", task.getDataString());
989         assertEquals(0L, listener.getDataOffset());
990         assertEquals(4L, listener.getDataLength());
991         assertEquals(1, listener.getStartedCount());
992         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
993         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
994         assertEquals(
995                 HttpTransportProperty.HttpVersion.HTTP_2,
996                 listener.getTransportProperties().get(HttpTransportProperty.Key.HTTP_VERSION));
997         httpServer.getLogEntries().forEach(log -> {
998             assertEquals(HttpVersion.HTTP_2, log.getVersion());
999         });
1000     }
1001 
1002     @Test
1003     protected void testGet_Redirect() throws Exception {
1004         httpServer.addHttp2ConnectorWithMutualTLS();
1005         RecordingTransportListener listener = new RecordingTransportListener();
1006         GetTask task = new GetTask(URI.create("redirect/file.txt?scheme=https")).setListener(listener);
1007         transporter.get(task);
1008         assertEquals("test", task.getDataString());
1009         assertEquals(0L, listener.getDataOffset());
1010         assertEquals(4L, listener.getDataLength());
1011         assertEquals(1, listener.getStartedCount());
1012         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1013         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
1014     }
1015 
1016     @Test
1017     protected void testGet_Resume() throws Exception {
1018         File file = TestFileUtils.createTempFile("re");
1019         RecordingTransportListener listener = new RecordingTransportListener();
1020         GetTask task = new GetTask(URI.create("repo/resume.txt"))
1021                 .setDataPath(file.toPath(), true)
1022                 .setListener(listener);
1023         transporter.get(task);
1024         assertEquals("resumable", TestFileUtils.readString(file));
1025         assertEquals(1L, listener.getStartedCount());
1026         assertEquals(2L, listener.getDataOffset());
1027         assertEquals(9, listener.getDataLength());
1028         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1029         assertEquals("sumable", listener.getBaos().toString(StandardCharsets.UTF_8));
1030     }
1031 
1032     @Test
1033     protected void testGet_ResumeLocalContentsOutdated() throws Exception {
1034         File file = TestFileUtils.createTempFile("re");
1035         file.setLastModified(System.currentTimeMillis() - 5 * 60 * 1000);
1036         RecordingTransportListener listener = new RecordingTransportListener();
1037         GetTask task = new GetTask(URI.create("repo/resume.txt"))
1038                 .setDataPath(file.toPath(), true)
1039                 .setListener(listener);
1040         transporter.get(task);
1041         assertEquals("resumable", TestFileUtils.readString(file));
1042         assertEquals(1L, listener.getStartedCount());
1043         assertEquals(0L, listener.getDataOffset());
1044         assertEquals(9, listener.getDataLength());
1045         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1046         assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
1047     }
1048 
1049     @Test
1050     protected void testGet_ResumeRangesNotSupportedByServer() throws Exception {
1051         httpServer.setRangeSupport(false);
1052         File file = TestFileUtils.createTempFile("re");
1053         RecordingTransportListener listener = new RecordingTransportListener();
1054         GetTask task = new GetTask(URI.create("repo/resume.txt"))
1055                 .setDataPath(file.toPath(), true)
1056                 .setListener(listener);
1057         transporter.get(task);
1058         assertEquals("resumable", TestFileUtils.readString(file));
1059         assertEquals(1L, listener.getStartedCount());
1060         assertEquals(0L, listener.getDataOffset());
1061         assertEquals(9, listener.getDataLength());
1062         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1063         assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
1064     }
1065 
1066     @Test
1067     protected void testGet_Checksums_Nexus() throws Exception {
1068         httpServer.setChecksumHeader(HttpServer.ChecksumHeader.NEXUS);
1069         GetTask task = new GetTask(URI.create("repo/file.txt"));
1070         transporter.get(task);
1071         assertEquals("test", task.getDataString());
1072         assertEquals(
1073                 "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
1074     }
1075 
1076     @Test
1077     protected void testGet_Checksums_XChecksum() throws Exception {
1078         httpServer.setChecksumHeader(HttpServer.ChecksumHeader.XCHECKSUM);
1079         GetTask task = new GetTask(URI.create("repo/file.txt"));
1080         transporter.get(task);
1081         assertEquals("test", task.getDataString());
1082         assertEquals(
1083                 "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
1084     }
1085 
1086     @Test
1087     protected void testGet_Checksums_XChecksum_Google() throws Exception {
1088         httpServer.setChecksumHeader(HttpServer.ChecksumHeader.XCHECKSUM_GOOGLE);
1089         GetTask task = new GetTask(URI.create("repo/file.txt"));
1090         transporter.get(task);
1091         assertEquals("test", task.getDataString());
1092         assertEquals(
1093                 "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
1094     }
1095 
1096     @Test
1097     protected void testGet_Checksums_XChecksum_Amazon() throws Exception {
1098         httpServer.setChecksumHeader(HttpServer.ChecksumHeader.XCHECKSUM_AMAZON);
1099         GetTask task = new GetTask(URI.create("repo/file.txt"));
1100         transporter.get(task);
1101         assertEquals("test", task.getDataString());
1102         assertEquals(
1103                 "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
1104     }
1105 
1106     @Test
1107     protected void testGet_FileHandleLeak() throws Exception {
1108         for (int i = 0; i < 100; i++) {
1109             File file = TestFileUtils.createTempFile("failure");
1110             transporter.get(new GetTask(URI.create("repo/file.txt")).setDataPath(file.toPath()));
1111             assertTrue(file.delete(), i + ", " + file.getAbsolutePath());
1112         }
1113     }
1114 
1115     @Test
1116     protected void testGet_NotFound() throws Exception {
1117         try {
1118             transporter.get(new GetTask(URI.create("repo/missing.txt")));
1119             fail("Expected error");
1120         } catch (HttpTransporterException e) {
1121             assertEquals(404, e.getStatusCode());
1122             assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e));
1123         }
1124     }
1125 
1126     @Test
1127     protected void testGet_Closed() throws Exception {
1128         transporter.close();
1129         try {
1130             transporter.get(new GetTask(URI.create("repo/file.txt")));
1131             fail("Expected error");
1132         } catch (IllegalStateException e) {
1133             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1134         }
1135     }
1136 
1137     @Test
1138     protected void testGet_StartCancelled() throws Exception {
1139         RecordingTransportListener listener = new RecordingTransportListener();
1140         listener.cancelStart();
1141         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
1142         try {
1143             transporter.get(task);
1144             fail("Expected error");
1145         } catch (TransferCancelledException e) {
1146             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1147         }
1148         assertEquals(0L, listener.getDataOffset());
1149         assertEquals(4L, listener.getDataLength());
1150         assertEquals(1, listener.getStartedCount());
1151         assertEquals(0, listener.getProgressedCount());
1152     }
1153 
1154     @Test
1155     protected void testGet_ProgressCancelled() throws Exception {
1156         RecordingTransportListener listener = new RecordingTransportListener();
1157         listener.cancelProgress();
1158         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
1159         try {
1160             transporter.get(task);
1161             fail("Expected error");
1162         } catch (TransferCancelledException e) {
1163             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1164         }
1165         assertEquals(0L, listener.getDataOffset());
1166         assertEquals(4L, listener.getDataLength());
1167         assertEquals(1, listener.getStartedCount());
1168         assertEquals(1, listener.getProgressedCount());
1169     }
1170 
1171     @Test
1172     protected void testPut_FromMemory() throws Exception {
1173         RecordingTransportListener listener = new RecordingTransportListener();
1174         String payload = "upload";
1175         PutTask task =
1176                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString(payload);
1177         transporter.put(task);
1178         assertEquals(0L, listener.getDataOffset());
1179         assertEquals(6L, listener.getDataLength());
1180         assertEquals(1, listener.getStartedCount());
1181         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1182         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1183         assertEquals(
1184                 String.valueOf(payload.getBytes(StandardCharsets.UTF_8).length),
1185                 httpServer.getLogEntries().get(0).getRequestHeaders().get("Content-Length"));
1186     }
1187 
1188     @Test
1189     protected void testPut_AcceptsRfc9457() throws Exception {
1190         String payload = "upload";
1191         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString(payload);
1192         transporter.put(task);
1193         String accept = httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept");
1194         assertNotNull(accept, "Missing Accept header when retrieving artifact");
1195         assertTrue(
1196                 accept.contains("application/problem+json"),
1197                 "Expected Accept header to contain application/problem+json, but was: " + accept);
1198     }
1199 
1200     @Test
1201     protected void testPut_FromFile() throws Exception {
1202         File file = TestFileUtils.createTempFile("upload");
1203         RecordingTransportListener listener = new RecordingTransportListener();
1204         PutTask task =
1205                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataPath(file.toPath());
1206         transporter.put(task);
1207         assertEquals(0L, listener.getDataOffset());
1208         assertEquals(6L, listener.getDataLength());
1209         assertEquals(1, listener.getStartedCount());
1210         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1211         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1212         assertEquals(
1213                 String.valueOf(file.length()),
1214                 httpServer.getLogEntries().get(0).getRequestHeaders().get("Content-Length"));
1215     }
1216 
1217     @Test
1218     protected void testPut_EmptyResource() throws Exception {
1219         RecordingTransportListener listener = new RecordingTransportListener();
1220         PutTask task = new PutTask(URI.create("repo/file.txt")).setListener(listener);
1221         transporter.put(task);
1222         assertEquals(0L, listener.getDataOffset());
1223         assertEquals(0L, listener.getDataLength());
1224         // some transports may skip the upload for empty resources
1225         assertTrue(
1226                 listener.getStartedCount() <= 1,
1227                 "The transport should be started at most once but was started " + listener.getStartedCount()
1228                         + " times");
1229         assertEquals(0, listener.getProgressedCount());
1230         assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt")));
1231     }
1232 
1233     @Test
1234     protected void testPut_EncodedResourcePath() throws Exception {
1235         RecordingTransportListener listener = new RecordingTransportListener();
1236         PutTask task = new PutTask(URI.create("repo/some%20space.txt"))
1237                 .setListener(listener)
1238                 .setDataString("OK");
1239         transporter.put(task);
1240         assertEquals(0L, listener.getDataOffset());
1241         assertEquals(2L, listener.getDataLength());
1242         assertEquals(1, listener.getStartedCount());
1243         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1244         assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt")));
1245     }
1246 
1247     @Test
1248     protected void testPut_Authenticated_ExpectContinue() throws Exception {
1249         httpServer.setAuthentication("testuser", "testpass");
1250         auth = new AuthenticationBuilder()
1251                 .addUsername("testuser")
1252                 .addPassword("testpass")
1253                 .build();
1254         newTransporter(httpServer.getHttpUrl());
1255         RecordingTransportListener listener = new RecordingTransportListener();
1256         PutTask task =
1257                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1258         transporter.put(task);
1259         assertEquals(0L, listener.getDataOffset());
1260         assertEquals(6L, listener.getDataLength());
1261         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1262         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1263         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1264     }
1265 
1266     @Test
1267     protected void testPut_Authenticated_ExpectContinueBroken() throws Exception {
1268         // this makes OPTIONS recover, and have only 1 PUT (startedCount=1 as OPTIONS is not counted)
1269         session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true);
1270         httpServer.setAuthentication("testuser", "testpass");
1271         httpServer.setExpectSupport(HttpServer.ExpectContinue.BROKEN);
1272         auth = new AuthenticationBuilder()
1273                 .addUsername("testuser")
1274                 .addPassword("testpass")
1275                 .build();
1276         newTransporter(httpServer.getHttpUrl());
1277         RecordingTransportListener listener = new RecordingTransportListener();
1278         PutTask task =
1279                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1280         transporter.put(task);
1281         assertEquals(0L, listener.getDataOffset());
1282         assertEquals(6L, listener.getDataLength());
1283         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1284         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1285         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1286     }
1287 
1288     @Test
1289     protected void testPut_Authenticated_ExpectContinueRejected() throws Exception {
1290         httpServer.setAuthentication("testuser", "testpass");
1291         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
1292         auth = new AuthenticationBuilder()
1293                 .addUsername("testuser")
1294                 .addPassword("testpass")
1295                 .build();
1296         newTransporter(httpServer.getHttpUrl());
1297         RecordingTransportListener listener = new RecordingTransportListener();
1298         PutTask task =
1299                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1300         transporter.put(task);
1301         assertEquals(0L, listener.getDataOffset());
1302         assertEquals(6L, listener.getDataLength());
1303         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1304         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1305         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1306     }
1307 
1308     @Test
1309     protected void testPut_Authenticated_ExpectContinueDisabled() throws Exception {
1310         session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false);
1311         httpServer.setAuthentication("testuser", "testpass");
1312         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode
1313         auth = new AuthenticationBuilder()
1314                 .addUsername("testuser")
1315                 .addPassword("testpass")
1316                 .build();
1317         newTransporter(httpServer.getHttpUrl());
1318         RecordingTransportListener listener = new RecordingTransportListener();
1319         PutTask task =
1320                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1321         transporter.put(task);
1322         assertEquals(0L, listener.getDataOffset());
1323         assertEquals(6L, listener.getDataLength());
1324         assertEquals(
1325                 supportsPreemptiveAuth() ? 1 : 2,
1326                 listener.getStartedCount()); // w/ expectContinue enabled would have here 2/3
1327         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1328         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1329     }
1330 
1331     @Test
1332     protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception {
1333         Map<String, String> headers = new HashMap<>();
1334         headers.put("Expect", "100-continue");
1335         session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1336         httpServer.setAuthentication("testuser", "testpass");
1337         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
1338         auth = new AuthenticationBuilder()
1339                 .addUsername("testuser")
1340                 .addPassword("testpass")
1341                 .build();
1342         newTransporter(httpServer.getHttpUrl());
1343         RecordingTransportListener listener = new RecordingTransportListener();
1344         PutTask task =
1345                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1346         transporter.put(task);
1347         assertEquals(0L, listener.getDataOffset());
1348         assertEquals(6L, listener.getDataLength());
1349         assertEquals(1, listener.getStartedCount());
1350         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1351         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1352     }
1353 
1354     @Test
1355     protected void testPut_Unauthenticated() throws Exception {
1356         httpServer.setAuthentication("testuser", "testpass");
1357         RecordingTransportListener listener = new RecordingTransportListener();
1358         PutTask task =
1359                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1360         try {
1361             transporter.put(task);
1362             fail("Expected error");
1363         } catch (HttpTransporterException e) {
1364             assertEquals(401, e.getStatusCode());
1365             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1366         }
1367         assertEquals(0, listener.getStartedCount());
1368         assertEquals(0, listener.getProgressedCount());
1369     }
1370 
1371     @Test
1372     protected void testPut_ProxyAuthenticated() throws Exception {
1373         httpServer.setProxyAuthentication("testuser", "testpass");
1374         Authentication auth = new AuthenticationBuilder()
1375                 .addUsername("testuser")
1376                 .addPassword("testpass")
1377                 .build();
1378         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1379         newTransporter("http://bad.localhost:1/");
1380         RecordingTransportListener listener = new RecordingTransportListener();
1381         PutTask task =
1382                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1383         transporter.put(task);
1384         assertEquals(0L, listener.getDataOffset());
1385         assertEquals(6L, listener.getDataLength());
1386         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1387         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1388         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1389     }
1390 
1391     @Test
1392     protected void testPut_ProxyUnauthenticated() throws Exception {
1393         httpServer.setProxyAuthentication("testuser", "testpass");
1394         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1395         newTransporter("http://bad.localhost:1/");
1396         RecordingTransportListener listener = new RecordingTransportListener();
1397         PutTask task =
1398                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1399         try {
1400             transporter.put(task);
1401             fail("Expected error");
1402         } catch (HttpTransporterException e) {
1403             assertEquals(407, e.getStatusCode());
1404             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1405         }
1406         assertEquals(0, listener.getStartedCount());
1407         assertEquals(0, listener.getProgressedCount());
1408     }
1409 
1410     @Test
1411     protected void testPut_SSL() throws Exception {
1412         httpServer.addHttp2ConnectorWithMutualTLS();
1413         httpServer.setAuthentication("testuser", "testpass");
1414         auth = new AuthenticationBuilder()
1415                 .addUsername("testuser")
1416                 .addPassword("testpass")
1417                 .build();
1418         newTransporter(httpServer.getHttpsUrl());
1419         RecordingTransportListener listener = new RecordingTransportListener();
1420         PutTask task =
1421                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1422         transporter.put(task);
1423         assertEquals(0L, listener.getDataOffset());
1424         assertEquals(6L, listener.getDataLength());
1425         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1426         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1427         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1428         assertEquals(
1429                 HttpTransportProperty.SslProtocol.TLS_1_3,
1430                 listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
1431     }
1432 
1433     @Test
1434     protected void testPut_FileHandleLeak() throws Exception {
1435         for (int i = 0; i < 100; i++) {
1436             File src = TestFileUtils.createTempFile("upload");
1437             File dst = new File(repoDir, "file.txt");
1438             transporter.put(new PutTask(URI.create("repo/file.txt")).setDataPath(src.toPath()));
1439             assertTrue(src.delete(), i + ", " + src.getAbsolutePath());
1440             assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath());
1441         }
1442     }
1443 
1444     @Test
1445     protected void testPut_Closed() throws Exception {
1446         transporter.close();
1447         try {
1448             transporter.put(new PutTask(URI.create("repo/missing.txt")));
1449             fail("Expected error");
1450         } catch (IllegalStateException e) {
1451             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1452         }
1453     }
1454 
1455     @Test
1456     protected void testPut_StartCancelled() throws Exception {
1457         RecordingTransportListener listener = new RecordingTransportListener();
1458         listener.cancelStart();
1459         PutTask task =
1460                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1461         try {
1462             transporter.put(task);
1463             fail("Expected error");
1464         } catch (TransferCancelledException e) {
1465             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1466         }
1467         assertEquals(0L, listener.getDataOffset());
1468         assertEquals(6L, listener.getDataLength());
1469         assertEquals(1, listener.getStartedCount());
1470         assertEquals(0, listener.getProgressedCount());
1471     }
1472 
1473     @Test
1474     protected void testPut_ProgressCancelled() throws Exception {
1475         RecordingTransportListener listener = new RecordingTransportListener();
1476         listener.cancelProgress();
1477         PutTask task =
1478                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1479         try {
1480             transporter.put(task);
1481             fail("Expected error");
1482         } catch (TransferCancelledException e) {
1483             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1484         }
1485         assertEquals(0L, listener.getDataOffset());
1486         assertEquals(6L, listener.getDataLength());
1487         assertEquals(1, listener.getStartedCount());
1488         assertEquals(1, listener.getProgressedCount());
1489     }
1490 
1491     @Test
1492     protected void testGetPut_AuthCache() throws Exception {
1493         httpServer.setAuthentication("testuser", "testpass");
1494         auth = new AuthenticationBuilder()
1495                 .addUsername("testuser")
1496                 .addPassword("testpass")
1497                 .build();
1498         newTransporter(httpServer.getHttpUrl());
1499         GetTask get = new GetTask(URI.create("repo/file.txt"));
1500         transporter.get(get);
1501         RecordingTransportListener listener = new RecordingTransportListener();
1502         PutTask task =
1503                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1504         transporter.put(task);
1505         assertEquals(1, listener.getStartedCount());
1506     }
1507 
1508     @Test
1509     protected void testPut_PreemptiveIsDefault() throws Exception {
1510         httpServer.setAuthentication("testuser", "testpass");
1511         auth = new AuthenticationBuilder()
1512                 .addUsername("testuser")
1513                 .addPassword("testpass")
1514                 .build();
1515         newTransporter(httpServer.getHttpUrl());
1516         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1517         transporter.put(task);
1518         assertEquals(
1519                 supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size()); // put w/ auth
1520     }
1521 
1522     @Test
1523     protected void testPut_AuthCache() throws Exception {
1524         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH, false);
1525         httpServer.setAuthentication("testuser", "testpass");
1526         auth = new AuthenticationBuilder()
1527                 .addUsername("testuser")
1528                 .addPassword("testpass")
1529                 .build();
1530         newTransporter(httpServer.getHttpUrl());
1531         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1532         transporter.put(task);
1533         assertEquals(2, httpServer.getLogEntries().size()); // put (challenged) + put w/ auth
1534         httpServer.getLogEntries().clear();
1535         task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1536         transporter.put(task);
1537         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1538     }
1539 
1540     @Test
1541     protected void testPut_AuthCache_Preemptive() throws Exception {
1542         httpServer.setAuthentication("testuser", "testpass");
1543         auth = new AuthenticationBuilder()
1544                 .addUsername("testuser")
1545                 .addPassword("testpass")
1546                 .build();
1547         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1548         newTransporter(httpServer.getHttpUrl());
1549         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1550         transporter.put(task);
1551         assertEquals(
1552                 supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size()); // put w/ auth
1553         httpServer.getLogEntries().clear();
1554         task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1555         transporter.put(task);
1556         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1557     }
1558 
1559     @Test
1560     protected void testPut_WithResponseBody() throws Exception {
1561         httpServer.setAuthentication("testuser", "testpass");
1562         httpServer.setResponseBodyForPut("Some dummy response body");
1563         auth = new AuthenticationBuilder()
1564                 .addUsername("testuser")
1565                 .addPassword("testpass")
1566                 .build();
1567         newTransporter(httpServer.getHttpUrl());
1568         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1569         transporter.put(task);
1570         // this leads to stuck threads in some transporters if the response body is not consumed
1571     }
1572 
1573     @Test
1574     @Timeout(20)
1575     protected void testConcurrency() throws Exception {
1576         httpServer.setAuthentication("testuser", "testpass");
1577         auth = new AuthenticationBuilder()
1578                 .addUsername("testuser")
1579                 .addPassword("testpass")
1580                 .build();
1581         newTransporter(httpServer.getHttpUrl());
1582         final AtomicReference<Throwable> error = new AtomicReference<>();
1583         Thread[] threads = new Thread[20];
1584         for (int i = 0; i < threads.length; i++) {
1585             final String path = "repo/file.txt?i=" + i;
1586             threads[i] = new Thread(() -> {
1587                 try {
1588                     for (int j = 0; j < 100; j++) {
1589                         GetTask task = new GetTask(URI.create(path));
1590                         transporter.get(task);
1591                         assertEquals("test", task.getDataString());
1592                     }
1593                 } catch (Throwable t) {
1594                     error.compareAndSet(null, t);
1595                     System.err.println(path);
1596                     t.printStackTrace();
1597                 }
1598             });
1599             threads[i].setName("Task-" + i);
1600         }
1601         for (Thread thread : threads) {
1602             thread.start();
1603         }
1604         for (Thread thread : threads) {
1605             thread.join();
1606         }
1607         assertNull(error.get(), String.valueOf(error.get()));
1608     }
1609 
1610     @Test
1611     @Timeout(10)
1612     protected void testConnectTimeout() throws Exception {
1613         session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, 100);
1614         int port = 1;
1615         newTransporter("http://localhost:" + port);
1616         try {
1617             transporter.get(new GetTask(URI.create("repo/file.txt")));
1618             fail("Expected error");
1619         } catch (Exception e) {
1620             // impl specific "timeout" exception
1621             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1622         }
1623     }
1624 
1625     @Test
1626     @Timeout(10)
1627     protected void testRequestTimeout() throws Exception {
1628         session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 100);
1629         ServerSocket server = new ServerSocket(0);
1630         try (server) {
1631             newTransporter("http://localhost:" + server.getLocalPort());
1632             try {
1633                 transporter.get(new GetTask(URI.create("repo/file.txt")));
1634                 fail("Expected error");
1635             } catch (Exception e) {
1636                 assertTrue(e.getClass().getSimpleName().contains("Timeout"));
1637                 assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1638             }
1639         }
1640     }
1641 
1642     @Test
1643     protected void testUserAgent() throws Exception {
1644         session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1645         newTransporter(httpServer.getHttpUrl());
1646         transporter.get(new GetTask(URI.create("repo/file.txt")));
1647         assertEquals(1, httpServer.getLogEntries().size());
1648         for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1649             assertEquals("SomeTest/1.0", log.getRequestHeaders().get("User-Agent"));
1650         }
1651     }
1652 
1653     @Test
1654     protected void testCustomHeaders() throws Exception {
1655         Map<String, String> headers = new HashMap<>();
1656         headers.put("User-Agent", "Custom/1.0");
1657         headers.put("X-CustomHeader", "Custom-Value");
1658         session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1659         session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1660         newTransporter(httpServer.getHttpUrl());
1661         transporter.get(new GetTask(URI.create("repo/file.txt")));
1662         assertEquals(1, httpServer.getLogEntries().size());
1663         for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1664             for (Map.Entry<String, String> entry : headers.entrySet()) {
1665                 assertEquals(entry.getValue(), log.getRequestHeaders().get(entry.getKey()), entry.getKey());
1666             }
1667         }
1668     }
1669 
1670     @Test
1671     protected void testServerAuthScope_NotUsedForProxy() throws Exception {
1672         String username = "testuser", password = "testpass";
1673         httpServer.setProxyAuthentication(username, password);
1674         auth = new AuthenticationBuilder()
1675                 .addUsername(username)
1676                 .addPassword(password)
1677                 .build();
1678         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1679         newTransporter("http://" + httpServer.getHost() + ":12/");
1680         try {
1681             transporter.get(new GetTask(URI.create("repo/file.txt")));
1682             fail("Server auth must not be used as proxy auth");
1683         } catch (HttpTransporterException e) {
1684             assertEquals(407, e.getStatusCode());
1685         } catch (IOException e) {
1686             // accepted as well: point is to fail
1687         }
1688     }
1689 
1690     @Test
1691     protected void testProxyAuthScope_NotUsedForServer() throws Exception {
1692         String username = "testuser", password = "testpass";
1693         httpServer.setAuthentication(username, password);
1694         Authentication auth = new AuthenticationBuilder()
1695                 .addUsername(username)
1696                 .addPassword(password)
1697                 .build();
1698         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1699         newTransporter("http://" + httpServer.getHost() + ":12/");
1700         try {
1701             transporter.get(new GetTask(URI.create("repo/file.txt")));
1702             fail("Proxy auth must not be used as server auth");
1703         } catch (HttpTransporterException e) {
1704             assertEquals(401, e.getStatusCode());
1705         } catch (IOException e) {
1706             // accepted as well: point is to fail
1707         }
1708     }
1709 
1710     @Test
1711     protected void testAuthSchemeReuse() throws Exception {
1712         httpServer.setAuthentication("testuser", "testpass");
1713         httpServer.setProxyAuthentication("proxyuser", "proxypass");
1714         session.setCache(new DefaultRepositoryCache());
1715         auth = new AuthenticationBuilder()
1716                 .addUsername("testuser")
1717                 .addPassword("testpass")
1718                 .build();
1719         Authentication auth = new AuthenticationBuilder()
1720                 .addUsername("proxyuser")
1721                 .addPassword("proxypass")
1722                 .build();
1723         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1724         newTransporter("http://bad.localhost:1/");
1725         GetTask task = new GetTask(URI.create("repo/file.txt"));
1726         transporter.get(task);
1727         assertEquals("test", task.getDataString());
1728         assertEquals(3, httpServer.getLogEntries().size());
1729         httpServer.getLogEntries().clear();
1730         newTransporter("http://bad.localhost:1/");
1731         task = new GetTask(URI.create("repo/file.txt"));
1732         transporter.get(task);
1733         assertEquals("test", task.getDataString());
1734         assertEquals(1, httpServer.getLogEntries().size());
1735         assertNotNull(httpServer.getLogEntries().get(0).getRequestHeaders().get("Authorization"));
1736         assertNotNull(httpServer.getLogEntries().get(0).getRequestHeaders().get("Proxy-Authorization"));
1737     }
1738 
1739     @Test
1740     protected void testAuthSchemePreemptive() throws Exception {
1741         httpServer.setAuthentication("testuser", "testpass");
1742         session.setCache(new DefaultRepositoryCache());
1743         auth = new AuthenticationBuilder()
1744                 .addUsername("testuser")
1745                 .addPassword("testpass")
1746                 .build();
1747 
1748         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, false);
1749         newTransporter(httpServer.getHttpUrl());
1750         GetTask task = new GetTask(URI.create("repo/file.txt"));
1751         transporter.get(task);
1752         assertEquals("test", task.getDataString());
1753         // there ARE challenge round-trips
1754         assertEquals(2, httpServer.getLogEntries().size());
1755 
1756         httpServer.getLogEntries().clear();
1757 
1758         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1759         newTransporter(httpServer.getHttpUrl());
1760         task = new GetTask(URI.create("repo/file.txt"));
1761         transporter.get(task);
1762         assertEquals("test", task.getDataString());
1763         // there are (potentially) NO challenge round-trips, all goes through at first
1764         assertEquals(
1765                 supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size());
1766     }
1767 
1768     @Test
1769     void testInit_BadProtocol() {
1770         assertThrows(NoTransporterException.class, () -> newTransporter("bad:/void"));
1771     }
1772 
1773     @Test
1774     void testInit_BadUrl() {
1775         assertThrows(NoTransporterException.class, () -> newTransporter("http://localhost:NaN"));
1776     }
1777 
1778     @Test
1779     void testInit_CaseInsensitiveProtocol() throws Exception {
1780         newTransporter("http://localhost");
1781         newTransporter("HTTP://localhost");
1782         newTransporter("Http://localhost");
1783         newTransporter("https://localhost");
1784         newTransporter("HTTPS://localhost");
1785         newTransporter("HttpS://localhost");
1786     }
1787 }