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         String payload = "upload";
861         PutTask task =
862                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString(payload);
863         transporter.put(task);
864         assertEquals(0L, listener.getDataOffset());
865         assertEquals(6L, listener.getDataLength());
866         assertEquals(1, listener.getStartedCount());
867         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
868         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
869         assertEquals(
870                 String.valueOf(payload.getBytes(StandardCharsets.UTF_8).length),
871                 httpServer.getLogEntries().get(0).getRequestHeaders().get("Content-Length"));
872     }
873 
874     @Test
875     protected void testPut_FromFile() throws Exception {
876         File file = TestFileUtils.createTempFile("upload");
877         RecordingTransportListener listener = new RecordingTransportListener();
878         PutTask task =
879                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataPath(file.toPath());
880         transporter.put(task);
881         assertEquals(0L, listener.getDataOffset());
882         assertEquals(6L, listener.getDataLength());
883         assertEquals(1, listener.getStartedCount());
884         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
885         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
886         assertEquals(
887                 String.valueOf(file.length()),
888                 httpServer.getLogEntries().get(0).getRequestHeaders().get("Content-Length"));
889     }
890 
891     @Test
892     protected void testPut_EmptyResource() throws Exception {
893         RecordingTransportListener listener = new RecordingTransportListener();
894         PutTask task = new PutTask(URI.create("repo/file.txt")).setListener(listener);
895         transporter.put(task);
896         assertEquals(0L, listener.getDataOffset());
897         assertEquals(0L, listener.getDataLength());
898         // some transports may skip the upload for empty resources
899         assertTrue(
900                 listener.getStartedCount() <= 1,
901                 "The transport should be started at most once but was started " + listener.getStartedCount()
902                         + " times");
903         assertEquals(0, listener.getProgressedCount());
904         assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt")));
905     }
906 
907     @Test
908     protected void testPut_EncodedResourcePath() throws Exception {
909         RecordingTransportListener listener = new RecordingTransportListener();
910         PutTask task = new PutTask(URI.create("repo/some%20space.txt"))
911                 .setListener(listener)
912                 .setDataString("OK");
913         transporter.put(task);
914         assertEquals(0L, listener.getDataOffset());
915         assertEquals(2L, listener.getDataLength());
916         assertEquals(1, listener.getStartedCount());
917         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
918         assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt")));
919     }
920 
921     @Test
922     protected void testPut_Authenticated_ExpectContinue() throws Exception {
923         httpServer.setAuthentication("testuser", "testpass");
924         auth = new AuthenticationBuilder()
925                 .addUsername("testuser")
926                 .addPassword("testpass")
927                 .build();
928         newTransporter(httpServer.getHttpUrl());
929         RecordingTransportListener listener = new RecordingTransportListener();
930         PutTask task =
931                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
932         transporter.put(task);
933         assertEquals(0L, listener.getDataOffset());
934         assertEquals(6L, listener.getDataLength());
935         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
936         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
937         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
938     }
939 
940     @Test
941     protected void testPut_Authenticated_ExpectContinueBroken() throws Exception {
942         // this makes OPTIONS recover, and have only 1 PUT (startedCount=1 as OPTIONS is not counted)
943         session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true);
944         httpServer.setAuthentication("testuser", "testpass");
945         httpServer.setExpectSupport(HttpServer.ExpectContinue.BROKEN);
946         auth = new AuthenticationBuilder()
947                 .addUsername("testuser")
948                 .addPassword("testpass")
949                 .build();
950         newTransporter(httpServer.getHttpUrl());
951         RecordingTransportListener listener = new RecordingTransportListener();
952         PutTask task =
953                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
954         transporter.put(task);
955         assertEquals(0L, listener.getDataOffset());
956         assertEquals(6L, listener.getDataLength());
957         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
958         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
959         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
960     }
961 
962     @Test
963     protected void testPut_Authenticated_ExpectContinueRejected() throws Exception {
964         httpServer.setAuthentication("testuser", "testpass");
965         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
966         auth = new AuthenticationBuilder()
967                 .addUsername("testuser")
968                 .addPassword("testpass")
969                 .build();
970         newTransporter(httpServer.getHttpUrl());
971         RecordingTransportListener listener = new RecordingTransportListener();
972         PutTask task =
973                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
974         transporter.put(task);
975         assertEquals(0L, listener.getDataOffset());
976         assertEquals(6L, listener.getDataLength());
977         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
978         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
979         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
980     }
981 
982     @Test
983     protected void testPut_Authenticated_ExpectContinueDisabled() throws Exception {
984         session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false);
985         httpServer.setAuthentication("testuser", "testpass");
986         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode
987         auth = new AuthenticationBuilder()
988                 .addUsername("testuser")
989                 .addPassword("testpass")
990                 .build();
991         newTransporter(httpServer.getHttpUrl());
992         RecordingTransportListener listener = new RecordingTransportListener();
993         PutTask task =
994                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
995         transporter.put(task);
996         assertEquals(0L, listener.getDataOffset());
997         assertEquals(6L, listener.getDataLength());
998         assertEquals(
999                 supportsPreemptiveAuth() ? 1 : 2,
1000                 listener.getStartedCount()); // w/ expectContinue enabled would have here 2/3
1001         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1002         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1003     }
1004 
1005     @Test
1006     protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception {
1007         Map<String, String> headers = new HashMap<>();
1008         headers.put("Expect", "100-continue");
1009         session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1010         httpServer.setAuthentication("testuser", "testpass");
1011         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
1012         auth = new AuthenticationBuilder()
1013                 .addUsername("testuser")
1014                 .addPassword("testpass")
1015                 .build();
1016         newTransporter(httpServer.getHttpUrl());
1017         RecordingTransportListener listener = new RecordingTransportListener();
1018         PutTask task =
1019                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1020         transporter.put(task);
1021         assertEquals(0L, listener.getDataOffset());
1022         assertEquals(6L, listener.getDataLength());
1023         assertEquals(1, listener.getStartedCount());
1024         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1025         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1026     }
1027 
1028     @Test
1029     protected void testPut_Unauthenticated() throws Exception {
1030         httpServer.setAuthentication("testuser", "testpass");
1031         RecordingTransportListener listener = new RecordingTransportListener();
1032         PutTask task =
1033                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1034         try {
1035             transporter.put(task);
1036             fail("Expected error");
1037         } catch (HttpTransporterException e) {
1038             assertEquals(401, e.getStatusCode());
1039             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1040         }
1041         assertEquals(0, listener.getStartedCount());
1042         assertEquals(0, listener.getProgressedCount());
1043     }
1044 
1045     @Test
1046     protected void testPut_ProxyAuthenticated() throws Exception {
1047         httpServer.setProxyAuthentication("testuser", "testpass");
1048         Authentication auth = new AuthenticationBuilder()
1049                 .addUsername("testuser")
1050                 .addPassword("testpass")
1051                 .build();
1052         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1053         newTransporter("http://bad.localhost:1/");
1054         RecordingTransportListener listener = new RecordingTransportListener();
1055         PutTask task =
1056                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1057         transporter.put(task);
1058         assertEquals(0L, listener.getDataOffset());
1059         assertEquals(6L, listener.getDataLength());
1060         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1061         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1062         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1063     }
1064 
1065     @Test
1066     protected void testPut_ProxyUnauthenticated() throws Exception {
1067         httpServer.setProxyAuthentication("testuser", "testpass");
1068         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1069         newTransporter("http://bad.localhost:1/");
1070         RecordingTransportListener listener = new RecordingTransportListener();
1071         PutTask task =
1072                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1073         try {
1074             transporter.put(task);
1075             fail("Expected error");
1076         } catch (HttpTransporterException e) {
1077             assertEquals(407, e.getStatusCode());
1078             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1079         }
1080         assertEquals(0, listener.getStartedCount());
1081         assertEquals(0, listener.getProgressedCount());
1082     }
1083 
1084     @Test
1085     protected void testPut_SSL() throws Exception {
1086         httpServer.addSslConnector();
1087         httpServer.setAuthentication("testuser", "testpass");
1088         auth = new AuthenticationBuilder()
1089                 .addUsername("testuser")
1090                 .addPassword("testpass")
1091                 .build();
1092         newTransporter(httpServer.getHttpsUrl());
1093         RecordingTransportListener listener = new RecordingTransportListener();
1094         PutTask task =
1095                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1096         transporter.put(task);
1097         assertEquals(0L, listener.getDataOffset());
1098         assertEquals(6L, listener.getDataLength());
1099         assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1100         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1101         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1102     }
1103 
1104     @Test
1105     protected void testPut_FileHandleLeak() throws Exception {
1106         for (int i = 0; i < 100; i++) {
1107             File src = TestFileUtils.createTempFile("upload");
1108             File dst = new File(repoDir, "file.txt");
1109             transporter.put(new PutTask(URI.create("repo/file.txt")).setDataPath(src.toPath()));
1110             assertTrue(src.delete(), i + ", " + src.getAbsolutePath());
1111             assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath());
1112         }
1113     }
1114 
1115     @Test
1116     protected void testPut_Closed() throws Exception {
1117         transporter.close();
1118         try {
1119             transporter.put(new PutTask(URI.create("repo/missing.txt")));
1120             fail("Expected error");
1121         } catch (IllegalStateException e) {
1122             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1123         }
1124     }
1125 
1126     @Test
1127     protected void testPut_StartCancelled() throws Exception {
1128         RecordingTransportListener listener = new RecordingTransportListener();
1129         listener.cancelStart();
1130         PutTask task =
1131                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1132         try {
1133             transporter.put(task);
1134             fail("Expected error");
1135         } catch (TransferCancelledException e) {
1136             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1137         }
1138         assertEquals(0L, listener.getDataOffset());
1139         assertEquals(6L, listener.getDataLength());
1140         assertEquals(1, listener.getStartedCount());
1141         assertEquals(0, listener.getProgressedCount());
1142     }
1143 
1144     @Test
1145     protected void testPut_ProgressCancelled() throws Exception {
1146         RecordingTransportListener listener = new RecordingTransportListener();
1147         listener.cancelProgress();
1148         PutTask task =
1149                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1150         try {
1151             transporter.put(task);
1152             fail("Expected error");
1153         } catch (TransferCancelledException e) {
1154             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1155         }
1156         assertEquals(0L, listener.getDataOffset());
1157         assertEquals(6L, listener.getDataLength());
1158         assertEquals(1, listener.getStartedCount());
1159         assertEquals(1, listener.getProgressedCount());
1160     }
1161 
1162     @Test
1163     protected void testGetPut_AuthCache() throws Exception {
1164         httpServer.setAuthentication("testuser", "testpass");
1165         auth = new AuthenticationBuilder()
1166                 .addUsername("testuser")
1167                 .addPassword("testpass")
1168                 .build();
1169         newTransporter(httpServer.getHttpUrl());
1170         GetTask get = new GetTask(URI.create("repo/file.txt"));
1171         transporter.get(get);
1172         RecordingTransportListener listener = new RecordingTransportListener();
1173         PutTask task =
1174                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1175         transporter.put(task);
1176         assertEquals(1, listener.getStartedCount());
1177     }
1178 
1179     @Test
1180     protected void testPut_PreemptiveIsDefault() throws Exception {
1181         httpServer.setAuthentication("testuser", "testpass");
1182         auth = new AuthenticationBuilder()
1183                 .addUsername("testuser")
1184                 .addPassword("testpass")
1185                 .build();
1186         newTransporter(httpServer.getHttpUrl());
1187         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1188         transporter.put(task);
1189         assertEquals(
1190                 supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size()); // put w/ auth
1191     }
1192 
1193     @Test
1194     protected void testPut_AuthCache() throws Exception {
1195         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH, false);
1196         httpServer.setAuthentication("testuser", "testpass");
1197         auth = new AuthenticationBuilder()
1198                 .addUsername("testuser")
1199                 .addPassword("testpass")
1200                 .build();
1201         newTransporter(httpServer.getHttpUrl());
1202         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1203         transporter.put(task);
1204         assertEquals(2, httpServer.getLogEntries().size()); // put (challenged) + put w/ auth
1205         httpServer.getLogEntries().clear();
1206         task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1207         transporter.put(task);
1208         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1209     }
1210 
1211     @Test
1212     protected void testPut_AuthCache_Preemptive() throws Exception {
1213         httpServer.setAuthentication("testuser", "testpass");
1214         auth = new AuthenticationBuilder()
1215                 .addUsername("testuser")
1216                 .addPassword("testpass")
1217                 .build();
1218         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1219         newTransporter(httpServer.getHttpUrl());
1220         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1221         transporter.put(task);
1222         assertEquals(
1223                 supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size()); // put w/ auth
1224         httpServer.getLogEntries().clear();
1225         task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1226         transporter.put(task);
1227         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1228     }
1229 
1230     @Test
1231     @Timeout(20)
1232     protected void testConcurrency() throws Exception {
1233         httpServer.setAuthentication("testuser", "testpass");
1234         auth = new AuthenticationBuilder()
1235                 .addUsername("testuser")
1236                 .addPassword("testpass")
1237                 .build();
1238         newTransporter(httpServer.getHttpUrl());
1239         final AtomicReference<Throwable> error = new AtomicReference<>();
1240         Thread[] threads = new Thread[20];
1241         for (int i = 0; i < threads.length; i++) {
1242             final String path = "repo/file.txt?i=" + i;
1243             threads[i] = new Thread(() -> {
1244                 try {
1245                     for (int j = 0; j < 100; j++) {
1246                         GetTask task = new GetTask(URI.create(path));
1247                         transporter.get(task);
1248                         assertEquals("test", task.getDataString());
1249                     }
1250                 } catch (Throwable t) {
1251                     error.compareAndSet(null, t);
1252                     System.err.println(path);
1253                     t.printStackTrace();
1254                 }
1255             });
1256             threads[i].setName("Task-" + i);
1257         }
1258         for (Thread thread : threads) {
1259             thread.start();
1260         }
1261         for (Thread thread : threads) {
1262             thread.join();
1263         }
1264         assertNull(error.get(), String.valueOf(error.get()));
1265     }
1266 
1267     @Test
1268     @Timeout(10)
1269     protected void testConnectTimeout() throws Exception {
1270         session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, 100);
1271         int port = 1;
1272         newTransporter("http://localhost:" + port);
1273         try {
1274             transporter.get(new GetTask(URI.create("repo/file.txt")));
1275             fail("Expected error");
1276         } catch (Exception e) {
1277             // impl specific "timeout" exception
1278             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1279         }
1280     }
1281 
1282     @Test
1283     @Timeout(10)
1284     protected void testRequestTimeout() throws Exception {
1285         session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 100);
1286         ServerSocket server = new ServerSocket(0);
1287         try (server) {
1288             newTransporter("http://localhost:" + server.getLocalPort());
1289             try {
1290                 transporter.get(new GetTask(URI.create("repo/file.txt")));
1291                 fail("Expected error");
1292             } catch (Exception e) {
1293                 assertTrue(e.getClass().getSimpleName().contains("Timeout"));
1294                 assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1295             }
1296         }
1297     }
1298 
1299     @Test
1300     protected void testUserAgent() throws Exception {
1301         session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1302         newTransporter(httpServer.getHttpUrl());
1303         transporter.get(new GetTask(URI.create("repo/file.txt")));
1304         assertEquals(1, httpServer.getLogEntries().size());
1305         for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1306             assertEquals("SomeTest/1.0", log.getRequestHeaders().get("User-Agent"));
1307         }
1308     }
1309 
1310     @Test
1311     protected void testCustomHeaders() throws Exception {
1312         Map<String, String> headers = new HashMap<>();
1313         headers.put("User-Agent", "Custom/1.0");
1314         headers.put("X-CustomHeader", "Custom-Value");
1315         session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1316         session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1317         newTransporter(httpServer.getHttpUrl());
1318         transporter.get(new GetTask(URI.create("repo/file.txt")));
1319         assertEquals(1, httpServer.getLogEntries().size());
1320         for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1321             for (Map.Entry<String, String> entry : headers.entrySet()) {
1322                 assertEquals(entry.getValue(), log.getRequestHeaders().get(entry.getKey()), entry.getKey());
1323             }
1324         }
1325     }
1326 
1327     @Test
1328     protected void testServerAuthScope_NotUsedForProxy() throws Exception {
1329         String username = "testuser", password = "testpass";
1330         httpServer.setProxyAuthentication(username, password);
1331         auth = new AuthenticationBuilder()
1332                 .addUsername(username)
1333                 .addPassword(password)
1334                 .build();
1335         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1336         newTransporter("http://" + httpServer.getHost() + ":12/");
1337         try {
1338             transporter.get(new GetTask(URI.create("repo/file.txt")));
1339             fail("Server auth must not be used as proxy auth");
1340         } catch (HttpTransporterException e) {
1341             assertEquals(407, e.getStatusCode());
1342         } catch (IOException e) {
1343             // accepted as well: point is to fail
1344         }
1345     }
1346 
1347     @Test
1348     protected void testProxyAuthScope_NotUsedForServer() throws Exception {
1349         String username = "testuser", password = "testpass";
1350         httpServer.setAuthentication(username, password);
1351         Authentication auth = new AuthenticationBuilder()
1352                 .addUsername(username)
1353                 .addPassword(password)
1354                 .build();
1355         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1356         newTransporter("http://" + httpServer.getHost() + ":12/");
1357         try {
1358             transporter.get(new GetTask(URI.create("repo/file.txt")));
1359             fail("Proxy auth must not be used as server auth");
1360         } catch (HttpTransporterException e) {
1361             assertEquals(401, e.getStatusCode());
1362         } catch (IOException e) {
1363             // accepted as well: point is to fail
1364         }
1365     }
1366 
1367     @Test
1368     protected void testAuthSchemeReuse() throws Exception {
1369         httpServer.setAuthentication("testuser", "testpass");
1370         httpServer.setProxyAuthentication("proxyuser", "proxypass");
1371         session.setCache(new DefaultRepositoryCache());
1372         auth = new AuthenticationBuilder()
1373                 .addUsername("testuser")
1374                 .addPassword("testpass")
1375                 .build();
1376         Authentication auth = new AuthenticationBuilder()
1377                 .addUsername("proxyuser")
1378                 .addPassword("proxypass")
1379                 .build();
1380         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1381         newTransporter("http://bad.localhost:1/");
1382         GetTask task = new GetTask(URI.create("repo/file.txt"));
1383         transporter.get(task);
1384         assertEquals("test", task.getDataString());
1385         assertEquals(3, httpServer.getLogEntries().size());
1386         httpServer.getLogEntries().clear();
1387         newTransporter("http://bad.localhost:1/");
1388         task = new GetTask(URI.create("repo/file.txt"));
1389         transporter.get(task);
1390         assertEquals("test", task.getDataString());
1391         assertEquals(1, httpServer.getLogEntries().size());
1392         assertNotNull(httpServer.getLogEntries().get(0).getRequestHeaders().get("Authorization"));
1393         assertNotNull(httpServer.getLogEntries().get(0).getRequestHeaders().get("Proxy-Authorization"));
1394     }
1395 
1396     @Test
1397     protected void testAuthSchemePreemptive() throws Exception {
1398         httpServer.setAuthentication("testuser", "testpass");
1399         session.setCache(new DefaultRepositoryCache());
1400         auth = new AuthenticationBuilder()
1401                 .addUsername("testuser")
1402                 .addPassword("testpass")
1403                 .build();
1404 
1405         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, false);
1406         newTransporter(httpServer.getHttpUrl());
1407         GetTask task = new GetTask(URI.create("repo/file.txt"));
1408         transporter.get(task);
1409         assertEquals("test", task.getDataString());
1410         // there ARE challenge round-trips
1411         assertEquals(2, httpServer.getLogEntries().size());
1412 
1413         httpServer.getLogEntries().clear();
1414 
1415         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1416         newTransporter(httpServer.getHttpUrl());
1417         task = new GetTask(URI.create("repo/file.txt"));
1418         transporter.get(task);
1419         assertEquals("test", task.getDataString());
1420         // there are (potentially) NO challenge round-trips, all goes through at first
1421         assertEquals(
1422                 supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size());
1423     }
1424 
1425     @Test
1426     void testInit_BadProtocol() {
1427         assertThrows(NoTransporterException.class, () -> newTransporter("bad:/void"));
1428     }
1429 
1430     @Test
1431     void testInit_BadUrl() {
1432         assertThrows(NoTransporterException.class, () -> newTransporter("http://localhost:NaN"));
1433     }
1434 
1435     @Test
1436     void testInit_CaseInsensitiveProtocol() throws Exception {
1437         newTransporter("http://localhost");
1438         newTransporter("HTTP://localhost");
1439         newTransporter("Http://localhost");
1440         newTransporter("https://localhost");
1441         newTransporter("HTTPS://localhost");
1442         newTransporter("HttpS://localhost");
1443     }
1444 }