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