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:MagicNumber", "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(file.lastModified(), OLD_FILE_TIMESTAMP);
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(e.getPayload().getStatus(), 403);
491            assertEquals(e.getPayload().getTitle(), "You do not have enough credit.");
492            assertEquals(e.getPayload().getDetail(), "Your current balance is 30, but that costs 50.");
493            assertEquals(e.getPayload().getInstance(), URI.create("/account/12345/msgs/abc"));
494        }
495    }
496
497    @Test
498    protected void testGet_RFC9457Response_with_missing_fields() throws Exception {
499        try {
500            transporter.get(new GetTask(URI.create("rfc9457/missing_fields.txt")));
501            fail("Expected error");
502        } catch (HttpRFC9457Exception e) {
503            assertEquals(403, e.getStatusCode());
504            assertEquals(e.getPayload().getType(), URI.create("about:blank"));
505            assertNull(e.getPayload().getStatus());
506            assertNull(e.getPayload().getTitle());
507            assertNull(e.getPayload().getDetail());
508            assertNull(e.getPayload().getInstance());
509        }
510    }
511
512    @Test
513    protected void testGet_SSL() throws Exception {
514        httpServer.addSslConnector();
515        newTransporter(httpServer.getHttpsUrl());
516        RecordingTransportListener listener = new RecordingTransportListener();
517        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
518        transporter.get(task);
519        assertEquals("test", task.getDataString());
520        assertEquals(0L, listener.getDataOffset());
521        assertEquals(4L, listener.getDataLength());
522        assertEquals(1, listener.getStartedCount());
523        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
524        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
525    }
526
527    @Test
528    protected void testGet_SSL_WithServerErrors() throws Exception {
529        httpServer.setServerErrorsBeforeWorks(1);
530        httpServer.addSslConnector();
531        newTransporter(httpServer.getHttpsUrl());
532        for (int i = 1; i < 3; i++) {
533            try {
534                RecordingTransportListener listener = new RecordingTransportListener();
535                GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
536                transporter.get(task);
537                assertEquals("test", task.getDataString());
538                assertEquals(0L, listener.getDataOffset());
539                assertEquals(4L, listener.getDataLength());
540                assertEquals(1, listener.getStartedCount());
541                assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
542                assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
543            } catch (HttpTransporterException e) {
544                assertEquals(500, e.getStatusCode());
545            }
546        }
547    }
548
549    @Test
550    protected void testGet_HTTPS_Unknown_SecurityMode() throws Exception {
551        session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE, "unknown");
552        httpServer.addSelfSignedSslConnector();
553        try {
554            newTransporter(httpServer.getHttpsUrl());
555            fail("Unsupported security mode");
556        } catch (IllegalArgumentException a) {
557            // good
558        }
559    }
560
561    @Test
562    protected void testGet_HTTPS_Insecure_SecurityMode() throws Exception {
563        // here we use alternate server-store-selfigned key (as the key set it static initializer is probably already
564        // used to init SSLContext/SSLSocketFactory/etc
565        session.setConfigProperty(
566                ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
567        httpServer.addSelfSignedSslConnector();
568        newTransporter(httpServer.getHttpsUrl());
569        RecordingTransportListener listener = new RecordingTransportListener();
570        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
571        transporter.get(task);
572        assertEquals("test", task.getDataString());
573        assertEquals(0L, listener.getDataOffset());
574        assertEquals(4L, listener.getDataLength());
575        assertEquals(1, listener.getStartedCount());
576        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
577        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
578    }
579
580    @Test
581    protected void testGet_HTTPS_HTTP2Only_Insecure_SecurityMode() throws Exception {
582        // here we use alternate server-store-selfigned key (as the key set it static initializer is probably already
583        // used to init SSLContext/SSLSocketFactory/etc
584        enableHttp2Protocol();
585        session.setConfigProperty(
586                ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
587        httpServer.addSelfSignedSslConnectorHttp2Only();
588        newTransporter(httpServer.getHttpsUrl());
589        RecordingTransportListener listener = new RecordingTransportListener();
590        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
591        transporter.get(task);
592        assertEquals("test", task.getDataString());
593        assertEquals(0L, listener.getDataOffset());
594        assertEquals(4L, listener.getDataLength());
595        assertEquals(1, listener.getStartedCount());
596        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
597        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
598    }
599
600    protected void enableHttp2Protocol() {}
601
602    @Test
603    protected void testGet_Redirect() throws Exception {
604        httpServer.addSslConnector();
605        RecordingTransportListener listener = new RecordingTransportListener();
606        GetTask task = new GetTask(URI.create("redirect/file.txt?scheme=https")).setListener(listener);
607        transporter.get(task);
608        assertEquals("test", task.getDataString());
609        assertEquals(0L, listener.getDataOffset());
610        assertEquals(4L, listener.getDataLength());
611        assertEquals(1, listener.getStartedCount());
612        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
613        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
614    }
615
616    @Test
617    protected void testGet_Resume() throws Exception {
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(2L, listener.getDataOffset());
627        assertEquals(9, listener.getDataLength());
628        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
629        assertEquals("sumable", listener.getBaos().toString(StandardCharsets.UTF_8));
630    }
631
632    @Test
633    protected void testGet_ResumeLocalContentsOutdated() throws Exception {
634        File file = TestFileUtils.createTempFile("re");
635        file.setLastModified(System.currentTimeMillis() - 5 * 60 * 1000);
636        RecordingTransportListener listener = new RecordingTransportListener();
637        GetTask task = new GetTask(URI.create("repo/resume.txt"))
638                .setDataPath(file.toPath(), true)
639                .setListener(listener);
640        transporter.get(task);
641        assertEquals("resumable", TestFileUtils.readString(file));
642        assertEquals(1L, listener.getStartedCount());
643        assertEquals(0L, listener.getDataOffset());
644        assertEquals(9, listener.getDataLength());
645        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
646        assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
647    }
648
649    @Test
650    protected void testGet_ResumeRangesNotSupportedByServer() throws Exception {
651        httpServer.setRangeSupport(false);
652        File file = TestFileUtils.createTempFile("re");
653        RecordingTransportListener listener = new RecordingTransportListener();
654        GetTask task = new GetTask(URI.create("repo/resume.txt"))
655                .setDataPath(file.toPath(), true)
656                .setListener(listener);
657        transporter.get(task);
658        assertEquals("resumable", TestFileUtils.readString(file));
659        assertEquals(1L, listener.getStartedCount());
660        assertEquals(0L, listener.getDataOffset());
661        assertEquals(9, listener.getDataLength());
662        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
663        assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
664    }
665
666    @Test
667    protected void testGet_Checksums_Nexus() throws Exception {
668        httpServer.setChecksumHeader(HttpServer.ChecksumHeader.NEXUS);
669        GetTask task = new GetTask(URI.create("repo/file.txt"));
670        transporter.get(task);
671        assertEquals("test", task.getDataString());
672        assertEquals(
673                "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
674    }
675
676    @Test
677    protected void testGet_Checksums_XChecksum() throws Exception {
678        httpServer.setChecksumHeader(HttpServer.ChecksumHeader.XCHECKSUM);
679        GetTask task = new GetTask(URI.create("repo/file.txt"));
680        transporter.get(task);
681        assertEquals("test", task.getDataString());
682        assertEquals(
683                "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
684    }
685
686    @Test
687    protected void testGet_FileHandleLeak() throws Exception {
688        for (int i = 0; i < 100; i++) {
689            File file = TestFileUtils.createTempFile("failure");
690            transporter.get(new GetTask(URI.create("repo/file.txt")).setDataPath(file.toPath()));
691            assertTrue(file.delete(), i + ", " + file.getAbsolutePath());
692        }
693    }
694
695    @Test
696    protected void testGet_NotFound() throws Exception {
697        try {
698            transporter.get(new GetTask(URI.create("repo/missing.txt")));
699            fail("Expected error");
700        } catch (HttpTransporterException e) {
701            assertEquals(404, e.getStatusCode());
702            assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e));
703        }
704    }
705
706    @Test
707    protected void testGet_Closed() throws Exception {
708        transporter.close();
709        try {
710            transporter.get(new GetTask(URI.create("repo/file.txt")));
711            fail("Expected error");
712        } catch (IllegalStateException e) {
713            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
714        }
715    }
716
717    @Test
718    protected void testGet_StartCancelled() throws Exception {
719        RecordingTransportListener listener = new RecordingTransportListener();
720        listener.cancelStart();
721        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
722        try {
723            transporter.get(task);
724            fail("Expected error");
725        } catch (TransferCancelledException e) {
726            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
727        }
728        assertEquals(0L, listener.getDataOffset());
729        assertEquals(4L, listener.getDataLength());
730        assertEquals(1, listener.getStartedCount());
731        assertEquals(0, listener.getProgressedCount());
732    }
733
734    @Test
735    protected void testGet_ProgressCancelled() throws Exception {
736        RecordingTransportListener listener = new RecordingTransportListener();
737        listener.cancelProgress();
738        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
739        try {
740            transporter.get(task);
741            fail("Expected error");
742        } catch (TransferCancelledException e) {
743            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
744        }
745        assertEquals(0L, listener.getDataOffset());
746        assertEquals(4L, listener.getDataLength());
747        assertEquals(1, listener.getStartedCount());
748        assertEquals(1, listener.getProgressedCount());
749    }
750
751    @Test
752    protected void testPut_FromMemory() throws Exception {
753        RecordingTransportListener listener = new RecordingTransportListener();
754        PutTask task =
755                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
756        transporter.put(task);
757        assertEquals(0L, listener.getDataOffset());
758        assertEquals(6L, listener.getDataLength());
759        assertEquals(1, listener.getStartedCount());
760        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
761        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
762    }
763
764    @Test
765    protected void testPut_FromFile() throws Exception {
766        File file = TestFileUtils.createTempFile("upload");
767        RecordingTransportListener listener = new RecordingTransportListener();
768        PutTask task =
769                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataPath(file.toPath());
770        transporter.put(task);
771        assertEquals(0L, listener.getDataOffset());
772        assertEquals(6L, listener.getDataLength());
773        assertEquals(1, listener.getStartedCount());
774        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
775        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
776    }
777
778    @Test
779    protected void testPut_EmptyResource() throws Exception {
780        RecordingTransportListener listener = new RecordingTransportListener();
781        PutTask task = new PutTask(URI.create("repo/file.txt")).setListener(listener);
782        transporter.put(task);
783        assertEquals(0L, listener.getDataOffset());
784        assertEquals(0L, listener.getDataLength());
785        assertEquals(1, listener.getStartedCount());
786        assertEquals(0, listener.getProgressedCount());
787        assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt")));
788    }
789
790    @Test
791    protected void testPut_EncodedResourcePath() throws Exception {
792        RecordingTransportListener listener = new RecordingTransportListener();
793        PutTask task = new PutTask(URI.create("repo/some%20space.txt"))
794                .setListener(listener)
795                .setDataString("OK");
796        transporter.put(task);
797        assertEquals(0L, listener.getDataOffset());
798        assertEquals(2L, listener.getDataLength());
799        assertEquals(1, listener.getStartedCount());
800        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
801        assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt")));
802    }
803
804    @Test
805    protected void testPut_Authenticated_ExpectContinue() throws Exception {
806        httpServer.setAuthentication("testuser", "testpass");
807        auth = new AuthenticationBuilder()
808                .addUsername("testuser")
809                .addPassword("testpass")
810                .build();
811        newTransporter(httpServer.getHttpUrl());
812        RecordingTransportListener listener = new RecordingTransportListener();
813        PutTask task =
814                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
815        transporter.put(task);
816        assertEquals(0L, listener.getDataOffset());
817        assertEquals(6L, listener.getDataLength());
818        assertEquals(1, listener.getStartedCount());
819        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
820        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
821    }
822
823    @Test
824    protected void testPut_Authenticated_ExpectContinueBroken() throws Exception {
825        // this makes OPTIONS recover, and have only 1 PUT (startedCount=1 as OPTIONS is not counted)
826        session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true);
827        httpServer.setAuthentication("testuser", "testpass");
828        httpServer.setExpectSupport(HttpServer.ExpectContinue.BROKEN);
829        auth = new AuthenticationBuilder()
830                .addUsername("testuser")
831                .addPassword("testpass")
832                .build();
833        newTransporter(httpServer.getHttpUrl());
834        RecordingTransportListener listener = new RecordingTransportListener();
835        PutTask task =
836                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
837        transporter.put(task);
838        assertEquals(0L, listener.getDataOffset());
839        assertEquals(6L, listener.getDataLength());
840        assertEquals(1, listener.getStartedCount());
841        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
842        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
843    }
844
845    @Test
846    protected void testPut_Authenticated_ExpectContinueRejected() throws Exception {
847        httpServer.setAuthentication("testuser", "testpass");
848        httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
849        auth = new AuthenticationBuilder()
850                .addUsername("testuser")
851                .addPassword("testpass")
852                .build();
853        newTransporter(httpServer.getHttpUrl());
854        RecordingTransportListener listener = new RecordingTransportListener();
855        PutTask task =
856                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
857        transporter.put(task);
858        assertEquals(0L, listener.getDataOffset());
859        assertEquals(6L, listener.getDataLength());
860        assertEquals(1, listener.getStartedCount());
861        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
862        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
863    }
864
865    @Test
866    protected void testPut_Authenticated_ExpectContinueDisabled() throws Exception {
867        session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false);
868        httpServer.setAuthentication("testuser", "testpass");
869        httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode
870        auth = new AuthenticationBuilder()
871                .addUsername("testuser")
872                .addPassword("testpass")
873                .build();
874        newTransporter(httpServer.getHttpUrl());
875        RecordingTransportListener listener = new RecordingTransportListener();
876        PutTask task =
877                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
878        transporter.put(task);
879        assertEquals(0L, listener.getDataOffset());
880        assertEquals(6L, listener.getDataLength());
881        assertEquals(1, listener.getStartedCount()); // w/ expectContinue enabled would have here 2
882        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
883        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
884    }
885
886    @Test
887    protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception {
888        Map<String, String> headers = new HashMap<>();
889        headers.put("Expect", "100-continue");
890        session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
891        httpServer.setAuthentication("testuser", "testpass");
892        httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
893        auth = new AuthenticationBuilder()
894                .addUsername("testuser")
895                .addPassword("testpass")
896                .build();
897        newTransporter(httpServer.getHttpUrl());
898        RecordingTransportListener listener = new RecordingTransportListener();
899        PutTask task =
900                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
901        transporter.put(task);
902        assertEquals(0L, listener.getDataOffset());
903        assertEquals(6L, listener.getDataLength());
904        assertEquals(1, listener.getStartedCount());
905        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
906        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
907    }
908
909    @Test
910    protected void testPut_Unauthenticated() throws Exception {
911        httpServer.setAuthentication("testuser", "testpass");
912        RecordingTransportListener listener = new RecordingTransportListener();
913        PutTask task =
914                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
915        try {
916            transporter.put(task);
917            fail("Expected error");
918        } catch (HttpTransporterException e) {
919            assertEquals(401, e.getStatusCode());
920            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
921        }
922        assertEquals(0, listener.getStartedCount());
923        assertEquals(0, listener.getProgressedCount());
924    }
925
926    @Test
927    protected void testPut_ProxyAuthenticated() throws Exception {
928        httpServer.setProxyAuthentication("testuser", "testpass");
929        Authentication auth = new AuthenticationBuilder()
930                .addUsername("testuser")
931                .addPassword("testpass")
932                .build();
933        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
934        newTransporter("http://bad.localhost:1/");
935        RecordingTransportListener listener = new RecordingTransportListener();
936        PutTask task =
937                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
938        transporter.put(task);
939        assertEquals(0L, listener.getDataOffset());
940        assertEquals(6L, listener.getDataLength());
941        assertEquals(1, listener.getStartedCount());
942        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
943        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
944    }
945
946    @Test
947    protected void testPut_ProxyUnauthenticated() throws Exception {
948        httpServer.setProxyAuthentication("testuser", "testpass");
949        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
950        newTransporter("http://bad.localhost:1/");
951        RecordingTransportListener listener = new RecordingTransportListener();
952        PutTask task =
953                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
954        try {
955            transporter.put(task);
956            fail("Expected error");
957        } catch (HttpTransporterException e) {
958            assertEquals(407, e.getStatusCode());
959            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
960        }
961        assertEquals(0, listener.getStartedCount());
962        assertEquals(0, listener.getProgressedCount());
963    }
964
965    @Test
966    protected void testPut_SSL() throws Exception {
967        httpServer.addSslConnector();
968        httpServer.setAuthentication("testuser", "testpass");
969        auth = new AuthenticationBuilder()
970                .addUsername("testuser")
971                .addPassword("testpass")
972                .build();
973        newTransporter(httpServer.getHttpsUrl());
974        RecordingTransportListener listener = new RecordingTransportListener();
975        PutTask task =
976                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
977        transporter.put(task);
978        assertEquals(0L, listener.getDataOffset());
979        assertEquals(6L, listener.getDataLength());
980        assertEquals(1, listener.getStartedCount());
981        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
982        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
983    }
984
985    @Test
986    protected void testPut_FileHandleLeak() throws Exception {
987        for (int i = 0; i < 100; i++) {
988            File src = TestFileUtils.createTempFile("upload");
989            File dst = new File(repoDir, "file.txt");
990            transporter.put(new PutTask(URI.create("repo/file.txt")).setDataPath(src.toPath()));
991            assertTrue(src.delete(), i + ", " + src.getAbsolutePath());
992            assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath());
993        }
994    }
995
996    @Test
997    protected void testPut_Closed() throws Exception {
998        transporter.close();
999        try {
1000            transporter.put(new PutTask(URI.create("repo/missing.txt")));
1001            fail("Expected error");
1002        } catch (IllegalStateException e) {
1003            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1004        }
1005    }
1006
1007    @Test
1008    protected void testPut_StartCancelled() throws Exception {
1009        RecordingTransportListener listener = new RecordingTransportListener();
1010        listener.cancelStart();
1011        PutTask task =
1012                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1013        try {
1014            transporter.put(task);
1015            fail("Expected error");
1016        } catch (TransferCancelledException e) {
1017            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1018        }
1019        assertEquals(0L, listener.getDataOffset());
1020        assertEquals(6L, listener.getDataLength());
1021        assertEquals(1, listener.getStartedCount());
1022        assertEquals(0, listener.getProgressedCount());
1023    }
1024
1025    @Test
1026    protected void testPut_ProgressCancelled() throws Exception {
1027        RecordingTransportListener listener = new RecordingTransportListener();
1028        listener.cancelProgress();
1029        PutTask task =
1030                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1031        try {
1032            transporter.put(task);
1033            fail("Expected error");
1034        } catch (TransferCancelledException e) {
1035            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1036        }
1037        assertEquals(0L, listener.getDataOffset());
1038        assertEquals(6L, listener.getDataLength());
1039        assertEquals(1, listener.getStartedCount());
1040        assertEquals(1, listener.getProgressedCount());
1041    }
1042
1043    @Test
1044    protected void testGetPut_AuthCache() throws Exception {
1045        httpServer.setAuthentication("testuser", "testpass");
1046        auth = new AuthenticationBuilder()
1047                .addUsername("testuser")
1048                .addPassword("testpass")
1049                .build();
1050        newTransporter(httpServer.getHttpUrl());
1051        GetTask get = new GetTask(URI.create("repo/file.txt"));
1052        transporter.get(get);
1053        RecordingTransportListener listener = new RecordingTransportListener();
1054        PutTask task =
1055                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1056        transporter.put(task);
1057        assertEquals(1, listener.getStartedCount());
1058    }
1059
1060    @Test
1061    protected void testPut_PreemptiveIsDefault() throws Exception {
1062        httpServer.setAuthentication("testuser", "testpass");
1063        auth = new AuthenticationBuilder()
1064                .addUsername("testuser")
1065                .addPassword("testpass")
1066                .build();
1067        newTransporter(httpServer.getHttpUrl());
1068        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1069        transporter.put(task);
1070        assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1071    }
1072
1073    @Test
1074    protected void testPut_AuthCache() throws Exception {
1075        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH, false);
1076        httpServer.setAuthentication("testuser", "testpass");
1077        auth = new AuthenticationBuilder()
1078                .addUsername("testuser")
1079                .addPassword("testpass")
1080                .build();
1081        newTransporter(httpServer.getHttpUrl());
1082        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1083        transporter.put(task);
1084        assertEquals(2, httpServer.getLogEntries().size()); // put (challenged) + put w/ auth
1085        httpServer.getLogEntries().clear();
1086        task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1087        transporter.put(task);
1088        assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1089    }
1090
1091    @Test
1092    protected void testPut_AuthCache_Preemptive() throws Exception {
1093        httpServer.setAuthentication("testuser", "testpass");
1094        auth = new AuthenticationBuilder()
1095                .addUsername("testuser")
1096                .addPassword("testpass")
1097                .build();
1098        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1099        newTransporter(httpServer.getHttpUrl());
1100        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1101        transporter.put(task);
1102        assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1103        httpServer.getLogEntries().clear();
1104        task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1105        transporter.put(task);
1106        assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1107    }
1108
1109    @Test
1110    @Timeout(20)
1111    protected void testConcurrency() throws Exception {
1112        httpServer.setAuthentication("testuser", "testpass");
1113        auth = new AuthenticationBuilder()
1114                .addUsername("testuser")
1115                .addPassword("testpass")
1116                .build();
1117        newTransporter(httpServer.getHttpUrl());
1118        final AtomicReference<Throwable> error = new AtomicReference<>();
1119        Thread[] threads = new Thread[20];
1120        for (int i = 0; i < threads.length; i++) {
1121            final String path = "repo/file.txt?i=" + i;
1122            threads[i] = new Thread(() -> {
1123                try {
1124                    for (int j = 0; j < 100; j++) {
1125                        GetTask task = new GetTask(URI.create(path));
1126                        transporter.get(task);
1127                        assertEquals("test", task.getDataString());
1128                    }
1129                } catch (Throwable t) {
1130                    error.compareAndSet(null, t);
1131                    System.err.println(path);
1132                    t.printStackTrace();
1133                }
1134            });
1135            threads[i].setName("Task-" + i);
1136        }
1137        for (Thread thread : threads) {
1138            thread.start();
1139        }
1140        for (Thread thread : threads) {
1141            thread.join();
1142        }
1143        assertNull(error.get(), String.valueOf(error.get()));
1144    }
1145
1146    @Test
1147    @Timeout(10)
1148    protected void testConnectTimeout() throws Exception {
1149        session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, 100);
1150        int port = 1;
1151        newTransporter("http://localhost:" + port);
1152        try {
1153            transporter.get(new GetTask(URI.create("repo/file.txt")));
1154            fail("Expected error");
1155        } catch (Exception e) {
1156            // impl specific "timeout" exception
1157            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1158        }
1159    }
1160
1161    @Test
1162    @Timeout(10)
1163    protected void testRequestTimeout() throws Exception {
1164        session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 100);
1165        ServerSocket server = new ServerSocket(0);
1166        try (server) {
1167            newTransporter("http://localhost:" + server.getLocalPort());
1168            try {
1169                transporter.get(new GetTask(URI.create("repo/file.txt")));
1170                fail("Expected error");
1171            } catch (Exception e) {
1172                assertTrue(e.getClass().getSimpleName().contains("Timeout"));
1173                assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1174            }
1175        }
1176    }
1177
1178    @Test
1179    protected void testUserAgent() throws Exception {
1180        session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1181        newTransporter(httpServer.getHttpUrl());
1182        transporter.get(new GetTask(URI.create("repo/file.txt")));
1183        assertEquals(1, httpServer.getLogEntries().size());
1184        for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1185            assertEquals("SomeTest/1.0", log.getHeaders().get("User-Agent"));
1186        }
1187    }
1188
1189    @Test
1190    protected void testCustomHeaders() throws Exception {
1191        Map<String, String> headers = new HashMap<>();
1192        headers.put("User-Agent", "Custom/1.0");
1193        headers.put("X-CustomHeader", "Custom-Value");
1194        session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1195        session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1196        newTransporter(httpServer.getHttpUrl());
1197        transporter.get(new GetTask(URI.create("repo/file.txt")));
1198        assertEquals(1, httpServer.getLogEntries().size());
1199        for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1200            for (Map.Entry<String, String> entry : headers.entrySet()) {
1201                assertEquals(entry.getValue(), log.getHeaders().get(entry.getKey()), entry.getKey());
1202            }
1203        }
1204    }
1205
1206    @Test
1207    protected void testServerAuthScope_NotUsedForProxy() throws Exception {
1208        String username = "testuser", password = "testpass";
1209        httpServer.setProxyAuthentication(username, password);
1210        auth = new AuthenticationBuilder()
1211                .addUsername(username)
1212                .addPassword(password)
1213                .build();
1214        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1215        newTransporter("http://" + httpServer.getHost() + ":12/");
1216        try {
1217            transporter.get(new GetTask(URI.create("repo/file.txt")));
1218            fail("Server auth must not be used as proxy auth");
1219        } catch (HttpTransporterException e) {
1220            assertEquals(407, e.getStatusCode());
1221        } catch (IOException e) {
1222            // accepted as well: point is to fail
1223        }
1224    }
1225
1226    @Test
1227    protected void testProxyAuthScope_NotUsedForServer() throws Exception {
1228        String username = "testuser", password = "testpass";
1229        httpServer.setAuthentication(username, password);
1230        Authentication auth = new AuthenticationBuilder()
1231                .addUsername(username)
1232                .addPassword(password)
1233                .build();
1234        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1235        newTransporter("http://" + httpServer.getHost() + ":12/");
1236        try {
1237            transporter.get(new GetTask(URI.create("repo/file.txt")));
1238            fail("Proxy auth must not be used as server auth");
1239        } catch (HttpTransporterException e) {
1240            assertEquals(401, e.getStatusCode());
1241        } catch (IOException e) {
1242            // accepted as well: point is to fail
1243        }
1244    }
1245
1246    @Test
1247    protected void testAuthSchemeReuse() throws Exception {
1248        httpServer.setAuthentication("testuser", "testpass");
1249        httpServer.setProxyAuthentication("proxyuser", "proxypass");
1250        session.setCache(new DefaultRepositoryCache());
1251        auth = new AuthenticationBuilder()
1252                .addUsername("testuser")
1253                .addPassword("testpass")
1254                .build();
1255        Authentication auth = new AuthenticationBuilder()
1256                .addUsername("proxyuser")
1257                .addPassword("proxypass")
1258                .build();
1259        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1260        newTransporter("http://bad.localhost:1/");
1261        GetTask task = new GetTask(URI.create("repo/file.txt"));
1262        transporter.get(task);
1263        assertEquals("test", task.getDataString());
1264        assertEquals(3, httpServer.getLogEntries().size());
1265        httpServer.getLogEntries().clear();
1266        newTransporter("http://bad.localhost:1/");
1267        task = new GetTask(URI.create("repo/file.txt"));
1268        transporter.get(task);
1269        assertEquals("test", task.getDataString());
1270        assertEquals(1, httpServer.getLogEntries().size());
1271        assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Authorization"));
1272        assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Proxy-Authorization"));
1273    }
1274
1275    @Test
1276    protected void testAuthSchemePreemptive() throws Exception {
1277        httpServer.setAuthentication("testuser", "testpass");
1278        session.setCache(new DefaultRepositoryCache());
1279        auth = new AuthenticationBuilder()
1280                .addUsername("testuser")
1281                .addPassword("testpass")
1282                .build();
1283
1284        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, false);
1285        newTransporter(httpServer.getHttpUrl());
1286        GetTask task = new GetTask(URI.create("repo/file.txt"));
1287        transporter.get(task);
1288        assertEquals("test", task.getDataString());
1289        // there ARE challenge round-trips
1290        assertEquals(2, httpServer.getLogEntries().size());
1291
1292        httpServer.getLogEntries().clear();
1293
1294        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1295        newTransporter(httpServer.getHttpUrl());
1296        task = new GetTask(URI.create("repo/file.txt"));
1297        transporter.get(task);
1298        assertEquals("test", task.getDataString());
1299        // there are NO challenge round-trips, all goes through at first
1300        assertEquals(1, httpServer.getLogEntries().size());
1301    }
1302
1303    @Test
1304    void testInit_BadProtocol() {
1305        assertThrows(NoTransporterException.class, () -> newTransporter("bad:/void"));
1306    }
1307
1308    @Test
1309    void testInit_BadUrl() {
1310        assertThrows(NoTransporterException.class, () -> newTransporter("http://localhost:NaN"));
1311    }
1312
1313    @Test
1314    void testInit_CaseInsensitiveProtocol() throws Exception {
1315        newTransporter("http://localhost");
1316        newTransporter("HTTP://localhost");
1317        newTransporter("Http://localhost");
1318        newTransporter("https://localhost");
1319        newTransporter("HTTPS://localhost");
1320        newTransporter("HttpS://localhost");
1321    }
1322}