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