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