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        String payload = "upload";
861        PutTask task =
862                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString(payload);
863        transporter.put(task);
864        assertEquals(0L, listener.getDataOffset());
865        assertEquals(6L, listener.getDataLength());
866        assertEquals(1, listener.getStartedCount());
867        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
868        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
869        assertEquals(
870                String.valueOf(payload.getBytes(StandardCharsets.UTF_8).length),
871                httpServer.getLogEntries().get(0).getRequestHeaders().get("Content-Length"));
872    }
873
874    @Test
875    protected void testPut_FromFile() throws Exception {
876        File file = TestFileUtils.createTempFile("upload");
877        RecordingTransportListener listener = new RecordingTransportListener();
878        PutTask task =
879                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataPath(file.toPath());
880        transporter.put(task);
881        assertEquals(0L, listener.getDataOffset());
882        assertEquals(6L, listener.getDataLength());
883        assertEquals(1, listener.getStartedCount());
884        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
885        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
886        assertEquals(
887                String.valueOf(file.length()),
888                httpServer.getLogEntries().get(0).getRequestHeaders().get("Content-Length"));
889    }
890
891    @Test
892    protected void testPut_EmptyResource() throws Exception {
893        RecordingTransportListener listener = new RecordingTransportListener();
894        PutTask task = new PutTask(URI.create("repo/file.txt")).setListener(listener);
895        transporter.put(task);
896        assertEquals(0L, listener.getDataOffset());
897        assertEquals(0L, listener.getDataLength());
898        // some transports may skip the upload for empty resources
899        assertTrue(
900                listener.getStartedCount() <= 1,
901                "The transport should be started at most once but was started " + listener.getStartedCount()
902                        + " times");
903        assertEquals(0, listener.getProgressedCount());
904        assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt")));
905    }
906
907    @Test
908    protected void testPut_EncodedResourcePath() throws Exception {
909        RecordingTransportListener listener = new RecordingTransportListener();
910        PutTask task = new PutTask(URI.create("repo/some%20space.txt"))
911                .setListener(listener)
912                .setDataString("OK");
913        transporter.put(task);
914        assertEquals(0L, listener.getDataOffset());
915        assertEquals(2L, listener.getDataLength());
916        assertEquals(1, listener.getStartedCount());
917        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
918        assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt")));
919    }
920
921    @Test
922    protected void testPut_Authenticated_ExpectContinue() throws Exception {
923        httpServer.setAuthentication("testuser", "testpass");
924        auth = new AuthenticationBuilder()
925                .addUsername("testuser")
926                .addPassword("testpass")
927                .build();
928        newTransporter(httpServer.getHttpUrl());
929        RecordingTransportListener listener = new RecordingTransportListener();
930        PutTask task =
931                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
932        transporter.put(task);
933        assertEquals(0L, listener.getDataOffset());
934        assertEquals(6L, listener.getDataLength());
935        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
936        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
937        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
938    }
939
940    @Test
941    protected void testPut_Authenticated_ExpectContinueBroken() throws Exception {
942        // this makes OPTIONS recover, and have only 1 PUT (startedCount=1 as OPTIONS is not counted)
943        session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true);
944        httpServer.setAuthentication("testuser", "testpass");
945        httpServer.setExpectSupport(HttpServer.ExpectContinue.BROKEN);
946        auth = new AuthenticationBuilder()
947                .addUsername("testuser")
948                .addPassword("testpass")
949                .build();
950        newTransporter(httpServer.getHttpUrl());
951        RecordingTransportListener listener = new RecordingTransportListener();
952        PutTask task =
953                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
954        transporter.put(task);
955        assertEquals(0L, listener.getDataOffset());
956        assertEquals(6L, listener.getDataLength());
957        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
958        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
959        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
960    }
961
962    @Test
963    protected void testPut_Authenticated_ExpectContinueRejected() throws Exception {
964        httpServer.setAuthentication("testuser", "testpass");
965        httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
966        auth = new AuthenticationBuilder()
967                .addUsername("testuser")
968                .addPassword("testpass")
969                .build();
970        newTransporter(httpServer.getHttpUrl());
971        RecordingTransportListener listener = new RecordingTransportListener();
972        PutTask task =
973                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
974        transporter.put(task);
975        assertEquals(0L, listener.getDataOffset());
976        assertEquals(6L, listener.getDataLength());
977        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
978        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
979        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
980    }
981
982    @Test
983    protected void testPut_Authenticated_ExpectContinueDisabled() throws Exception {
984        session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false);
985        httpServer.setAuthentication("testuser", "testpass");
986        httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode
987        auth = new AuthenticationBuilder()
988                .addUsername("testuser")
989                .addPassword("testpass")
990                .build();
991        newTransporter(httpServer.getHttpUrl());
992        RecordingTransportListener listener = new RecordingTransportListener();
993        PutTask task =
994                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
995        transporter.put(task);
996        assertEquals(0L, listener.getDataOffset());
997        assertEquals(6L, listener.getDataLength());
998        assertEquals(
999                supportsPreemptiveAuth() ? 1 : 2,
1000                listener.getStartedCount()); // w/ expectContinue enabled would have here 2/3
1001        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1002        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1003    }
1004
1005    @Test
1006    protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception {
1007        Map<String, String> headers = new HashMap<>();
1008        headers.put("Expect", "100-continue");
1009        session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1010        httpServer.setAuthentication("testuser", "testpass");
1011        httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
1012        auth = new AuthenticationBuilder()
1013                .addUsername("testuser")
1014                .addPassword("testpass")
1015                .build();
1016        newTransporter(httpServer.getHttpUrl());
1017        RecordingTransportListener listener = new RecordingTransportListener();
1018        PutTask task =
1019                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1020        transporter.put(task);
1021        assertEquals(0L, listener.getDataOffset());
1022        assertEquals(6L, listener.getDataLength());
1023        assertEquals(1, listener.getStartedCount());
1024        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1025        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1026    }
1027
1028    @Test
1029    protected void testPut_Unauthenticated() throws Exception {
1030        httpServer.setAuthentication("testuser", "testpass");
1031        RecordingTransportListener listener = new RecordingTransportListener();
1032        PutTask task =
1033                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1034        try {
1035            transporter.put(task);
1036            fail("Expected error");
1037        } catch (HttpTransporterException e) {
1038            assertEquals(401, e.getStatusCode());
1039            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1040        }
1041        assertEquals(0, listener.getStartedCount());
1042        assertEquals(0, listener.getProgressedCount());
1043    }
1044
1045    @Test
1046    protected void testPut_ProxyAuthenticated() throws Exception {
1047        httpServer.setProxyAuthentication("testuser", "testpass");
1048        Authentication auth = new AuthenticationBuilder()
1049                .addUsername("testuser")
1050                .addPassword("testpass")
1051                .build();
1052        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1053        newTransporter("http://bad.localhost:1/");
1054        RecordingTransportListener listener = new RecordingTransportListener();
1055        PutTask task =
1056                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1057        transporter.put(task);
1058        assertEquals(0L, listener.getDataOffset());
1059        assertEquals(6L, listener.getDataLength());
1060        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1061        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1062        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1063    }
1064
1065    @Test
1066    protected void testPut_ProxyUnauthenticated() throws Exception {
1067        httpServer.setProxyAuthentication("testuser", "testpass");
1068        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1069        newTransporter("http://bad.localhost:1/");
1070        RecordingTransportListener listener = new RecordingTransportListener();
1071        PutTask task =
1072                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1073        try {
1074            transporter.put(task);
1075            fail("Expected error");
1076        } catch (HttpTransporterException e) {
1077            assertEquals(407, e.getStatusCode());
1078            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1079        }
1080        assertEquals(0, listener.getStartedCount());
1081        assertEquals(0, listener.getProgressedCount());
1082    }
1083
1084    @Test
1085    protected void testPut_SSL() throws Exception {
1086        httpServer.addSslConnector();
1087        httpServer.setAuthentication("testuser", "testpass");
1088        auth = new AuthenticationBuilder()
1089                .addUsername("testuser")
1090                .addPassword("testpass")
1091                .build();
1092        newTransporter(httpServer.getHttpsUrl());
1093        RecordingTransportListener listener = new RecordingTransportListener();
1094        PutTask task =
1095                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1096        transporter.put(task);
1097        assertEquals(0L, listener.getDataOffset());
1098        assertEquals(6L, listener.getDataLength());
1099        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1100        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1101        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1102    }
1103
1104    @Test
1105    protected void testPut_FileHandleLeak() throws Exception {
1106        for (int i = 0; i < 100; i++) {
1107            File src = TestFileUtils.createTempFile("upload");
1108            File dst = new File(repoDir, "file.txt");
1109            transporter.put(new PutTask(URI.create("repo/file.txt")).setDataPath(src.toPath()));
1110            assertTrue(src.delete(), i + ", " + src.getAbsolutePath());
1111            assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath());
1112        }
1113    }
1114
1115    @Test
1116    protected void testPut_Closed() throws Exception {
1117        transporter.close();
1118        try {
1119            transporter.put(new PutTask(URI.create("repo/missing.txt")));
1120            fail("Expected error");
1121        } catch (IllegalStateException e) {
1122            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1123        }
1124    }
1125
1126    @Test
1127    protected void testPut_StartCancelled() throws Exception {
1128        RecordingTransportListener listener = new RecordingTransportListener();
1129        listener.cancelStart();
1130        PutTask task =
1131                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1132        try {
1133            transporter.put(task);
1134            fail("Expected error");
1135        } catch (TransferCancelledException e) {
1136            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1137        }
1138        assertEquals(0L, listener.getDataOffset());
1139        assertEquals(6L, listener.getDataLength());
1140        assertEquals(1, listener.getStartedCount());
1141        assertEquals(0, listener.getProgressedCount());
1142    }
1143
1144    @Test
1145    protected void testPut_ProgressCancelled() throws Exception {
1146        RecordingTransportListener listener = new RecordingTransportListener();
1147        listener.cancelProgress();
1148        PutTask task =
1149                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1150        try {
1151            transporter.put(task);
1152            fail("Expected error");
1153        } catch (TransferCancelledException e) {
1154            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1155        }
1156        assertEquals(0L, listener.getDataOffset());
1157        assertEquals(6L, listener.getDataLength());
1158        assertEquals(1, listener.getStartedCount());
1159        assertEquals(1, listener.getProgressedCount());
1160    }
1161
1162    @Test
1163    protected void testGetPut_AuthCache() throws Exception {
1164        httpServer.setAuthentication("testuser", "testpass");
1165        auth = new AuthenticationBuilder()
1166                .addUsername("testuser")
1167                .addPassword("testpass")
1168                .build();
1169        newTransporter(httpServer.getHttpUrl());
1170        GetTask get = new GetTask(URI.create("repo/file.txt"));
1171        transporter.get(get);
1172        RecordingTransportListener listener = new RecordingTransportListener();
1173        PutTask task =
1174                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1175        transporter.put(task);
1176        assertEquals(1, listener.getStartedCount());
1177    }
1178
1179    @Test
1180    protected void testPut_PreemptiveIsDefault() throws Exception {
1181        httpServer.setAuthentication("testuser", "testpass");
1182        auth = new AuthenticationBuilder()
1183                .addUsername("testuser")
1184                .addPassword("testpass")
1185                .build();
1186        newTransporter(httpServer.getHttpUrl());
1187        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1188        transporter.put(task);
1189        assertEquals(
1190                supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size()); // put w/ auth
1191    }
1192
1193    @Test
1194    protected void testPut_AuthCache() throws Exception {
1195        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH, false);
1196        httpServer.setAuthentication("testuser", "testpass");
1197        auth = new AuthenticationBuilder()
1198                .addUsername("testuser")
1199                .addPassword("testpass")
1200                .build();
1201        newTransporter(httpServer.getHttpUrl());
1202        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1203        transporter.put(task);
1204        assertEquals(2, httpServer.getLogEntries().size()); // put (challenged) + put w/ auth
1205        httpServer.getLogEntries().clear();
1206        task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1207        transporter.put(task);
1208        assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1209    }
1210
1211    @Test
1212    protected void testPut_AuthCache_Preemptive() throws Exception {
1213        httpServer.setAuthentication("testuser", "testpass");
1214        auth = new AuthenticationBuilder()
1215                .addUsername("testuser")
1216                .addPassword("testpass")
1217                .build();
1218        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1219        newTransporter(httpServer.getHttpUrl());
1220        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1221        transporter.put(task);
1222        assertEquals(
1223                supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size()); // put w/ auth
1224        httpServer.getLogEntries().clear();
1225        task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1226        transporter.put(task);
1227        assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1228    }
1229
1230    @Test
1231    @Timeout(20)
1232    protected void testConcurrency() throws Exception {
1233        httpServer.setAuthentication("testuser", "testpass");
1234        auth = new AuthenticationBuilder()
1235                .addUsername("testuser")
1236                .addPassword("testpass")
1237                .build();
1238        newTransporter(httpServer.getHttpUrl());
1239        final AtomicReference<Throwable> error = new AtomicReference<>();
1240        Thread[] threads = new Thread[20];
1241        for (int i = 0; i < threads.length; i++) {
1242            final String path = "repo/file.txt?i=" + i;
1243            threads[i] = new Thread(() -> {
1244                try {
1245                    for (int j = 0; j < 100; j++) {
1246                        GetTask task = new GetTask(URI.create(path));
1247                        transporter.get(task);
1248                        assertEquals("test", task.getDataString());
1249                    }
1250                } catch (Throwable t) {
1251                    error.compareAndSet(null, t);
1252                    System.err.println(path);
1253                    t.printStackTrace();
1254                }
1255            });
1256            threads[i].setName("Task-" + i);
1257        }
1258        for (Thread thread : threads) {
1259            thread.start();
1260        }
1261        for (Thread thread : threads) {
1262            thread.join();
1263        }
1264        assertNull(error.get(), String.valueOf(error.get()));
1265    }
1266
1267    @Test
1268    @Timeout(10)
1269    protected void testConnectTimeout() throws Exception {
1270        session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, 100);
1271        int port = 1;
1272        newTransporter("http://localhost:" + port);
1273        try {
1274            transporter.get(new GetTask(URI.create("repo/file.txt")));
1275            fail("Expected error");
1276        } catch (Exception e) {
1277            // impl specific "timeout" exception
1278            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1279        }
1280    }
1281
1282    @Test
1283    @Timeout(10)
1284    protected void testRequestTimeout() throws Exception {
1285        session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 100);
1286        ServerSocket server = new ServerSocket(0);
1287        try (server) {
1288            newTransporter("http://localhost:" + server.getLocalPort());
1289            try {
1290                transporter.get(new GetTask(URI.create("repo/file.txt")));
1291                fail("Expected error");
1292            } catch (Exception e) {
1293                assertTrue(e.getClass().getSimpleName().contains("Timeout"));
1294                assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1295            }
1296        }
1297    }
1298
1299    @Test
1300    protected void testUserAgent() throws Exception {
1301        session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1302        newTransporter(httpServer.getHttpUrl());
1303        transporter.get(new GetTask(URI.create("repo/file.txt")));
1304        assertEquals(1, httpServer.getLogEntries().size());
1305        for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1306            assertEquals("SomeTest/1.0", log.getRequestHeaders().get("User-Agent"));
1307        }
1308    }
1309
1310    @Test
1311    protected void testCustomHeaders() throws Exception {
1312        Map<String, String> headers = new HashMap<>();
1313        headers.put("User-Agent", "Custom/1.0");
1314        headers.put("X-CustomHeader", "Custom-Value");
1315        session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1316        session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1317        newTransporter(httpServer.getHttpUrl());
1318        transporter.get(new GetTask(URI.create("repo/file.txt")));
1319        assertEquals(1, httpServer.getLogEntries().size());
1320        for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1321            for (Map.Entry<String, String> entry : headers.entrySet()) {
1322                assertEquals(entry.getValue(), log.getRequestHeaders().get(entry.getKey()), entry.getKey());
1323            }
1324        }
1325    }
1326
1327    @Test
1328    protected void testServerAuthScope_NotUsedForProxy() throws Exception {
1329        String username = "testuser", password = "testpass";
1330        httpServer.setProxyAuthentication(username, password);
1331        auth = new AuthenticationBuilder()
1332                .addUsername(username)
1333                .addPassword(password)
1334                .build();
1335        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1336        newTransporter("http://" + httpServer.getHost() + ":12/");
1337        try {
1338            transporter.get(new GetTask(URI.create("repo/file.txt")));
1339            fail("Server auth must not be used as proxy auth");
1340        } catch (HttpTransporterException e) {
1341            assertEquals(407, e.getStatusCode());
1342        } catch (IOException e) {
1343            // accepted as well: point is to fail
1344        }
1345    }
1346
1347    @Test
1348    protected void testProxyAuthScope_NotUsedForServer() throws Exception {
1349        String username = "testuser", password = "testpass";
1350        httpServer.setAuthentication(username, password);
1351        Authentication auth = new AuthenticationBuilder()
1352                .addUsername(username)
1353                .addPassword(password)
1354                .build();
1355        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1356        newTransporter("http://" + httpServer.getHost() + ":12/");
1357        try {
1358            transporter.get(new GetTask(URI.create("repo/file.txt")));
1359            fail("Proxy auth must not be used as server auth");
1360        } catch (HttpTransporterException e) {
1361            assertEquals(401, e.getStatusCode());
1362        } catch (IOException e) {
1363            // accepted as well: point is to fail
1364        }
1365    }
1366
1367    @Test
1368    protected void testAuthSchemeReuse() throws Exception {
1369        httpServer.setAuthentication("testuser", "testpass");
1370        httpServer.setProxyAuthentication("proxyuser", "proxypass");
1371        session.setCache(new DefaultRepositoryCache());
1372        auth = new AuthenticationBuilder()
1373                .addUsername("testuser")
1374                .addPassword("testpass")
1375                .build();
1376        Authentication auth = new AuthenticationBuilder()
1377                .addUsername("proxyuser")
1378                .addPassword("proxypass")
1379                .build();
1380        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1381        newTransporter("http://bad.localhost:1/");
1382        GetTask task = new GetTask(URI.create("repo/file.txt"));
1383        transporter.get(task);
1384        assertEquals("test", task.getDataString());
1385        assertEquals(3, httpServer.getLogEntries().size());
1386        httpServer.getLogEntries().clear();
1387        newTransporter("http://bad.localhost:1/");
1388        task = new GetTask(URI.create("repo/file.txt"));
1389        transporter.get(task);
1390        assertEquals("test", task.getDataString());
1391        assertEquals(1, httpServer.getLogEntries().size());
1392        assertNotNull(httpServer.getLogEntries().get(0).getRequestHeaders().get("Authorization"));
1393        assertNotNull(httpServer.getLogEntries().get(0).getRequestHeaders().get("Proxy-Authorization"));
1394    }
1395
1396    @Test
1397    protected void testAuthSchemePreemptive() throws Exception {
1398        httpServer.setAuthentication("testuser", "testpass");
1399        session.setCache(new DefaultRepositoryCache());
1400        auth = new AuthenticationBuilder()
1401                .addUsername("testuser")
1402                .addPassword("testpass")
1403                .build();
1404
1405        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, false);
1406        newTransporter(httpServer.getHttpUrl());
1407        GetTask task = new GetTask(URI.create("repo/file.txt"));
1408        transporter.get(task);
1409        assertEquals("test", task.getDataString());
1410        // there ARE challenge round-trips
1411        assertEquals(2, httpServer.getLogEntries().size());
1412
1413        httpServer.getLogEntries().clear();
1414
1415        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1416        newTransporter(httpServer.getHttpUrl());
1417        task = new GetTask(URI.create("repo/file.txt"));
1418        transporter.get(task);
1419        assertEquals("test", task.getDataString());
1420        // there are (potentially) NO challenge round-trips, all goes through at first
1421        assertEquals(
1422                supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size());
1423    }
1424
1425    @Test
1426    void testInit_BadProtocol() {
1427        assertThrows(NoTransporterException.class, () -> newTransporter("bad:/void"));
1428    }
1429
1430    @Test
1431    void testInit_BadUrl() {
1432        assertThrows(NoTransporterException.class, () -> newTransporter("http://localhost:NaN"));
1433    }
1434
1435    @Test
1436    void testInit_CaseInsensitiveProtocol() throws Exception {
1437        newTransporter("http://localhost");
1438        newTransporter("HTTP://localhost");
1439        newTransporter("Http://localhost");
1440        newTransporter("https://localhost");
1441        newTransporter("HTTPS://localhost");
1442        newTransporter("HttpS://localhost");
1443    }
1444}