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