diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 0d507e8cb..cb7cb7ebe 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -27,5 +27,5 @@ jobs: uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v2 with: ff-site-run: false - + jdk-matrix: '[ "11", "17" ]' diff --git a/maven-resolver-test-http/pom.xml b/maven-resolver-test-http/pom.xml new file mode 100644 index 000000000..904ffdc5e --- /dev/null +++ b/maven-resolver-test-http/pom.xml @@ -0,0 +1,116 @@ + + + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver + 1.9.3-SNAPSHOT + + + maven-resolver-test-http + + Maven Artifact Resolver Test Utilities for HTTP + + A collection of utility classes to ease testing of the HTTP transports. + + + + 11 + ${javaVersion} + ${javaVersion} + + org.apache.maven.resolver.test.http + ${Automatic-Module-Name} + + 10.0.13 + + + + + org.apache.maven.resolver + maven-resolver-api + + + org.apache.maven.resolver + maven-resolver-spi + + + org.apache.maven.resolver + maven-resolver-util + + + org.apache.maven.resolver + maven-resolver-test-util + compile + + + org.eclipse.jetty + jetty-server + ${jettyVersion} + + + org.eclipse.jetty + jetty-alpn-server + ${jettyVersion} + + + org.eclipse.jetty + jetty-alpn-java-server + ${jettyVersion} + + + org.eclipse.jetty.http2 + http2-server + ${jettyVersion} + + + junit + junit + compile + + + org.hamcrest + hamcrest-core + compile + + + + + + + biz.aQute.bnd + bnd-maven-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + diff --git a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpServer.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/http/HttpServer.java similarity index 75% rename from maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpServer.java rename to maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/http/HttpServer.java index 67cb450f6..f669b1b79 100644 --- a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpServer.java +++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/http/HttpServer.java @@ -1,4 +1,4 @@ -package org.eclipse.aether.transport.http; +package org.eclipse.aether.internal.test.http; /* * Licensed to the Apache Software Foundation (ASF) under one @@ -8,9 +8,9 @@ * 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 @@ -19,11 +19,15 @@ * under the License. */ +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Base64; import java.util.Collections; import java.util.Enumeration; import java.util.List; @@ -32,28 +36,40 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.aether.util.ChecksumUtils; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.HandlerList; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.IO; -import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class HttpServer +import static java.util.Objects.requireNonNull; + +/** + * A real HTTP server used as target for transport testing. Is able to mimic various conditions. When HTTPS enabled, + * it will support both HTTP/1.1 and HTTP/2 protocols. + *

+ * This class is utility for testing, hence for simplicity it has some checkstyle rules relaxed. + */ +@SuppressWarnings( "checkstyle:visibilitymodifier" ) +public final class HttpServer { + /** + * In memory server log entries. + */ public static class LogEntry { @@ -78,12 +94,18 @@ public String toString() } + /** + * Behaviour of Expect. + */ public enum ExpectContinue { FAIL, PROPER, BROKEN } - public enum ChecksumHeader + /** + * Checksum modes. + */ + public enum ChecksumMode { NEXUS, XCHECKSUM } @@ -98,7 +120,9 @@ public enum ChecksumHeader private ExpectContinue expectContinue = ExpectContinue.PROPER; - private ChecksumHeader checksumHeader; + private ChecksumMode checksumMode; + + private String sha1Checksums; private Server server; @@ -114,7 +138,15 @@ public enum ChecksumHeader private String proxyPassword; - private List logEntries = Collections.synchronizedList( new ArrayList() ); + private String keyStorePath; + + private String keyStorePassword; + + private String trustStorePath; + + private String trustStorePassword; + + private final List logEntries = Collections.synchronizedList( new ArrayList<>() ); public String getHost() { @@ -145,13 +177,37 @@ public HttpServer addSslConnector() { if ( httpsConnector == null ) { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.addCustomizer( new SecureRequestCustomizer() ); + HttpConnectionFactory http11 = new HttpConnectionFactory( httpConfig ); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory( httpConfig ); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol( http11.getProtocol() ); + SslContextFactory.Server ssl = new SslContextFactory.Server(); ssl.setNeedClientAuth( true ); - ssl.setKeyStorePath( new File( "src/test/resources/ssl/server-store" ).getAbsolutePath() ); - ssl.setKeyStorePassword( "server-pwd" ); - ssl.setTrustStorePath( new File( "src/test/resources/ssl/client-store" ).getAbsolutePath() ); - ssl.setTrustStorePassword( "client-pwd" ); - httpsConnector = new ServerConnector( server, ssl ); + if ( keyStorePath != null ) + { + ssl.setKeyStorePath( keyStorePath ); + if ( keyStorePassword != null ) + { + ssl.setKeyStorePassword( keyStorePassword ); + } + + } + if ( trustStorePath != null ) + { + ssl.setTrustStorePath( trustStorePath ); + if ( trustStorePassword != null ) + { + ssl.setTrustStorePassword( trustStorePassword ); + } + } + + SslConnectionFactory tls = new SslConnectionFactory( ssl, alpn.getProtocol() ); + + httpsConnector = new ServerConnector( server, tls, alpn, h2, http11 ); + server.addConnector( httpsConnector ); try { @@ -194,9 +250,18 @@ public HttpServer setExpectSupport( ExpectContinue expectContinue ) return this; } - public HttpServer setChecksumHeader( ChecksumHeader checksumHeader ) + public HttpServer setChecksumHeader( ChecksumMode checksumMode, String sha1Checksums ) { - this.checksumHeader = checksumHeader; + if ( checksumMode == null ) + { + this.checksumMode = null; + this.sha1Checksums = null; + } + else + { + this.checksumMode = checksumMode; + this.sha1Checksums = requireNonNull( sha1Checksums ); + } return this; } @@ -214,8 +279,38 @@ public HttpServer setProxyAuthentication( String username, String password ) return this; } + public HttpServer setKeyStore( String keyStorePath, String keyStorePassword ) + { + if ( keyStorePath == null ) + { + this.keyStorePath = null; + this.keyStorePassword = null; + } + else + { + this.keyStorePath = keyStorePath; + this.keyStorePassword = keyStorePassword; + } + return this; + } + + public HttpServer setTrustStore( String trustStorePath, String trustStorePassword ) + { + if ( trustStorePath == null ) + { + this.trustStorePath = null; + this.trustStorePassword = null; + } + else + { + this.trustStorePath = trustStorePath; + this.trustStorePassword = trustStorePassword; + } + return this; + } + public HttpServer start() - throws Exception + throws Exception { if ( server != null ) { @@ -239,7 +334,7 @@ public HttpServer start() } public void stop() - throws Exception + throws Exception { if ( server != null ) { @@ -251,13 +346,13 @@ public void stop() } private class LogHandler - extends AbstractHandler + extends AbstractHandler { public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response ) { LOGGER.info( "{} {}{}", req.getMethod(), req.getRequestURL(), - req.getQueryString() != null ? "?" + req.getQueryString() : ""); + req.getQueryString() != null ? "?" + req.getQueryString() : "" ); Map headers = new TreeMap<>( String.CASE_INSENSITIVE_ORDER ); for ( Enumeration en = req.getHeaderNames(); en.hasMoreElements(); ) @@ -274,19 +369,21 @@ public void handle( String target, Request req, HttpServletRequest request, Http } headers.put( name, buffer.toString() ); } - logEntries.add( new LogEntry( req.getMethod(), req.getPathInfo(), Collections.unmodifiableMap( headers ) ) ); + logEntries.add( + new LogEntry( req.getMethod(), req.getPathInfo(), Collections.unmodifiableMap( headers ) ) ); } } + private static final Pattern SIMPLE_RANGE = Pattern.compile( "bytes=([0-9])+-" ); + private class RepoHandler - extends AbstractHandler + extends AbstractHandler { - private final Pattern SIMPLE_RANGE = Pattern.compile( "bytes=([0-9])+-" ); - + @SuppressWarnings( "checkstyle:methodlength" ) public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response ) - throws IOException + throws IOException { String path = req.getPathInfo().substring( 1 ); @@ -296,7 +393,8 @@ public void handle( String target, Request req, HttpServletRequest request, Http } req.setHandled( true ); - if ( ExpectContinue.FAIL.equals( expectContinue ) && request.getHeader( HttpHeader.EXPECT.asString() ) != null ) + if ( ExpectContinue.FAIL.equals( expectContinue ) + && request.getHeader( HttpHeader.EXPECT.asString() ) != null ) { response.setStatus( HttpServletResponse.SC_EXPECTATION_FAILED ); return; @@ -337,24 +435,25 @@ public void handle( String target, Request req, HttpServletRequest request, Http return; } } - response.setStatus( ( offset > 0L ) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK ); + response.setStatus( + ( offset > 0L ) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK ); response.setDateHeader( HttpHeader.LAST_MODIFIED.asString(), file.lastModified() ); response.setHeader( HttpHeader.CONTENT_LENGTH.asString(), Long.toString( file.length() - offset ) ); if ( offset > 0L ) { - response.setHeader( HttpHeader.CONTENT_RANGE.asString(), "bytes " + offset + "-" + ( file.length() - 1L ) - + "/" + file.length() ); + response.setHeader( HttpHeader.CONTENT_RANGE.asString(), + "bytes " + offset + "-" + ( file.length() - 1L ) + + "/" + file.length() ); } - if ( checksumHeader != null ) + if ( checksumMode != null ) { - Map checksums = ChecksumUtils.calc( file, Collections.singleton( "SHA-1" ) ); - if ( checksumHeader == ChecksumHeader.NEXUS ) + if ( checksumMode == ChecksumMode.NEXUS ) { - response.setHeader( HttpHeader.ETAG.asString(), "{SHA1{" + checksums.get( "SHA-1" ) + "}}" ); + response.setHeader( HttpHeader.ETAG.asString(), "{SHA1{" + sha1Checksums + "}}" ); } - else if ( checksumHeader == ChecksumHeader.XCHECKSUM ) + else if ( checksumMode == ChecksumMode.XCHECKSUM ) { - response.setHeader( "x-checksum-sha1", checksums.get( "SHA-1" ).toString() ); + response.setHeader( "x-checksum-sha1", sha1Checksums ); } } if ( HttpMethod.HEAD.is( req.getMethod() ) ) @@ -470,7 +569,7 @@ else if ( file.mkdir() ) } private class RedirectHandler - extends AbstractHandler + extends AbstractHandler { public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response ) @@ -507,14 +606,14 @@ else if ( "https".equalsIgnoreCase( scheme ) ) } private class AuthHandler - extends AbstractHandler + extends AbstractHandler { public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response ) - throws IOException + throws IOException { if ( ExpectContinue.BROKEN.equals( expectContinue ) - && "100-continue".equalsIgnoreCase( request.getHeader( HttpHeader.EXPECT.asString() ) ) ) + && "100-continue".equalsIgnoreCase( request.getHeader( HttpHeader.EXPECT.asString() ) ) ) { request.getInputStream(); } @@ -534,14 +633,15 @@ public void handle( String target, Request req, HttpServletRequest request, Http } private class ProxyAuthHandler - extends AbstractHandler + extends AbstractHandler { public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response ) { if ( proxyUsername != null && proxyPassword != null ) { - if ( checkBasicAuth( request.getHeader( HttpHeader.PROXY_AUTHORIZATION.asString() ), proxyUsername, proxyPassword ) ) + if ( checkBasicAuth( request.getHeader( HttpHeader.PROXY_AUTHORIZATION.asString() ), proxyUsername, + proxyPassword ) ) { return; } @@ -564,16 +664,13 @@ static boolean checkBasicAuth( String credentials, String username, String passw if ( "basic".equalsIgnoreCase( method ) ) { credentials = credentials.substring( space + 1 ); - credentials = B64Code.decode( credentials, StringUtil.__ISO_8859_1 ); + credentials = new String( Base64.getDecoder().decode( credentials ) ); int i = credentials.indexOf( ':' ); if ( i > 0 ) { String user = credentials.substring( 0, i ); String pass = credentials.substring( i + 1 ); - if ( username.equals( user ) && password.equals( pass ) ) - { - return true; - } + return username.equals( user ) && password.equals( pass ); } } } diff --git a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/http/HttpTransporterTestSupport.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/http/HttpTransporterTestSupport.java new file mode 100644 index 000000000..1b1f4a196 --- /dev/null +++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/http/HttpTransporterTestSupport.java @@ -0,0 +1,1172 @@ +package org.eclipse.aether.internal.test.http; + +/* + * 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. + */ + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.ServerSocket; +import java.net.URI; +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 org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystemSession; +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.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.transfer.TransferCancelledException; +import org.eclipse.aether.util.repository.AuthenticationBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +/** + * Support class for HTTP transports. + *

+ * This class is utility for testing, hence for simplicity it has some checkstyle rules relaxed. + */ +@SuppressWarnings( {"checkstyle:magicnumber", "checkstyle:methodname"} ) +public abstract class HttpTransporterTestSupport +{ + private static final String KEY_STORE_PATH; + + private static final String KEY_STORE_PASSWORD = "server-pwd"; + + private static final String TRUST_STORE_PATH; + + private static final String TRUST_STORE_PASSWORD = "client-pwd"; + + static + { + // see src/resources/ssl this module carries the test SSL material as well, but in the module where is used + // we need to make it available, hence we copy them from classpath into temporary file to be able to refer them + // by file paths + try + { + // surefire hack + Files.createDirectories( Paths.get( System.getProperty( "java.io.tmpdir" ) ) ); + + try ( InputStream keyStore = HttpTransporterTestSupport.class.getResourceAsStream( "/ssl/server-store" ) ) + { + if ( keyStore != null ) + { + Path file = Files.createTempFile( "keystore", "tmp" ); + Files.copy( keyStore, file, StandardCopyOption.REPLACE_EXISTING ); + KEY_STORE_PATH = file.toAbsolutePath().toString(); + } + else + { + KEY_STORE_PATH = null; + } + } + try ( InputStream trustStore = HttpTransporterTestSupport.class.getResourceAsStream( "/ssl/client-store" ) ) + { + if ( trustStore != null ) + { + Path file = Files.createTempFile( "truststore", "tmp" ); + Files.copy( trustStore, file, StandardCopyOption.REPLACE_EXISTING ); + TRUST_STORE_PATH = file.toAbsolutePath().toString(); + } + else + { + TRUST_STORE_PATH = null; + } + } + + if ( KEY_STORE_PATH != null ) + { + System.setProperty( "javax.net.ssl.keyStore", KEY_STORE_PATH ); + System.setProperty( "javax.net.ssl.keyStorePassword", KEY_STORE_PASSWORD ); + } + if ( TRUST_STORE_PATH != null ) + { + System.setProperty( "javax.net.ssl.trustStore", TRUST_STORE_PATH ); + System.setProperty( "javax.net.ssl.trustStorePassword", TRUST_STORE_PASSWORD ); + } + } + catch ( IOException e ) + { + throw new UncheckedIOException( e ); + } + } + + @Rule + public TestName testName = new TestName(); + + protected DefaultRepositorySystemSession session; + + protected TransporterFactory factory; + + protected Transporter transporter; + + protected File repoDir; + + protected HttpServer httpServer; + + protected Authentication auth; + + protected Proxy proxy; + + private 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; + } + transporter = newTransporter( session, newRepo( url ) ); + } + + protected abstract TransporterFactory newTransporterFactory( RepositorySystemSession session ); + + protected Transporter newTransporter( RepositorySystemSession session, RemoteRepository repository ) + throws NoTransporterException + { + return factory.newInstance( session, repository ); + } + + protected abstract boolean isWebDAVSupported(); + + protected abstract boolean enableWebDavSupport( Transporter transporter ); + + @Before + public void setUp() + throws Exception + { + System.out.println( "=== " + testName.getMethodName() + " ===" ); + session = TestUtils.newSession(); + factory = newTransporterFactory( session ); + 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, "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(); + // cross connect them: server and client must have these "crossed" to make them trust each other + if ( KEY_STORE_PATH != null ) + { + httpServer.setTrustStore( KEY_STORE_PATH, KEY_STORE_PASSWORD ); + } + if ( TRUST_STORE_PATH != null ) + { + httpServer.setKeyStore( TRUST_STORE_PATH, TRUST_STORE_PASSWORD ); + } + httpServer.setRepoDir( repoDir ).start(); + newTransporter( httpServer.getHttpUrl() ); + } + + @After + public void tearDown() + throws Exception + { + if ( transporter != null ) + { + transporter.close(); + transporter = null; + } + if ( httpServer != null ) + { + httpServer.stop(); + httpServer = null; + } + factory = null; + session = null; + } + + @Test + public void testPeek() + throws Exception + { + transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) ); + } + + @Test + public void testPeek_NotFound() + { + try + { + transporter.peek( new PeekTask( URI.create( "repo/missing.txt" ) ) ); + fail( "Expected error" ); + } + catch ( Exception e ) + { + assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) ); + } + } + + @Test + public 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 + public 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 + public void testPeek_Unauthenticated() + { + httpServer.setAuthentication( "testuser", "testpass" ); + try + { + transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) ); + fail( "Expected error" ); + } + catch ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + } + + @Test + public 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 + public 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 ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + } + + @Test + public void testPeek_SSL() + throws Exception + { + httpServer.addSslConnector(); + newTransporter( httpServer.getHttpsUrl() ); + transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) ); + } + + @Test + public 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 + public 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.dataOffset ); + assertEquals( 4L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( task.getDataString(), listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public 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.dataOffset ); + assertEquals( 4L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "test", listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public 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.dataOffset ); + assertEquals( 0L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertEquals( 0, listener.progressedCount ); + assertEquals( "", listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public void testGet_EncodedResourcePath() + throws Exception + { + GetTask task = new GetTask( URI.create( "repo/some%20space.txt" ) ); + transporter.get( task ); + assertEquals( "space", task.getDataString() ); + } + + @Test + public 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.dataOffset ); + assertEquals( 4L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( task.getDataString(), listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public void testGet_Unauthenticated() + { + httpServer.setAuthentication( "testuser", "testpass" ); + try + { + transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) ); + fail( "Expected error" ); + } + catch ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + } + + @Test + public 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.dataOffset ); + assertEquals( 4L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( task.getDataString(), listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public 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 ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + } + + @Test + public 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.dataOffset ); + assertEquals( 4L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( task.getDataString(), listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public void testGet_WebDav() + throws Exception + { + httpServer.setWebDav( true ); + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask( URI.create( "repo/dir/file.txt" ) ).setListener( listener ); + if ( isWebDAVSupported() ) + { + assumeTrue( enableWebDavSupport( transporter ) ); + } + transporter.get( task ); + assertEquals( "test", task.getDataString() ); + assertEquals( 0L, listener.dataOffset ); + assertEquals( 4L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( task.getDataString(), listener.baos.toString( StandardCharsets.UTF_8 ) ); + assertEquals( httpServer.getLogEntries().toString(), 1, httpServer.getLogEntries().size() ); + } + + @Test + public 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.dataOffset ); + assertEquals( 4L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( task.getDataString(), listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public 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.startedCount ); + assertEquals( 2L, listener.dataOffset ); + assertEquals( 9, listener.dataLength ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "sumable", listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public 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.startedCount ); + assertEquals( 0L, listener.dataOffset ); + assertEquals( 9, listener.dataLength ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "resumable", listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public 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.startedCount ); + assertEquals( 0L, listener.dataOffset ); + assertEquals( 9, listener.dataLength ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "resumable", listener.baos.toString( StandardCharsets.UTF_8 ) ); + } + + @Test + public void testGet_Checksums_Nexus() + throws Exception + { + httpServer.setChecksumHeader( HttpServer.ChecksumMode.NEXUS, "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" ); + GetTask task = new GetTask( URI.create( "repo/file.txt" ) ); + transporter.get( task ); + assertEquals( "test", task.getDataString() ); + assertEquals( "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get( "SHA-1" ) ); + } + + @Test + public void testGet_Checksums_XChecksum() + throws Exception + { + httpServer.setChecksumHeader( HttpServer.ChecksumMode.XCHECKSUM, "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" ); + GetTask task = new GetTask( URI.create( "repo/file.txt" ) ); + transporter.get( task ); + assertEquals( "test", task.getDataString() ); + assertEquals( "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get( "SHA-1" ) ); + } + + @Test + public 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( i + ", " + file.getAbsolutePath(), file.delete() ); + } + } + + @Test + public void testGet_NotFound() + { + try + { + transporter.get( new GetTask( URI.create( "repo/missing.txt" ) ) ); + fail( "Expected error" ); + } + catch ( Exception e ) + { + assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) ); + } + } + + @Test + public 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 + public void testGet_StartCancelled() + throws Exception + { + RecordingTransportListener listener = new RecordingTransportListener(); + listener.cancelStart = true; + 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.dataOffset ); + assertEquals( 4L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertEquals( 0, listener.progressedCount ); + } + + @Test + public void testGet_ProgressCancelled() + throws Exception + { + RecordingTransportListener listener = new RecordingTransportListener(); + listener.cancelProgress = true; + 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.dataOffset ); + assertEquals( 4L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertEquals( 1, listener.progressedCount ); + } + + @Test + public 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.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); + } + + @Test + public 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.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); + } + + @Test + public 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.dataOffset ); + assertEquals( 0L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertEquals( 0, listener.progressedCount ); + assertEquals( "", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); + } + + @Test + public 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.dataOffset ); + assertEquals( 2L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "OK", TestFileUtils.readString( new File( repoDir, "some space.txt" ) ) ); + } + + @Test + public 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.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); + } + + @Test + public void testPut_Authenticated_ExpectContinueBroken() + throws Exception + { + 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.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); + } + + @Test + public 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.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); + } + + @Test + public void testPut_Unauthenticated() + { + 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 ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + // these assertions does not make sense in async mode, as it may "prepare ahead". + // In other words, outcome is the important, not that no file was read. + // (these below pass with HttpClient but not with any HTTP/2 client) + // + // assertEquals( 0, listener.startedCount ); + // assertEquals( 0, listener.progressedCount ); + } + + @Test + public 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.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); + } + + @Test + public 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 ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + // these assertions does not make sense in async mode, as it may "prepare ahead". + // In other words, outcome is the important, not that no file was read. + // (these below pass with HttpClient but not with any HTTP/2 client) + // + // assertEquals( 0, listener.startedCount ); + // assertEquals( 0, listener.progressedCount ); + } + + @Test + public 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.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); + } + + @Test + public void testPut_WebDav() + throws Exception + { + assumeTrue( isWebDAVSupported() ); + httpServer.setWebDav( true ); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask( URI.create( "repo/dir1/dir2/file.txt" ) ).setListener( listener ) + .setDataString( "upload" ); + transporter.put( task ); + assertEquals( 0L, listener.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); + assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "dir1/dir2/file.txt" ) ) ); + + assertEquals( 5, httpServer.getLogEntries().size() ); + assertEquals( "OPTIONS", httpServer.getLogEntries().get( 0 ).method ); + assertEquals( "MKCOL", httpServer.getLogEntries().get( 1 ).method ); + assertEquals( "/repo/dir1/dir2/", httpServer.getLogEntries().get( 1 ).path ); + assertEquals( "MKCOL", httpServer.getLogEntries().get( 2 ).method ); + assertEquals( "/repo/dir1/", httpServer.getLogEntries().get( 2 ).path ); + assertEquals( "MKCOL", httpServer.getLogEntries().get( 3 ).method ); + assertEquals( "/repo/dir1/dir2/", httpServer.getLogEntries().get( 3 ).path ); + assertEquals( "PUT", httpServer.getLogEntries().get( 4 ).method ); + } + + @Test + public 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( i + ", " + src.getAbsolutePath(), src.delete() ); + assertTrue( i + ", " + dst.getAbsolutePath(), dst.delete() ); + } + } + + @Test + public 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 + public void testPut_StartCancelled() + throws Exception + { + RecordingTransportListener listener = new RecordingTransportListener(); + listener.cancelStart = true; + 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.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertEquals( 0, listener.progressedCount ); + } + + @Test + public void testPut_ProgressCancelled() + throws Exception + { + RecordingTransportListener listener = new RecordingTransportListener(); + listener.cancelProgress = true; + 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.dataOffset ); + assertEquals( 6L, listener.dataLength ); + assertEquals( 1, listener.startedCount ); + assertEquals( 1, listener.progressedCount ); + } + + @Test + public 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.startedCount ); + } + + @Test( timeout = 20000L ) + 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( () -> + { + 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( String.valueOf( error.get() ), error.get() ); + } + + @Test( timeout = 1000L ) + public void testConnectTimeout() + throws Exception + { + session.setConfigProperty( ConfigurationProperties.CONNECT_TIMEOUT, 200 ); + int port = 1; + newTransporter( "http://localhost:" + port ); + try + { + transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) ); + fail( "Expected error" ); + } + catch ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + } + + @Test( timeout = 1000L ) + public void testRequestTimeout() + throws Exception + { + session.setConfigProperty( ConfigurationProperties.REQUEST_TIMEOUT, 200 ); + try ( ServerSocket server = new ServerSocket( 0 ) ) + { + newTransporter( "http://localhost:" + server.getLocalPort() ); + try + { + transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) ); + fail( "Expected error" ); + } + catch ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + } + } + + @Test + public 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.headers.get( "User-Agent" ) ); + } + } + + @Test + public 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.getKey(), entry.getValue(), log.headers.get( entry.getKey() ) ); + } + } + } + + @Test + public 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 ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + } + + @Test + public 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 ( Exception e ) + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) ); + } + } + + @Test( expected = NoTransporterException.class ) + public void testInit_BadProtocol() + throws Exception + { + newTransporter( "bad:/void" ); + } + + @Test( expected = NoTransporterException.class ) + public void testInit_BadUrl() + throws Exception + { + newTransporter( "http://localhost:NaN" ); + } + + @Test + public 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-http/src/test/java/org/eclipse/aether/transport/http/RecordingTransportListener.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/http/RecordingTransportListener.java similarity index 81% rename from maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/RecordingTransportListener.java rename to maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/http/RecordingTransportListener.java index e7ca7c969..cd5306375 100644 --- a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/RecordingTransportListener.java +++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/http/RecordingTransportListener.java @@ -1,4 +1,4 @@ -package org.eclipse.aether.transport.http; +package org.eclipse.aether.internal.test.http; /* * Licensed to the Apache Software Foundation (ASF) under one @@ -26,8 +26,14 @@ import org.eclipse.aether.spi.connector.transport.TransportListener; import org.eclipse.aether.transfer.TransferCancelledException; -class RecordingTransportListener - extends TransportListener +/** + * Listener used in {@link HttpTransporterTestSupport}. + *

+ * This class is utility for testing, hence for simplicity it has some checkstyle rules relaxed. + */ +@SuppressWarnings( "checkstyle:visibilitymodifier" ) +public class RecordingTransportListener + extends TransportListener { public final ByteArrayOutputStream baos = new ByteArrayOutputStream( 1024 ); @@ -46,7 +52,7 @@ class RecordingTransportListener @Override public void transportStarted( long dataOffset, long dataLength ) - throws TransferCancelledException + throws TransferCancelledException { startedCount++; progressedCount = 0; @@ -61,7 +67,7 @@ public void transportStarted( long dataOffset, long dataLength ) @Override public void transportProgressed( ByteBuffer data ) - throws TransferCancelledException + throws TransferCancelledException { progressedCount++; baos.write( data.array(), data.arrayOffset() + ( (Buffer) data ).position(), data.remaining() ); diff --git a/maven-resolver-transport-http/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-http/src/test/resources/ssl/README.txt rename to maven-resolver-test-http/src/main/resources/ssl/README.txt diff --git a/maven-resolver-transport-http/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-http/src/test/resources/ssl/client-store rename to maven-resolver-test-http/src/main/resources/ssl/client-store diff --git a/maven-resolver-transport-http/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-http/src/test/resources/ssl/server-store rename to maven-resolver-test-http/src/main/resources/ssl/server-store diff --git a/maven-resolver-test-http/src/site/site.xml b/maven-resolver-test-http/src/site/site.xml new file mode 100644 index 000000000..0f278ab79 --- /dev/null +++ b/maven-resolver-test-http/src/site/site.xml @@ -0,0 +1,37 @@ + + + + + + +

+ + + + + + + + + + \ No newline at end of file diff --git a/maven-resolver-test-util/pom.xml b/maven-resolver-test-util/pom.xml index 663df514d..66e272028 100644 --- a/maven-resolver-test-util/pom.xml +++ b/maven-resolver-test-util/pom.xml @@ -36,7 +36,7 @@ - org.apache.maven.resolver.testutil + org.apache.maven.resolver.test.util ${Automatic-Module-Name} diff --git a/maven-resolver-transport-classpath/pom.xml b/maven-resolver-transport-classpath/pom.xml index a724d12a0..a760aac69 100644 --- a/maven-resolver-transport-classpath/pom.xml +++ b/maven-resolver-transport-classpath/pom.xml @@ -59,6 +59,7 @@ provided true + com.google.inject guice diff --git a/maven-resolver-transport-file/pom.xml b/maven-resolver-transport-file/pom.xml index 500d1ca72..49e6f9b5d 100644 --- a/maven-resolver-transport-file/pom.xml +++ b/maven-resolver-transport-file/pom.xml @@ -55,6 +55,7 @@ provided true + com.google.inject guice @@ -85,10 +86,6 @@ maven-resolver-test-util test - - org.slf4j - slf4j-api - org.slf4j slf4j-simple diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java index d2c971cd3..112c7e11e 100644 --- a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java +++ b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java @@ -29,8 +29,6 @@ import org.eclipse.aether.spi.connector.transport.PutTask; import org.eclipse.aether.spi.connector.transport.TransportTask; import org.eclipse.aether.transfer.NoTransporterException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A transporter using {@link java.io.File}. @@ -39,8 +37,6 @@ final class FileTransporter extends AbstractTransporter { - private static final Logger LOGGER = LoggerFactory.getLogger( FileTransporter.class ); - private final File basedir; FileTransporter( RemoteRepository repository ) @@ -94,10 +90,7 @@ protected void implPut( PutTask task ) } catch ( Exception e ) { - if ( !file.delete() && file.exists() ) - { - LOGGER.debug( "Could not delete partial file {}", file ); - } + file.delete(); throw e; } } diff --git a/maven-resolver-transport-http/pom.xml b/maven-resolver-transport-http/pom.xml index c38eeb61f..20743ec20 100644 --- a/maven-resolver-transport-http/pom.xml +++ b/maven-resolver-transport-http/pom.xml @@ -38,7 +38,6 @@ org.apache.maven.resolver.transport.http ${Automatic-Module-Name} - 9.4.49.v20220914 @@ -65,6 +64,17 @@ org.apache.maven.resolver maven-resolver-util + + javax.inject + javax.inject + provided + true + + + org.slf4j + slf4j-api + + org.apache.httpcomponents httpclient @@ -88,12 +98,7 @@ ${slf4jVersion} runtime - - javax.inject - javax.inject - provided - true - + com.google.inject guice @@ -125,39 +130,16 @@ test - org.eclipse.jetty - jetty-server - ${jettyVersion} - test - - - org.eclipse.jetty - jetty-util - ${jettyVersion} - test - - - org.eclipse.jetty - jetty-http - ${jettyVersion} + org.apache.maven.resolver + maven-resolver-test-http test - - org.slf4j - slf4j-api - org.slf4j slf4j-simple ${slf4jVersion} test - - javax.servlet - javax.servlet-api - 3.1.0 - test - diff --git a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java index 745514f9f..bc84602ad 100644 --- a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java +++ b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java @@ -19,128 +19,57 @@ * under the License. */ -import static org.junit.Assert.*; - 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.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.RepositorySystemSession; +import org.eclipse.aether.internal.test.http.HttpServer; +import org.eclipse.aether.internal.test.http.HttpTransporterTestSupport; +import org.eclipse.aether.internal.test.http.RecordingTransportListener; 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.TransporterFactory; -import org.eclipse.aether.transfer.NoTransporterException; -import org.eclipse.aether.transfer.TransferCancelledException; import org.eclipse.aether.util.repository.AuthenticationBuilder; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TestName; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** + * */ -public class HttpTransporterTest +public class HttpTransporterTest extends HttpTransporterTestSupport { - - 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" ); - } - - @Rule - public TestName testName = new TestName(); - - 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 + @Override + protected TransporterFactory newTransporterFactory( RepositorySystemSession session ) { - if ( transporter != null ) - { - transporter.close(); - transporter = null; - } - transporter = factory.newInstance( session, newRepo( url ) ); + return new HttpTransporterFactory(); } - @Before - public void setUp() - throws Exception + @Override + protected boolean isWebDAVSupported() { - System.out.println( "=== " + testName.getMethodName() + " ===" ); - session = TestUtils.newSession(); - factory = new HttpTransporterFactory( ); - 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, "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() ); + return true; } - @After - public void tearDown() - throws Exception + @Override + protected boolean enableWebDavSupport( Transporter transporter ) { - if ( transporter != null ) - { - transporter.close(); - transporter = null; - } - if ( httpServer != null ) - { - httpServer.stop(); - httpServer = null; - } - factory = null; - session = null; + ( (HttpTransporter) transporter ).getState().setWebDav( true ); + return true; } @Test @@ -148,1022 +77,87 @@ public 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 - public void testPeek() - throws Exception - { - transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) ); - } - - @Test - public 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 - public 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 - public 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" ) ) ); + assertEquals( Transporter.ERROR_NOT_FOUND, + transporter.classify( new HttpResponseException( 404, "Not Found" ) ) ); } + /** + * HttpClient specific test: ensures that auth scheme state is reused (see {@link LocalState} and + * {@link GlobalState}), as this transport implementation does quite fine-grained control of the HTTP client and + * the HTTP transactions. + */ @Test - public void testPeek_Unauthenticated() - throws Exception + public void testAuthSchemeReuse() + 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 - public void testPeek_ProxyAuthenticated() - throws Exception - { - httpServer.setProxyAuthentication( "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/" ); - transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) ); - } - - @Test - public 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 - public void testPeek_SSL() - throws Exception - { - httpServer.addSslConnector(); - newTransporter( httpServer.getHttpsUrl() ); - transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) ); - } - - @Test - public 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 - public void testGet_ToMemory() - throws Exception - { - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask( URI.create( "repo/file.txt" ) ).setListener( listener ); + GetTask task = new GetTask( URI.create( "repo/file.txt" ) ); transporter.get( task ); assertEquals( "test", task.getDataString() ); - assertEquals( 0L, listener.dataOffset ); - assertEquals( 4L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public 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.dataOffset ); - assertEquals( 4L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "test", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public 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.dataOffset ); - assertEquals( 0L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertEquals( 0, listener.progressedCount ); - assertEquals( "", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public void testGet_EncodedResourcePath() - throws Exception - { - GetTask task = new GetTask( URI.create( "repo/some%20space.txt" ) ); + 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( "space", task.getDataString() ); + assertEquals( "test", task.getDataString() ); + assertEquals( 1, httpServer.getLogEntries().size() ); + assertNotNull( httpServer.getLogEntries().get( 0 ).headers.get( "Authorization" ) ); + assertNotNull( httpServer.getLogEntries().get( 0 ).headers.get( "Proxy-Authorization" ) ); } + /** + * HttpClient specific tests: no other transport client overrides headers set by user, but in this test two + * "hardly justified things" make this work, but only with HttpClient transport. For start, unsure why would + * a user set EXPECT header as configuration, while HttpClient goes into game and removes it as needed. Many + * other clients (notably Java11 or Jetty) simply does not allow EXPECT as "custom header" at all or, as expected + * by protocol, returns HTTP 417 Expectation Failed. + */ @Test - public void testGet_Authenticated() - throws Exception + public 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(); - GetTask task = new GetTask( URI.create( "repo/file.txt" ) ).setListener( listener ); - transporter.get( task ); - assertEquals( "test", task.getDataString() ); - assertEquals( 0L, listener.dataOffset ); - assertEquals( 4L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public 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 - public 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.dataOffset ); - assertEquals( 4L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public 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 - public 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.dataOffset ); - assertEquals( 4L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public void testGet_WebDav() - throws Exception - { - httpServer.setWebDav( true ); - RecordingTransportListener listener = new RecordingTransportListener(); - GetTask task = new GetTask( URI.create( "repo/dir/file.txt" ) ).setListener( listener ); - ( (HttpTransporter) transporter ).getState().setWebDav( true ); - transporter.get( task ); - assertEquals( "test", task.getDataString() ); - assertEquals( 0L, listener.dataOffset ); - assertEquals( 4L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - assertEquals( httpServer.getLogEntries().toString(), 1, httpServer.getLogEntries().size() ); - } - - @Test - public 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() ); + PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" ); + transporter.put( task ); assertEquals( 0L, listener.dataOffset ); - assertEquals( 4L, listener.dataLength ); + assertEquals( 6L, listener.dataLength ); assertEquals( 1, listener.startedCount ); assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public 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.startedCount ); - assertEquals( 2L, listener.dataOffset ); - assertEquals( 9, listener.dataLength ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "sumable", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public 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.startedCount ); - assertEquals( 0L, listener.dataOffset ); - assertEquals( 9, listener.dataLength ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "resumable", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public 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.startedCount ); - assertEquals( 0L, listener.dataOffset ); - assertEquals( 9, listener.dataLength ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "resumable", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) ); - } - - @Test - public 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" ) ); + assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); } + /** + * HttpClient specific test: uses HttpClient internal API to assert that HTTP/1.1 connections are pooled. + */ @Test - public void testGet_Checksums_XChecksum() + public void testConnectionReuse() 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 - public 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( i + ", " + file.getAbsolutePath(), file.delete() ); - } - } - - @Test - public void testGet_NotFound() - throws Exception - { - try - { - transporter.get( new GetTask( URI.create( "repo/missing.txt" ) ) ); - fail( "Expected error" ); - } - catch ( HttpResponseException e ) + httpServer.addSslConnector(); + session.setCache( new DefaultRepositoryCache() ); + for ( int i = 0; i < 3; i++ ) { - assertEquals( 404, e.getStatusCode() ); - assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) ); + newTransporter( httpServer.getHttpsUrl() ); + GetTask task = new GetTask( URI.create( "repo/file.txt" ) ); + transporter.get( task ); + assertEquals( "test", task.getDataString() ); } + PoolStats stats = + ( (ConnPoolControl) ( (HttpTransporter) transporter ).getState() + .getConnectionManager() ).getTotalStats(); + assertEquals( stats.toString(), 1, stats.getAvailable() ); } - - @Test - public 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 - public void testGet_StartCancelled() - throws Exception - { - RecordingTransportListener listener = new RecordingTransportListener(); - listener.cancelStart = true; - 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.dataOffset ); - assertEquals( 4L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertEquals( 0, listener.progressedCount ); - } - - @Test - public void testGet_ProgressCancelled() - throws Exception - { - RecordingTransportListener listener = new RecordingTransportListener(); - listener.cancelProgress = true; - 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.dataOffset ); - assertEquals( 4L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertEquals( 1, listener.progressedCount ); - } - - @Test - public 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); - } - - @Test - public 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); - } - - @Test - public 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.dataOffset ); - assertEquals( 0L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertEquals( 0, listener.progressedCount ); - assertEquals( "", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); - } - - @Test - public 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.dataOffset ); - assertEquals( 2L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "OK", TestFileUtils.readString( new File( repoDir, "some space.txt" ) ) ); - } - - @Test - public 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); - } - - @Test - public void testPut_Authenticated_ExpectContinueBroken() - throws Exception - { - 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); - } - - @Test - public 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); - } - - @Test - public 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); - } - - @Test - public 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.startedCount ); - assertEquals( 0, listener.progressedCount ); - } - - @Test - public 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); - } - - @Test - public 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.startedCount ); - assertEquals( 0, listener.progressedCount ); - } - - @Test - public 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) ); - } - - @Test - public void testPut_WebDav() - throws Exception - { - httpServer.setWebDav( true ); - RecordingTransportListener listener = new RecordingTransportListener(); - PutTask task = - new PutTask( URI.create( "repo/dir1/dir2/file.txt" ) ).setListener( listener ).setDataString( "upload" ); - transporter.put( task ); - assertEquals( 0L, listener.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 ); - assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "dir1/dir2/file.txt" ) ) ); - - assertEquals( 5, httpServer.getLogEntries().size() ); - assertEquals( "OPTIONS", httpServer.getLogEntries().get( 0 ).method ); - assertEquals( "MKCOL", httpServer.getLogEntries().get( 1 ).method ); - assertEquals( "/repo/dir1/dir2/", httpServer.getLogEntries().get( 1 ).path ); - assertEquals( "MKCOL", httpServer.getLogEntries().get( 2 ).method ); - assertEquals( "/repo/dir1/", httpServer.getLogEntries().get( 2 ).path ); - assertEquals( "MKCOL", httpServer.getLogEntries().get( 3 ).method ); - assertEquals( "/repo/dir1/dir2/", httpServer.getLogEntries().get( 3 ).path ); - assertEquals( "PUT", httpServer.getLogEntries().get( 4 ).method ); - } - - @Test - public 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( i + ", " + src.getAbsolutePath(), src.delete() ); - assertTrue( i + ", " + dst.getAbsolutePath(), dst.delete() ); - } - } - - @Test - public 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 - public void testPut_StartCancelled() - throws Exception - { - RecordingTransportListener listener = new RecordingTransportListener(); - listener.cancelStart = true; - 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertEquals( 0, listener.progressedCount ); - } - - @Test - public void testPut_ProgressCancelled() - throws Exception - { - RecordingTransportListener listener = new RecordingTransportListener(); - listener.cancelProgress = true; - 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.dataOffset ); - assertEquals( 6L, listener.dataLength ); - assertEquals( 1, listener.startedCount ); - assertEquals( 1, listener.progressedCount ); - } - - @Test - public 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.startedCount ); - } - - @Test( timeout = 20000L ) - 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( String.valueOf( error.get() ), error.get() ); - } - - @Test( timeout = 1000L ) - 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 = 1000L ) - 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 - public 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.headers.get( "User-Agent" ) ); - } - } - - @Test - public 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.getKey(), entry.getValue(), log.headers.get( entry.getKey() ) ); - } - } - } - - @Test - public 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 - public 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 - public 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 ).headers.get( "Authorization" ) ); - assertNotNull( httpServer.getLogEntries().get( 0 ).headers.get( "Proxy-Authorization" ) ); - } - - @Test - public void testConnectionReuse() - throws Exception - { - httpServer.addSslConnector(); - session.setCache( new DefaultRepositoryCache() ); - for ( int i = 0; i < 3; i++ ) - { - newTransporter( httpServer.getHttpsUrl() ); - GetTask task = new GetTask( URI.create( "repo/file.txt" ) ); - transporter.get( task ); - assertEquals( "test", task.getDataString() ); - } - PoolStats stats = - ( (ConnPoolControl) ( (HttpTransporter) transporter ).getState().getConnectionManager() ).getTotalStats(); - assertEquals( stats.toString(), 1, stats.getAvailable() ); - } - - @Test( expected = NoTransporterException.class ) - public void testInit_BadProtocol() - throws Exception - { - newTransporter( "bad:/void" ); - } - - @Test( expected = NoTransporterException.class ) - public void testInit_BadUrl() - throws Exception - { - newTransporter( "http://localhost:NaN" ); - } - - @Test - public 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-http/src/test/resources/logback.xml b/maven-resolver-transport-http/src/test/resources/logback.xml deleted file mode 100644 index 9addbd505..000000000 --- a/maven-resolver-transport-http/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-java11/pom.xml b/maven-resolver-transport-java11/pom.xml new file mode 100644 index 000000000..9d6d439ea --- /dev/null +++ b/maven-resolver-transport-java11/pom.xml @@ -0,0 +1,130 @@ + + + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver + 1.9.3-SNAPSHOT + + + maven-resolver-transport-java11 + + Maven Artifact Resolver Transport Java 11 + + A transport implementation for repositories using http:// and https:// URLs using Java 11 HttpClient. + + + + 11 + + org.apache.maven.resolver.transport.java11 + ${Automatic-Module-Name} + 9.4.49.v20220914 + + + + + org.apache.maven.resolver + maven-resolver-api + + + org.apache.maven.resolver + maven-resolver-spi + + + org.apache.maven.resolver + maven-resolver-util + + + javax.inject + javax.inject + provided + true + + + + com.google.inject + guice + test + + + com.google.guava + guava + test + + + com.google.guava + failureaccess + test + + + junit + junit + test + + + org.hamcrest + hamcrest-core + test + + + org.apache.maven.resolver + maven-resolver-test-util + test + + + org.apache.maven.resolver + maven-resolver-test-http + test + + + org.slf4j + slf4j-simple + ${slf4jVersion} + test + + + + + + + org.eclipse.sisu + sisu-maven-plugin + + + biz.aQute.bnd + bnd-maven-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + diff --git a/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/Java11HttpException.java b/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/Java11HttpException.java new file mode 100644 index 000000000..6c51f9a0a --- /dev/null +++ b/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/Java11HttpException.java @@ -0,0 +1,40 @@ +package org.eclipse.aether.transport.java11; + +/* + * 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. + */ + +/** + * Exception thrown by {@link Java11HttpTransporter} in case of errors. + */ +final class Java11HttpException + extends Exception +{ + private final int statusCode; + + Java11HttpException( int statusCode ) + { + super( "HTTP Status: " + statusCode ); + this.statusCode = statusCode; + } + + public int getStatusCode() + { + return statusCode; + } +} diff --git a/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/Java11HttpTransporter.java b/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/Java11HttpTransporter.java new file mode 100644 index 000000000..91cd8dbde --- /dev/null +++ b/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/Java11HttpTransporter.java @@ -0,0 +1,497 @@ +package org.eclipse.aether.transport.java11; + +/* + * 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. + */ + +import javax.net.ssl.SSLContext; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URISyntaxException; +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.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.AuthenticationContext; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.AbstractTransporter; +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.TransportTask; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.FileUtils; + +/** + * A transporter for HTTP/HTTPS. + */ +final class Java11HttpTransporter + extends AbstractTransporter +{ + private static final int MULTIPLE_CHOICES = 300; + + private static final int NOT_FOUND = 404; + + private static final int PRECONDITION_FAILED = 412; + + private static final long MODIFICATION_THRESHOLD = 60L * 1000L; + + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + + private static final String CACHE_CONTROL = "Cache-Control"; + + private static final String CONTENT_LENGTH = "Content-Length"; + + private static final String CONTENT_RANGE = "Content-Range"; + + private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + + private static final String RANGE = "Range"; + + private static final String USER_AGENT = "User-Agent"; + + private static final Pattern CONTENT_RANGE_PATTERN = + Pattern.compile( "\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*" ); + + private final URI baseUri; + + private final HttpClient client; + + private final Map headers; + + private final int requestTimeout; + + Java11HttpTransporter( RepositorySystemSession session, RemoteRepository repository ) throws NoTransporterException + { + try + { + URI uri = new URI( repository.getUrl() ).parseServerAuthority(); + if ( uri.isOpaque() ) + { + throw new URISyntaxException( repository.getUrl(), "URL must not be opaque" ); + } + if ( uri.getRawFragment() != null || uri.getRawQuery() != null ) + { + throw new URISyntaxException( repository.getUrl(), "URL must not have fragment or query" ); + } + String path = uri.getPath(); + if ( path == null ) + { + path = "/"; + } + if ( !path.startsWith( "/" ) ) + { + path = "/" + path; + } + if ( !path.endsWith( "/" ) ) + { + path = path + "/"; + } + this.baseUri = URI.create( uri.getScheme() + "://" + uri.getRawAuthority() + path ); + } + catch ( URISyntaxException e ) + { + throw new NoTransporterException( repository, e.getMessage(), e ); + } + + HashMap headers = new HashMap<>(); + String userAgent = ConfigUtils.getString( session, + ConfigurationProperties.DEFAULT_USER_AGENT, + ConfigurationProperties.USER_AGENT ); + if ( userAgent != null ) + { + headers.put( USER_AGENT, userAgent ); + } + @SuppressWarnings( "unchecked" ) + Map configuredHeaders = + (Map) ConfigUtils.getMap( session, Collections.emptyMap(), + ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(), + ConfigurationProperties.HTTP_HEADERS ); + if ( configuredHeaders != null ) + { + configuredHeaders.forEach( + ( k, v ) -> headers.put( String.valueOf( k ), v != null ? String.valueOf( v ) : null ) ); + } + headers.put( CACHE_CONTROL, "no-cache, no-store" ); + + this.requestTimeout = ConfigUtils.getInteger( session, + ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, + ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.REQUEST_TIMEOUT ); + + this.headers = headers; + this.client = getOrCreateClient( session, repository ); + } + + private URI resolve( TransportTask task ) + { + return baseUri.resolve( task.getLocation() ); + } + + @Override + public int classify( Throwable error ) + { + if ( error instanceof Java11HttpException + && ( (Java11HttpException) error ).getStatusCode() == NOT_FOUND ) + { + return ERROR_NOT_FOUND; + } + return ERROR_OTHER; + } + + @Override + protected void implPeek( PeekTask task ) + throws Exception + { + HttpRequest.Builder request = HttpRequest.newBuilder() + .uri( resolve( task ) ) + .timeout( Duration.ofMillis( requestTimeout ) ) + .method( "HEAD", HttpRequest.BodyPublishers.noBody() ); + headers.forEach( request::setHeader ); + HttpResponse response = client.send( request.build(), HttpResponse.BodyHandlers.discarding() ); + if ( response.statusCode() >= MULTIPLE_CHOICES ) + { + throw new Java11HttpException( response.statusCode() ); + } + } + + @Override + protected void implGet( GetTask task ) + throws Exception + { + boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null; + HttpResponse response; + + while ( true ) + { + HttpRequest.Builder request = HttpRequest.newBuilder() + .uri( resolve( task ) ) + .timeout( Duration.ofMillis( requestTimeout ) ) + .method( "GET", HttpRequest.BodyPublishers.noBody() ); + headers.forEach( request::setHeader ); + + if ( resume ) + { + long resumeOffset = task.getResumeOffset(); + request.header( RANGE, "bytes=" + resumeOffset + '-' ); + request.header( IF_UNMODIFIED_SINCE, toHttpDate( new Date( + task.getDataFile().lastModified() - MODIFICATION_THRESHOLD ) ) ); + request.header( ACCEPT_ENCODING, "identity" ); + } + + response = client.send( request.build(), HttpResponse.BodyHandlers.ofInputStream() ); + if ( response.statusCode() >= MULTIPLE_CHOICES ) + { + if ( resume && response.statusCode() == PRECONDITION_FAILED ) + { + resume = false; + continue; + } + throw new Java11HttpException( response.statusCode() ); + } + break; + } + + long offset = 0L, length = response.headers().firstValueAsLong( CONTENT_LENGTH ).orElse( -1L ); + if ( resume ) + { + String range = response.headers().firstValue( CONTENT_RANGE ).orElse( null ); + if ( range != null ) + { + Matcher m = CONTENT_RANGE_PATTERN.matcher( range ); + if ( !m.matches() ) + { + throw new IOException( "Invalid Content-Range header for partial download: " + range ); + } + offset = Long.parseLong( m.group( 1 ) ); + length = Long.parseLong( m.group( 2 ) ) + 1L; + if ( offset < 0L || offset >= length || ( offset > 0L && offset != task.getResumeOffset() ) ) + { + throw new IOException( "Invalid Content-Range header for partial download from offset " + + task.getResumeOffset() + ": " + range ); + } + } + } + + final boolean downloadResumed = offset > 0L; + final File dataFile = task.getDataFile(); + if ( dataFile == null ) + { + try ( InputStream is = response.body() ) + { + utilGet( task, is, true, length, downloadResumed ); + } + } + else + { + try ( FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile( dataFile.toPath() ) ) + { + task.setDataFile( tempFile.getPath().toFile(), downloadResumed ); + if ( downloadResumed && Files.isRegularFile( dataFile.toPath() ) ) + { + try ( InputStream inputStream = Files.newInputStream( dataFile.toPath() ) ) + { + Files.copy( inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING ); + } + } + try ( InputStream is = response.body() ) + { + utilGet( task, is, true, length, downloadResumed ); + } + tempFile.move(); + } + finally + { + task.setDataFile( dataFile ); + } + } + Map checksums = extractXChecksums( response ); + if ( checksums != null ) + { + checksums.forEach( task::setChecksum ); + return; + } + checksums = extractNexus2Checksums( response ); + if ( checksums != null ) + { + checksums.forEach( task::setChecksum ); + } + } + + private static Map extractXChecksums( HttpResponse response ) + { + String value; + HashMap result = new HashMap<>(); + // Central style: x-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.headers().firstValue( "x-checksum-sha1" ).orElse( null ); + if ( value != null ) + { + result.put( "SHA-1", value ); + } + // Central style: x-checksum-md5: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.headers().firstValue( "x-checksum-md5" ).orElse( null ); + if ( value != null ) + { + result.put( "MD5", value ); + } + if ( !result.isEmpty() ) + { + return result; + } + // Google style: x-goog-meta-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.headers().firstValue( "x-goog-meta-checksum-sha1" ).orElse( null ); + if ( value != null ) + { + result.put( "SHA-1", value ); + } + // Central style: x-goog-meta-checksum-sha1: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.headers().firstValue( "x-goog-meta-checksum-md5" ).orElse( null ); + if ( value != null ) + { + result.put( "MD5", value ); + } + + return result.isEmpty() ? null : result; + } + + private static Map extractNexus2Checksums( HttpResponse response ) + { + // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}" + String etag = response.headers().firstValue( "ETag" ).orElse( null ); + if ( etag != null ) + { + int start = etag.indexOf( "SHA1{" ), end = etag.indexOf( "}", start + 5 ); + if ( start >= 0 && end > start ) + { + return Collections.singletonMap( "SHA-1", etag.substring( start + 5, end ) ); + } + } + return null; + } + + @Override + protected void implPut( PutTask task ) + throws Exception + { + HttpRequest.Builder request = HttpRequest.newBuilder() + .uri( resolve( task ) ) + .timeout( Duration.ofMillis( requestTimeout ) ); + headers.forEach( request::setHeader ); + try ( FileUtils.TempFile tempFile = FileUtils.newTempFile() ) + { + utilPut( task, Files.newOutputStream( tempFile.getPath() ), true ); + request.method( "PUT", HttpRequest.BodyPublishers.ofFile( tempFile.getPath() ) ); + + HttpResponse response = client.send( request.build(), HttpResponse.BodyHandlers.discarding() ); + if ( response.statusCode() >= MULTIPLE_CHOICES ) + { + throw new Java11HttpException( response.statusCode() ); + } + } + } + + @Override + protected void implClose() + { + // nop + } + + private static final TimeZone GMT = TimeZone.getTimeZone( "GMT" ); + + static String toHttpDate( Date date ) + { + return createRFC7231().format( date ); + } + + private static SimpleDateFormat createRFC7231() + { + SimpleDateFormat sdf = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US ); + sdf.setLenient( false ); + sdf.setTimeZone( GMT ); + return sdf; + } + + /** + * Visible for testing. + */ + static final String HTTP_INSTANCE_KEY_PREFIX = Java11HttpTransporterFactory.class.getName() + ".http."; + + private static HttpClient getOrCreateClient( RepositorySystemSession session, RemoteRepository repository ) + throws NoTransporterException + { + final String instanceKey = HTTP_INSTANCE_KEY_PREFIX + repository.getId(); + + try + { + return (HttpClient) session.getData().computeIfAbsent( instanceKey, () -> + { + HashMap authentications = new HashMap<>(); + SSLContext sslContext = null; + try + { + try ( AuthenticationContext repoAuthContext = AuthenticationContext.forRepository( session, + repository ) ) + { + if ( repoAuthContext != null ) + { + sslContext = repoAuthContext.get( AuthenticationContext.SSL_CONTEXT, SSLContext.class ); + + String username = repoAuthContext.get( AuthenticationContext.USERNAME ); + String password = repoAuthContext.get( AuthenticationContext.PASSWORD ); + + authentications.put( Authenticator.RequestorType.SERVER, + new PasswordAuthentication( username, password.toCharArray() ) ); + } + } + + if ( sslContext == null ) + { + sslContext = SSLContext.getDefault(); + } + + int connectTimeout = ConfigUtils.getInteger( session, + ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, + ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.CONNECT_TIMEOUT ); + + HttpClient.Builder builder = HttpClient.newBuilder() + .version( HttpClient.Version.HTTP_2 ) + .followRedirects( HttpClient.Redirect.NORMAL ) + .connectTimeout( Duration.ofMillis( connectTimeout ) ) + .sslContext( sslContext ); + + if ( repository.getProxy() != null ) + { + ProxySelector proxy = ProxySelector.of( new InetSocketAddress( repository.getProxy().getHost(), + repository.getProxy().getPort() ) ); + + builder.proxy( proxy ); + try ( AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy( session, + repository ) ) + { + if ( proxyAuthContext != null ) + { + String username = proxyAuthContext.get( AuthenticationContext.USERNAME ); + String password = proxyAuthContext.get( AuthenticationContext.PASSWORD ); + + authentications.put( Authenticator.RequestorType.PROXY, + new PasswordAuthentication( username, password.toCharArray() ) ); + } + } + + } + + if ( !authentications.isEmpty() ) + { + builder.authenticator( + new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return authentications.get( getRequestorType() ); + } + } + ); + } + + return builder.build(); + } + catch ( NoSuchAlgorithmException e ) + { + throw new WrapperEx( e ); + } + } ); + } + catch ( WrapperEx e ) + { + throw new NoTransporterException( repository, e.getCause() ); + } + } + + private static final class WrapperEx extends RuntimeException + { + private WrapperEx( Throwable cause ) + { + super( cause ); + } + } +} diff --git a/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/Java11HttpTransporterFactory.java b/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/Java11HttpTransporterFactory.java new file mode 100644 index 000000000..51d40e6d8 --- /dev/null +++ b/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/Java11HttpTransporterFactory.java @@ -0,0 +1,77 @@ +package org.eclipse.aether.transport.java11; + +/* + * 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. + */ + +import javax.inject.Named; + +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.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * A transporter factory for repositories using the {@code http:} or {@code https:} protocol backed by Java 11+ + * HttpClient. + * + * @since TBD + */ +@Named( "java11" ) +public final class Java11HttpTransporterFactory + implements TransporterFactory +{ + private float priority = 15.0f; + + @Override + public float getPriority() + { + return priority; + } + + /** + * Sets the priority of this component. + * + * @param priority The priority. + * @return This component for chaining, never {@code null}. + */ + public Java11HttpTransporterFactory setPriority( float priority ) + { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance( RepositorySystemSession session, RemoteRepository repository ) + throws NoTransporterException + { + requireNonNull( session, "session cannot be null" ); + requireNonNull( repository, "repository cannot be null" ); + + if ( !"http".equalsIgnoreCase( repository.getProtocol() ) + && !"https".equalsIgnoreCase( repository.getProtocol() ) ) + { + throw new NoTransporterException( repository, "Only HTTP/HTTPS is supported" ); + } + + return new Java11HttpTransporter( session, repository ); + } +} diff --git a/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/package-info.java b/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/package-info.java new file mode 100644 index 000000000..9edd51666 --- /dev/null +++ b/maven-resolver-transport-java11/src/main/java/org/eclipse/aether/transport/java11/package-info.java @@ -0,0 +1,27 @@ +// CHECKSTYLE_OFF: RegexpHeader +/* + * 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. + */ +/** + * Support for downloads/uploads via the HTTP and HTTPS protocols. The current implementation is backed by + * Java 11 HttpClient. + * + * @since TBD + */ +package org.eclipse.aether.transport.java11; + diff --git a/maven-resolver-transport-java11/src/site/site.xml b/maven-resolver-transport-java11/src/site/site.xml new file mode 100644 index 000000000..514f3b664 --- /dev/null +++ b/maven-resolver-transport-java11/src/site/site.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/maven-resolver-transport-java11/src/test/java/org/eclipse/aether/transport/java11/Java11HttpTransporterTest.java b/maven-resolver-transport-java11/src/test/java/org/eclipse/aether/transport/java11/Java11HttpTransporterTest.java new file mode 100644 index 000000000..b5bb9cf9f --- /dev/null +++ b/maven-resolver-transport-java11/src/test/java/org/eclipse/aether/transport/java11/Java11HttpTransporterTest.java @@ -0,0 +1,72 @@ +package org.eclipse.aether.transport.java11; + +/* + * 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. + */ + +import java.io.FileNotFoundException; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.internal.test.http.HttpTransporterTestSupport; +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.transfer.NoTransporterException; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * + */ +public class Java11HttpTransporterTest extends HttpTransporterTestSupport +{ + @Override + protected TransporterFactory newTransporterFactory( RepositorySystemSession session ) + { + return new Java11HttpTransporterFactory(); + } + + @Override + protected Transporter newTransporter( RepositorySystemSession session, RemoteRepository repository ) + throws NoTransporterException + { + session.getData().set( Java11HttpTransporter.HTTP_INSTANCE_KEY_PREFIX + repository.getId(), null ); + return super.newTransporter( session, repository ); + } + + @Override + protected boolean isWebDAVSupported() + { + return false; + } + + @Override + protected boolean enableWebDavSupport( Transporter transporter ) + { + throw new IllegalStateException( "webDAV is not supported" ); + } + + @Test + public void testClassify() + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( new FileNotFoundException() ) ); + assertEquals( Transporter.ERROR_OTHER, transporter.classify( new Java11HttpException( 403 ) ) ); + assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( new Java11HttpException( 404 ) ) ); + } +} diff --git a/maven-resolver-transport-jetty/pom.xml b/maven-resolver-transport-jetty/pom.xml new file mode 100644 index 000000000..692b97cfa --- /dev/null +++ b/maven-resolver-transport-jetty/pom.xml @@ -0,0 +1,169 @@ + + + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver + 1.9.3-SNAPSHOT + + + maven-resolver-transport-jetty + + Maven Artifact Resolver Transport Jetty + + A transport implementation for repositories using http:// and https:// URLs using Jetty Client. + + + + 11 + + org.apache.maven.resolver.transport.jetty + ${Automatic-Module-Name} + 10.0.13 + + + + + org.apache.maven.resolver + maven-resolver-api + + + org.apache.maven.resolver + maven-resolver-spi + + + org.apache.maven.resolver + maven-resolver-util + + + javax.inject + javax.inject + provided + true + + + + org.eclipse.jetty + jetty-client + ${jettyVersion} + + + org.eclipse.jetty.http2 + http2-client + ${jettyVersion} + + + org.eclipse.jetty.http2 + http2-http-client-transport + ${jettyVersion} + + + + com.google.inject + guice + test + + + com.google.guava + guava + test + + + com.google.guava + failureaccess + test + + + junit + junit + test + + + org.mockito + mockito-core + test + + + org.hamcrest + hamcrest-core + test + + + org.apache.maven.resolver + maven-resolver-test-util + test + + + org.apache.maven.resolver + maven-resolver-test-http + test + + + org.slf4j + slf4j-simple + ${slf4jVersion} + test + + + + + + + org.eclipse.sisu + sisu-maven-plugin + + + biz.aQute.bnd + bnd-maven-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + pkg + package + + single + + + + src/assembly/pkg.xml + + + + + + + + diff --git a/maven-resolver-transport-jetty/src/assembly/pkg.xml b/maven-resolver-transport-jetty/src/assembly/pkg.xml new file mode 100644 index 000000000..d70c7d86d --- /dev/null +++ b/maven-resolver-transport-jetty/src/assembly/pkg.xml @@ -0,0 +1,43 @@ + + + + + + pkg + + zip + + false + + + / + true + false + runtime + + org.slf4j:slf4j-api + org.apache.maven.resolver:maven-resolver-api + org.apache.maven.resolver:maven-resolver-spi + org.apache.maven.resolver:maven-resolver-util + + + + \ No newline at end of file diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java new file mode 100644 index 000000000..be80e80e5 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java @@ -0,0 +1,40 @@ +package org.eclipse.aether.transport.jetty; + +/* + * 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. + */ + +/** + * Exception thrown by {@link JettyTransporter} in case of errors. + */ +final class JettyException + extends Exception +{ + private final int statusCode; + + JettyException( int statusCode ) + { + super( "HTTP Status: " + statusCode ); + this.statusCode = statusCode; + } + + public int getStatusCode() + { + return statusCode; + } +} 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 new file mode 100644 index 000000000..fdc4eb685 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -0,0 +1,539 @@ +package org.eclipse.aether.transport.jetty; + +/* + * 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. + */ + +import javax.net.ssl.SSLContext; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +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.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.AuthenticationContext; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.AbstractTransporter; +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.TransportTask; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.FileUtils; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +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; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +/** + * A transporter for HTTP/HTTPS. + */ +final class JettyTransporter + extends AbstractTransporter +{ + private static final int MULTIPLE_CHOICES = 300; + + private static final int NOT_FOUND = 404; + + private static final int PRECONDITION_FAILED = 412; + + private static final long MODIFICATION_THRESHOLD = 60L * 1000L; + + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + + private static final String CONTENT_LENGTH = "Content-Length"; + + private static final String CONTENT_RANGE = "Content-Range"; + + private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + + private static final String RANGE = "Range"; + + private static final String USER_AGENT = "User-Agent"; + + private static final Pattern CONTENT_RANGE_PATTERN = + Pattern.compile( "\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*" ); + + private final URI baseUri; + + private final HttpClient client; + + private final int requestTimeout; + + private final Map headers; + + JettyTransporter( RepositorySystemSession session, RemoteRepository repository ) throws NoTransporterException + { + try + { + URI uri = new URI( repository.getUrl() ).parseServerAuthority(); + if ( uri.isOpaque() ) + { + throw new URISyntaxException( repository.getUrl(), "URL must not be opaque" ); + } + if ( uri.getRawFragment() != null || uri.getRawQuery() != null ) + { + throw new URISyntaxException( repository.getUrl(), "URL must not have fragment or query" ); + } + String path = uri.getPath(); + if ( path == null ) + { + path = "/"; + } + if ( !path.startsWith( "/" ) ) + { + path = "/" + path; + } + if ( !path.endsWith( "/" ) ) + { + path = path + "/"; + } + this.baseUri = URI.create( uri.getScheme() + "://" + uri.getRawAuthority() + path ); + } + catch ( URISyntaxException e ) + { + throw new NoTransporterException( repository, e.getMessage(), e ); + } + + HashMap headers = new HashMap<>(); + String userAgent = ConfigUtils.getString( session, + ConfigurationProperties.DEFAULT_USER_AGENT, + ConfigurationProperties.USER_AGENT ); + if ( userAgent != null ) + { + headers.put( USER_AGENT, userAgent ); + } + @SuppressWarnings( "unchecked" ) + Map configuredHeaders = + (Map) ConfigUtils.getMap( session, Collections.emptyMap(), + ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(), + ConfigurationProperties.HTTP_HEADERS ); + if ( configuredHeaders != null ) + { + configuredHeaders.forEach( + ( k, v ) -> headers.put( String.valueOf( k ), v != null ? String.valueOf( v ) : null ) ); + } + + this.headers = headers; + + this.requestTimeout = ConfigUtils.getInteger( session, + ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, + ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.REQUEST_TIMEOUT ); + + this.client = getOrCreateClient( session, repository ); + } + + private URI resolve( TransportTask task ) + { + return baseUri.resolve( task.getLocation() ); + } + + @Override + public int classify( Throwable error ) + { + if ( error instanceof JettyException + && ( (JettyException) error ).getStatusCode() == NOT_FOUND ) + { + return ERROR_NOT_FOUND; + } + return ERROR_OTHER; + } + + @Override + protected void implPeek( PeekTask task ) + throws Exception + { + Request request = client.newRequest( resolve( task ) ) + .timeout( requestTimeout, TimeUnit.MILLISECONDS ) + .method( "HEAD" ); + request.headers( m -> headers.forEach( m::add ) ); + Response response = request.send(); + if ( response.getStatus() >= MULTIPLE_CHOICES ) + { + throw new JettyException( response.getStatus() ); + } + } + + @Override + protected void implGet( GetTask task ) + throws Exception + { + boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null; + Response response; + InputStreamResponseListener listener; + + while ( true ) + { + Request request = client.newRequest( resolve( task ) ) + .timeout( requestTimeout, TimeUnit.MILLISECONDS ) + .method( "GET" ); + request.headers( m -> headers.forEach( m::add ) ); + + if ( resume ) + { + long resumeOffset = task.getResumeOffset(); + request.headers( h -> + { + h.add( RANGE, "bytes=" + resumeOffset + '-' ); + h.addDateField( IF_UNMODIFIED_SINCE, + task.getDataFile().lastModified() - MODIFICATION_THRESHOLD ); + h.remove( HttpHeader.ACCEPT_ENCODING ); + h.add( ACCEPT_ENCODING, "identity" ); + } ); + } + + listener = new InputStreamResponseListener(); + request.send( listener ); + try + { + response = listener.get( requestTimeout, TimeUnit.MILLISECONDS ); + } + catch ( ExecutionException e ) + { + Throwable t = e.getCause(); + if ( t instanceof Exception ) + { + throw (Exception) t; + } + else + { + throw new RuntimeException( t ); + } + } + if ( response.getStatus() >= MULTIPLE_CHOICES ) + { + if ( resume && response.getStatus() == PRECONDITION_FAILED ) + { + resume = false; + continue; + } + throw new JettyException( response.getStatus() ); + } + break; + } + + long offset = 0L, length = response.getHeaders().getLongField( CONTENT_LENGTH ); + if ( resume ) + { + String range = response.getHeaders().get( CONTENT_RANGE ); + if ( range != null ) + { + Matcher m = CONTENT_RANGE_PATTERN.matcher( range ); + if ( !m.matches() ) + { + throw new IOException( "Invalid Content-Range header for partial download: " + range ); + } + offset = Long.parseLong( m.group( 1 ) ); + length = Long.parseLong( m.group( 2 ) ) + 1L; + if ( offset < 0L || offset >= length || ( offset > 0L && offset != task.getResumeOffset() ) ) + { + throw new IOException( "Invalid Content-Range header for partial download from offset " + + task.getResumeOffset() + ": " + range ); + } + } + } + + final boolean downloadResumed = offset > 0L; + final File dataFile = task.getDataFile(); + if ( dataFile == null ) + { + try ( InputStream is = listener.getInputStream() ) + { + utilGet( task, is, true, length, downloadResumed ); + } + } + else + { + try ( FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile( dataFile.toPath() ) ) + { + task.setDataFile( tempFile.getPath().toFile(), downloadResumed ); + if ( downloadResumed && Files.isRegularFile( dataFile.toPath() ) ) + { + try ( InputStream inputStream = Files.newInputStream( dataFile.toPath() ) ) + { + Files.copy( inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING ); + } + } + try ( InputStream is = listener.getInputStream() ) + { + utilGet( task, is, true, length, downloadResumed ); + } + tempFile.move(); + } + finally + { + task.setDataFile( dataFile ); + } + } + Map checksums = extractXChecksums( response ); + if ( checksums != null ) + { + checksums.forEach( task::setChecksum ); + return; + } + checksums = extractNexus2Checksums( response ); + if ( checksums != null ) + { + checksums.forEach( task::setChecksum ); + } + } + + private static Map extractXChecksums( Response response ) + { + String value; + HashMap result = new HashMap<>(); + // Central style: x-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.getHeaders().get( "x-checksum-sha1" ); + if ( value != null ) + { + result.put( "SHA-1", value ); + } + // Central style: x-checksum-md5: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.getHeaders().get( "x-checksum-md5" ); + if ( value != null ) + { + result.put( "MD5", value ); + } + if ( !result.isEmpty() ) + { + return result; + } + // Google style: x-goog-meta-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.getHeaders().get( "x-goog-meta-checksum-sha1" ); + if ( value != null ) + { + result.put( "SHA-1", value ); + } + // Central style: x-goog-meta-checksum-sha1: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.getHeaders().get( "x-goog-meta-checksum-md5" ); + if ( value != null ) + { + result.put( "MD5", value ); + } + + return result.isEmpty() ? null : result; + } + + private static Map extractNexus2Checksums( Response response ) + { + // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}" + String etag = response.getHeaders().get( "ETag" ); + if ( etag != null ) + { + int start = etag.indexOf( "SHA1{" ), end = etag.indexOf( "}", start + 5 ); + if ( start >= 0 && end > start ) + { + return Collections.singletonMap( "SHA-1", etag.substring( start + 5, end ) ); + } + } + return null; + } + + @Override + 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; + } + else + { + throw new RuntimeException( t ); + } + } + if ( response.getStatus() >= MULTIPLE_CHOICES ) + { + throw new JettyException( response.getStatus() ); + } + } + } + + @Override + protected void implClose() + { + // noop + } + + /** + * Visible for testing. + */ + static final String JETTY_INSTANCE_KEY_PREFIX = JettyTransporterFactory.class.getName() + ".jetty."; + + @SuppressWarnings( "checkstyle:methodlength" ) + private static HttpClient getOrCreateClient( RepositorySystemSession session, RemoteRepository repository ) + throws NoTransporterException + { + + final String instanceKey = JETTY_INSTANCE_KEY_PREFIX + repository.getId(); + + try + { + return (HttpClient) session.getData().computeIfAbsent( instanceKey, () -> + { + SSLContext sslContext = null; + BasicAuthentication basicAuthentication = null; + try + { + try ( AuthenticationContext repoAuthContext = AuthenticationContext.forRepository( session, + repository ) ) + { + if ( repoAuthContext != null ) + { + sslContext = repoAuthContext.get( AuthenticationContext.SSL_CONTEXT, SSLContext.class ); + + String username = repoAuthContext.get( AuthenticationContext.USERNAME ); + String password = repoAuthContext.get( AuthenticationContext.PASSWORD ); + + basicAuthentication = + new BasicAuthentication( URI.create( repository.getUrl() ), + Authentication.ANY_REALM, username, password ); + } + } + + if ( sslContext == null ) + { + sslContext = SSLContext.getDefault(); + } + + int connectTimeout = ConfigUtils.getInteger( session, + ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, + ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.CONNECT_TIMEOUT ); + + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + sslContextFactory.setSslContext( sslContext ); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory( sslContextFactory ); + + HTTP2Client http2Client = new HTTP2Client( clientConnector ); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = + new ClientConnectionFactoryOverHTTP2.HTTP2( http2Client ); + + HttpClientTransportDynamic transport; + if ( "https".equalsIgnoreCase( repository.getProtocol() ) ) + { + transport = new HttpClientTransportDynamic( clientConnector, + http2, HttpClientConnectionFactory.HTTP11 ); // HTTPS, prefer H2 + } + else + { + transport = new HttpClientTransportDynamic( clientConnector, + HttpClientConnectionFactory.HTTP11, http2 ); // plaintext HTTP, H2 cannot be used + } + + HttpClient httpClient = new HttpClient( transport ); + httpClient.setConnectTimeout( connectTimeout ); + httpClient.setFollowRedirects( true ); + httpClient.setMaxRedirects( 2 ); + + httpClient.setUserAgentField( null ); // we manage it + + if ( basicAuthentication != null ) + { + httpClient.getAuthenticationStore().addAuthentication( basicAuthentication ); + } + + if ( repository.getProxy() != null ) + { + HttpProxy proxy = + new HttpProxy( repository.getProxy().getHost(), repository.getProxy().getPort() ); + + httpClient.getProxyConfiguration().addProxy( proxy ); + try ( AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy( session, + repository ) ) + { + if ( proxyAuthContext != null ) + { + String username = proxyAuthContext.get( AuthenticationContext.USERNAME ); + String password = proxyAuthContext.get( AuthenticationContext.PASSWORD ); + + BasicAuthentication proxyAuthentication = + new BasicAuthentication( proxy.getURI(), Authentication.ANY_REALM, + username, password ); + + httpClient.getAuthenticationStore().addAuthentication( proxyAuthentication ); + } + } + } + httpClient.start(); + + return httpClient; + } + catch ( Exception e ) + { + throw new WrapperEx( e ); + } + } ); + } + catch ( WrapperEx e ) + { + throw new NoTransporterException( repository, e.getCause() ); + } + } + + private static final class WrapperEx extends RuntimeException + { + private WrapperEx( Throwable cause ) + { + super( cause ); + } + } +} 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 new file mode 100644 index 000000000..d3b77ad58 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java @@ -0,0 +1,77 @@ +package org.eclipse.aether.transport.jetty; + +/* + * 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. + */ + +import javax.inject.Named; + +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.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * A transporter factory for repositories using the {@code http:} or {@code https:} protocol. + * + * @since TBD + */ +@Named( "jetty" ) +public final class JettyTransporterFactory + implements TransporterFactory +{ + + private float priority = 10.0f; + + @Override + public float getPriority() + { + return priority; + } + + /** + * Sets the priority of this component. + * + * @param priority The priority. + * @return This component for chaining, never {@code null}. + */ + public JettyTransporterFactory setPriority( float priority ) + { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance( RepositorySystemSession session, RemoteRepository repository ) + throws NoTransporterException + { + requireNonNull( session, "session cannot be null" ); + requireNonNull( repository, "repository cannot be null" ); + + if ( !"http".equalsIgnoreCase( repository.getProtocol() ) + && !"https".equalsIgnoreCase( repository.getProtocol() ) ) + { + throw new NoTransporterException( repository ); + } + + return new JettyTransporter( session, repository ); + } +} diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/package-info.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/package-info.java new file mode 100644 index 000000000..498fd1aa0 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/package-info.java @@ -0,0 +1,28 @@ +// CHECKSTYLE_OFF: RegexpHeader +/* + * 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. + */ +/** + * Support for downloads/uploads via the HTTP and HTTPS protocols. The current implementation is backed by + * Jetty Client. + * + * @since TBD + */ +package org.eclipse.aether.transport.jetty; + diff --git a/maven-resolver-transport-jetty/src/site/site.xml b/maven-resolver-transport-jetty/src/site/site.xml new file mode 100644 index 000000000..ba6b8ddb3 --- /dev/null +++ b/maven-resolver-transport-jetty/src/site/site.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyHttpTransporterTest.java b/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyHttpTransporterTest.java new file mode 100644 index 000000000..0b11ebbdc --- /dev/null +++ b/maven-resolver-transport-jetty/src/test/java/org/eclipse/aether/transport/jetty/JettyHttpTransporterTest.java @@ -0,0 +1,72 @@ +package org.eclipse.aether.transport.jetty; + +/* + * 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. + */ + +import java.io.FileNotFoundException; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.internal.test.http.HttpTransporterTestSupport; +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.transfer.NoTransporterException; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * + */ +public class JettyHttpTransporterTest extends HttpTransporterTestSupport +{ + @Override + protected TransporterFactory newTransporterFactory( RepositorySystemSession session ) + { + return new JettyTransporterFactory(); + } + + @Override + protected Transporter newTransporter( RepositorySystemSession session, RemoteRepository repository ) + throws NoTransporterException + { + session.getData().set( JettyTransporter.JETTY_INSTANCE_KEY_PREFIX + repository.getId(), null ); + return super.newTransporter( session, repository ); + } + + @Override + protected boolean isWebDAVSupported() + { + return false; + } + + @Override + protected boolean enableWebDavSupport( Transporter transporter ) + { + throw new IllegalStateException( "WebDAV not supported" ); + } + + @Test + public void testClassify() + { + assertEquals( Transporter.ERROR_OTHER, transporter.classify( new FileNotFoundException() ) ); + assertEquals( Transporter.ERROR_OTHER, transporter.classify( new JettyException( 403 ) ) ); + assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( new JettyException( 404 ) ) ); + } +} diff --git a/maven-resolver-transport-wagon/pom.xml b/maven-resolver-transport-wagon/pom.xml index 1fd0cba13..6fe4f0231 100644 --- a/maven-resolver-transport-wagon/pom.xml +++ b/maven-resolver-transport-wagon/pom.xml @@ -53,17 +53,22 @@ org.apache.maven.resolver maven-resolver-util - - org.apache.maven.wagon - wagon-provider-api - 3.5.2 - javax.inject javax.inject provided true + + org.slf4j + slf4j-api + + + + org.apache.maven.wagon + wagon-provider-api + 3.5.2 + org.codehaus.plexus plexus-classworlds @@ -80,6 +85,7 @@ org.eclipse.sisu.plexus true + com.google.inject guice @@ -110,10 +116,6 @@ maven-resolver-test-util test - - org.slf4j - slf4j-api - org.slf4j slf4j-simple diff --git a/pom.xml b/pom.xml index 3660d8a24..e4a0b183f 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,11 @@ 8 + ${javaVersion} + ${javaVersion} + ${javaVersion} + 11 + true ${surefire.redirectTestOutputToFile} resolver-archives/resolver-LATEST @@ -93,10 +98,13 @@ maven-resolver-named-locks-redisson maven-resolver-impl maven-resolver-test-util + maven-resolver-test-http maven-resolver-connector-basic maven-resolver-transport-classpath maven-resolver-transport-file maven-resolver-transport-http + maven-resolver-transport-java11 + maven-resolver-transport-jetty maven-resolver-transport-wagon maven-resolver-demos @@ -159,6 +167,12 @@ ${project.version} test + + org.apache.maven.resolver + maven-resolver-test-http + ${project.version} + test + org.hamcrest @@ -550,23 +564,24 @@ - org.codehaus.mojo - animal-sniffer-maven-plugin - 1.22 - - - org.codehaus.mojo.signature - java18 - 1.0 - - + org.apache.maven.plugins + maven-enforcer-plugin - check-java-compat - process-classes + enforce-bytecode-version - check + enforce + + + + + ${maven.compiler.target} + test,provided + + + true +