diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpTransporter.java similarity index 68% rename from maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java rename to maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpTransporter.java index 6137861ac..954ce8891 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java +++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpTransporter.java @@ -16,22 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -package org.eclipse.aether.transport.jetty; +package org.eclipse.aether.spi.connector.transport.http; + +import org.eclipse.aether.spi.connector.transport.Transporter; /** - * Exception thrown by {@link JettyTransporter} in case of errors. + * A transporter using HTTP protocol. * * @since 2.0.0 */ -final class JettyException extends Exception { - private final int statusCode; - - JettyException(int statusCode) { - super("HTTP Status: " + statusCode); - this.statusCode = statusCode; - } - - public int getStatusCode() { - return statusCode; - } -} +public interface HttpTransporter extends Transporter {} diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkException.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpTransporterException.java similarity index 81% rename from maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkException.java rename to maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpTransporterException.java index a0eaa8904..06a440493 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkException.java +++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpTransporterException.java @@ -16,17 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -package org.eclipse.aether.transport.jdk; +package org.eclipse.aether.spi.connector.transport.http; /** - * Exception thrown by {@link JdkTransporter} in case of errors. + * Exception thrown by {@link HttpTransporter} in case of errors. * * @since 2.0.0 */ -final class JdkException extends Exception { +public class HttpTransporterException extends Exception { private final int statusCode; - JdkException(int statusCode) { + public HttpTransporterException(int statusCode) { super("HTTP Status: " + statusCode); this.statusCode = statusCode; } diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpTransporterFactory.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpTransporterFactory.java new file mode 100644 index 000000000..a3c64bc5b --- /dev/null +++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpTransporterFactory.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.spi.connector.transport.http; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; + +/** + * A factory for {@link HttpTransporter}. + * + * @since 2.0.0 + */ +public interface HttpTransporterFactory extends TransporterFactory { + + /** + * Tries to create HTTP transporter for the specified remote repository. + * + * @param session The repository system session from which to configure the transporter, must not be {@code null}. + * In particular, a transporter should obey the timeouts configured for the session. + * @param repository The remote repository to create a transporter for, must not be {@code null}. + * @return The transporter for the given repository, never {@code null}. + * @throws NoTransporterException If the factory cannot create a transporter for the specified remote repository. + */ + @Override + HttpTransporter newInstance(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException; +} diff --git a/maven-resolver-test-http/pom.xml b/maven-resolver-test-http/pom.xml index 99c9863d0..45aff0374 100644 --- a/maven-resolver-test-http/pom.xml +++ b/maven-resolver-test-http/pom.xml @@ -68,6 +68,18 @@ org.eclipse.jetty jetty-http + + org.eclipse.jetty + jetty-alpn-server + + + org.eclipse.jetty + jetty-alpn-java-server + + + org.eclipse.jetty.http2 + http2-server + org.slf4j slf4j-api diff --git a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java index 7bb97299d..be684b149 100644 --- a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java +++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java @@ -39,15 +39,11 @@ import org.eclipse.aether.internal.impl.checksum.Sha1ChecksumAlgorithmFactory; import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.server.SecureRequestCustomizer; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.*; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.util.IO; @@ -162,24 +158,37 @@ public HttpServer addSelfSignedSslConnector() { private HttpServer addSslConnector(boolean needClientAuth) { if (httpsConnector == null) { SslContextFactory.Server ssl = new SslContextFactory.Server(); - if (needClientAuth) { - ssl.setNeedClientAuth(true); - ssl.setKeyStorePath(new File("src/test/resources/ssl/server-store").getAbsolutePath()); + ssl.setNeedClientAuth(needClientAuth); + if (!needClientAuth) { + ssl.setKeyStorePath(HttpTransporterTest.KEY_STORE_SELF_SIGNED_PATH + .toAbsolutePath() + .toString()); ssl.setKeyStorePassword("server-pwd"); - ssl.setTrustStorePath(new File("src/test/resources/ssl/client-store").getAbsolutePath()); - ssl.setTrustStorePassword("client-pwd"); ssl.setSniRequired(false); } else { - ssl.setNeedClientAuth(false); - ssl.setKeyStorePath(new File("src/test/resources/ssl/server-store-selfsigned").getAbsolutePath()); + ssl.setKeyStorePath( + HttpTransporterTest.KEY_STORE_PATH.toAbsolutePath().toString()); ssl.setKeyStorePassword("server-pwd"); + ssl.setTrustStorePath( + HttpTransporterTest.TRUST_STORE_PATH.toAbsolutePath().toString()); + ssl.setTrustStorePassword("client-pwd"); ssl.setSniRequired(false); } + HttpConfiguration httpsConfig = new HttpConfiguration(); SecureRequestCustomizer customizer = new SecureRequestCustomizer(); customizer.setSniHostCheck(false); httpsConfig.addCustomizer(customizer); - httpsConnector = new ServerConnector(server, ssl, new HttpConnectionFactory(httpsConfig)); + + HttpConnectionFactory http1 = new HttpConnectionFactory(httpsConfig); + + HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpsConfig); + + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(http1.getProtocol()); + + SslConnectionFactory tls = new SslConnectionFactory(ssl, alpn.getProtocol()); + httpsConnector = new ServerConnector(server, tls, alpn, http2, http1); server.addConnector(httpsConnector); try { httpsConnector.start(); @@ -268,6 +277,7 @@ public void stop() throws Exception { } private class ConnectionClosingHandler extends AbstractHandler { + @Override public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { if (connectionsToClose.getAndDecrement() > 0) { Response jettyResponse = (Response) response; @@ -277,7 +287,7 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS } private class LogHandler extends AbstractHandler { - + @Override public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { LOGGER.info( "{} {}{}", @@ -304,7 +314,7 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS private static final Pattern SIMPLE_RANGE = Pattern.compile("bytes=([0-9])+-"); private class RepoHandler extends AbstractHandler { - + @Override public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) throws IOException { String path = req.getPathInfo().substring(1); @@ -438,7 +448,7 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS } private class RedirectHandler extends AbstractHandler { - + @Override public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { String path = req.getPathInfo(); if (!path.startsWith("/redirect/")) { @@ -465,7 +475,7 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS } private class AuthHandler extends AbstractHandler { - + @Override public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) throws IOException { if (ExpectContinue.BROKEN.equals(expectContinue) @@ -485,7 +495,7 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS } private class ProxyAuthHandler extends AbstractHandler { - + @Override public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { if (proxyUsername != null && proxyPassword != null) { if (checkBasicAuth( diff --git a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java new file mode 100644 index 000000000..4c82cb75f --- /dev/null +++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java @@ -0,0 +1,1208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.internal.test.util.http; + +import java.io.*; +import java.net.ServerSocket; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.DefaultRepositoryCache; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.DefaultSessionData; +import org.eclipse.aether.internal.test.util.TestFileUtils; +import org.eclipse.aether.internal.test.util.TestUtils; +import org.eclipse.aether.repository.Authentication; +import org.eclipse.aether.repository.Proxy; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.GetTask; +import org.eclipse.aether.spi.connector.transport.PeekTask; +import org.eclipse.aether.spi.connector.transport.PutTask; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.transfer.TransferCancelledException; +import org.eclipse.aether.util.repository.AuthenticationBuilder; +import org.junit.jupiter.api.*; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Common set of tests against Http transporter. + */ +@SuppressWarnings({"checkstyle:MagicNumber", "checkstyle:MethodName"}) +public class HttpTransporterTest { + + protected static final Path KEY_STORE_PATH = Paths.get("target/keystore"); + + protected static final Path KEY_STORE_SELF_SIGNED_PATH = Paths.get("target/keystore-self-signed"); + + protected static final Path TRUST_STORE_PATH = Paths.get("target/trustStore"); + + static { + // Warning: "cross connected" with HttpServer! + System.setProperty( + "javax.net.ssl.trustStore", KEY_STORE_PATH.toAbsolutePath().toString()); + System.setProperty("javax.net.ssl.trustStorePassword", "server-pwd"); + System.setProperty( + "javax.net.ssl.keyStore", TRUST_STORE_PATH.toAbsolutePath().toString()); + System.setProperty("javax.net.ssl.keyStorePassword", "client-pwd"); + + System.setProperty("javax.net.ssl.trustStoreType", "jks"); + System.setProperty("javax.net.ssl.keyStoreType", "jks"); + System.setProperty("javax.net.debug", "all"); + } + + private final Supplier transporterFactorySupplier; + + protected DefaultRepositorySystemSession session; + + protected HttpTransporterFactory factory; + + protected HttpTransporter transporter; + + protected File repoDir; + + protected HttpServer httpServer; + + protected Authentication auth; + + protected Proxy proxy; + + protected HttpTransporterTest(Supplier transporterFactorySupplier) { + this.transporterFactorySupplier = requireNonNull(transporterFactorySupplier); + + if (!Files.isRegularFile(KEY_STORE_PATH)) { + URL keyStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store"); + URL keyStoreSelfSignedUrl = + HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store-selfsigned"); + URL trustStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/client-store"); + + try { + try (InputStream keyStoreStream = keyStoreUrl.openStream(); + InputStream keyStoreSelfSignedStream = keyStoreSelfSignedUrl.openStream(); + InputStream trustStoreStream = trustStoreUrl.openStream()) { + Files.copy(keyStoreStream, KEY_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); + Files.copy( + keyStoreSelfSignedStream, KEY_STORE_SELF_SIGNED_PATH, StandardCopyOption.REPLACE_EXISTING); + Files.copy(trustStoreStream, TRUST_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + protected RemoteRepository newRepo(String url) { + return new RemoteRepository.Builder("test", "default", url) + .setAuthentication(auth) + .setProxy(proxy) + .build(); + } + + protected void newTransporter(String url) throws Exception { + if (transporter != null) { + transporter.close(); + transporter = null; + } + // here we "simulate" onSessionClose + // TODO: in UTs currently we cannot do it, sort it out + session = new DefaultRepositorySystemSession(session); + session.setData(new DefaultSessionData()); + transporter = factory.newInstance(session, newRepo(url)); + } + + protected static final long OLD_FILE_TIMESTAMP = 160660800000L; + + @BeforeEach + protected void setUp(TestInfo testInfo) throws Exception { + System.out.println("=== " + testInfo.getDisplayName() + " ==="); + session = TestUtils.newSession(); + factory = transporterFactorySupplier.get(); + repoDir = TestFileUtils.createTempDir(); + TestFileUtils.writeString(new File(repoDir, "file.txt"), "test"); + TestFileUtils.writeString(new File(repoDir, "dir/file.txt"), "test"); + TestFileUtils.writeString(new File(repoDir, "dir/oldFile.txt"), "oldTest", OLD_FILE_TIMESTAMP); + TestFileUtils.writeString(new File(repoDir, "empty.txt"), ""); + TestFileUtils.writeString(new File(repoDir, "some space.txt"), "space"); + File resumable = new File(repoDir, "resume.txt"); + TestFileUtils.writeString(resumable, "resumable"); + resumable.setLastModified(System.currentTimeMillis() - 90 * 1000); + httpServer = new HttpServer().setRepoDir(repoDir).start(); + newTransporter(httpServer.getHttpUrl()); + } + + @AfterEach + protected void tearDown() throws Exception { + if (transporter != null) { + transporter.close(); + transporter = null; + } + if (httpServer != null) { + httpServer.stop(); + httpServer = null; + } + factory = null; + session = null; + } + + @Test + protected void testClassify() { + assertEquals(Transporter.ERROR_OTHER, transporter.classify(new FileNotFoundException())); + assertEquals(Transporter.ERROR_OTHER, transporter.classify(new HttpTransporterException(403))); + assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(new HttpTransporterException(404))); + } + + @Test + protected void testPeek() throws Exception { + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + } + + @Test + protected void testRetryHandler_defaultCount_positive() throws Exception { + httpServer.setConnectionsToClose(3); + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + } + + @Test + protected void testRetryHandler_defaultCount_negative() throws Exception { + httpServer.setConnectionsToClose(4); + try { + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + fail("Expected error"); + } catch (Exception expected) { + } + } + + @Test + protected void testRetryHandler_explicitCount_positive() throws Exception { + session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 10); + newTransporter(httpServer.getHttpUrl()); + httpServer.setConnectionsToClose(10); + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + } + + @Test + protected void testRetryHandler_disabled() throws Exception { + session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 0); + newTransporter(httpServer.getHttpUrl()); + httpServer.setConnectionsToClose(1); + try { + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + } catch (Exception expected) { + } + } + + @Test + protected void testPeek_NotFound() throws Exception { + try { + transporter.peek(new PeekTask(URI.create("repo/missing.txt"))); + fail("Expected error"); + } catch (HttpTransporterException e) { + assertEquals(404, e.getStatusCode()); + assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e)); + } + } + + @Test + protected void testPeek_Closed() throws Exception { + transporter.close(); + try { + transporter.peek(new PeekTask(URI.create("repo/missing.txt"))); + fail("Expected error"); + } catch (IllegalStateException e) { + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + } + + @Test + protected void testPeek_Authenticated() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + } + + @Test + protected void testPeek_Unauthenticated() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + try { + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + fail("Expected error"); + } catch (HttpTransporterException e) { + assertEquals(401, e.getStatusCode()); + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + } + + @Test + protected void testPeek_ProxyAuthenticated() throws Exception { + httpServer.setProxyAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); + newTransporter("http://bad.localhost:1/"); + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + } + + @Test + protected void testPeek_ProxyUnauthenticated() throws Exception { + httpServer.setProxyAuthentication("testuser", "testpass"); + proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort()); + newTransporter("http://bad.localhost:1/"); + try { + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + fail("Expected error"); + } catch (HttpTransporterException e) { + assertEquals(407, e.getStatusCode()); + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + } + + @Test + protected void testPeek_SSL() throws Exception { + httpServer.addSslConnector(); + newTransporter(httpServer.getHttpsUrl()); + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + } + + @Test + protected void testPeek_Redirect() throws Exception { + httpServer.addSslConnector(); + transporter.peek(new PeekTask(URI.create("redirect/file.txt"))); + transporter.peek(new PeekTask(URI.create("redirect/file.txt?scheme=https"))); + } + + @Test + protected void testGet_ToMemory() throws Exception { + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_ToFile() throws Exception { + File file = TestFileUtils.createTempFile("failure"); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = + new GetTask(URI.create("repo/file.txt")).setDataFile(file).setListener(listener); + transporter.get(task); + assertEquals("test", TestFileUtils.readString(file)); + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("test", listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_ToFileTimestamp() throws Exception { + File file = TestFileUtils.createTempFile("failure"); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/dir/oldFile.txt")) + .setDataFile(file) + .setListener(listener); + transporter.get(task); + assertEquals("oldTest", TestFileUtils.readString(file)); + assertEquals(0L, listener.getDataOffset()); + assertEquals(7L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("oldTest", listener.getBaos().toString(StandardCharsets.UTF_8)); + assertEquals(file.lastModified(), OLD_FILE_TIMESTAMP); + } + + @Test + protected void testGet_EmptyResource() throws Exception { + File file = TestFileUtils.createTempFile("failure"); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = + new GetTask(URI.create("repo/empty.txt")).setDataFile(file).setListener(listener); + transporter.get(task); + assertEquals("", TestFileUtils.readString(file)); + assertEquals(0L, listener.getDataOffset()); + assertEquals(0L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertEquals(0, listener.getProgressedCount()); + assertEquals("", listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_EncodedResourcePath() throws Exception { + GetTask task = new GetTask(URI.create("repo/some%20space.txt")); + transporter.get(task); + assertEquals("space", task.getDataString()); + } + + @Test + protected void testGet_Authenticated() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_Unauthenticated() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + try { + transporter.get(new GetTask(URI.create("repo/file.txt"))); + fail("Expected error"); + } catch (HttpTransporterException e) { + assertEquals(401, e.getStatusCode()); + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + } + + @Test + protected void testGet_ProxyAuthenticated() throws Exception { + httpServer.setProxyAuthentication("testuser", "testpass"); + Authentication auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); + newTransporter("http://bad.localhost:1/"); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_ProxyUnauthenticated() throws Exception { + httpServer.setProxyAuthentication("testuser", "testpass"); + proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort()); + newTransporter("http://bad.localhost:1/"); + try { + transporter.get(new GetTask(URI.create("repo/file.txt"))); + fail("Expected error"); + } catch (HttpTransporterException e) { + assertEquals(407, e.getStatusCode()); + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + } + + @Test + protected void testGet_SSL() throws Exception { + httpServer.addSslConnector(); + newTransporter(httpServer.getHttpsUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_HTTPS_Unknown_SecurityMode() throws Exception { + session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE, "unknown"); + httpServer.addSelfSignedSslConnector(); + try { + newTransporter(httpServer.getHttpsUrl()); + fail("Unsupported security mode"); + } catch (IllegalArgumentException a) { + // good + } + } + + @Test + protected void testGet_HTTPS_Insecure_SecurityMode() throws Exception { + // here we use alternate server-store-selfigned key (as the key set it static initalizer is probably already + // used to init SSLContext/SSLSocketFactory/etc + session.setConfigProperty( + ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE); + httpServer.addSelfSignedSslConnector(); + newTransporter(httpServer.getHttpsUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_Redirect() throws Exception { + httpServer.addSslConnector(); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("redirect/file.txt?scheme=https")).setListener(listener); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_Resume() throws Exception { + File file = TestFileUtils.createTempFile("re"); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/resume.txt")) + .setDataFile(file, true) + .setListener(listener); + transporter.get(task); + assertEquals("resumable", TestFileUtils.readString(file)); + assertEquals(1L, listener.getStartedCount()); + assertEquals(2L, listener.getDataOffset()); + assertEquals(9, listener.getDataLength()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("sumable", listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_ResumeLocalContentsOutdated() throws Exception { + File file = TestFileUtils.createTempFile("re"); + file.setLastModified(System.currentTimeMillis() - 5 * 60 * 1000); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/resume.txt")) + .setDataFile(file, true) + .setListener(listener); + transporter.get(task); + assertEquals("resumable", TestFileUtils.readString(file)); + assertEquals(1L, listener.getStartedCount()); + assertEquals(0L, listener.getDataOffset()); + assertEquals(9, listener.getDataLength()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_ResumeRangesNotSupportedByServer() throws Exception { + httpServer.setRangeSupport(false); + File file = TestFileUtils.createTempFile("re"); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/resume.txt")) + .setDataFile(file, true) + .setListener(listener); + transporter.get(task); + assertEquals("resumable", TestFileUtils.readString(file)); + assertEquals(1L, listener.getStartedCount()); + assertEquals(0L, listener.getDataOffset()); + assertEquals(9, listener.getDataLength()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8)); + } + + @Test + protected void testGet_Checksums_Nexus() throws Exception { + httpServer.setChecksumHeader(HttpServer.ChecksumHeader.NEXUS); + GetTask task = new GetTask(URI.create("repo/file.txt")); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals( + "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1")); + } + + @Test + protected void testGet_Checksums_XChecksum() throws Exception { + httpServer.setChecksumHeader(HttpServer.ChecksumHeader.XCHECKSUM); + GetTask task = new GetTask(URI.create("repo/file.txt")); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals( + "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1")); + } + + @Test + protected void testGet_FileHandleLeak() throws Exception { + for (int i = 0; i < 100; i++) { + File file = TestFileUtils.createTempFile("failure"); + transporter.get(new GetTask(URI.create("repo/file.txt")).setDataFile(file)); + assertTrue(file.delete(), i + ", " + file.getAbsolutePath()); + } + } + + @Test + protected void testGet_NotFound() throws Exception { + try { + transporter.get(new GetTask(URI.create("repo/missing.txt"))); + fail("Expected error"); + } catch (HttpTransporterException e) { + assertEquals(404, e.getStatusCode()); + assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e)); + } + } + + @Test + protected void testGet_Closed() throws Exception { + transporter.close(); + try { + transporter.get(new GetTask(URI.create("repo/file.txt"))); + fail("Expected error"); + } catch (IllegalStateException e) { + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + } + + @Test + protected void testGet_StartCancelled() throws Exception { + RecordingTransportListener listener = new RecordingTransportListener(); + listener.cancelStart(); + GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); + try { + transporter.get(task); + fail("Expected error"); + } catch (TransferCancelledException e) { + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertEquals(0, listener.getProgressedCount()); + } + + @Test + protected void testGet_ProgressCancelled() throws Exception { + RecordingTransportListener listener = new RecordingTransportListener(); + listener.cancelProgress(); + GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); + try { + transporter.get(task); + fail("Expected error"); + } catch (TransferCancelledException e) { + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertEquals(1, listener.getProgressedCount()); + } + + @Test + protected void testPut_FromMemory() throws Exception { + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_FromFile() throws Exception { + File file = TestFileUtils.createTempFile("upload"); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataFile(file); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_EmptyResource() throws Exception { + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = new PutTask(URI.create("repo/file.txt")).setListener(listener); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(0L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertEquals(0, listener.getProgressedCount()); + assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_EncodedResourcePath() throws Exception { + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = new PutTask(URI.create("repo/some%20space.txt")) + .setListener(listener) + .setDataString("OK"); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(2L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt"))); + } + + @Test + protected void testPut_Authenticated_ExpectContinue() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_Authenticated_ExpectContinueBroken() throws Exception { + // this makes OPTIONS recover, and have only 1 PUT (startedCount=1 as OPTIONS is not counted) + session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true); + httpServer.setAuthentication("testuser", "testpass"); + httpServer.setExpectSupport(HttpServer.ExpectContinue.BROKEN); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_Authenticated_ExpectContinueRejected() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_Authenticated_ExpectContinueDisabled() throws Exception { + session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false); + httpServer.setAuthentication("testuser", "testpass"); + httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); // w/ expectContinue enabled would have here 2 + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception { + Map headers = new HashMap<>(); + headers.put("Expect", "100-continue"); + session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers); + httpServer.setAuthentication("testuser", "testpass"); + httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_Unauthenticated() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + try { + transporter.put(task); + fail("Expected error"); + } catch (HttpTransporterException e) { + assertEquals(401, e.getStatusCode()); + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + assertEquals(0, listener.getStartedCount()); + assertEquals(0, listener.getProgressedCount()); + } + + @Test + protected void testPut_ProxyAuthenticated() throws Exception { + httpServer.setProxyAuthentication("testuser", "testpass"); + Authentication auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); + newTransporter("http://bad.localhost:1/"); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_ProxyUnauthenticated() throws Exception { + httpServer.setProxyAuthentication("testuser", "testpass"); + proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort()); + newTransporter("http://bad.localhost:1/"); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + try { + transporter.put(task); + fail("Expected error"); + } catch (HttpTransporterException e) { + assertEquals(407, e.getStatusCode()); + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + assertEquals(0, listener.getStartedCount()); + assertEquals(0, listener.getProgressedCount()); + } + + @Test + protected void testPut_SSL() throws Exception { + httpServer.addSslConnector(); + httpServer.setAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpsUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + + @Test + protected void testPut_FileHandleLeak() throws Exception { + for (int i = 0; i < 100; i++) { + File src = TestFileUtils.createTempFile("upload"); + File dst = new File(repoDir, "file.txt"); + transporter.put(new PutTask(URI.create("repo/file.txt")).setDataFile(src)); + assertTrue(src.delete(), i + ", " + src.getAbsolutePath()); + assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath()); + } + } + + @Test + protected void testPut_Closed() throws Exception { + transporter.close(); + try { + transporter.put(new PutTask(URI.create("repo/missing.txt"))); + fail("Expected error"); + } catch (IllegalStateException e) { + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + } + + @Test + protected void testPut_StartCancelled() throws Exception { + RecordingTransportListener listener = new RecordingTransportListener(); + listener.cancelStart(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + try { + transporter.put(task); + fail("Expected error"); + } catch (TransferCancelledException e) { + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertEquals(0, listener.getProgressedCount()); + } + + @Test + protected void testPut_ProgressCancelled() throws Exception { + RecordingTransportListener listener = new RecordingTransportListener(); + listener.cancelProgress(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + try { + transporter.put(task); + fail("Expected error"); + } catch (TransferCancelledException e) { + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + assertEquals(0L, listener.getDataOffset()); + assertEquals(6L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertEquals(1, listener.getProgressedCount()); + } + + @Test + protected void testGetPut_AuthCache() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + GetTask get = new GetTask(URI.create("repo/file.txt")); + transporter.get(get); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(1, listener.getStartedCount()); + } + + @Test + protected void testPut_PreemptiveIsDefault() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); + transporter.put(task); + assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth + } + + @Test + protected void testPut_AuthCache() throws Exception { + session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH, false); + httpServer.setAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); + transporter.put(task); + assertEquals(2, httpServer.getLogEntries().size()); // put (challenged) + put w/ auth + httpServer.getLogEntries().clear(); + task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); + transporter.put(task); + assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth + } + + @Test + protected void testPut_AuthCache_Preemptive() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true); + newTransporter(httpServer.getHttpUrl()); + PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); + transporter.put(task); + assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth + httpServer.getLogEntries().clear(); + task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); + transporter.put(task); + assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth + } + + @Test + @Timeout(20) + protected void testConcurrency() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + final AtomicReference error = new AtomicReference<>(); + Thread[] threads = new Thread[20]; + for (int i = 0; i < threads.length; i++) { + final String path = "repo/file.txt?i=" + i; + threads[i] = new Thread(() -> { + try { + for (int j = 0; j < 100; j++) { + GetTask task = new GetTask(URI.create(path)); + transporter.get(task); + assertEquals("test", task.getDataString()); + } + } catch (Throwable t) { + error.compareAndSet(null, t); + System.err.println(path); + t.printStackTrace(); + } + }); + threads[i].setName("Task-" + i); + } + for (Thread thread : threads) { + thread.start(); + } + for (Thread thread : threads) { + thread.join(); + } + assertNull(error.get(), String.valueOf(error.get())); + } + + @Test + @Timeout(10) + protected void testConnectTimeout() throws Exception { + session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, 100); + int port = 1; + newTransporter("http://localhost:" + port); + try { + transporter.get(new GetTask(URI.create("repo/file.txt"))); + fail("Expected error"); + } catch (Exception e) { + // impl specific "timeout" exception + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + } + + @Test + @Timeout(10) + protected void testRequestTimeout() throws Exception { + session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 100); + ServerSocket server = new ServerSocket(0); + try (server) { + newTransporter("http://localhost:" + server.getLocalPort()); + try { + transporter.get(new GetTask(URI.create("repo/file.txt"))); + fail("Expected error"); + } catch (Exception e) { + assertTrue(e.getClass().getSimpleName().contains("Timeout")); + assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); + } + } + } + + @Test + protected void testUserAgent() throws Exception { + session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0"); + newTransporter(httpServer.getHttpUrl()); + transporter.get(new GetTask(URI.create("repo/file.txt"))); + assertEquals(1, httpServer.getLogEntries().size()); + for (HttpServer.LogEntry log : httpServer.getLogEntries()) { + assertEquals("SomeTest/1.0", log.getHeaders().get("User-Agent")); + } + } + + @Test + protected void testCustomHeaders() throws Exception { + Map headers = new HashMap<>(); + headers.put("User-Agent", "Custom/1.0"); + headers.put("X-CustomHeader", "Custom-Value"); + session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0"); + session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers); + newTransporter(httpServer.getHttpUrl()); + transporter.get(new GetTask(URI.create("repo/file.txt"))); + assertEquals(1, httpServer.getLogEntries().size()); + for (HttpServer.LogEntry log : httpServer.getLogEntries()) { + for (Map.Entry entry : headers.entrySet()) { + assertEquals(entry.getValue(), log.getHeaders().get(entry.getKey()), entry.getKey()); + } + } + } + + @Test + protected void testServerAuthScope_NotUsedForProxy() throws Exception { + String username = "testuser", password = "testpass"; + httpServer.setProxyAuthentication(username, password); + auth = new AuthenticationBuilder() + .addUsername(username) + .addPassword(password) + .build(); + proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort()); + newTransporter("http://" + httpServer.getHost() + ":12/"); + try { + transporter.get(new GetTask(URI.create("repo/file.txt"))); + fail("Server auth must not be used as proxy auth"); + } catch (HttpTransporterException e) { + assertEquals(407, e.getStatusCode()); + } catch (IOException e) { + // accepted as well: point is to fail + } + } + + @Test + protected void testProxyAuthScope_NotUsedForServer() throws Exception { + String username = "testuser", password = "testpass"; + httpServer.setAuthentication(username, password); + Authentication auth = new AuthenticationBuilder() + .addUsername(username) + .addPassword(password) + .build(); + proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); + newTransporter("http://" + httpServer.getHost() + ":12/"); + try { + transporter.get(new GetTask(URI.create("repo/file.txt"))); + fail("Proxy auth must not be used as server auth"); + } catch (HttpTransporterException e) { + assertEquals(401, e.getStatusCode()); + } catch (IOException e) { + // accepted as well: point is to fail + } + } + + @Test + protected void testAuthSchemeReuse() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + httpServer.setProxyAuthentication("proxyuser", "proxypass"); + session.setCache(new DefaultRepositoryCache()); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + Authentication auth = new AuthenticationBuilder() + .addUsername("proxyuser") + .addPassword("proxypass") + .build(); + proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); + newTransporter("http://bad.localhost:1/"); + GetTask task = new GetTask(URI.create("repo/file.txt")); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals(3, httpServer.getLogEntries().size()); + httpServer.getLogEntries().clear(); + newTransporter("http://bad.localhost:1/"); + task = new GetTask(URI.create("repo/file.txt")); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals(1, httpServer.getLogEntries().size()); + assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Authorization")); + assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Proxy-Authorization")); + } + + @Test + protected void testAuthSchemePreemptive() throws Exception { + httpServer.setAuthentication("testuser", "testpass"); + session.setCache(new DefaultRepositoryCache()); + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + + session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, false); + newTransporter(httpServer.getHttpUrl()); + GetTask task = new GetTask(URI.create("repo/file.txt")); + transporter.get(task); + assertEquals("test", task.getDataString()); + // there ARE challenge round-trips + assertEquals(2, httpServer.getLogEntries().size()); + + httpServer.getLogEntries().clear(); + + session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true); + newTransporter(httpServer.getHttpUrl()); + task = new GetTask(URI.create("repo/file.txt")); + transporter.get(task); + assertEquals("test", task.getDataString()); + // there are NO challenge round-trips, all goes through at first + assertEquals(1, httpServer.getLogEntries().size()); + } + + @Test + void testInit_BadProtocol() { + assertThrows(NoTransporterException.class, () -> newTransporter("bad:/void")); + } + + @Test + void testInit_BadUrl() { + assertThrows(NoTransporterException.class, () -> newTransporter("http://localhost:NaN")); + } + + @Test + void testInit_CaseInsensitiveProtocol() throws Exception { + newTransporter("http://localhost"); + newTransporter("HTTP://localhost"); + newTransporter("Http://localhost"); + newTransporter("https://localhost"); + newTransporter("HTTPS://localhost"); + newTransporter("HttpS://localhost"); + } +} diff --git a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/RecordingTransportListener.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/RecordingTransportListener.java index 8031ce026..7dac9b666 100644 --- a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/RecordingTransportListener.java +++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/RecordingTransportListener.java @@ -56,7 +56,14 @@ public void transportStarted(long dataOffset, long dataLength) throws TransferCa @Override public void transportProgressed(ByteBuffer data) throws TransferCancelledException { progressedCount++; - baos.write(data.array(), data.arrayOffset() + ((Buffer) data).position(), data.remaining()); + if (data.hasArray()) { + baos.write(data.array(), data.arrayOffset() + ((Buffer) data).position(), data.remaining()); + } else { + byte[] arr = new byte[data.remaining()]; + data.mark(); + data.get(arr); + data.reset(); + } if (cancelProgress) { throw new TransferCancelledException(); } diff --git a/maven-resolver-transport-apache/src/test/resources/ssl/README.txt b/maven-resolver-test-http/src/main/resources/ssl/README.txt similarity index 100% rename from maven-resolver-transport-apache/src/test/resources/ssl/README.txt rename to maven-resolver-test-http/src/main/resources/ssl/README.txt diff --git a/maven-resolver-transport-apache/src/test/resources/ssl/client-store b/maven-resolver-test-http/src/main/resources/ssl/client-store similarity index 100% rename from maven-resolver-transport-apache/src/test/resources/ssl/client-store rename to maven-resolver-test-http/src/main/resources/ssl/client-store diff --git a/maven-resolver-transport-apache/src/test/resources/ssl/server-store b/maven-resolver-test-http/src/main/resources/ssl/server-store similarity index 100% rename from maven-resolver-transport-apache/src/test/resources/ssl/server-store rename to maven-resolver-test-http/src/main/resources/ssl/server-store diff --git a/maven-resolver-transport-apache/src/test/resources/ssl/server-store-selfsigned b/maven-resolver-test-http/src/main/resources/ssl/server-store-selfsigned similarity index 100% rename from maven-resolver-transport-apache/src/test/resources/ssl/server-store-selfsigned rename to maven-resolver-test-http/src/main/resources/ssl/server-store-selfsigned diff --git a/maven-resolver-transport-apache/pom.xml b/maven-resolver-transport-apache/pom.xml index 869fbb16a..ba6a19b9b 100644 --- a/maven-resolver-transport-apache/pom.xml +++ b/maven-resolver-transport-apache/pom.xml @@ -93,14 +93,20 @@ provided true + + + org.junit.jupiter + junit-jupiter-api + test + com.google.inject guice test - org.junit.jupiter - junit-jupiter-api + org.slf4j + slf4j-simple test diff --git a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java index b7df5c1a8..56ab1ea82 100644 --- a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java +++ b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java @@ -93,6 +93,8 @@ import org.eclipse.aether.spi.connector.transport.PeekTask; import org.eclipse.aether.spi.connector.transport.PutTask; import org.eclipse.aether.spi.connector.transport.TransportTask; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; import org.eclipse.aether.transfer.NoTransporterException; import org.eclipse.aether.transfer.TransferCancelledException; import org.eclipse.aether.util.ConfigUtils; @@ -112,7 +114,7 @@ /** * A transporter for HTTP/HTTPS. */ -final class ApacheTransporter extends AbstractTransporter { +final class ApacheTransporter extends AbstractTransporter implements HttpTransporter { private static final Pattern CONTENT_RANGE_PATTERN = Pattern.compile("\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*"); @@ -396,8 +398,8 @@ private URI resolve(TransportTask task) { @Override public int classify(Throwable error) { - if (error instanceof HttpResponseException - && ((HttpResponseException) error).getStatusCode() == HttpStatus.SC_NOT_FOUND) { + if (error instanceof HttpTransporterException + && ((HttpTransporterException) error).getStatusCode() == HttpStatus.SC_NOT_FOUND) { return ERROR_NOT_FOUND; } return ERROR_OTHER; @@ -406,7 +408,11 @@ public int classify(Throwable error) { @Override protected void implPeek(PeekTask task) throws Exception { HttpHead request = commonHeaders(new HttpHead(resolve(task))); - execute(request, null); + try { + execute(request, null); + } catch (HttpResponseException e) { + throw new HttpTransporterException(e.getStatusCode()); + } } @Override @@ -430,7 +436,7 @@ protected void implGet(GetTask task) throws Exception { resume = false; continue; } - throw e; + throw new HttpTransporterException(e.getStatusCode()); } } } @@ -448,7 +454,7 @@ protected void implPut(PutTask task) throws Exception { execute(request, null); return; } - throw e; + throw new HttpTransporterException(e.getStatusCode()); } } @@ -475,7 +481,7 @@ private void execute(HttpUriRequest request, EntityGetter getter) throws Excepti } } - private void prepare(HttpUriRequest request, SharingHttpContext context) { + private void prepare(HttpUriRequest request, SharingHttpContext context) throws HttpTransporterException { final boolean put = HttpPut.METHOD_NAME.equalsIgnoreCase(request.getMethod()); if (preemptiveAuth || (preemptivePutAuth && put)) { context.getAuthCache().put(server, new BasicScheme()); @@ -497,7 +503,7 @@ private void prepare(HttpUriRequest request, SharingHttpContext context) { } @SuppressWarnings("checkstyle:magicnumber") - private void mkdirs(URI uri, SharingHttpContext context) { + private void mkdirs(URI uri, SharingHttpContext context) throws HttpTransporterException { List dirs = UriUtils.getDirectories(baseUri, uri); int index = 0; for (; index < dirs.size(); index++) { diff --git a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporterFactory.java b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporterFactory.java index 31bfec544..621188eed 100644 --- a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporterFactory.java +++ b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporterFactory.java @@ -22,8 +22,8 @@ import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.spi.connector.transport.Transporter; -import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory; import org.eclipse.aether.transfer.NoTransporterException; import static java.util.Objects.requireNonNull; @@ -33,7 +33,7 @@ * support uploads to WebDAV servers and resumable downloads. */ @Named(ApacheTransporterFactory.NAME) -public final class ApacheTransporterFactory implements TransporterFactory { +public final class ApacheTransporterFactory implements HttpTransporterFactory { public static final String NAME = "apache"; private float priority = 5.0f; @@ -55,7 +55,7 @@ public ApacheTransporterFactory setPriority(float priority) { } @Override - public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + public HttpTransporter newInstance(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { requireNonNull(session, "session cannot be null"); requireNonNull(repository, "repository cannot be null"); diff --git a/maven-resolver-transport-apache/src/test/java/org/eclipse/aether/transport/apache/ApacheTransporterTest.java b/maven-resolver-transport-apache/src/test/java/org/eclipse/aether/transport/apache/ApacheTransporterTest.java index 4efc131d0..d0bce405d 100644 --- a/maven-resolver-transport-apache/src/test/java/org/eclipse/aether/transport/apache/ApacheTransporterTest.java +++ b/maven-resolver-transport-apache/src/test/java/org/eclipse/aether/transport/apache/ApacheTransporterTest.java @@ -19,430 +19,30 @@ package org.eclipse.aether.transport.apache; import java.io.File; -import java.io.FileNotFoundException; -import java.net.ConnectException; -import java.net.ServerSocket; -import java.net.SocketTimeoutException; import java.net.URI; import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import org.apache.http.NoHttpResponseException; -import org.apache.http.client.HttpResponseException; -import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.pool.ConnPoolControl; import org.apache.http.pool.PoolStats; import org.eclipse.aether.ConfigurationProperties; import org.eclipse.aether.DefaultRepositoryCache; -import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.internal.test.util.TestFileUtils; -import org.eclipse.aether.internal.test.util.TestUtils; -import org.eclipse.aether.internal.test.util.http.HttpServer; +import org.eclipse.aether.internal.test.util.http.HttpTransporterTest; import org.eclipse.aether.internal.test.util.http.RecordingTransportListener; -import org.eclipse.aether.repository.Authentication; -import org.eclipse.aether.repository.Proxy; -import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.spi.connector.transport.GetTask; -import org.eclipse.aether.spi.connector.transport.PeekTask; import org.eclipse.aether.spi.connector.transport.PutTask; -import org.eclipse.aether.spi.connector.transport.Transporter; -import org.eclipse.aether.spi.connector.transport.TransporterFactory; -import org.eclipse.aether.transfer.NoTransporterException; -import org.eclipse.aether.transfer.TransferCancelledException; -import org.eclipse.aether.util.repository.AuthenticationBuilder; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; -import org.junit.jupiter.api.Timeout; import static org.junit.jupiter.api.Assertions.*; /** + * Apache Transporter UT. + * It does support WebDAV. */ -public class ApacheTransporterTest { +class ApacheTransporterTest extends HttpTransporterTest { - static { - System.setProperty( - "javax.net.ssl.trustStore", new File("src/test/resources/ssl/server-store").getAbsolutePath()); - System.setProperty("javax.net.ssl.trustStorePassword", "server-pwd"); - System.setProperty("javax.net.ssl.keyStore", new File("src/test/resources/ssl/client-store").getAbsolutePath()); - System.setProperty("javax.net.ssl.keyStorePassword", "client-pwd"); - } - - private DefaultRepositorySystemSession session; - - private TransporterFactory factory; - - private Transporter transporter; - - private File repoDir; - - private HttpServer httpServer; - - private Authentication auth; - - private Proxy proxy; - - private RemoteRepository newRepo(String url) { - return new RemoteRepository.Builder("test", "default", url) - .setAuthentication(auth) - .setProxy(proxy) - .build(); - } - - private void newTransporter(String url) throws Exception { - if (transporter != null) { - transporter.close(); - transporter = null; - } - transporter = factory.newInstance(session, newRepo(url)); - } - - private static final long OLD_FILE_TIMESTAMP = 160660800000L; - - @BeforeEach - void setUp(TestInfo testInfo) throws Exception { - System.out.println("=== " + testInfo.getDisplayName() + " ==="); - session = TestUtils.newSession(); - factory = new ApacheTransporterFactory(); - repoDir = TestFileUtils.createTempDir(); - TestFileUtils.writeString(new File(repoDir, "file.txt"), "test"); - TestFileUtils.writeString(new File(repoDir, "dir/file.txt"), "test"); - TestFileUtils.writeString(new File(repoDir, "dir/oldFile.txt"), "oldTest", OLD_FILE_TIMESTAMP); - TestFileUtils.writeString(new File(repoDir, "empty.txt"), ""); - TestFileUtils.writeString(new File(repoDir, "some space.txt"), "space"); - File resumable = new File(repoDir, "resume.txt"); - TestFileUtils.writeString(resumable, "resumable"); - resumable.setLastModified(System.currentTimeMillis() - 90 * 1000); - httpServer = new HttpServer().setRepoDir(repoDir).start(); - newTransporter(httpServer.getHttpUrl()); - } - - @AfterEach - void tearDown() throws Exception { - if (transporter != null) { - transporter.close(); - transporter = null; - } - if (httpServer != null) { - httpServer.stop(); - httpServer = null; - } - factory = null; - session = null; - } - - @Test - void testClassify() { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(new FileNotFoundException())); - assertEquals(Transporter.ERROR_OTHER, transporter.classify(new HttpResponseException(403, "Forbidden"))); - assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(new HttpResponseException(404, "Not Found"))); - } - - @Test - void testPeek() throws Exception { - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - } - - @Test - void testRetryHandler_defaultCount_positive() throws Exception { - httpServer.setConnectionsToClose(3); - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - } - - @Test - void testRetryHandler_defaultCount_negative() throws Exception { - httpServer.setConnectionsToClose(4); - try { - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - fail("Expected error"); - } catch (NoHttpResponseException expected) { - } - } - - @Test - void testRetryHandler_explicitCount_positive() throws Exception { - session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 10); - newTransporter(httpServer.getHttpUrl()); - httpServer.setConnectionsToClose(10); - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - } - - @Test - void testRetryHandler_disabled() throws Exception { - session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 0); - newTransporter(httpServer.getHttpUrl()); - httpServer.setConnectionsToClose(1); - try { - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - } catch (NoHttpResponseException expected) { - } - } - - @Test - void testPeek_NotFound() throws Exception { - try { - transporter.peek(new PeekTask(URI.create("repo/missing.txt"))); - fail("Expected error"); - } catch (HttpResponseException e) { - assertEquals(404, e.getStatusCode()); - assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e)); - } - } - - @Test - void testPeek_Closed() throws Exception { - transporter.close(); - try { - transporter.peek(new PeekTask(URI.create("repo/missing.txt"))); - fail("Expected error"); - } catch (IllegalStateException e) { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - } - - @Test - void testPeek_Authenticated() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - } - - @Test - void testPeek_Unauthenticated() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - try { - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - fail("Expected error"); - } catch (HttpResponseException e) { - assertEquals(401, e.getStatusCode()); - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - } - - @Test - void testPeek_ProxyAuthenticated() throws Exception { - httpServer.setProxyAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); - newTransporter("http://bad.localhost:1/"); - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - } - - @Test - void testPeek_ProxyUnauthenticated() throws Exception { - httpServer.setProxyAuthentication("testuser", "testpass"); - proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort()); - newTransporter("http://bad.localhost:1/"); - try { - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - fail("Expected error"); - } catch (HttpResponseException e) { - assertEquals(407, e.getStatusCode()); - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - } - - @Test - void testPeek_SSL() throws Exception { - httpServer.addSslConnector(); - newTransporter(httpServer.getHttpsUrl()); - transporter.peek(new PeekTask(URI.create("repo/file.txt"))); - } - - @Test - void testPeek_Redirect() throws Exception { - httpServer.addSslConnector(); - transporter.peek(new PeekTask(URI.create("redirect/file.txt"))); - transporter.peek(new PeekTask(URI.create("redirect/file.txt?scheme=https"))); - } - - @Test - void testGet_ToMemory() throws Exception { - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals(0L, listener.getDataOffset()); - assertEquals(4L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals(task.getDataString(), new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_ToFile() throws Exception { - File file = TestFileUtils.createTempFile("failure"); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = - new GetTask(URI.create("repo/file.txt")).setDataFile(file).setListener(listener); - transporter.get(task); - assertEquals("test", TestFileUtils.readString(file)); - assertEquals(0L, listener.getDataOffset()); - assertEquals(4L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("test", new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_ToFileTimestamp() throws Exception { - File file = TestFileUtils.createTempFile("failure"); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("repo/dir/oldFile.txt")) - .setDataFile(file) - .setListener(listener); - transporter.get(task); - assertEquals("oldTest", TestFileUtils.readString(file)); - assertEquals(0L, listener.getDataOffset()); - assertEquals(7L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("oldTest", new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - assertEquals(file.lastModified(), OLD_FILE_TIMESTAMP); - } - - @Test - void testGet_EmptyResource() throws Exception { - File file = TestFileUtils.createTempFile("failure"); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = - new GetTask(URI.create("repo/empty.txt")).setDataFile(file).setListener(listener); - transporter.get(task); - assertEquals("", TestFileUtils.readString(file)); - assertEquals(0L, listener.getDataOffset()); - assertEquals(0L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertEquals(0, listener.getProgressedCount()); - assertEquals("", new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_EncodedResourcePath() throws Exception { - GetTask task = new GetTask(URI.create("repo/some%20space.txt")); - transporter.get(task); - assertEquals("space", task.getDataString()); - } - - @Test - void testGet_Authenticated() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals(0L, listener.getDataOffset()); - assertEquals(4L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals(task.getDataString(), new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_Unauthenticated() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - try { - transporter.get(new GetTask(URI.create("repo/file.txt"))); - fail("Expected error"); - } catch (HttpResponseException e) { - assertEquals(401, e.getStatusCode()); - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - } - - @Test - void testGet_ProxyAuthenticated() throws Exception { - httpServer.setProxyAuthentication("testuser", "testpass"); - Authentication auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); - newTransporter("http://bad.localhost:1/"); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals(0L, listener.getDataOffset()); - assertEquals(4L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals(task.getDataString(), new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_ProxyUnauthenticated() throws Exception { - httpServer.setProxyAuthentication("testuser", "testpass"); - proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort()); - newTransporter("http://bad.localhost:1/"); - try { - transporter.get(new GetTask(URI.create("repo/file.txt"))); - fail("Expected error"); - } catch (HttpResponseException e) { - assertEquals(407, e.getStatusCode()); - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - } - - @Test - void testGet_SSL() throws Exception { - httpServer.addSslConnector(); - newTransporter(httpServer.getHttpsUrl()); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals(0L, listener.getDataOffset()); - assertEquals(4L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals(task.getDataString(), new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_HTTPS_Unknown_SecurityMode() throws Exception { - session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE, "unknown"); - httpServer.addSelfSignedSslConnector(); - try { - newTransporter(httpServer.getHttpsUrl()); - fail("Unsupported security mode"); - } catch (IllegalArgumentException a) { - // good - } - } - - @Test - void testGet_HTTPS_Insecure_SecurityMode() throws Exception { - // here we use alternate server-store-selfigned key (as the key set it static initalizer is probably already - // used to init SSLContext/SSLSocketFactory/etc - session.setConfigProperty( - ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE); - httpServer.addSelfSignedSslConnector(); - newTransporter(httpServer.getHttpsUrl()); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals(0L, listener.getDataOffset()); - assertEquals(4L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals(task.getDataString(), new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); + public ApacheTransporterTest() { + super(ApacheTransporterFactory::new); } @Test @@ -462,389 +62,6 @@ void testGet_WebDav() throws Exception { 1, httpServer.getLogEntries().size(), httpServer.getLogEntries().toString()); } - @Test - void testGet_Redirect() throws Exception { - httpServer.addSslConnector(); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("redirect/file.txt?scheme=https")).setListener(listener); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals(0L, listener.getDataOffset()); - assertEquals(4L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals(task.getDataString(), new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_Resume() throws Exception { - File file = TestFileUtils.createTempFile("re"); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("repo/resume.txt")) - .setDataFile(file, true) - .setListener(listener); - transporter.get(task); - assertEquals("resumable", TestFileUtils.readString(file)); - assertEquals(1L, listener.getStartedCount()); - assertEquals(2L, listener.getDataOffset()); - assertEquals(9, listener.getDataLength()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("sumable", new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_ResumeLocalContentsOutdated() throws Exception { - File file = TestFileUtils.createTempFile("re"); - file.setLastModified(System.currentTimeMillis() - 5 * 60 * 1000); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("repo/resume.txt")) - .setDataFile(file, true) - .setListener(listener); - transporter.get(task); - assertEquals("resumable", TestFileUtils.readString(file)); - assertEquals(1L, listener.getStartedCount()); - assertEquals(0L, listener.getDataOffset()); - assertEquals(9, listener.getDataLength()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("resumable", new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_ResumeRangesNotSupportedByServer() throws Exception { - httpServer.setRangeSupport(false); - File file = TestFileUtils.createTempFile("re"); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask(URI.create("repo/resume.txt")) - .setDataFile(file, true) - .setListener(listener); - transporter.get(task); - assertEquals("resumable", TestFileUtils.readString(file)); - assertEquals(1L, listener.getStartedCount()); - assertEquals(0L, listener.getDataOffset()); - assertEquals(9, listener.getDataLength()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("resumable", new String(listener.getBaos().toByteArray(), StandardCharsets.UTF_8)); - } - - @Test - void testGet_Checksums_Nexus() throws Exception { - httpServer.setChecksumHeader(HttpServer.ChecksumHeader.NEXUS); - GetTask task = new GetTask(URI.create("repo/file.txt")); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals( - "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1")); - } - - @Test - void testGet_Checksums_XChecksum() throws Exception { - httpServer.setChecksumHeader(HttpServer.ChecksumHeader.XCHECKSUM); - GetTask task = new GetTask(URI.create("repo/file.txt")); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals( - "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1")); - } - - @Test - void testGet_FileHandleLeak() throws Exception { - for (int i = 0; i < 100; i++) { - File file = TestFileUtils.createTempFile("failure"); - transporter.get(new GetTask(URI.create("repo/file.txt")).setDataFile(file)); - assertTrue(file.delete(), i + ", " + file.getAbsolutePath()); - } - } - - @Test - void testGet_NotFound() throws Exception { - try { - transporter.get(new GetTask(URI.create("repo/missing.txt"))); - fail("Expected error"); - } catch (HttpResponseException e) { - assertEquals(404, e.getStatusCode()); - assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e)); - } - } - - @Test - void testGet_Closed() throws Exception { - transporter.close(); - try { - transporter.get(new GetTask(URI.create("repo/file.txt"))); - fail("Expected error"); - } catch (IllegalStateException e) { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - } - - @Test - void testGet_StartCancelled() throws Exception { - RecordingTransportListener listener = new RecordingTransportListener(); - listener.cancelStart(); - GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); - try { - transporter.get(task); - fail("Expected error"); - } catch (TransferCancelledException e) { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - assertEquals(0L, listener.getDataOffset()); - assertEquals(4L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertEquals(0, listener.getProgressedCount()); - } - - @Test - void testGet_ProgressCancelled() throws Exception { - RecordingTransportListener listener = new RecordingTransportListener(); - listener.cancelProgress(); - GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); - try { - transporter.get(task); - fail("Expected error"); - } catch (TransferCancelledException e) { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - assertEquals(0L, listener.getDataOffset()); - assertEquals(4L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertEquals(1, listener.getProgressedCount()); - } - - @Test - void testPut_FromMemory() throws Exception { - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - - @Test - void testPut_FromFile() throws Exception { - File file = TestFileUtils.createTempFile("upload"); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataFile(file); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - - @Test - void testPut_EmptyResource() throws Exception { - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = new PutTask(URI.create("repo/file.txt")).setListener(listener); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(0L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertEquals(0, listener.getProgressedCount()); - assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - - @Test - void testPut_EncodedResourcePath() throws Exception { - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = new PutTask(URI.create("repo/some%20space.txt")) - .setListener(listener) - .setDataString("OK"); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(2L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt"))); - } - - @Test - void testPut_Authenticated_ExpectContinue() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - - @Test - void testPut_Authenticated_ExpectContinueBroken() throws Exception { - // this makes OPTIONS recover, and have only 1 PUT (startedCount=1 as OPTIONS is not counted) - session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true); - httpServer.setAuthentication("testuser", "testpass"); - httpServer.setExpectSupport(HttpServer.ExpectContinue.BROKEN); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - - @Test - void testPut_Authenticated_ExpectContinueRejected() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - - @Test - void testPut_Authenticated_ExpectContinueDisabled() throws Exception { - session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false); - httpServer.setAuthentication("testuser", "testpass"); - httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); // w/ expectContinue enabled would have here 2 - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - - @Test - void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception { - Map headers = new HashMap<>(); - headers.put("Expect", "100-continue"); - session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers); - httpServer.setAuthentication("testuser", "testpass"); - httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - - @Test - void testPut_Unauthenticated() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - try { - transporter.put(task); - fail("Expected error"); - } catch (HttpResponseException e) { - assertEquals(401, e.getStatusCode()); - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - assertEquals(0, listener.getStartedCount()); - assertEquals(0, listener.getProgressedCount()); - } - - @Test - void testPut_ProxyAuthenticated() throws Exception { - httpServer.setProxyAuthentication("testuser", "testpass"); - Authentication auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); - newTransporter("http://bad.localhost:1/"); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - - @Test - void testPut_ProxyUnauthenticated() throws Exception { - httpServer.setProxyAuthentication("testuser", "testpass"); - proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort()); - newTransporter("http://bad.localhost:1/"); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - try { - transporter.put(task); - fail("Expected error"); - } catch (HttpResponseException e) { - assertEquals(407, e.getStatusCode()); - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - assertEquals(0, listener.getStartedCount()); - assertEquals(0, listener.getProgressedCount()); - } - - @Test - void testPut_SSL() throws Exception { - httpServer.addSslConnector(); - httpServer.setAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpsUrl()); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - transporter.put(task); - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); - assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); - } - @Test void testPut_WebDav() throws Exception { httpServer.setWebDav(true); @@ -873,323 +90,6 @@ void testPut_WebDav() throws Exception { assertEquals("PUT", httpServer.getLogEntries().get(4).getMethod()); } - @Test - void testPut_FileHandleLeak() throws Exception { - for (int i = 0; i < 100; i++) { - File src = TestFileUtils.createTempFile("upload"); - File dst = new File(repoDir, "file.txt"); - transporter.put(new PutTask(URI.create("repo/file.txt")).setDataFile(src)); - assertTrue(src.delete(), i + ", " + src.getAbsolutePath()); - assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath()); - } - } - - @Test - void testPut_Closed() throws Exception { - transporter.close(); - try { - transporter.put(new PutTask(URI.create("repo/missing.txt"))); - fail("Expected error"); - } catch (IllegalStateException e) { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - } - - @Test - void testPut_StartCancelled() throws Exception { - RecordingTransportListener listener = new RecordingTransportListener(); - listener.cancelStart(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - try { - transporter.put(task); - fail("Expected error"); - } catch (TransferCancelledException e) { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertEquals(0, listener.getProgressedCount()); - } - - @Test - void testPut_ProgressCancelled() throws Exception { - RecordingTransportListener listener = new RecordingTransportListener(); - listener.cancelProgress(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - try { - transporter.put(task); - fail("Expected error"); - } catch (TransferCancelledException e) { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - assertEquals(0L, listener.getDataOffset()); - assertEquals(6L, listener.getDataLength()); - assertEquals(1, listener.getStartedCount()); - assertEquals(1, listener.getProgressedCount()); - } - - @Test - void testGetPut_AuthCache() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - GetTask get = new GetTask(URI.create("repo/file.txt")); - transporter.get(get); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); - transporter.put(task); - assertEquals(1, listener.getStartedCount()); - } - - @Test - void testPut_PreemptiveIsDefault() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); - transporter.put(task); - assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth - } - - @Test - void testPut_AuthCache() throws Exception { - session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH, false); - httpServer.setAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); - transporter.put(task); - assertEquals(2, httpServer.getLogEntries().size()); // put (challenged) + put w/ auth - httpServer.getLogEntries().clear(); - task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); - transporter.put(task); - assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth - } - - @Test - void testPut_AuthCache_Preemptive() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true); - newTransporter(httpServer.getHttpUrl()); - PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); - transporter.put(task); - assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth - httpServer.getLogEntries().clear(); - task = new PutTask(URI.create("repo/file.txt")).setDataString("upload"); - transporter.put(task); - assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth - } - - @Test - @Timeout(20) - public void testConcurrency() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - newTransporter(httpServer.getHttpUrl()); - final AtomicReference error = new AtomicReference<>(); - Thread[] threads = new Thread[20]; - for (int i = 0; i < threads.length; i++) { - final String path = "repo/file.txt?i=" + i; - threads[i] = new Thread() { - @Override - public void run() { - try { - for (int j = 0; j < 100; j++) { - GetTask task = new GetTask(URI.create(path)); - transporter.get(task); - assertEquals("test", task.getDataString()); - } - } catch (Throwable t) { - error.compareAndSet(null, t); - System.err.println(path); - t.printStackTrace(); - } - } - }; - threads[i].setName("Task-" + i); - } - for (Thread thread : threads) { - thread.start(); - } - for (Thread thread : threads) { - thread.join(); - } - assertNull(error.get(), String.valueOf(error.get())); - } - - @Test - @Timeout(10) - public void testConnectTimeout() throws Exception { - session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, 100); - int port = 1; - newTransporter("http://localhost:" + port); - try { - transporter.get(new GetTask(URI.create("repo/file.txt"))); - fail("Expected error"); - } catch (ConnectTimeoutException | ConnectException e) { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - } - - @Test - @Timeout(10) - public void testRequestTimeout() throws Exception { - session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 100); - ServerSocket server = new ServerSocket(0); - newTransporter("http://localhost:" + server.getLocalPort()); - try { - try { - transporter.get(new GetTask(URI.create("repo/file.txt"))); - fail("Expected error"); - } catch (SocketTimeoutException e) { - assertEquals(Transporter.ERROR_OTHER, transporter.classify(e)); - } - } finally { - server.close(); - } - } - - @Test - void testUserAgent() throws Exception { - session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0"); - newTransporter(httpServer.getHttpUrl()); - transporter.get(new GetTask(URI.create("repo/file.txt"))); - assertEquals(1, httpServer.getLogEntries().size()); - for (HttpServer.LogEntry log : httpServer.getLogEntries()) { - assertEquals("SomeTest/1.0", log.getHeaders().get("User-Agent")); - } - } - - @Test - void testCustomHeaders() throws Exception { - Map headers = new HashMap<>(); - headers.put("User-Agent", "Custom/1.0"); - headers.put("X-CustomHeader", "Custom-Value"); - session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0"); - session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers); - newTransporter(httpServer.getHttpUrl()); - transporter.get(new GetTask(URI.create("repo/file.txt"))); - assertEquals(1, httpServer.getLogEntries().size()); - for (HttpServer.LogEntry log : httpServer.getLogEntries()) { - for (Map.Entry entry : headers.entrySet()) { - assertEquals(entry.getValue(), log.getHeaders().get(entry.getKey()), entry.getKey()); - } - } - } - - @Test - void testServerAuthScope_NotUsedForProxy() throws Exception { - String username = "testuser", password = "testpass"; - httpServer.setProxyAuthentication(username, password); - auth = new AuthenticationBuilder() - .addUsername(username) - .addPassword(password) - .build(); - proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort()); - newTransporter("http://" + httpServer.getHost() + ":12/"); - try { - transporter.get(new GetTask(URI.create("repo/file.txt"))); - fail("Server auth must not be used as proxy auth"); - } catch (HttpResponseException e) { - assertEquals(407, e.getStatusCode()); - } - } - - @Test - void testProxyAuthScope_NotUsedForServer() throws Exception { - String username = "testuser", password = "testpass"; - httpServer.setAuthentication(username, password); - Authentication auth = new AuthenticationBuilder() - .addUsername(username) - .addPassword(password) - .build(); - proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); - newTransporter("http://" + httpServer.getHost() + ":12/"); - try { - transporter.get(new GetTask(URI.create("repo/file.txt"))); - fail("Proxy auth must not be used as server auth"); - } catch (HttpResponseException e) { - assertEquals(401, e.getStatusCode()); - } - } - - @Test - void testAuthSchemeReuse() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - httpServer.setProxyAuthentication("proxyuser", "proxypass"); - session.setCache(new DefaultRepositoryCache()); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - Authentication auth = new AuthenticationBuilder() - .addUsername("proxyuser") - .addPassword("proxypass") - .build(); - proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth); - newTransporter("http://bad.localhost:1/"); - GetTask task = new GetTask(URI.create("repo/file.txt")); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals(3, httpServer.getLogEntries().size()); - httpServer.getLogEntries().clear(); - newTransporter("http://bad.localhost:1/"); - task = new GetTask(URI.create("repo/file.txt")); - transporter.get(task); - assertEquals("test", task.getDataString()); - assertEquals(1, httpServer.getLogEntries().size()); - assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Authorization")); - assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Proxy-Authorization")); - } - - @Test - void testAuthSchemePreemptive() throws Exception { - httpServer.setAuthentication("testuser", "testpass"); - session.setCache(new DefaultRepositoryCache()); - auth = new AuthenticationBuilder() - .addUsername("testuser") - .addPassword("testpass") - .build(); - - session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, false); - newTransporter(httpServer.getHttpUrl()); - GetTask task = new GetTask(URI.create("repo/file.txt")); - transporter.get(task); - assertEquals("test", task.getDataString()); - // there ARE challenge round-trips - assertEquals(2, httpServer.getLogEntries().size()); - - httpServer.getLogEntries().clear(); - - session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true); - newTransporter(httpServer.getHttpUrl()); - task = new GetTask(URI.create("repo/file.txt")); - transporter.get(task); - assertEquals("test", task.getDataString()); - // there are NO challenge round-trips, all goes through at first - assertEquals(1, httpServer.getLogEntries().size()); - } - @Test void testConnectionReuse() throws Exception { httpServer.addSslConnector(); @@ -1222,24 +122,4 @@ void testConnectionNoReuse() throws Exception { .getTotalStats(); assertEquals(0, stats.getAvailable(), stats.toString()); } - - @Test - void testInit_BadProtocol() { - assertThrows(NoTransporterException.class, () -> newTransporter("bad:/void")); - } - - @Test - void testInit_BadUrl() { - assertThrows(NoTransporterException.class, () -> newTransporter("http://localhost:NaN")); - } - - @Test - void testInit_CaseInsensitiveProtocol() throws Exception { - newTransporter("http://localhost"); - newTransporter("HTTP://localhost"); - newTransporter("Http://localhost"); - newTransporter("https://localhost"); - newTransporter("HTTPS://localhost"); - newTransporter("HttpS://localhost"); - } } diff --git a/maven-resolver-transport-apache/src/test/resources/logback.xml b/maven-resolver-transport-apache/src/test/resources/logback.xml deleted file mode 100644 index 9addbd505..000000000 --- a/maven-resolver-transport-apache/src/test/resources/logback.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - diff --git a/maven-resolver-transport-apache/src/test/resources/simplelogger.properties b/maven-resolver-transport-apache/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..c52c663ce --- /dev/null +++ b/maven-resolver-transport-apache/src/test/resources/simplelogger.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +org.slf4j.simpleLogger.defaultLogLevel=debug diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/pom.xml b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/pom.xml index cd3b54cbe..a07653c1a 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/pom.xml +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/pom.xml @@ -68,11 +68,21 @@ junit-jupiter-api test + + org.slf4j + slf4j-simple + test + org.apache.maven.resolver maven-resolver-test-util test + + org.apache.maven.resolver + maven-resolver-test-http + test + org.apache.maven.resolver maven-resolver-impl diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java index 6197148aa..62c5080e7 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java @@ -18,29 +18,21 @@ */ package org.eclipse.aether.transport.jdk; -import javax.net.ssl.SSLContext; +import javax.net.ssl.*; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.net.Authenticator; -import java.net.ConnectException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.PasswordAuthentication; -import java.net.ProxySelector; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.UnknownHostException; +import java.net.*; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileTime; -import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; @@ -64,6 +56,8 @@ import org.eclipse.aether.spi.connector.transport.PeekTask; import org.eclipse.aether.spi.connector.transport.PutTask; import org.eclipse.aether.spi.connector.transport.TransportTask; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; import org.eclipse.aether.transfer.NoTransporterException; import org.eclipse.aether.util.ConfigUtils; import org.eclipse.aether.util.FileUtils; @@ -79,7 +73,7 @@ * @since 2.0.0 */ @SuppressWarnings({"checkstyle:magicnumber"}) -final class JdkTransporter extends AbstractTransporter { +final class JdkTransporter extends AbstractTransporter implements HttpTransporter { private static final Logger LOGGER = LoggerFactory.getLogger(JdkTransporter.class); private static final DateTimeFormatter RFC7231 = DateTimeFormatter.ofPattern( @@ -202,7 +196,8 @@ private ConnectException enhance(ConnectException connectException) { @Override public int classify(Throwable error) { - if (error instanceof JdkException && ((JdkException) error).getStatusCode() == NOT_FOUND) { + if (error instanceof HttpTransporterException + && ((HttpTransporterException) error).getStatusCode() == NOT_FOUND) { return ERROR_NOT_FOUND; } return ERROR_OTHER; @@ -218,7 +213,7 @@ protected void implPeek(PeekTask task) throws Exception { try { HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.discarding()); if (response.statusCode() >= MULTIPLE_CHOICES) { - throw new JdkException(response.statusCode()); + throw new HttpTransporterException(response.statusCode()); } } catch (ConnectException e) { throw enhance(e); @@ -254,7 +249,7 @@ protected void implGet(GetTask task) throws Exception { resume = false; continue; } - throw new JdkException(response.statusCode()); + throw new HttpTransporterException(response.statusCode()); } } catch (ConnectException e) { throw enhance(e); @@ -343,7 +338,7 @@ protected void implPut(PutTask task) throws Exception { try { HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.discarding()); if (response.statusCode() >= MULTIPLE_CHOICES) { - throw new JdkException(response.statusCode()); + throw new HttpTransporterException(response.statusCode()); } } catch (ConnectException e) { throw enhance(e); @@ -425,6 +420,18 @@ private HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepo throws NoTransporterException { final String instanceKey = HTTP_INSTANCE_KEY_PREFIX + repository.getId(); + final String httpsSecurityMode = ConfigUtils.getString( + session, + ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT, + ConfigurationProperties.HTTPS_SECURITY_MODE + "." + repository.getId(), + ConfigurationProperties.HTTPS_SECURITY_MODE); + + if (!ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT.equals(httpsSecurityMode) + && !ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode)) { + throw new IllegalArgumentException("Unsupported '" + httpsSecurityMode + "' HTTPS security mode."); + } + final boolean insecure = ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode); + // todo: normally a single client per JVM is sufficient - in particular cause part of the config // is global and not per instance so we should create a client only when conf changes for a repo // else fallback on a global client @@ -448,7 +455,40 @@ private HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepo } if (sslContext == null) { - sslContext = SSLContext.getDefault(); + if (insecure) { + sslContext = SSLContext.getInstance("TLS"); + X509ExtendedTrustManager tm = new X509ExtendedTrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkClientTrusted( + X509Certificate[] chain, String authType, Socket socket) {} + + @Override + public void checkServerTrusted( + X509Certificate[] chain, String authType, Socket socket) {} + + @Override + public void checkClientTrusted( + X509Certificate[] chain, String authType, SSLEngine engine) {} + + @Override + public void checkServerTrusted( + X509Certificate[] chain, String authType, SSLEngine engine) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + sslContext.init(null, new X509TrustManager[] {tm}, null); + } else { + sslContext = SSLContext.getDefault(); + } } int connectTimeout = ConfigUtils.getInteger( @@ -467,6 +507,12 @@ private HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepo .connectTimeout(Duration.ofMillis(connectTimeout)) .sslContext(sslContext); + if (insecure) { + SSLParameters sslParameters = new SSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm(null); + builder.sslParameters(sslParameters); + } + setLocalAddress(builder, () -> getHttpLocalAddress(session, repository)); if (repository.getProxy() != null) { @@ -512,7 +558,7 @@ protected PasswordAuthentication getPasswordAuthentication() { } return result; - } catch (NoSuchAlgorithmException e) { + } catch (Exception e) { throw new WrapperEx(e); } }); diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java index 2188db26a..988fd65bb 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java @@ -22,8 +22,8 @@ import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.spi.connector.transport.Transporter; -import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory; import org.eclipse.aether.transfer.NoTransporterException; import static java.util.Objects.requireNonNull; @@ -34,7 +34,7 @@ * @since 2.0.0 */ @Named(JdkTransporterFactory.NAME) -public final class JdkTransporterFactory implements TransporterFactory { +public final class JdkTransporterFactory implements HttpTransporterFactory { public static final String NAME = "jdk"; private float priority = 10.0f; @@ -50,7 +50,7 @@ public JdkTransporterFactory setPriority(float priority) { } @Override - public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + public HttpTransporter newInstance(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { requireNonNull(session, "session cannot be null"); requireNonNull(repository, "repository cannot be null"); diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java index 266660702..44cf3f38d 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java @@ -22,19 +22,53 @@ import java.net.URI; import org.eclipse.aether.internal.test.util.TestUtils; +import org.eclipse.aether.internal.test.util.http.HttpTransporterTest; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.spi.connector.transport.PeekTask; import org.eclipse.aether.spi.connector.transport.Transporter; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; /** - * JDK Transport UT. + * JDK Transporter UT. */ @SuppressWarnings({"checkstyle:magicnumber"}) -final class JdkTransporterTest { +class JdkTransporterTest extends HttpTransporterTest { + + @Disabled + protected void testAuthSchemeReuse() throws Exception {} + + @Disabled + protected void testPut_ProxyUnauthenticated() throws Exception {} + + @Disabled + protected void testAuthSchemePreemptive() throws Exception {} + + @Disabled + protected void testPut_AuthCache_Preemptive() throws Exception {} + + @Disabled + protected void testPut_Unauthenticated() throws Exception {} + + @Disabled + protected void testPut_PreemptiveIsDefault() throws Exception {} + + @Disabled + protected void testRetryHandler_defaultCount_positive() throws Exception {} + + @Disabled + protected void testRetryHandler_explicitCount_positive() throws Exception {} + + @Disabled + protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception {} + + public JdkTransporterTest() { + super(JdkTransporterFactory::new); + } + @Test void enhanceConnectExceptionMessages() { String uri = "https://localhost:12345/"; diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/resources/simplelogger.properties b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..c52c663ce --- /dev/null +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/resources/simplelogger.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +org.slf4j.simpleLogger.defaultLogLevel=debug diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java index 94da7b7f3..98de637c8 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java @@ -22,8 +22,8 @@ import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.spi.connector.transport.Transporter; -import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory; import org.eclipse.aether.transfer.NoTransporterException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,7 +36,7 @@ * @since 2.0.0 */ @Named(JdkTransporterFactory.NAME) -public final class JdkTransporterFactory implements TransporterFactory { +public final class JdkTransporterFactory implements HttpTransporterFactory { public static final String NAME = "jdk"; private static final Logger LOGGER = LoggerFactory.getLogger(JdkTransporterFactory.class); @@ -54,7 +54,7 @@ public JdkTransporterFactory setPriority(float priority) { } @Override - public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + public HttpTransporter newInstance(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { requireNonNull(session, "session cannot be null"); requireNonNull(repository, "repository cannot be null"); diff --git a/maven-resolver-transport-jetty/pom.xml b/maven-resolver-transport-jetty/pom.xml index a93a34152..cbd0c5a0f 100644 --- a/maven-resolver-transport-jetty/pom.xml +++ b/maven-resolver-transport-jetty/pom.xml @@ -75,6 +75,32 @@ org.eclipse.jetty.http2 http2-http-client-transport + + + org.junit.jupiter + junit-jupiter-api + test + + + org.slf4j + slf4j-simple + test + + + org.apache.maven.resolver + maven-resolver-test-util + test + + + org.apache.maven.resolver + maven-resolver-test-http + test + + + org.apache.maven.resolver + maven-resolver-impl + test + diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java index 1e87b986b..398feda6b 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -18,7 +18,7 @@ */ package org.eclipse.aether.transport.jetty; -import javax.net.ssl.SSLContext; +import javax.net.ssl.*; import java.io.File; import java.io.IOException; @@ -27,11 +27,16 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.security.cert.X509Certificate; +import java.time.format.DateTimeParseException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,7 +49,10 @@ import org.eclipse.aether.spi.connector.transport.PeekTask; import org.eclipse.aether.spi.connector.transport.PutTask; import org.eclipse.aether.spi.connector.transport.TransportTask; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.transfer.TransferCancelledException; import org.eclipse.aether.util.ConfigUtils; import org.eclipse.aether.util.FileUtils; import org.eclipse.jetty.client.HttpClient; @@ -56,7 +64,6 @@ import org.eclipse.jetty.client.http.HttpClientConnectionFactory; import org.eclipse.jetty.client.util.BasicAuthentication; import org.eclipse.jetty.client.util.InputStreamResponseListener; -import org.eclipse.jetty.client.util.PathRequestContent; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; @@ -70,7 +77,7 @@ * * @since 2.0.0 */ -final class JettyTransporter extends AbstractTransporter { +final class JettyTransporter extends AbstractTransporter implements HttpTransporter { private static final int MULTIPLE_CHOICES = 300; private static final int NOT_FOUND = 404; @@ -85,6 +92,8 @@ final class JettyTransporter extends AbstractTransporter { private static final String CONTENT_RANGE = "Content-Range"; + private static final String LAST_MODIFIED = "Last-Modified"; + private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; private static final String RANGE = "Range"; @@ -102,6 +111,14 @@ final class JettyTransporter extends AbstractTransporter { private final Map headers; + private final boolean preemptiveAuth; + + private final boolean preemptivePutAuth; + + private final BasicAuthentication.BasicResult basicServerAuthenticationResult; + + private final BasicAuthentication.BasicResult basicProxyAuthenticationResult; + JettyTransporter(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { try { URI uri = new URI(repository.getUrl()).parseServerAuthority(); @@ -149,8 +166,24 @@ final class JettyTransporter extends AbstractTransporter { ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), ConfigurationProperties.REQUEST_TIMEOUT); + this.preemptiveAuth = ConfigUtils.getBoolean( + session, + ConfigurationProperties.DEFAULT_HTTP_PREEMPTIVE_AUTH, + ConfigurationProperties.HTTP_PREEMPTIVE_AUTH + "." + repository.getId(), + ConfigurationProperties.HTTP_PREEMPTIVE_AUTH); + this.preemptivePutAuth = ConfigUtils.getBoolean( + session, + ConfigurationProperties.DEFAULT_HTTP_PREEMPTIVE_PUT_AUTH, + ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH + "." + repository.getId(), + ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH); this.client = getOrCreateClient(session, repository); + + final String instanceKey = JETTY_INSTANCE_KEY_PREFIX + repository.getId(); + this.basicServerAuthenticationResult = + (BasicAuthentication.BasicResult) session.getData().get(instanceKey + ".serverAuth"); + this.basicProxyAuthenticationResult = + (BasicAuthentication.BasicResult) session.getData().get(instanceKey + ".proxyAuth"); } private URI resolve(TransportTask task) { @@ -159,7 +192,8 @@ private URI resolve(TransportTask task) { @Override public int classify(Throwable error) { - if (error instanceof JettyException && ((JettyException) error).getStatusCode() == NOT_FOUND) { + if (error instanceof HttpTransporterException + && ((HttpTransporterException) error).getStatusCode() == NOT_FOUND) { return ERROR_NOT_FOUND; } return ERROR_OTHER; @@ -171,9 +205,17 @@ protected void implPeek(PeekTask task) throws Exception { .timeout(requestTimeout, TimeUnit.MILLISECONDS) .method("HEAD"); request.headers(m -> headers.forEach(m::add)); + if (preemptiveAuth) { + if (basicServerAuthenticationResult != null) { + basicServerAuthenticationResult.apply(request); + } + if (basicProxyAuthenticationResult != null) { + basicProxyAuthenticationResult.apply(request); + } + } Response response = request.send(); if (response.getStatus() >= MULTIPLE_CHOICES) { - throw new JettyException(response.getStatus()); + throw new HttpTransporterException(response.getStatus()); } } @@ -188,6 +230,14 @@ protected void implGet(GetTask task) throws Exception { .timeout(requestTimeout, TimeUnit.MILLISECONDS) .method("GET"); request.headers(m -> headers.forEach(m::add)); + if (preemptiveAuth) { + if (basicServerAuthenticationResult != null) { + basicServerAuthenticationResult.apply(request); + } + if (basicProxyAuthenticationResult != null) { + basicProxyAuthenticationResult.apply(request); + } + } if (resume) { long resumeOffset = task.getResumeOffset(); @@ -216,7 +266,7 @@ protected void implGet(GetTask task) throws Exception { resume = false; continue; } - throw new JettyException(response.getStatus()); + throw new HttpTransporterException(response.getStatus()); } break; } @@ -260,6 +310,17 @@ protected void implGet(GetTask task) throws Exception { task.setDataFile(dataFile); } } + if (task.getDataFile() != null && response.getHeaders().getDateField(LAST_MODIFIED) != -1) { + long lastModified = + response.getHeaders().getDateField(LAST_MODIFIED); // note: Wagon also does first not last + if (lastModified != -1) { + try { + Files.setLastModifiedTime(task.getDataFile().toPath(), FileTime.fromMillis(lastModified)); + } catch (DateTimeParseException e) { + // fall through + } + } + } Map checksums = extractXChecksums(response); if (checksums != null) { checksums.forEach(task::setChecksum); @@ -317,24 +378,62 @@ private static Map extractNexus2Checksums(Response response) { protected void implPut(PutTask task) throws Exception { Request request = client.newRequest(resolve(task)).method("PUT").timeout(requestTimeout, TimeUnit.MILLISECONDS); request.headers(m -> headers.forEach(m::add)); - try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) { - utilPut(task, Files.newOutputStream(tempFile.getPath()), true); - request.body(new PathRequestContent(tempFile.getPath())); - - Response response; - try { - response = request.send(); - } catch (ExecutionException e) { - Throwable t = e.getCause(); - if (t instanceof Exception) { - throw (Exception) t; + if (preemptiveAuth || preemptivePutAuth) { + if (basicServerAuthenticationResult != null) { + basicServerAuthenticationResult.apply(request); + } + if (basicProxyAuthenticationResult != null) { + basicProxyAuthenticationResult.apply(request); + } + } + request.body(new PutTaskRequestContent(task)); + AtomicBoolean started = new AtomicBoolean(false); + Response response; + try { + response = request.onRequestCommit(r -> { + if (task.getDataLength() == 0) { + if (started.compareAndSet(false, true)) { + try { + task.getListener().transportStarted(0, task.getDataLength()); + } catch (TransferCancelledException e) { + r.abort(e); + } + } + } + }) + .onRequestContent((r, b) -> { + if (started.compareAndSet(false, true)) { + try { + task.getListener().transportStarted(0, task.getDataLength()); + } catch (TransferCancelledException e) { + r.abort(e); + return; + } + } + try { + task.getListener().transportProgressed(b); + } catch (TransferCancelledException e) { + r.abort(e); + } + }) + .send(); + } catch (ExecutionException e) { + Throwable t = e.getCause(); + if (t instanceof IOException) { + IOException ioex = (IOException) t; + if (ioex.getCause() instanceof TransferCancelledException) { + throw (TransferCancelledException) ioex.getCause(); } else { - throw new RuntimeException(t); + throw ioex; } + } else if (t instanceof Exception) { + throw (Exception) t; + } else { + throw new RuntimeException(t); } - if (response.getStatus() >= MULTIPLE_CHOICES) { - throw new JettyException(response.getStatus()); - } + } + if (response.getStatus() >= MULTIPLE_CHOICES) { + throw new HttpTransporterException(response.getStatus()); } } @@ -351,13 +450,27 @@ protected void implClose() { static final Logger LOGGER = LoggerFactory.getLogger(JettyTransporter.class); @SuppressWarnings("checkstyle:methodlength") - private static HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository) + private HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { final String instanceKey = JETTY_INSTANCE_KEY_PREFIX + repository.getId(); + final String httpsSecurityMode = ConfigUtils.getString( + session, + ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT, + ConfigurationProperties.HTTPS_SECURITY_MODE + "." + repository.getId(), + ConfigurationProperties.HTTPS_SECURITY_MODE); + + if (!ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT.equals(httpsSecurityMode) + && !ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode)) { + throw new IllegalArgumentException("Unsupported '" + httpsSecurityMode + "' HTTPS security mode."); + } + final boolean insecure = ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode); + try { - return (HttpClient) session.getData().computeIfAbsent(instanceKey, () -> { + AtomicReference serverAuth = new AtomicReference<>(null); + AtomicReference proxyAuth = new AtomicReference<>(null); + HttpClient client = (HttpClient) session.getData().computeIfAbsent(instanceKey, () -> { SSLContext sslContext = null; BasicAuthentication basicAuthentication = null; try { @@ -369,13 +482,35 @@ private static HttpClient getOrCreateClient(RepositorySystemSession session, Rem String username = repoAuthContext.get(AuthenticationContext.USERNAME); String password = repoAuthContext.get(AuthenticationContext.PASSWORD); - basicAuthentication = new BasicAuthentication( - URI.create(repository.getUrl()), Authentication.ANY_REALM, username, password); + URI uri = URI.create(repository.getUrl()); + basicAuthentication = + new BasicAuthentication(uri, Authentication.ANY_REALM, username, password); + if (preemptiveAuth || preemptivePutAuth) { + serverAuth.set(new BasicAuthentication.BasicResult( + uri, HttpHeader.AUTHORIZATION, username, password)); + } } } if (sslContext == null) { - sslContext = SSLContext.getDefault(); + if (insecure) { + sslContext = SSLContext.getInstance("TLS"); + X509TrustManager tm = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + sslContext.init(null, new X509TrustManager[] {tm}, null); + } else { + sslContext = SSLContext.getDefault(); + } } int connectTimeout = ConfigUtils.getInteger( @@ -386,6 +521,10 @@ private static HttpClient getOrCreateClient(RepositorySystemSession session, Rem SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); sslContextFactory.setSslContext(sslContext); + if (insecure) { + sslContextFactory.setEndpointIdentificationAlgorithm(null); + sslContextFactory.setHostnameVerifier((name, context) -> true); + } ClientConnector clientConnector = new ClientConnector(); clientConnector.setSslContextFactory(sslContextFactory); @@ -432,6 +571,10 @@ private static HttpClient getOrCreateClient(RepositorySystemSession session, Rem proxy.getURI(), Authentication.ANY_REALM, username, password); httpClient.getAuthenticationStore().addAuthentication(proxyAuthentication); + if (preemptiveAuth || preemptivePutAuth) { + proxyAuth.set(new BasicAuthentication.BasicResult( + proxy.getURI(), HttpHeader.PROXY_AUTHORIZATION, username, password)); + } } } } @@ -451,6 +594,13 @@ private static HttpClient getOrCreateClient(RepositorySystemSession session, Rem throw new WrapperEx(e); } }); + if (serverAuth.get() != null) { + session.getData().set(instanceKey + ".serverAuth", serverAuth.get()); + } + if (proxyAuth.get() != null) { + session.getData().set(instanceKey + ".proxyAuth", proxyAuth.get()); + } + return client; } catch (WrapperEx e) { throw new NoTransporterException(repository, e.getCause()); } diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java index 79dc586fa..216d33099 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java @@ -22,8 +22,8 @@ import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.spi.connector.transport.Transporter; -import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory; import org.eclipse.aether.transfer.NoTransporterException; import static java.util.Objects.requireNonNull; @@ -34,7 +34,7 @@ * @since 2.0.0 */ @Named(JettyTransporterFactory.NAME) -public final class JettyTransporterFactory implements TransporterFactory { +public final class JettyTransporterFactory implements HttpTransporterFactory { public static final String NAME = "jetty"; private float priority = 15.0f; @@ -50,7 +50,7 @@ public JettyTransporterFactory setPriority(float priority) { } @Override - public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + public HttpTransporter newInstance(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { requireNonNull(session, "session cannot be null"); requireNonNull(repository, "repository cannot be null"); diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/PutTaskRequestContent.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/PutTaskRequestContent.java new file mode 100644 index 000000000..889187ff8 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/PutTaskRequestContent.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.transport.jetty; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; + +import org.eclipse.aether.spi.connector.transport.PutTask; +import org.eclipse.jetty.client.util.AbstractRequestContent; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IO; + +class PutTaskRequestContent extends AbstractRequestContent { + private final PutTask putTask; + private final int bufferSize; + private ByteBufferPool bufferPool; + private boolean useDirectByteBuffers = true; + + @SuppressWarnings("checkstyle:MagicNumber") + PutTaskRequestContent(PutTask putTask) { + this(putTask, 4096); + } + + PutTaskRequestContent(PutTask putTask, int bufferSize) { + super("application/octet-stream"); + this.putTask = putTask; + this.bufferSize = bufferSize; + } + + @Override + public long getLength() { + return putTask.getDataLength(); + } + + @Override + public boolean isReproducible() { + return true; + } + + public ByteBufferPool getByteBufferPool() { + return bufferPool; + } + + public void setByteBufferPool(ByteBufferPool byteBufferPool) { + this.bufferPool = byteBufferPool; + } + + public boolean isUseDirectByteBuffers() { + return useDirectByteBuffers; + } + + public void setUseDirectByteBuffers(boolean useDirectByteBuffers) { + this.useDirectByteBuffers = useDirectByteBuffers; + } + + @Override + protected Subscription newSubscription(Consumer consumer, boolean emitInitialContent) { + return new SubscriptionImpl(consumer, emitInitialContent); + } + + private class SubscriptionImpl extends AbstractSubscription { + private ReadableByteChannel channel; + private long readTotal; + + private SubscriptionImpl(Consumer consumer, boolean emitInitialContent) { + super(consumer, emitInitialContent); + } + + @Override + protected boolean produceContent(Producer producer) throws IOException { + ByteBuffer buffer; + boolean last; + if (channel == null) { + if (putTask.getDataFile() != null) { + channel = Files.newByteChannel(putTask.getDataFile().toPath(), StandardOpenOption.READ); + } else { + channel = Channels.newChannel(putTask.newInputStream()); + } + } + + buffer = bufferPool == null + ? BufferUtil.allocate(bufferSize, isUseDirectByteBuffers()) + : bufferPool.acquire(bufferSize, isUseDirectByteBuffers()); + + BufferUtil.clearToFill(buffer); + int read = channel.read(buffer); + BufferUtil.flipToFlush(buffer, 0); + if (!channel.isOpen() && read < 0) { + throw new EOFException("EOF reached for " + putTask); + } + if (read > 0) { + readTotal += read; + } + last = readTotal == getLength(); + if (last) { + IO.close(channel); + } + return producer.produce(buffer, last, Callback.from(() -> release(buffer))); + } + + private void release(ByteBuffer buffer) { + if (bufferPool != null) { + bufferPool.release(buffer); + } + } + + @Override + public void fail(Throwable failure) { + super.fail(failure); + IO.close(channel); + } + } +} diff --git a/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyTransporterTest.java b/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyTransporterTest.java new file mode 100644 index 000000000..1cac4fe3e --- /dev/null +++ b/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyTransporterTest.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.transport.jetty; + +import org.eclipse.aether.internal.test.util.http.HttpTransporterTest; +import org.junit.jupiter.api.Disabled; + +/** + * Jetty transporter UT. + */ +class JettyTransporterTest extends HttpTransporterTest { + + @Disabled + protected void testAuthSchemeReuse() throws Exception {} + + @Disabled + protected void testPut_ProxyUnauthenticated() throws Exception {} + + @Disabled + protected void testPut_Unauthenticated() throws Exception {} + + @Disabled + protected void testRetryHandler_defaultCount_positive() throws Exception {} + + @Disabled + protected void testRetryHandler_explicitCount_positive() throws Exception {} + + @Disabled + protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception {} + + public JettyTransporterTest() { + super(JettyTransporterFactory::new); + } +} diff --git a/maven-resolver-transport-jetty/src/test/resources/simplelogger.properties b/maven-resolver-transport-jetty/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..c52c663ce --- /dev/null +++ b/maven-resolver-transport-jetty/src/test/resources/simplelogger.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +org.slf4j.simpleLogger.defaultLogLevel=debug diff --git a/maven-resolver-transport-wagon/src/test/resources/logback-test.xml b/maven-resolver-transport-wagon/src/test/resources/logback-test.xml deleted file mode 100644 index d031998b7..000000000 --- a/maven-resolver-transport-wagon/src/test/resources/logback-test.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - %d{HH:mm:ss.SSS} [%-18thread] %c{1} [%p] %m%n - - - - - - - - - - -