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