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