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