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