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