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 javax.net.ssl.KeyManagerFactory;
022import javax.net.ssl.SSLContext;
023import javax.net.ssl.TrustManagerFactory;
024
025import java.io.File;
026import java.io.FileNotFoundException;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.UncheckedIOException;
030import java.net.ServerSocket;
031import java.net.URI;
032import java.nio.charset.StandardCharsets;
033import java.nio.file.Files;
034import java.nio.file.Path;
035import java.nio.file.Paths;
036import java.nio.file.StandardCopyOption;
037import java.security.KeyStore;
038import java.security.NoSuchAlgorithmException;
039import java.util.Enumeration;
040import java.util.HashMap;
041import java.util.Map;
042import java.util.concurrent.TimeUnit;
043import java.util.concurrent.atomic.AtomicReference;
044import java.util.function.Supplier;
045import java.util.stream.Stream;
046
047import org.awaitility.Awaitility;
048import org.eclipse.aether.ConfigurationProperties;
049import org.eclipse.aether.DefaultRepositoryCache;
050import org.eclipse.aether.DefaultRepositorySystemSession;
051import org.eclipse.aether.DefaultSessionData;
052import org.eclipse.aether.internal.impl.transport.http.DefaultChecksumExtractor;
053import org.eclipse.aether.internal.impl.transport.http.Nx2ChecksumExtractor;
054import org.eclipse.aether.internal.impl.transport.http.XChecksumExtractor;
055import org.eclipse.aether.internal.test.util.TestFileUtils;
056import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager;
057import org.eclipse.aether.repository.Authentication;
058import org.eclipse.aether.repository.Proxy;
059import org.eclipse.aether.repository.RemoteRepository;
060import org.eclipse.aether.spi.connector.transport.GetTask;
061import org.eclipse.aether.spi.connector.transport.PeekTask;
062import org.eclipse.aether.spi.connector.transport.PutTask;
063import org.eclipse.aether.spi.connector.transport.Transporter;
064import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
065import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractorStrategy;
066import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
067import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
068import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory;
069import org.eclipse.aether.spi.connector.transport.http.RFC9457.HttpRFC9457Exception;
070import org.eclipse.aether.transfer.HttpTransportProperty;
071import org.eclipse.aether.transfer.NoTransporterException;
072import org.eclipse.aether.transfer.TransferCancelledException;
073import org.eclipse.aether.util.repository.AuthenticationBuilder;
074import org.eclipse.jetty.http.HttpVersion;
075import org.junit.jupiter.api.AfterAll;
076import org.junit.jupiter.api.AfterEach;
077import org.junit.jupiter.api.BeforeAll;
078import org.junit.jupiter.api.BeforeEach;
079import org.junit.jupiter.api.MethodOrderer;
080import org.junit.jupiter.api.Test;
081import org.junit.jupiter.api.TestInfo;
082import org.junit.jupiter.api.TestMethodOrder;
083import org.junit.jupiter.api.Timeout;
084import org.junit.jupiter.params.ParameterizedTest;
085import org.junit.jupiter.params.provider.ValueSource;
086
087import static java.util.Objects.requireNonNull;
088import static org.junit.jupiter.api.Assertions.assertEquals;
089import static org.junit.jupiter.api.Assertions.assertNotNull;
090import static org.junit.jupiter.api.Assertions.assertNull;
091import static org.junit.jupiter.api.Assertions.assertThrows;
092import static org.junit.jupiter.api.Assertions.assertTrue;
093import static org.junit.jupiter.api.Assertions.fail;
094import static org.junit.jupiter.api.Assumptions.assumeFalse;
095import static org.junit.jupiter.api.Assumptions.assumeTrue;
096
097/**
098 * Common set of tests against Http transporter.
099 */
100@SuppressWarnings({"checkstyle:MethodName"})
101@TestMethodOrder(MethodOrderer.MethodName.class)
102public abstract class HttpTransporterTest {
103
104    protected static final Path SERVER_STORE_PATH = Paths.get("target/server-store");
105
106    protected static final Path CLIENT_STORE_PATH = Paths.get("target/client-store");
107
108    protected static final Path PEM_QUICHE_SERVER_PATH = Paths.get("target/pems");
109
110    protected static SSLContext defaultSslContext;
111
112    static {
113        // uncomment to enable SSL debugging for easier troubleshooting of SSL related test failures
114        // System.setProperty("javax.net.debug", "all");
115    }
116
117    @BeforeAll
118    protected static void beforeAll() throws NoSuchAlgorithmException {
119        // populate custom keystore and truststore
120        try {
121            try (InputStream keyStoreStream =
122                            HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/server-store");
123                    InputStream trustStoreStream =
124                            HttpTransporterTest.class.getClassLoader().getResourceAsStream("ssl/client-store"); ) {
125                Files.copy(keyStoreStream, SERVER_STORE_PATH, StandardCopyOption.REPLACE_EXISTING);
126                Files.copy(trustStoreStream, CLIENT_STORE_PATH, StandardCopyOption.REPLACE_EXISTING);
127                Files.createDirectories(PEM_QUICHE_SERVER_PATH);
128            }
129        } catch (IOException e) {
130            throw new UncheckedIOException(e);
131        }
132        // override default SSLContext to include our custom keystore and truststore (which are "cross connected" with
133        // HttpServer)
134        defaultSslContext = SSLContext.getDefault();
135        SSLContext.setDefault(createClientSSLContext());
136    }
137
138    @AfterAll
139    protected static void afterAll() {
140        if (defaultSslContext != null) {
141            SSLContext.setDefault(defaultSslContext);
142        }
143    }
144
145    /**
146     * Creates an {@link SSLContext} for the client that extends the default keystore and truststore with the entries
147     * from {@link #SERVER_STORE_PATH} (password {@code "server-pwd"}) and {@link #CLIENT_STORE_PATH}
148     * (password {@code "client-pwd"}).
149     *
150     * @return an {@link SSLContext} combining default and custom key/trust material
151     */
152    protected static SSLContext createClientSSLContext() {
153        try {
154            // Load custom key store (KEY_STORE_PATH acts as truststore in "cross connected" setup)
155            KeyStore customTrustStore = KeyStore.getInstance("pkcs12");
156            try (InputStream is = Files.newInputStream(SERVER_STORE_PATH)) {
157                customTrustStore.load(is, "server-pwd".toCharArray());
158            }
159
160            // Load custom trust store (TRUST_STORE_PATH acts as keystore in "cross connected" setup)
161            KeyStore customKeyStore = KeyStore.getInstance("pkcs12");
162            try (InputStream is = Files.newInputStream(CLIENT_STORE_PATH)) {
163                customKeyStore.load(is, "client-pwd".toCharArray());
164            }
165
166            // Load default truststore and merge custom entries
167            KeyStore defaultTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
168            Path defaultTrustStorePath = Path.of(System.getProperty("java.home"), "lib", "security", "cacerts");
169            if (Files.exists(defaultTrustStorePath)) {
170                try (InputStream is = Files.newInputStream(defaultTrustStorePath)) {
171                    defaultTrustStore.load(is, "changeit".toCharArray());
172                }
173            } else {
174                defaultTrustStore.load(null, null);
175            }
176            Enumeration<String> aliases = customTrustStore.aliases();
177            while (aliases.hasMoreElements()) {
178                String alias = aliases.nextElement();
179                defaultTrustStore.setCertificateEntry("custom-trust-" + alias, customTrustStore.getCertificate(alias));
180            }
181
182            // Load default keystore and merge custom entries
183            KeyStore mergedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
184            mergedKeyStore.load(null, null);
185            Enumeration<String> keyAliases = customKeyStore.aliases();
186            while (keyAliases.hasMoreElements()) {
187                String alias = keyAliases.nextElement();
188                if (customKeyStore.isKeyEntry(alias)) {
189                    mergedKeyStore.setKeyEntry(
190                            alias,
191                            customKeyStore.getKey(alias, "client-pwd".toCharArray()),
192                            "client-pwd".toCharArray(),
193                            customKeyStore.getCertificateChain(alias));
194                } else {
195                    mergedKeyStore.setCertificateEntry(alias, customKeyStore.getCertificate(alias));
196                }
197            }
198
199            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
200            tmf.init(defaultTrustStore);
201
202            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
203            kmf.init(mergedKeyStore, "client-pwd".toCharArray());
204
205            SSLContext sslContext = SSLContext.getInstance("TLS");
206            sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
207            return sslContext;
208        } catch (Exception e) {
209            throw new RuntimeException("Failed to create SSLContext", e);
210        }
211    }
212
213    private final Supplier<HttpTransporterFactory> transporterFactorySupplier;
214
215    protected DefaultRepositorySystemSession session;
216
217    protected HttpTransporterFactory factory;
218
219    protected HttpTransporter transporter;
220
221    protected Runnable closer;
222
223    protected File repoDir;
224
225    protected HttpServer httpServer;
226
227    protected Authentication auth;
228
229    protected Proxy proxy;
230
231    protected HttpTransporterTest(Supplier<HttpTransporterFactory> transporterFactorySupplier) {
232        this.transporterFactorySupplier = requireNonNull(transporterFactorySupplier);
233    }
234
235    protected static ChecksumExtractor standardChecksumExtractor() {
236        HashMap<String, ChecksumExtractorStrategy> strategies = new HashMap<>();
237        strategies.put("1", new Nx2ChecksumExtractor());
238        strategies.put("2", new XChecksumExtractor());
239        return new DefaultChecksumExtractor(strategies);
240    }
241
242    protected RemoteRepository newRepo(String url) {
243        return new RemoteRepository.Builder("test", "default", url)
244                .setAuthentication(auth)
245                .setProxy(proxy)
246                .build();
247    }
248
249    protected void newTransporter(String url) throws Exception {
250        if (transporter != null) {
251            transporter.close();
252            transporter = null;
253        }
254        if (closer != null) {
255            closer.run();
256            closer = null;
257        }
258        session = new DefaultRepositorySystemSession(session);
259        session.setData(new DefaultSessionData());
260        transporter = factory.newInstance(session, newRepo(url));
261    }
262
263    protected static final long OLD_FILE_TIMESTAMP = 160660800000L;
264
265    /** HTTP status code for "Too Many Requests". */
266    private static final int SC_TOO_MANY_REQUESTS = 429;
267
268    @BeforeEach
269    protected void setUp(TestInfo testInfo) throws Exception {
270        System.out.println("=== " + testInfo.getDisplayName() + " ===");
271        session = new DefaultRepositorySystemSession(h -> {
272            this.closer = h;
273            return true;
274        });
275        session.setLocalRepositoryManager(new TestLocalRepositoryManager());
276        factory = transporterFactorySupplier.get();
277        repoDir = TestFileUtils.createTempDir();
278        TestFileUtils.writeString(new File(repoDir, "file.txt"), "test");
279        TestFileUtils.writeString(new File(repoDir, "artifact.pom"), "<xml>pom</xml>");
280        TestFileUtils.writeString(new File(repoDir, "dir/file.txt"), "test");
281        TestFileUtils.writeString(new File(repoDir, "dir/oldFile.txt"), "oldTest", OLD_FILE_TIMESTAMP);
282        TestFileUtils.writeString(new File(repoDir, "empty.txt"), "");
283        TestFileUtils.writeString(new File(repoDir, "some space.txt"), "space");
284        try (InputStream is = getCompressibleFileStream()) {
285            Files.copy(is, repoDir.toPath().resolve("compressible-file.xml"));
286        }
287        File resumable = new File(repoDir, "resume.txt");
288        TestFileUtils.writeString(resumable, "resumable");
289        resumable.setLastModified(System.currentTimeMillis() - 90 * 1000);
290        httpServer = new HttpServer().setRepoDir(repoDir).start();
291        // always create a transporter connecting to the Http URL
292        newTransporter(httpServer.getHttpUrl());
293    }
294
295    private static InputStream getCompressibleFileStream() {
296        return HttpTransporterTest.class.getClassLoader().getResourceAsStream("compressible-file.xml");
297    }
298
299    @AfterEach
300    protected void tearDown() throws Exception {
301        if (transporter != null) {
302            transporter.close();
303            transporter = null;
304        }
305        if (closer != null) {
306            closer.run();
307            closer = null;
308        }
309        if (httpServer != null) {
310            if (closesAllConnectionsOnTransporterClose()) {
311                // check for leaked connections (e.g., due to not closing response body streams)
312                Awaitility.await().atMost(3, TimeUnit.SECONDS).until(() -> httpServer.getNumConnectedEndPoints() == 0);
313            }
314            httpServer.stop();
315            httpServer = null;
316        }
317        factory = null;
318        session = null;
319    }
320
321    /**
322     * Indicates whether the transporter implementation closes all connections when the transporter is closed.
323     * @return {@code true} if all connections are closed on transporter close, {@code false} otherwise.
324     */
325    protected boolean closesAllConnectionsOnTransporterClose() {
326        return true;
327    }
328
329    /**
330     * Indicates whether the transporter implementation supports preemptive authentication (i.e., sending credentials with the first request).
331     * @return {@code true} if preemptive authentication is supported, {@code false} otherwise.
332     */
333    protected boolean supportsPreemptiveAuth() {
334        return true;
335    }
336
337    /**
338     * Indicates whether the transporter implementation supports HTTP/3.
339     * @return {@code true} if HTTP/3 is supported, {@code false} otherwise.
340     */
341    protected boolean supportsHttp3() {
342        // skip on ASF Jenkins due to incompatible GLIBC version
343        // (https://github.com/jetty-project/jetty-quiche-native/issues/180 and
344        // https://issues.apache.org/jira/browse/INFRA-28128)
345        // identified via property "os.version" exposed in https://ci-maven.apache.org/computer/maven6/systemInfo
346        assumeFalse(
347                System.getProperty("os.version").equals("5.15.0-1089-azure"),
348                "Skipping HTTP/3 tests on ASF Jenkins Linux Nodes");
349        return true;
350    }
351
352    /**
353     * Indicates whether the transporter implementation supports HTTP/2.
354     * @return {@code true} if HTTP/2 is supported, {@code false} otherwise.
355     */
356    protected boolean supportsHttp2() {
357        return true;
358    }
359
360    @Test
361    protected void testClassify() {
362        assertEquals(Transporter.ERROR_OTHER, transporter.classify(new FileNotFoundException()));
363        assertEquals(Transporter.ERROR_OTHER, transporter.classify(new HttpTransporterException(403)));
364        assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(new HttpTransporterException(404)));
365        assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(new HttpTransporterException(410)));
366    }
367
368    @Test
369    protected void testPeek() throws Exception {
370        transporter.peek(new PeekTask(URI.create("repo/file.txt")));
371    }
372
373    @Test
374    protected void testPeek_DoesNotAcceptRfc9457() throws Exception {
375        // peek is HEAD request, therefore cannot support RFC 9457
376        transporter.peek(new PeekTask(URI.create("repo/file.txt")));
377        String accept = httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept");
378        assertNull(accept, "No accept header expected for HEAD request, but was: " + accept);
379    }
380
381    @Test
382    protected void testRetryHandler_defaultCount_positive() throws Exception {
383        httpServer.setConnectionsToClose(3);
384        transporter.peek(new PeekTask(URI.create("repo/file.txt")));
385    }
386
387    @Test
388    protected void testRetryHandler_defaultCount_negative() throws Exception {
389        httpServer.setConnectionsToClose(4);
390        try {
391            transporter.peek(new PeekTask(URI.create("repo/file.txt")));
392            fail("Expected error");
393        } catch (Exception expected) {
394        }
395    }
396
397    @Test
398    protected void testRetryHandler_tooManyRequests_explicitCount_positive() throws Exception {
399        // set low retry count as this involves back off delays
400        session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 1);
401        int retryIntervalMs = 500;
402        session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL, retryIntervalMs);
403        newTransporter(httpServer.getHttpUrl());
404        httpServer.setServerErrorsBeforeWorks(1, SC_TOO_MANY_REQUESTS);
405        long startTime = System.currentTimeMillis();
406        transporter.peek(new PeekTask(URI.create("repo/file.txt")));
407        assertTrue(
408                System.currentTimeMillis() - startTime >= retryIntervalMs,
409                "Expected back off delay of at least " + retryIntervalMs);
410    }
411
412    @Test
413    protected void testRetryHandler_tooManyRequests_explicitCount_negative() throws Exception {
414        // set low retry count as this involves back off delays
415        session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 3);
416        int retryIntervalMs = 100;
417        session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL, retryIntervalMs);
418        newTransporter(httpServer.getHttpUrl());
419        httpServer.setServerErrorsBeforeWorks(4, SC_TOO_MANY_REQUESTS);
420        long startTime = System.currentTimeMillis();
421        try {
422            transporter.peek(new PeekTask(URI.create("repo/file.txt")));
423            fail("Expected error");
424        } catch (Exception expected) {
425        }
426        // linear backoff: 1x + 2x + 3x
427        long expectedMinimumDuration = retryIntervalMs * (1 + 2 + 3);
428        assertTrue(
429                System.currentTimeMillis() - startTime >= expectedMinimumDuration,
430                "Expected back off delay of at least " + expectedMinimumDuration);
431    }
432
433    @Test
434    protected void testRetryHandler_explicitCount_positive() throws Exception {
435        session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 10);
436        newTransporter(httpServer.getHttpUrl());
437        httpServer.setConnectionsToClose(10);
438        transporter.peek(new PeekTask(URI.create("repo/file.txt")));
439    }
440
441    @Test
442    protected void testRetryHandler_disabled() throws Exception {
443        session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 0);
444        newTransporter(httpServer.getHttpUrl());
445        httpServer.setConnectionsToClose(1);
446        try {
447            transporter.peek(new PeekTask(URI.create("repo/file.txt")));
448        } catch (Exception expected) {
449        }
450    }
451
452    @Test
453    protected void testPeek_NotFound() throws Exception {
454        try {
455            transporter.peek(new PeekTask(URI.create("repo/missing.txt")));
456            fail("Expected error");
457        } catch (HttpTransporterException e) {
458            assertEquals(404, e.getStatusCode());
459            assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e));
460        }
461    }
462
463    @Test
464    protected void testPeek_Closed() throws Exception {
465        transporter.close();
466        try {
467            transporter.peek(new PeekTask(URI.create("repo/missing.txt")));
468            fail("Expected error");
469        } catch (IllegalStateException e) {
470            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
471        }
472    }
473
474    @Test
475    protected void testPeek_Authenticated() throws Exception {
476        httpServer.setAuthentication("testuser", "testpass");
477        auth = new AuthenticationBuilder()
478                .addUsername("testuser")
479                .addPassword("testpass")
480                .build();
481        newTransporter(httpServer.getHttpUrl());
482        transporter.peek(new PeekTask(URI.create("repo/file.txt")));
483    }
484
485    @Test
486    protected void testPeek_Unauthenticated() throws Exception {
487        httpServer.setAuthentication("testuser", "testpass");
488        try {
489            transporter.peek(new PeekTask(URI.create("repo/file.txt")));
490            fail("Expected error");
491        } catch (HttpTransporterException e) {
492            assertEquals(401, e.getStatusCode());
493            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
494        }
495    }
496
497    @Test
498    protected void testPeek_ProxyAuthenticated() throws Exception {
499        httpServer.setProxyAuthentication("testuser", "testpass");
500        auth = new AuthenticationBuilder()
501                .addUsername("testuser")
502                .addPassword("testpass")
503                .build();
504        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
505        newTransporter("http://bad.localhost:1/");
506        transporter.peek(new PeekTask(URI.create("repo/file.txt")));
507    }
508
509    @Test
510    protected void testPeek_ProxyUnauthenticated() throws Exception {
511        httpServer.setProxyAuthentication("testuser", "testpass");
512        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
513        newTransporter("http://bad.localhost:1/");
514        try {
515            transporter.peek(new PeekTask(URI.create("repo/file.txt")));
516            fail("Expected error");
517        } catch (HttpTransporterException e) {
518            assertEquals(407, e.getStatusCode());
519            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
520        }
521    }
522
523    @Test
524    protected void testPeek_SSL() throws Exception {
525        httpServer.addHttp2ConnectorWithMutualTLS();
526        newTransporter(httpServer.getHttpsUrl());
527        RecordingTransportListener listener = new RecordingTransportListener();
528        PeekTask task = new PeekTask(URI.create("repo/file.txt")).setListener(listener);
529        transporter.peek(task);
530        if (exposeContentCodingInTransportProperties()) {
531            assertEquals(
532                    HttpTransportProperty.SslProtocol.TLS_1_3,
533                    listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
534        }
535    }
536
537    @Test
538    protected void testPeek_Redirect() throws Exception {
539        httpServer.addHttp2ConnectorWithMutualTLS();
540        transporter.peek(new PeekTask(URI.create("redirect/file.txt")));
541        transporter.peek(new PeekTask(URI.create("redirect/file.txt?scheme=https")));
542    }
543
544    @Test
545    protected void testGet_ToMemory() throws Exception {
546        RecordingTransportListener listener = new RecordingTransportListener();
547        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
548        transporter.get(task);
549        assertEquals("test", task.getDataString());
550        assertEquals(0L, listener.getDataOffset());
551        assertEquals(4L, listener.getDataLength());
552        assertEquals(1, listener.getStartedCount());
553        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
554        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
555    }
556
557    @Test
558    protected void testGet_ToFile() throws Exception {
559        File file = TestFileUtils.createTempFile("failure");
560        RecordingTransportListener listener = new RecordingTransportListener();
561        GetTask task = new GetTask(URI.create("repo/file.txt"))
562                .setDataPath(file.toPath())
563                .setListener(listener);
564        transporter.get(task);
565        assertEquals("test", TestFileUtils.readString(file));
566        assertEquals(0L, listener.getDataOffset());
567        assertEquals(4L, listener.getDataLength());
568        assertEquals(1, listener.getStartedCount());
569        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
570        assertEquals("test", listener.getBaos().toString(StandardCharsets.UTF_8));
571    }
572
573    @Test
574    protected void testGet_ToFileTimestamp() throws Exception {
575        File file = TestFileUtils.createTempFile("failure");
576        RecordingTransportListener listener = new RecordingTransportListener();
577        GetTask task = new GetTask(URI.create("repo/dir/oldFile.txt"))
578                .setDataPath(file.toPath())
579                .setListener(listener);
580        transporter.get(task);
581        assertEquals("oldTest", TestFileUtils.readString(file));
582        assertEquals(0L, listener.getDataOffset());
583        assertEquals(7L, listener.getDataLength());
584        assertEquals(1, listener.getStartedCount());
585        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
586        assertEquals("oldTest", listener.getBaos().toString(StandardCharsets.UTF_8));
587        assertEquals(OLD_FILE_TIMESTAMP, file.lastModified());
588    }
589
590    @Test
591    protected void testGet_AcceptsRfc9457() throws Exception {
592        GetTask task = new GetTask(URI.create("repo/file.txt"));
593        transporter.get(task);
594        String accept = httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept");
595        assertNotNull(accept, "Missing Accept header when retrieving artifact");
596        assertTrue(
597                accept.contains("application/problem+json"),
598                "Expected Accept header to contain application/problem+json, but was: " + accept);
599    }
600
601    @Test
602    protected void testGet_ParseRfc9457() throws Exception {
603        // use Maven Central (Cloudflare CDN) as endpoints that return RFC 9457 responses
604        newTransporter("https://repo.maven.apache.org");
605        try {
606            // https://blog.cloudflare.com/rfc-9457-agent-error-pages/#how-to-use-it
607            GetTask task = new GetTask(URI.create("cdn-cgi/error/1020"));
608            transporter.get(task);
609            fail("Should have throw HttpRFC9457Exception");
610        } catch (HttpRFC9457Exception e) {
611            // Expected exception, verify the content of the RFC 9457 message.
612            assertEquals(403, e.getStatusCode());
613            assertEquals("Error 1020: Access denied", e.getPayload().getTitle());
614            assertEquals(
615                    "The request was blocked by a Cloudflare firewall rule configured by the site owner.",
616                    e.getPayload().getDetail());
617        }
618    }
619
620    /**
621     * Provides compression algorithms supported by the transporter implementation.
622     * This should be the string value passed in the {@code Accept-Encoding} header.
623     *
624     * @return stream of supported compression algorithm names
625     * @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding#directives">Accept-Encoding directives</a>
626     */
627    protected abstract Stream<String> supportedCompressionAlgorithms();
628
629    protected boolean exposeContentCodingInTransportProperties() {
630        return true;
631    }
632
633    @ParameterizedTest
634    // DEFLATE isn't supported by Jetty server (https://github.com/jetty/jetty.project/issues/280)
635    @ValueSource(strings = {"br", "gzip", "zstd"})
636    protected void testGet_WithCompression(String encoding) throws Exception {
637        assumeTrue(
638                supportedCompressionAlgorithms().anyMatch(supported -> supported.equals(encoding)),
639                () -> "Transporter does not support compression algorithm: " + encoding);
640        RecordingTransportListener listener = new RecordingTransportListener();
641        // requires a file with at least 48/50 bytes (otherwise compression is disabled,
642        // 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)
643        GetTask task = new GetTask(URI.create(encoding + "/repo/compressible-file.xml")).setListener(listener);
644        transporter.get(task);
645        String acceptEncoding =
646                httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept-Encoding");
647        assertNotNull(acceptEncoding, "Missing Accept-Encoding header when retrieving pom");
648        assertTrue(acceptEncoding.contains(encoding));
649        // check original response header sent by server (client transparently handles compression and removes it)
650        // see https://issues.apache.org/jira/browse/HTTPCORE-792
651        // and https://github.com/mizosoft/methanol/issues/182
652        for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
653            assertEquals(encoding, log.getResponseHeaders().get("Content-Encoding"));
654        }
655        String expectedResourceData;
656        try (InputStream is = getCompressibleFileStream()) {
657            expectedResourceData = new String(is.readAllBytes(), StandardCharsets.UTF_8);
658        }
659        assertEquals(expectedResourceData, task.getDataString());
660        assertEquals(0L, listener.getDataOffset());
661        // data length is unknown as chunked transfer encoding is used with compression
662        assertEquals(-1, listener.getDataLength());
663        assertEquals(1, listener.getStartedCount());
664        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
665        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
666        if (exposeContentCodingInTransportProperties()) {
667            assertEquals(encoding, listener.getTransportProperties().get(HttpTransportProperty.Key.CONTENT_CODING));
668        }
669    }
670
671    @Test
672    protected void testGet_EmptyResource() throws Exception {
673        File file = TestFileUtils.createTempFile("failure");
674        RecordingTransportListener listener = new RecordingTransportListener();
675        GetTask task = new GetTask(URI.create("repo/empty.txt"))
676                .setDataPath(file.toPath())
677                .setListener(listener);
678        transporter.get(task);
679        assertEquals("", TestFileUtils.readString(file));
680        assertEquals(0L, listener.getDataOffset());
681        assertEquals(0L, listener.getDataLength());
682        assertEquals(1, listener.getStartedCount());
683        assertEquals(0, listener.getProgressedCount());
684        assertEquals("", listener.getBaos().toString(StandardCharsets.UTF_8));
685    }
686
687    @Test
688    protected void testGet_EncodedResourcePath() throws Exception {
689        GetTask task = new GetTask(URI.create("repo/some%20space.txt"));
690        transporter.get(task);
691        assertEquals("space", task.getDataString());
692    }
693
694    @Test
695    protected void testGet_Authenticated() throws Exception {
696        httpServer.setAuthentication("testuser", "testpass");
697        auth = new AuthenticationBuilder()
698                .addUsername("testuser")
699                .addPassword("testpass")
700                .build();
701        newTransporter(httpServer.getHttpUrl());
702        RecordingTransportListener listener = new RecordingTransportListener();
703        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
704        transporter.get(task);
705        assertEquals("test", task.getDataString());
706        assertEquals(0L, listener.getDataOffset());
707        assertEquals(4L, listener.getDataLength());
708        assertEquals(1, listener.getStartedCount());
709        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
710        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
711    }
712
713    @Test
714    protected void testGet_Unauthenticated() throws Exception {
715        httpServer.setAuthentication("testuser", "testpass");
716        try {
717            transporter.get(new GetTask(URI.create("repo/file.txt")));
718            fail("Expected error");
719        } catch (HttpTransporterException e) {
720            assertEquals(401, e.getStatusCode());
721            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
722        }
723    }
724
725    @Test
726    protected void testGet_ProxyAuthenticated() throws Exception {
727        httpServer.setProxyAuthentication("testuser", "testpass");
728        Authentication auth = new AuthenticationBuilder()
729                .addUsername("testuser")
730                .addPassword("testpass")
731                .build();
732        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
733        newTransporter("http://bad.localhost:1/");
734        RecordingTransportListener listener = new RecordingTransportListener();
735        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
736        transporter.get(task);
737        assertEquals("test", task.getDataString());
738        assertEquals(0L, listener.getDataOffset());
739        assertEquals(4L, listener.getDataLength());
740        assertEquals(1, listener.getStartedCount());
741        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
742        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
743    }
744
745    @Test
746    protected void testGet_ProxyUnauthenticated() throws Exception {
747        httpServer.setProxyAuthentication("testuser", "testpass");
748        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
749        newTransporter("http://bad.localhost:1/");
750        try {
751            transporter.get(new GetTask(URI.create("repo/file.txt")));
752            fail("Expected error");
753        } catch (HttpTransporterException e) {
754            assertEquals(407, e.getStatusCode());
755            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
756        }
757    }
758
759    @Test
760    protected void testGet_RFC9457Response() throws Exception {
761        try {
762            transporter.get(new GetTask(URI.create("rfc9457/file.txt")));
763            fail("Expected error");
764        } catch (HttpRFC9457Exception e) {
765            assertEquals(403, e.getStatusCode());
766            assertEquals(e.getPayload().getType(), URI.create("https://example.com/probs/out-of-credit"));
767            assertEquals(403, e.getPayload().getStatus());
768            assertEquals("You do not have enough credit.", e.getPayload().getTitle());
769            assertEquals(
770                    "Your current balance is 30, but that costs 50.",
771                    e.getPayload().getDetail());
772            assertEquals(URI.create("/account/12345/msgs/abc"), e.getPayload().getInstance());
773        }
774    }
775
776    @Test
777    protected void testGet_RFC9457Response_with_missing_fields() throws Exception {
778        try {
779            transporter.get(new GetTask(URI.create("rfc9457/missing_fields.txt")));
780            fail("Expected error");
781        } catch (HttpRFC9457Exception e) {
782            assertEquals(403, e.getStatusCode());
783            assertEquals(e.getPayload().getType(), URI.create("about:blank"));
784            assertNull(e.getPayload().getStatus());
785            assertNull(e.getPayload().getTitle());
786            assertNull(e.getPayload().getDetail());
787            assertNull(e.getPayload().getInstance());
788        }
789    }
790
791    @Test
792    protected void testGet_SSL() throws Exception {
793        httpServer.addHttp2ConnectorWithMutualTLS();
794        newTransporter(httpServer.getHttpsUrl());
795        RecordingTransportListener listener = new RecordingTransportListener();
796        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
797        transporter.get(task);
798        assertEquals("test", task.getDataString());
799        assertEquals(0L, listener.getDataOffset());
800        assertEquals(4L, listener.getDataLength());
801        assertEquals(1, listener.getStartedCount());
802        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
803        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
804        if (exposeContentCodingInTransportProperties()) {
805            assertEquals(
806                    HttpTransportProperty.SslProtocol.TLS_1_3,
807                    listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
808            assertEquals(
809                    "TLS_AES_256_GCM_SHA384",
810                    listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_CIPHER_SUITE));
811        }
812    }
813
814    @Test
815    protected void testGet_SSL_WithServerErrors() throws Exception {
816        httpServer.setServerErrorsBeforeWorks(1);
817        httpServer.addHttp2ConnectorWithMutualTLS();
818        newTransporter(httpServer.getHttpsUrl());
819        for (int i = 1; i < 3; i++) {
820            try {
821                RecordingTransportListener listener = new RecordingTransportListener();
822                GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
823                transporter.get(task);
824                assertEquals("test", task.getDataString());
825                assertEquals(0L, listener.getDataOffset());
826                assertEquals(4L, listener.getDataLength());
827                assertEquals(1, listener.getStartedCount());
828                assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
829                assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
830            } catch (HttpTransporterException e) {
831                assertEquals(500, e.getStatusCode());
832            }
833        }
834    }
835
836    @Test
837    protected void testGet_HTTPS_Unknown_SecurityMode() throws Exception {
838        session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE, "unknown");
839        httpServer.addHttp2Connector();
840        try {
841            newTransporter(httpServer.getHttpsUrl());
842            fail("Unsupported security mode");
843        } catch (IllegalArgumentException a) {
844            // good
845        }
846    }
847
848    @Test
849    protected void testGet_HTTPS_Insecure_SecurityMode() throws Exception {
850        // we have to reset the default ssl context to avoid the default truststore being used, which would make the
851        // test pass even if the security mode is not set to insecure
852        SSLContext.setDefault(defaultSslContext);
853        try {
854            session.setConfigProperty(
855                    ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
856            httpServer.addHttp2Connector();
857            newTransporter(httpServer.getHttpsUrl());
858            RecordingTransportListener listener = new RecordingTransportListener();
859            GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
860            transporter.get(task);
861            assertEquals("test", task.getDataString());
862            assertEquals(0L, listener.getDataOffset());
863            assertEquals(4L, listener.getDataLength());
864            assertEquals(1, listener.getStartedCount());
865            assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
866            assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
867        } finally {
868            // restore the default SSL context used for all other tests
869            SSLContext.setDefault(createClientSSLContext());
870        }
871    }
872
873    @Test
874    protected void testGet_HTTPS_HTTP2Only_Insecure_SecurityMode() throws Exception {
875        assumeTrue(supportsHttp2(), "Transporter does not support HTTP/2");
876        // we have to reset the default ssl context to avoid the default truststore being used, which would make the
877        // test pass even if the security mode is not set to insecure
878        SSLContext.setDefault(defaultSslContext);
879        try {
880            session.setConfigProperty(ConfigurationProperties.HTTP_VERSION, ConfigurationProperties.HttpVersion.HTTP_2);
881            session.setConfigProperty(
882                    ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
883            httpServer.addHttp2OnlyConnector();
884            newTransporter(httpServer.getHttpsUrl());
885            RecordingTransportListener listener = new RecordingTransportListener();
886            GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
887            transporter.get(task);
888            assertEquals("test", task.getDataString());
889            assertEquals(0L, listener.getDataOffset());
890            assertEquals(4L, listener.getDataLength());
891            assertEquals(1, listener.getStartedCount());
892            assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
893            assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
894            httpServer.getLogEntries().forEach(log -> {
895                assertEquals(HttpVersion.HTTP_2, log.getVersion());
896            });
897        } finally {
898            // restore the default SSL context used for all other tests
899            SSLContext.setDefault(createClientSSLContext());
900        }
901    }
902
903    @Test
904    protected void testGet_HTTP3Only() throws Exception {
905        assumeTrue(supportsHttp3(), "Transporter does not support HTTP/3");
906        session.setConfigProperty(ConfigurationProperties.HTTP_VERSION, ConfigurationProperties.HttpVersion.HTTP_3);
907        httpServer.addHttp3Connector(false);
908        httpServer.start();
909        newTransporter(httpServer.getHttp3Url());
910        RecordingTransportListener listener = new RecordingTransportListener();
911        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
912        transporter.get(task);
913        assertEquals("test", task.getDataString());
914        assertEquals(0L, listener.getDataOffset());
915        assertEquals(4L, listener.getDataLength());
916        assertEquals(1, listener.getStartedCount());
917        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
918        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
919        httpServer.getLogEntries().forEach(log -> {
920            assertEquals(HttpVersion.HTTP_3, log.getVersion());
921        });
922    }
923
924    @Test
925    protected void testGet_HTTP3() throws Exception {
926        assumeTrue(supportsHttp3(), "Transporter does not support HTTP/3");
927        session.setConfigProperty(ConfigurationProperties.HTTP_VERSION, ConfigurationProperties.HttpVersion.HTTP_3);
928        // both HTTP2 and HTTP3 endpoints are available at the same port
929        httpServer.addHttp2OnlyConnectorWithMutualTLS();
930        httpServer.addHttp3Connector(false, httpServer.getHttpsPort());
931        // alt-svc header should point to http/3
932        httpServer.start();
933        newTransporter(httpServer.getHttp3Url());
934        RecordingTransportListener listener = new RecordingTransportListener();
935        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
936        // issue 2 requests to ensure that the second request is HTTP/3 (the first one may be HTTP/2 if the TCP
937        // connection is faster)
938        transporter.get(task);
939        transporter.get(task);
940        assertEquals("test", task.getDataString());
941        assertEquals(0L, listener.getDataOffset());
942        assertEquals(4L, listener.getDataLength());
943        assertEquals(2, listener.getStartedCount());
944        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
945        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
946        // the last request should be HTTP/3 finally
947        assertEquals(
948                HttpVersion.HTTP_3,
949                httpServer
950                        .getLogEntries()
951                        .get(httpServer.getLogEntries().size() - 1)
952                        .getVersion());
953    }
954
955    @Test
956    protected void testGet_HTTP3FallbackToHTTP2() throws Exception {
957        assumeTrue(supportsHttp3(), "Transporter does not support HTTP/3");
958        session.setConfigProperty(ConfigurationProperties.HTTP_VERSION, ConfigurationProperties.HttpVersion.HTTP_3);
959        httpServer.addHttp2OnlyConnectorWithMutualTLS();
960        httpServer.start();
961        newTransporter(httpServer.getHttpsUrl());
962        RecordingTransportListener listener = new RecordingTransportListener();
963        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
964        transporter.get(task);
965        assertEquals("test", task.getDataString());
966        assertEquals(0L, listener.getDataOffset());
967        assertEquals(4L, listener.getDataLength());
968        assertEquals(1, listener.getStartedCount());
969        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
970        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
971        assertEquals(
972                HttpTransportProperty.HttpVersion.HTTP_2,
973                listener.getTransportProperties().get(HttpTransportProperty.Key.HTTP_VERSION));
974        httpServer.getLogEntries().forEach(log -> {
975            assertEquals(HttpVersion.HTTP_2, log.getVersion());
976        });
977    }
978
979    @Test
980    protected void testGet_Redirect() throws Exception {
981        httpServer.addHttp2ConnectorWithMutualTLS();
982        RecordingTransportListener listener = new RecordingTransportListener();
983        GetTask task = new GetTask(URI.create("redirect/file.txt?scheme=https")).setListener(listener);
984        transporter.get(task);
985        assertEquals("test", task.getDataString());
986        assertEquals(0L, listener.getDataOffset());
987        assertEquals(4L, listener.getDataLength());
988        assertEquals(1, listener.getStartedCount());
989        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
990        assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
991    }
992
993    @Test
994    protected void testGet_Resume() throws Exception {
995        File file = TestFileUtils.createTempFile("re");
996        RecordingTransportListener listener = new RecordingTransportListener();
997        GetTask task = new GetTask(URI.create("repo/resume.txt"))
998                .setDataPath(file.toPath(), true)
999                .setListener(listener);
1000        transporter.get(task);
1001        assertEquals("resumable", TestFileUtils.readString(file));
1002        assertEquals(1L, listener.getStartedCount());
1003        assertEquals(2L, listener.getDataOffset());
1004        assertEquals(9, listener.getDataLength());
1005        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1006        assertEquals("sumable", listener.getBaos().toString(StandardCharsets.UTF_8));
1007    }
1008
1009    @Test
1010    protected void testGet_ResumeLocalContentsOutdated() throws Exception {
1011        File file = TestFileUtils.createTempFile("re");
1012        file.setLastModified(System.currentTimeMillis() - 5 * 60 * 1000);
1013        RecordingTransportListener listener = new RecordingTransportListener();
1014        GetTask task = new GetTask(URI.create("repo/resume.txt"))
1015                .setDataPath(file.toPath(), true)
1016                .setListener(listener);
1017        transporter.get(task);
1018        assertEquals("resumable", TestFileUtils.readString(file));
1019        assertEquals(1L, listener.getStartedCount());
1020        assertEquals(0L, listener.getDataOffset());
1021        assertEquals(9, listener.getDataLength());
1022        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1023        assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
1024    }
1025
1026    @Test
1027    protected void testGet_ResumeRangesNotSupportedByServer() throws Exception {
1028        httpServer.setRangeSupport(false);
1029        File file = TestFileUtils.createTempFile("re");
1030        RecordingTransportListener listener = new RecordingTransportListener();
1031        GetTask task = new GetTask(URI.create("repo/resume.txt"))
1032                .setDataPath(file.toPath(), true)
1033                .setListener(listener);
1034        transporter.get(task);
1035        assertEquals("resumable", TestFileUtils.readString(file));
1036        assertEquals(1L, listener.getStartedCount());
1037        assertEquals(0L, listener.getDataOffset());
1038        assertEquals(9, listener.getDataLength());
1039        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1040        assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
1041    }
1042
1043    @Test
1044    protected void testGet_Checksums_Nexus() throws Exception {
1045        httpServer.setChecksumHeader(HttpServer.ChecksumHeader.NEXUS);
1046        GetTask task = new GetTask(URI.create("repo/file.txt"));
1047        transporter.get(task);
1048        assertEquals("test", task.getDataString());
1049        assertEquals(
1050                "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
1051    }
1052
1053    @Test
1054    protected void testGet_Checksums_XChecksum() throws Exception {
1055        httpServer.setChecksumHeader(HttpServer.ChecksumHeader.XCHECKSUM);
1056        GetTask task = new GetTask(URI.create("repo/file.txt"));
1057        transporter.get(task);
1058        assertEquals("test", task.getDataString());
1059        assertEquals(
1060                "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
1061    }
1062
1063    @Test
1064    protected void testGet_FileHandleLeak() throws Exception {
1065        for (int i = 0; i < 100; i++) {
1066            File file = TestFileUtils.createTempFile("failure");
1067            transporter.get(new GetTask(URI.create("repo/file.txt")).setDataPath(file.toPath()));
1068            assertTrue(file.delete(), i + ", " + file.getAbsolutePath());
1069        }
1070    }
1071
1072    @Test
1073    protected void testGet_NotFound() throws Exception {
1074        try {
1075            transporter.get(new GetTask(URI.create("repo/missing.txt")));
1076            fail("Expected error");
1077        } catch (HttpTransporterException e) {
1078            assertEquals(404, e.getStatusCode());
1079            assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e));
1080        }
1081    }
1082
1083    @Test
1084    protected void testGet_Closed() throws Exception {
1085        transporter.close();
1086        try {
1087            transporter.get(new GetTask(URI.create("repo/file.txt")));
1088            fail("Expected error");
1089        } catch (IllegalStateException e) {
1090            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1091        }
1092    }
1093
1094    @Test
1095    protected void testGet_StartCancelled() throws Exception {
1096        RecordingTransportListener listener = new RecordingTransportListener();
1097        listener.cancelStart();
1098        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
1099        try {
1100            transporter.get(task);
1101            fail("Expected error");
1102        } catch (TransferCancelledException e) {
1103            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1104        }
1105        assertEquals(0L, listener.getDataOffset());
1106        assertEquals(4L, listener.getDataLength());
1107        assertEquals(1, listener.getStartedCount());
1108        assertEquals(0, listener.getProgressedCount());
1109    }
1110
1111    @Test
1112    protected void testGet_ProgressCancelled() throws Exception {
1113        RecordingTransportListener listener = new RecordingTransportListener();
1114        listener.cancelProgress();
1115        GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
1116        try {
1117            transporter.get(task);
1118            fail("Expected error");
1119        } catch (TransferCancelledException e) {
1120            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1121        }
1122        assertEquals(0L, listener.getDataOffset());
1123        assertEquals(4L, listener.getDataLength());
1124        assertEquals(1, listener.getStartedCount());
1125        assertEquals(1, listener.getProgressedCount());
1126    }
1127
1128    @Test
1129    protected void testPut_FromMemory() throws Exception {
1130        RecordingTransportListener listener = new RecordingTransportListener();
1131        String payload = "upload";
1132        PutTask task =
1133                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString(payload);
1134        transporter.put(task);
1135        assertEquals(0L, listener.getDataOffset());
1136        assertEquals(6L, listener.getDataLength());
1137        assertEquals(1, listener.getStartedCount());
1138        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1139        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1140        assertEquals(
1141                String.valueOf(payload.getBytes(StandardCharsets.UTF_8).length),
1142                httpServer.getLogEntries().get(0).getRequestHeaders().get("Content-Length"));
1143    }
1144
1145    @Test
1146    protected void testPut_AcceptsRfc9457() throws Exception {
1147        String payload = "upload";
1148        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString(payload);
1149        transporter.put(task);
1150        String accept = httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept");
1151        assertNotNull(accept, "Missing Accept header when retrieving artifact");
1152        assertTrue(
1153                accept.contains("application/problem+json"),
1154                "Expected Accept header to contain application/problem+json, but was: " + accept);
1155    }
1156
1157    @Test
1158    protected void testPut_FromFile() throws Exception {
1159        File file = TestFileUtils.createTempFile("upload");
1160        RecordingTransportListener listener = new RecordingTransportListener();
1161        PutTask task =
1162                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataPath(file.toPath());
1163        transporter.put(task);
1164        assertEquals(0L, listener.getDataOffset());
1165        assertEquals(6L, listener.getDataLength());
1166        assertEquals(1, listener.getStartedCount());
1167        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1168        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1169        assertEquals(
1170                String.valueOf(file.length()),
1171                httpServer.getLogEntries().get(0).getRequestHeaders().get("Content-Length"));
1172    }
1173
1174    @Test
1175    protected void testPut_EmptyResource() throws Exception {
1176        RecordingTransportListener listener = new RecordingTransportListener();
1177        PutTask task = new PutTask(URI.create("repo/file.txt")).setListener(listener);
1178        transporter.put(task);
1179        assertEquals(0L, listener.getDataOffset());
1180        assertEquals(0L, listener.getDataLength());
1181        // some transports may skip the upload for empty resources
1182        assertTrue(
1183                listener.getStartedCount() <= 1,
1184                "The transport should be started at most once but was started " + listener.getStartedCount()
1185                        + " times");
1186        assertEquals(0, listener.getProgressedCount());
1187        assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt")));
1188    }
1189
1190    @Test
1191    protected void testPut_EncodedResourcePath() throws Exception {
1192        RecordingTransportListener listener = new RecordingTransportListener();
1193        PutTask task = new PutTask(URI.create("repo/some%20space.txt"))
1194                .setListener(listener)
1195                .setDataString("OK");
1196        transporter.put(task);
1197        assertEquals(0L, listener.getDataOffset());
1198        assertEquals(2L, listener.getDataLength());
1199        assertEquals(1, listener.getStartedCount());
1200        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1201        assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt")));
1202    }
1203
1204    @Test
1205    protected void testPut_Authenticated_ExpectContinue() throws Exception {
1206        httpServer.setAuthentication("testuser", "testpass");
1207        auth = new AuthenticationBuilder()
1208                .addUsername("testuser")
1209                .addPassword("testpass")
1210                .build();
1211        newTransporter(httpServer.getHttpUrl());
1212        RecordingTransportListener listener = new RecordingTransportListener();
1213        PutTask task =
1214                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1215        transporter.put(task);
1216        assertEquals(0L, listener.getDataOffset());
1217        assertEquals(6L, listener.getDataLength());
1218        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1219        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1220        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1221    }
1222
1223    @Test
1224    protected void testPut_Authenticated_ExpectContinueBroken() throws Exception {
1225        // this makes OPTIONS recover, and have only 1 PUT (startedCount=1 as OPTIONS is not counted)
1226        session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true);
1227        httpServer.setAuthentication("testuser", "testpass");
1228        httpServer.setExpectSupport(HttpServer.ExpectContinue.BROKEN);
1229        auth = new AuthenticationBuilder()
1230                .addUsername("testuser")
1231                .addPassword("testpass")
1232                .build();
1233        newTransporter(httpServer.getHttpUrl());
1234        RecordingTransportListener listener = new RecordingTransportListener();
1235        PutTask task =
1236                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1237        transporter.put(task);
1238        assertEquals(0L, listener.getDataOffset());
1239        assertEquals(6L, listener.getDataLength());
1240        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1241        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1242        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1243    }
1244
1245    @Test
1246    protected void testPut_Authenticated_ExpectContinueRejected() throws Exception {
1247        httpServer.setAuthentication("testuser", "testpass");
1248        httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
1249        auth = new AuthenticationBuilder()
1250                .addUsername("testuser")
1251                .addPassword("testpass")
1252                .build();
1253        newTransporter(httpServer.getHttpUrl());
1254        RecordingTransportListener listener = new RecordingTransportListener();
1255        PutTask task =
1256                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1257        transporter.put(task);
1258        assertEquals(0L, listener.getDataOffset());
1259        assertEquals(6L, listener.getDataLength());
1260        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1261        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1262        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1263    }
1264
1265    @Test
1266    protected void testPut_Authenticated_ExpectContinueDisabled() throws Exception {
1267        session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false);
1268        httpServer.setAuthentication("testuser", "testpass");
1269        httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode
1270        auth = new AuthenticationBuilder()
1271                .addUsername("testuser")
1272                .addPassword("testpass")
1273                .build();
1274        newTransporter(httpServer.getHttpUrl());
1275        RecordingTransportListener listener = new RecordingTransportListener();
1276        PutTask task =
1277                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1278        transporter.put(task);
1279        assertEquals(0L, listener.getDataOffset());
1280        assertEquals(6L, listener.getDataLength());
1281        assertEquals(
1282                supportsPreemptiveAuth() ? 1 : 2,
1283                listener.getStartedCount()); // w/ expectContinue enabled would have here 2/3
1284        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1285        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1286    }
1287
1288    @Test
1289    protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception {
1290        Map<String, String> headers = new HashMap<>();
1291        headers.put("Expect", "100-continue");
1292        session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1293        httpServer.setAuthentication("testuser", "testpass");
1294        httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
1295        auth = new AuthenticationBuilder()
1296                .addUsername("testuser")
1297                .addPassword("testpass")
1298                .build();
1299        newTransporter(httpServer.getHttpUrl());
1300        RecordingTransportListener listener = new RecordingTransportListener();
1301        PutTask task =
1302                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1303        transporter.put(task);
1304        assertEquals(0L, listener.getDataOffset());
1305        assertEquals(6L, listener.getDataLength());
1306        assertEquals(1, listener.getStartedCount());
1307        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1308        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1309    }
1310
1311    @Test
1312    protected void testPut_Unauthenticated() throws Exception {
1313        httpServer.setAuthentication("testuser", "testpass");
1314        RecordingTransportListener listener = new RecordingTransportListener();
1315        PutTask task =
1316                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1317        try {
1318            transporter.put(task);
1319            fail("Expected error");
1320        } catch (HttpTransporterException e) {
1321            assertEquals(401, e.getStatusCode());
1322            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1323        }
1324        assertEquals(0, listener.getStartedCount());
1325        assertEquals(0, listener.getProgressedCount());
1326    }
1327
1328    @Test
1329    protected void testPut_ProxyAuthenticated() throws Exception {
1330        httpServer.setProxyAuthentication("testuser", "testpass");
1331        Authentication auth = new AuthenticationBuilder()
1332                .addUsername("testuser")
1333                .addPassword("testpass")
1334                .build();
1335        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1336        newTransporter("http://bad.localhost:1/");
1337        RecordingTransportListener listener = new RecordingTransportListener();
1338        PutTask task =
1339                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1340        transporter.put(task);
1341        assertEquals(0L, listener.getDataOffset());
1342        assertEquals(6L, listener.getDataLength());
1343        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1344        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1345        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1346    }
1347
1348    @Test
1349    protected void testPut_ProxyUnauthenticated() throws Exception {
1350        httpServer.setProxyAuthentication("testuser", "testpass");
1351        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1352        newTransporter("http://bad.localhost:1/");
1353        RecordingTransportListener listener = new RecordingTransportListener();
1354        PutTask task =
1355                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1356        try {
1357            transporter.put(task);
1358            fail("Expected error");
1359        } catch (HttpTransporterException e) {
1360            assertEquals(407, e.getStatusCode());
1361            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1362        }
1363        assertEquals(0, listener.getStartedCount());
1364        assertEquals(0, listener.getProgressedCount());
1365    }
1366
1367    @Test
1368    protected void testPut_SSL() throws Exception {
1369        httpServer.addHttp2ConnectorWithMutualTLS();
1370        httpServer.setAuthentication("testuser", "testpass");
1371        auth = new AuthenticationBuilder()
1372                .addUsername("testuser")
1373                .addPassword("testpass")
1374                .build();
1375        newTransporter(httpServer.getHttpsUrl());
1376        RecordingTransportListener listener = new RecordingTransportListener();
1377        PutTask task =
1378                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1379        transporter.put(task);
1380        assertEquals(0L, listener.getDataOffset());
1381        assertEquals(6L, listener.getDataLength());
1382        assertEquals(supportsPreemptiveAuth() ? 1 : 2, listener.getStartedCount());
1383        assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
1384        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
1385        assertEquals(
1386                HttpTransportProperty.SslProtocol.TLS_1_3,
1387                listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
1388    }
1389
1390    @Test
1391    protected void testPut_FileHandleLeak() throws Exception {
1392        for (int i = 0; i < 100; i++) {
1393            File src = TestFileUtils.createTempFile("upload");
1394            File dst = new File(repoDir, "file.txt");
1395            transporter.put(new PutTask(URI.create("repo/file.txt")).setDataPath(src.toPath()));
1396            assertTrue(src.delete(), i + ", " + src.getAbsolutePath());
1397            assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath());
1398        }
1399    }
1400
1401    @Test
1402    protected void testPut_Closed() throws Exception {
1403        transporter.close();
1404        try {
1405            transporter.put(new PutTask(URI.create("repo/missing.txt")));
1406            fail("Expected error");
1407        } catch (IllegalStateException e) {
1408            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1409        }
1410    }
1411
1412    @Test
1413    protected void testPut_StartCancelled() throws Exception {
1414        RecordingTransportListener listener = new RecordingTransportListener();
1415        listener.cancelStart();
1416        PutTask task =
1417                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1418        try {
1419            transporter.put(task);
1420            fail("Expected error");
1421        } catch (TransferCancelledException e) {
1422            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1423        }
1424        assertEquals(0L, listener.getDataOffset());
1425        assertEquals(6L, listener.getDataLength());
1426        assertEquals(1, listener.getStartedCount());
1427        assertEquals(0, listener.getProgressedCount());
1428    }
1429
1430    @Test
1431    protected void testPut_ProgressCancelled() throws Exception {
1432        RecordingTransportListener listener = new RecordingTransportListener();
1433        listener.cancelProgress();
1434        PutTask task =
1435                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1436        try {
1437            transporter.put(task);
1438            fail("Expected error");
1439        } catch (TransferCancelledException e) {
1440            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1441        }
1442        assertEquals(0L, listener.getDataOffset());
1443        assertEquals(6L, listener.getDataLength());
1444        assertEquals(1, listener.getStartedCount());
1445        assertEquals(1, listener.getProgressedCount());
1446    }
1447
1448    @Test
1449    protected void testGetPut_AuthCache() throws Exception {
1450        httpServer.setAuthentication("testuser", "testpass");
1451        auth = new AuthenticationBuilder()
1452                .addUsername("testuser")
1453                .addPassword("testpass")
1454                .build();
1455        newTransporter(httpServer.getHttpUrl());
1456        GetTask get = new GetTask(URI.create("repo/file.txt"));
1457        transporter.get(get);
1458        RecordingTransportListener listener = new RecordingTransportListener();
1459        PutTask task =
1460                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1461        transporter.put(task);
1462        assertEquals(1, listener.getStartedCount());
1463    }
1464
1465    @Test
1466    protected void testPut_PreemptiveIsDefault() throws Exception {
1467        httpServer.setAuthentication("testuser", "testpass");
1468        auth = new AuthenticationBuilder()
1469                .addUsername("testuser")
1470                .addPassword("testpass")
1471                .build();
1472        newTransporter(httpServer.getHttpUrl());
1473        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1474        transporter.put(task);
1475        assertEquals(
1476                supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size()); // put w/ auth
1477    }
1478
1479    @Test
1480    protected void testPut_AuthCache() throws Exception {
1481        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH, false);
1482        httpServer.setAuthentication("testuser", "testpass");
1483        auth = new AuthenticationBuilder()
1484                .addUsername("testuser")
1485                .addPassword("testpass")
1486                .build();
1487        newTransporter(httpServer.getHttpUrl());
1488        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1489        transporter.put(task);
1490        assertEquals(2, httpServer.getLogEntries().size()); // put (challenged) + put w/ auth
1491        httpServer.getLogEntries().clear();
1492        task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1493        transporter.put(task);
1494        assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1495    }
1496
1497    @Test
1498    protected void testPut_AuthCache_Preemptive() throws Exception {
1499        httpServer.setAuthentication("testuser", "testpass");
1500        auth = new AuthenticationBuilder()
1501                .addUsername("testuser")
1502                .addPassword("testpass")
1503                .build();
1504        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1505        newTransporter(httpServer.getHttpUrl());
1506        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1507        transporter.put(task);
1508        assertEquals(
1509                supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size()); // put w/ auth
1510        httpServer.getLogEntries().clear();
1511        task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1512        transporter.put(task);
1513        assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1514    }
1515
1516    @Test
1517    protected void testPut_WithResponseBody() throws Exception {
1518        httpServer.setAuthentication("testuser", "testpass");
1519        httpServer.setResponseBodyForPut("Some dummy response body");
1520        auth = new AuthenticationBuilder()
1521                .addUsername("testuser")
1522                .addPassword("testpass")
1523                .build();
1524        newTransporter(httpServer.getHttpUrl());
1525        PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1526        transporter.put(task);
1527        // this leads to stuck threads in some transporters if the response body is not consumed
1528    }
1529
1530    @Test
1531    @Timeout(20)
1532    protected void testConcurrency() throws Exception {
1533        httpServer.setAuthentication("testuser", "testpass");
1534        auth = new AuthenticationBuilder()
1535                .addUsername("testuser")
1536                .addPassword("testpass")
1537                .build();
1538        newTransporter(httpServer.getHttpUrl());
1539        final AtomicReference<Throwable> error = new AtomicReference<>();
1540        Thread[] threads = new Thread[20];
1541        for (int i = 0; i < threads.length; i++) {
1542            final String path = "repo/file.txt?i=" + i;
1543            threads[i] = new Thread(() -> {
1544                try {
1545                    for (int j = 0; j < 100; j++) {
1546                        GetTask task = new GetTask(URI.create(path));
1547                        transporter.get(task);
1548                        assertEquals("test", task.getDataString());
1549                    }
1550                } catch (Throwable t) {
1551                    error.compareAndSet(null, t);
1552                    System.err.println(path);
1553                    t.printStackTrace();
1554                }
1555            });
1556            threads[i].setName("Task-" + i);
1557        }
1558        for (Thread thread : threads) {
1559            thread.start();
1560        }
1561        for (Thread thread : threads) {
1562            thread.join();
1563        }
1564        assertNull(error.get(), String.valueOf(error.get()));
1565    }
1566
1567    @Test
1568    @Timeout(10)
1569    protected void testConnectTimeout() throws Exception {
1570        session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, 100);
1571        int port = 1;
1572        newTransporter("http://localhost:" + port);
1573        try {
1574            transporter.get(new GetTask(URI.create("repo/file.txt")));
1575            fail("Expected error");
1576        } catch (Exception e) {
1577            // impl specific "timeout" exception
1578            assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1579        }
1580    }
1581
1582    @Test
1583    @Timeout(10)
1584    protected void testRequestTimeout() throws Exception {
1585        session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 100);
1586        ServerSocket server = new ServerSocket(0);
1587        try (server) {
1588            newTransporter("http://localhost:" + server.getLocalPort());
1589            try {
1590                transporter.get(new GetTask(URI.create("repo/file.txt")));
1591                fail("Expected error");
1592            } catch (Exception e) {
1593                assertTrue(e.getClass().getSimpleName().contains("Timeout"));
1594                assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1595            }
1596        }
1597    }
1598
1599    @Test
1600    protected void testUserAgent() throws Exception {
1601        session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1602        newTransporter(httpServer.getHttpUrl());
1603        transporter.get(new GetTask(URI.create("repo/file.txt")));
1604        assertEquals(1, httpServer.getLogEntries().size());
1605        for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1606            assertEquals("SomeTest/1.0", log.getRequestHeaders().get("User-Agent"));
1607        }
1608    }
1609
1610    @Test
1611    protected void testCustomHeaders() throws Exception {
1612        Map<String, String> headers = new HashMap<>();
1613        headers.put("User-Agent", "Custom/1.0");
1614        headers.put("X-CustomHeader", "Custom-Value");
1615        session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1616        session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1617        newTransporter(httpServer.getHttpUrl());
1618        transporter.get(new GetTask(URI.create("repo/file.txt")));
1619        assertEquals(1, httpServer.getLogEntries().size());
1620        for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1621            for (Map.Entry<String, String> entry : headers.entrySet()) {
1622                assertEquals(entry.getValue(), log.getRequestHeaders().get(entry.getKey()), entry.getKey());
1623            }
1624        }
1625    }
1626
1627    @Test
1628    protected void testServerAuthScope_NotUsedForProxy() throws Exception {
1629        String username = "testuser", password = "testpass";
1630        httpServer.setProxyAuthentication(username, password);
1631        auth = new AuthenticationBuilder()
1632                .addUsername(username)
1633                .addPassword(password)
1634                .build();
1635        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1636        newTransporter("http://" + httpServer.getHost() + ":12/");
1637        try {
1638            transporter.get(new GetTask(URI.create("repo/file.txt")));
1639            fail("Server auth must not be used as proxy auth");
1640        } catch (HttpTransporterException e) {
1641            assertEquals(407, e.getStatusCode());
1642        } catch (IOException e) {
1643            // accepted as well: point is to fail
1644        }
1645    }
1646
1647    @Test
1648    protected void testProxyAuthScope_NotUsedForServer() throws Exception {
1649        String username = "testuser", password = "testpass";
1650        httpServer.setAuthentication(username, password);
1651        Authentication auth = new AuthenticationBuilder()
1652                .addUsername(username)
1653                .addPassword(password)
1654                .build();
1655        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1656        newTransporter("http://" + httpServer.getHost() + ":12/");
1657        try {
1658            transporter.get(new GetTask(URI.create("repo/file.txt")));
1659            fail("Proxy auth must not be used as server auth");
1660        } catch (HttpTransporterException e) {
1661            assertEquals(401, e.getStatusCode());
1662        } catch (IOException e) {
1663            // accepted as well: point is to fail
1664        }
1665    }
1666
1667    @Test
1668    protected void testAuthSchemeReuse() throws Exception {
1669        httpServer.setAuthentication("testuser", "testpass");
1670        httpServer.setProxyAuthentication("proxyuser", "proxypass");
1671        session.setCache(new DefaultRepositoryCache());
1672        auth = new AuthenticationBuilder()
1673                .addUsername("testuser")
1674                .addPassword("testpass")
1675                .build();
1676        Authentication auth = new AuthenticationBuilder()
1677                .addUsername("proxyuser")
1678                .addPassword("proxypass")
1679                .build();
1680        proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1681        newTransporter("http://bad.localhost:1/");
1682        GetTask task = new GetTask(URI.create("repo/file.txt"));
1683        transporter.get(task);
1684        assertEquals("test", task.getDataString());
1685        assertEquals(3, httpServer.getLogEntries().size());
1686        httpServer.getLogEntries().clear();
1687        newTransporter("http://bad.localhost:1/");
1688        task = new GetTask(URI.create("repo/file.txt"));
1689        transporter.get(task);
1690        assertEquals("test", task.getDataString());
1691        assertEquals(1, httpServer.getLogEntries().size());
1692        assertNotNull(httpServer.getLogEntries().get(0).getRequestHeaders().get("Authorization"));
1693        assertNotNull(httpServer.getLogEntries().get(0).getRequestHeaders().get("Proxy-Authorization"));
1694    }
1695
1696    @Test
1697    protected void testAuthSchemePreemptive() throws Exception {
1698        httpServer.setAuthentication("testuser", "testpass");
1699        session.setCache(new DefaultRepositoryCache());
1700        auth = new AuthenticationBuilder()
1701                .addUsername("testuser")
1702                .addPassword("testpass")
1703                .build();
1704
1705        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, false);
1706        newTransporter(httpServer.getHttpUrl());
1707        GetTask task = new GetTask(URI.create("repo/file.txt"));
1708        transporter.get(task);
1709        assertEquals("test", task.getDataString());
1710        // there ARE challenge round-trips
1711        assertEquals(2, httpServer.getLogEntries().size());
1712
1713        httpServer.getLogEntries().clear();
1714
1715        session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1716        newTransporter(httpServer.getHttpUrl());
1717        task = new GetTask(URI.create("repo/file.txt"));
1718        transporter.get(task);
1719        assertEquals("test", task.getDataString());
1720        // there are (potentially) NO challenge round-trips, all goes through at first
1721        assertEquals(
1722                supportsPreemptiveAuth() ? 1 : 2, httpServer.getLogEntries().size());
1723    }
1724
1725    @Test
1726    void testInit_BadProtocol() {
1727        assertThrows(NoTransporterException.class, () -> newTransporter("bad:/void"));
1728    }
1729
1730    @Test
1731    void testInit_BadUrl() {
1732        assertThrows(NoTransporterException.class, () -> newTransporter("http://localhost:NaN"));
1733    }
1734
1735    @Test
1736    void testInit_CaseInsensitiveProtocol() throws Exception {
1737        newTransporter("http://localhost");
1738        newTransporter("HTTP://localhost");
1739        newTransporter("Http://localhost");
1740        newTransporter("https://localhost");
1741        newTransporter("HTTPS://localhost");
1742        newTransporter("HttpS://localhost");
1743    }
1744}