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