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