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