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