001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.internal.test.util.http;
020
021import java.io.File;
022import java.io.FileNotFoundException;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.UncheckedIOException;
026import java.net.ServerSocket;
027import java.net.URI;
028import java.net.URL;
029import java.nio.charset.StandardCharsets;
030import java.nio.file.Files;
031import java.nio.file.Path;
032import java.nio.file.Paths;
033import java.nio.file.StandardCopyOption;
034import java.util.HashMap;
035import java.util.Map;
036import java.util.concurrent.atomic.AtomicReference;
037import java.util.function.Supplier;
038
039import org.eclipse.aether.ConfigurationProperties;
040import org.eclipse.aether.DefaultRepositoryCache;
041import org.eclipse.aether.DefaultRepositorySystemSession;
042import org.eclipse.aether.DefaultSessionData;
043import org.eclipse.aether.internal.impl.transport.http.DefaultChecksumExtractor;
044import org.eclipse.aether.internal.impl.transport.http.Nx2ChecksumExtractor;
045import org.eclipse.aether.internal.impl.transport.http.XChecksumExtractor;
046import org.eclipse.aether.internal.test.util.TestFileUtils;
047import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager;
048import org.eclipse.aether.repository.Authentication;
049import org.eclipse.aether.repository.Proxy;
050import org.eclipse.aether.repository.RemoteRepository;
051import org.eclipse.aether.spi.connector.transport.GetTask;
052import org.eclipse.aether.spi.connector.transport.PeekTask;
053import org.eclipse.aether.spi.connector.transport.PutTask;
054import org.eclipse.aether.spi.connector.transport.Transporter;
055import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
056import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractorStrategy;
057import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
058import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
059import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory;
060import org.eclipse.aether.spi.connector.transport.http.RFC9457.HttpRFC9457Exception;
061import org.eclipse.aether.transfer.NoTransporterException;
062import org.eclipse.aether.transfer.TransferCancelledException;
063import org.eclipse.aether.util.repository.AuthenticationBuilder;
064import org.junit.jupiter.api.AfterEach;
065import org.junit.jupiter.api.BeforeEach;
066import org.junit.jupiter.api.Test;
067import org.junit.jupiter.api.TestInfo;
068import org.junit.jupiter.api.Timeout;
069
070import static java.util.Objects.requireNonNull;
071import static org.junit.jupiter.api.Assertions.assertEquals;
072import static org.junit.jupiter.api.Assertions.assertNotNull;
073import static org.junit.jupiter.api.Assertions.assertNull;
074import static org.junit.jupiter.api.Assertions.assertThrows;
075import static org.junit.jupiter.api.Assertions.assertTrue;
076import static org.junit.jupiter.api.Assertions.fail;
077
078/**
079 * Common set of tests against Http transporter.
080 */
081@SuppressWarnings({"checkstyle:MethodName"})
082public class HttpTransporterTest {
083
084    protected static final Path KEY_STORE_PATH = Paths.get("target/keystore");
085
086    protected static final Path KEY_STORE_SELF_SIGNED_PATH = Paths.get("target/keystore-self-signed");
087
088    protected static final Path TRUST_STORE_PATH = Paths.get("target/trustStore");
089
090    static {
091        // Warning: "cross connected" with HttpServer!
092        System.setProperty(
093                "javax.net.ssl.trustStore", KEY_STORE_PATH.toAbsolutePath().toString());
094        System.setProperty("javax.net.ssl.trustStorePassword", "server-pwd");
095        System.setProperty(
096                "javax.net.ssl.keyStore", TRUST_STORE_PATH.toAbsolutePath().toString());
097        System.setProperty("javax.net.ssl.keyStorePassword", "client-pwd");
098
099        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}