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