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