Copilot commented on code in PR #1966: URL: https://github.com/apache/maven-resolver/pull/1966#discussion_r3595250643
########## maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java: ########## @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.transport.url; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.zip.DeflaterInputStream; +import java.util.zip.GZIPInputStream; + +import org.eclipse.aether.Keys; +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.http.ChecksumExtractor; +import org.eclipse.aether.spi.connector.transport.http.HttpConstants; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; +import org.eclipse.aether.spi.io.PathProcessor; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.connector.transport.http.HttpTransporterUtils; + +/** + * A special, "read only" and limited capability transport usable for bootstrapping. It provides HTTP with minimal + * support (only basic auth, only GET/HEAD). + * + * @since 2.0.21 + */ +public class UrlTransporter extends AbstractTransporter implements HttpTransporter { + + private static final int MAX_REDIRECTS = 5; + private static final String METHOD_GET = "GET"; + private static final String METHOD_HEAD = "HEAD"; + private static final String HEADER_LOCATION = "Location"; + private static final String HEADER_AUTHORIZATION = "Authorization"; + private static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization"; + private static final String AUTH_SCHEME_BASIC = "Basic"; + + private final ChecksumExtractor checksumExtractor; + private final PathProcessor pathProcessor; + private final URI baseUri; + private final Map<String, String> headers; + private final String userAgent; + private final int connectTimeout; + private final int requestTimeout; + private final boolean preemptiveAuth; + private final String auth; + private final Proxy proxy; + private final String proxyAuth; + + private final Object authKey; + private final Object proxyAuthKey; + private final Function<Object, Boolean> cacheGetter; + private final Consumer<Object> cacheSetter; + + @FunctionalInterface + private interface IOSupplier<T> { + T get() throws IOException; + } + + public UrlTransporter( + RemoteRepository repository, + RepositorySystemSession session, + ChecksumExtractor checksumExtractor, + PathProcessor pathProcessor) + throws NoTransporterException { + this.checksumExtractor = checksumExtractor; + this.pathProcessor = pathProcessor; + try { + this.baseUri = HttpTransporterUtils.getBaseUri(repository); + } catch (URISyntaxException e) { + throw new NoTransporterException(repository, e.getMessage(), e); + } + + this.headers = HttpTransporterUtils.getHttpHeaders(session, repository); + this.userAgent = HttpTransporterUtils.getUserAgent(session, repository); + this.connectTimeout = HttpTransporterUtils.getHttpConnectTimeout(session, repository); + this.requestTimeout = HttpTransporterUtils.getHttpRequestTimeout(session, repository); + String authString = null; + try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) { + if (repoAuthContext != null) { + String username = repoAuthContext.get(AuthenticationContext.USERNAME); + String password = repoAuthContext.get(AuthenticationContext.PASSWORD); + if (username != null && password != null) { + authString = username + ":" + password; + } + } + } + this.auth = authString; + this.preemptiveAuth = this.auth != null && HttpTransporterUtils.isHttpPreemptiveAuth(session, repository); + + org.eclipse.aether.repository.Proxy repoProxy = repository.getProxy(); + this.proxy = repoProxy != null + ? new Proxy(Proxy.Type.HTTP, new InetSocketAddress(repoProxy.getHost(), repoProxy.getPort())) + : Proxy.NO_PROXY; + String proxyAuthString = null; + try (AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy(session, repository)) { + if (proxyAuthContext != null) { + String username = proxyAuthContext.get(AuthenticationContext.USERNAME); + String password = proxyAuthContext.get(AuthenticationContext.PASSWORD); + if (username != null && password != null) { + proxyAuthString = username + ":" + password; + } + } + } + this.proxyAuth = proxyAuthString; + + this.authKey = Keys.of(UrlTransporter.class, repository, "auth"); + this.proxyAuthKey = Keys.of(UrlTransporter.class, repository, "proxyAuth"); + if (session.getCache() != null) { + this.cacheGetter = k -> { + Boolean ret = (Boolean) session.getCache().get(session, k); + if (ret == null) { + return false; + } else { + return ret; + } + }; + this.cacheSetter = k -> session.getCache().put(session, k, Boolean.TRUE); + } else { + this.cacheGetter = k -> false; + this.cacheSetter = k -> {}; + } + } + + @Override + protected void implPeek(PeekTask task) throws Exception { + HttpURLConnection con = perform(METHOD_HEAD, baseUri.resolve(task.getLocation()), null); + if (HttpURLConnection.HTTP_OK != con.getResponseCode()) { + throw new HttpTransporterException(con.getResponseCode()); + } + } + + @Override + protected void implGet(GetTask task) throws Exception { + HttpURLConnection con = perform(METHOD_GET, baseUri.resolve(task.getLocation()), task); + if (HttpURLConnection.HTTP_OK != con.getResponseCode()) { + throw new HttpTransporterException(con.getResponseCode()); + } + IOSupplier<InputStream> inputStreamSupplier = () -> { + String contentEncoding = con.getHeaderField("Content-Encoding"); + if (contentEncoding != null) { + if ("gzip".equals(contentEncoding)) { + return new GZIPInputStream(con.getInputStream()); + } else if ("deflate".equals(contentEncoding)) { + return new DeflaterInputStream(con.getInputStream()); + } + } Review Comment: `deflate` Content-Encoding is being decoded with `DeflaterInputStream`, which compresses rather than decompresses and will corrupt the response body. Use `InflaterInputStream` for `deflate`, and compare encodings case-insensitively (servers may send `GZip`, etc.). ########## maven-resolver-transport-url/src/site/site.xml: ########## @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- +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. +--> + + +<site xmlns="http://maven.apache.org/SITE/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/SITE/2.0.0 https://maven.apache.org/xsd/site-2.0.0.xsd" name="Transport Apache"> + <body> Review Comment: The site descriptor still uses `name="Transport Apache"`, which looks like a copy/paste from the apache transport and will label the generated site incorrectly for this module. ########## maven-resolver-transport-url/README.md: ########## @@ -0,0 +1,19 @@ +# Maven Resolver URL Transport + +This is special Transport with limited HTTP capabilities. The main use case of this transport is outside of +Maven, but in case of other apps that are Java 8, and integrate Resolver mostly for **consumption purposes**. +Before this transport, the only option was to either up Java level to 11 and use JDK transport, or to use +the heavyweight Apache HttpClient transport, which is not always desirable. + +Supported features: +* HTTP 1.1 support, only for GET and HEAD methods +* HTTP redirects (max 5) +* HTTP gzip and deflate compression support +* HTTP Basic authentication (w/ preemptive support) +* HTTP proxy support (w/ Basic proxy authentication) +* HTTP auth caching (lowers "known to needed" HTTP round-trips) +* Smart checksums (extracts checksums from response headers, potentially halves the HTTP round-trips) +* Timeout for connection and request + +This transport is not a fully functional transport, and should be handled a such. It is quite usable in +artifact consumption scenarios, but it is not suitable for artifact deployment. Review Comment: Typo: "handled a such" should be "handled as such". ########## maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java: ########## @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.transport.url; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.zip.DeflaterInputStream; +import java.util.zip.GZIPInputStream; + +import org.eclipse.aether.Keys; +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.http.ChecksumExtractor; +import org.eclipse.aether.spi.connector.transport.http.HttpConstants; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; +import org.eclipse.aether.spi.io.PathProcessor; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.connector.transport.http.HttpTransporterUtils; + +/** + * A special, "read only" and limited capability transport usable for bootstrapping. It provides HTTP with minimal + * support (only basic auth, only GET/HEAD). + * + * @since 2.0.21 + */ +public class UrlTransporter extends AbstractTransporter implements HttpTransporter { + + private static final int MAX_REDIRECTS = 5; + private static final String METHOD_GET = "GET"; + private static final String METHOD_HEAD = "HEAD"; + private static final String HEADER_LOCATION = "Location"; + private static final String HEADER_AUTHORIZATION = "Authorization"; + private static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization"; + private static final String AUTH_SCHEME_BASIC = "Basic"; + + private final ChecksumExtractor checksumExtractor; + private final PathProcessor pathProcessor; + private final URI baseUri; + private final Map<String, String> headers; + private final String userAgent; + private final int connectTimeout; + private final int requestTimeout; + private final boolean preemptiveAuth; + private final String auth; + private final Proxy proxy; + private final String proxyAuth; + + private final Object authKey; + private final Object proxyAuthKey; + private final Function<Object, Boolean> cacheGetter; + private final Consumer<Object> cacheSetter; + + @FunctionalInterface + private interface IOSupplier<T> { + T get() throws IOException; + } + + public UrlTransporter( + RemoteRepository repository, + RepositorySystemSession session, + ChecksumExtractor checksumExtractor, + PathProcessor pathProcessor) + throws NoTransporterException { + this.checksumExtractor = checksumExtractor; + this.pathProcessor = pathProcessor; + try { + this.baseUri = HttpTransporterUtils.getBaseUri(repository); + } catch (URISyntaxException e) { + throw new NoTransporterException(repository, e.getMessage(), e); + } + + this.headers = HttpTransporterUtils.getHttpHeaders(session, repository); + this.userAgent = HttpTransporterUtils.getUserAgent(session, repository); + this.connectTimeout = HttpTransporterUtils.getHttpConnectTimeout(session, repository); + this.requestTimeout = HttpTransporterUtils.getHttpRequestTimeout(session, repository); + String authString = null; + try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) { + if (repoAuthContext != null) { + String username = repoAuthContext.get(AuthenticationContext.USERNAME); + String password = repoAuthContext.get(AuthenticationContext.PASSWORD); + if (username != null && password != null) { + authString = username + ":" + password; + } + } + } + this.auth = authString; + this.preemptiveAuth = this.auth != null && HttpTransporterUtils.isHttpPreemptiveAuth(session, repository); + + org.eclipse.aether.repository.Proxy repoProxy = repository.getProxy(); + this.proxy = repoProxy != null + ? new Proxy(Proxy.Type.HTTP, new InetSocketAddress(repoProxy.getHost(), repoProxy.getPort())) + : Proxy.NO_PROXY; + String proxyAuthString = null; + try (AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy(session, repository)) { + if (proxyAuthContext != null) { + String username = proxyAuthContext.get(AuthenticationContext.USERNAME); + String password = proxyAuthContext.get(AuthenticationContext.PASSWORD); + if (username != null && password != null) { + proxyAuthString = username + ":" + password; + } + } + } + this.proxyAuth = proxyAuthString; + + this.authKey = Keys.of(UrlTransporter.class, repository, "auth"); + this.proxyAuthKey = Keys.of(UrlTransporter.class, repository, "proxyAuth"); + if (session.getCache() != null) { + this.cacheGetter = k -> { + Boolean ret = (Boolean) session.getCache().get(session, k); + if (ret == null) { + return false; + } else { + return ret; + } + }; + this.cacheSetter = k -> session.getCache().put(session, k, Boolean.TRUE); + } else { + this.cacheGetter = k -> false; + this.cacheSetter = k -> {}; + } + } + + @Override + protected void implPeek(PeekTask task) throws Exception { + HttpURLConnection con = perform(METHOD_HEAD, baseUri.resolve(task.getLocation()), null); + if (HttpURLConnection.HTTP_OK != con.getResponseCode()) { + throw new HttpTransporterException(con.getResponseCode()); + } + } + + @Override + protected void implGet(GetTask task) throws Exception { + HttpURLConnection con = perform(METHOD_GET, baseUri.resolve(task.getLocation()), task); + if (HttpURLConnection.HTTP_OK != con.getResponseCode()) { + throw new HttpTransporterException(con.getResponseCode()); + } + IOSupplier<InputStream> inputStreamSupplier = () -> { + String contentEncoding = con.getHeaderField("Content-Encoding"); + if (contentEncoding != null) { + if ("gzip".equals(contentEncoding)) { + return new GZIPInputStream(con.getInputStream()); + } else if ("deflate".equals(contentEncoding)) { + return new DeflaterInputStream(con.getInputStream()); + } + } + return con.getInputStream(); + }; + final Path dataFile = task.getDataPath(); + if (dataFile == null) { + try (InputStream is = inputStreamSupplier.get()) { + utilGet(task, is, true, con.getContentLengthLong(), false); + } + } else { + try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) { + task.setDataPath(tempFile.getPath(), false); + try (InputStream is = inputStreamSupplier.get()) { + utilGet(task, is, true, con.getContentLengthLong(), false); + } + tempFile.move(); + } finally { + task.setDataPath(dataFile); + } + } + if (task.getDataPath() != null) { + long lastModified = con.getLastModified(); + if (lastModified != 0) { + pathProcessor.setLastModified(task.getDataPath(), lastModified); + } + } + } + + @Override + protected void implPut(PutTask task) throws Exception { + throw new IOException("unsupported operation"); + } + + @Override + protected void implClose() { + // nothing + } + + private HttpURLConnection perform(String method, URI target, GetTask task) throws IOException { + String currAuth = preemptiveAuth ? auth : null; + String currProxyAuth = null; + if (cacheGetter.apply(authKey)) { + currAuth = this.auth; + } + if (cacheGetter.apply(proxyAuthKey)) { + currProxyAuth = this.proxyAuth; + } + return perform(method, new ArrayList<>(Collections.singletonList(target)), currAuth, currProxyAuth, task); + } + + private HttpURLConnection perform( + String method, ArrayList<URI> target, String currAuth, String currProxyAuth, GetTask task) + throws IOException { + if (target.size() > MAX_REDIRECTS) { + throw new IOException("Too many redirects"); + } + HttpURLConnection con = (HttpURLConnection) target.get(0).toURL().openConnection(proxy); + con.setConnectTimeout(connectTimeout); + con.setReadTimeout(requestTimeout); + con.setRequestMethod(method); + con.setUseCaches(false); + con.setInstanceFollowRedirects(false); + con.setRequestProperty(HttpConstants.ACCEPT_ENCODING, "gzip"); Review Comment: The transporter advertises deflate support (tests/README), but the request only asks for gzip via `Accept-Encoding`. This prevents servers from choosing deflate and makes the deflate handling path effectively dead code. ########## maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java: ########## @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.transport.url; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.zip.DeflaterInputStream; +import java.util.zip.GZIPInputStream; + +import org.eclipse.aether.Keys; +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.http.ChecksumExtractor; +import org.eclipse.aether.spi.connector.transport.http.HttpConstants; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; +import org.eclipse.aether.spi.io.PathProcessor; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.connector.transport.http.HttpTransporterUtils; + +/** + * A special, "read only" and limited capability transport usable for bootstrapping. It provides HTTP with minimal + * support (only basic auth, only GET/HEAD). + * + * @since 2.0.21 + */ +public class UrlTransporter extends AbstractTransporter implements HttpTransporter { + + private static final int MAX_REDIRECTS = 5; + private static final String METHOD_GET = "GET"; + private static final String METHOD_HEAD = "HEAD"; + private static final String HEADER_LOCATION = "Location"; + private static final String HEADER_AUTHORIZATION = "Authorization"; + private static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization"; + private static final String AUTH_SCHEME_BASIC = "Basic"; + + private final ChecksumExtractor checksumExtractor; + private final PathProcessor pathProcessor; + private final URI baseUri; + private final Map<String, String> headers; + private final String userAgent; + private final int connectTimeout; + private final int requestTimeout; + private final boolean preemptiveAuth; + private final String auth; + private final Proxy proxy; + private final String proxyAuth; + + private final Object authKey; + private final Object proxyAuthKey; + private final Function<Object, Boolean> cacheGetter; + private final Consumer<Object> cacheSetter; + + @FunctionalInterface + private interface IOSupplier<T> { + T get() throws IOException; + } + + public UrlTransporter( + RemoteRepository repository, + RepositorySystemSession session, + ChecksumExtractor checksumExtractor, + PathProcessor pathProcessor) + throws NoTransporterException { + this.checksumExtractor = checksumExtractor; + this.pathProcessor = pathProcessor; + try { + this.baseUri = HttpTransporterUtils.getBaseUri(repository); + } catch (URISyntaxException e) { + throw new NoTransporterException(repository, e.getMessage(), e); + } + + this.headers = HttpTransporterUtils.getHttpHeaders(session, repository); + this.userAgent = HttpTransporterUtils.getUserAgent(session, repository); + this.connectTimeout = HttpTransporterUtils.getHttpConnectTimeout(session, repository); + this.requestTimeout = HttpTransporterUtils.getHttpRequestTimeout(session, repository); + String authString = null; + try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) { + if (repoAuthContext != null) { + String username = repoAuthContext.get(AuthenticationContext.USERNAME); + String password = repoAuthContext.get(AuthenticationContext.PASSWORD); + if (username != null && password != null) { + authString = username + ":" + password; + } + } + } + this.auth = authString; + this.preemptiveAuth = this.auth != null && HttpTransporterUtils.isHttpPreemptiveAuth(session, repository); + + org.eclipse.aether.repository.Proxy repoProxy = repository.getProxy(); + this.proxy = repoProxy != null + ? new Proxy(Proxy.Type.HTTP, new InetSocketAddress(repoProxy.getHost(), repoProxy.getPort())) + : Proxy.NO_PROXY; + String proxyAuthString = null; + try (AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy(session, repository)) { + if (proxyAuthContext != null) { + String username = proxyAuthContext.get(AuthenticationContext.USERNAME); + String password = proxyAuthContext.get(AuthenticationContext.PASSWORD); + if (username != null && password != null) { + proxyAuthString = username + ":" + password; + } + } + } + this.proxyAuth = proxyAuthString; + + this.authKey = Keys.of(UrlTransporter.class, repository, "auth"); + this.proxyAuthKey = Keys.of(UrlTransporter.class, repository, "proxyAuth"); + if (session.getCache() != null) { + this.cacheGetter = k -> { + Boolean ret = (Boolean) session.getCache().get(session, k); + if (ret == null) { + return false; + } else { + return ret; + } + }; + this.cacheSetter = k -> session.getCache().put(session, k, Boolean.TRUE); + } else { + this.cacheGetter = k -> false; + this.cacheSetter = k -> {}; + } + } + + @Override + protected void implPeek(PeekTask task) throws Exception { + HttpURLConnection con = perform(METHOD_HEAD, baseUri.resolve(task.getLocation()), null); + if (HttpURLConnection.HTTP_OK != con.getResponseCode()) { + throw new HttpTransporterException(con.getResponseCode()); + } + } + + @Override + protected void implGet(GetTask task) throws Exception { + HttpURLConnection con = perform(METHOD_GET, baseUri.resolve(task.getLocation()), task); + if (HttpURLConnection.HTTP_OK != con.getResponseCode()) { + throw new HttpTransporterException(con.getResponseCode()); + } + IOSupplier<InputStream> inputStreamSupplier = () -> { + String contentEncoding = con.getHeaderField("Content-Encoding"); + if (contentEncoding != null) { + if ("gzip".equals(contentEncoding)) { + return new GZIPInputStream(con.getInputStream()); + } else if ("deflate".equals(contentEncoding)) { + return new DeflaterInputStream(con.getInputStream()); + } + } + return con.getInputStream(); + }; + final Path dataFile = task.getDataPath(); + if (dataFile == null) { + try (InputStream is = inputStreamSupplier.get()) { + utilGet(task, is, true, con.getContentLengthLong(), false); + } + } else { + try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) { + task.setDataPath(tempFile.getPath(), false); + try (InputStream is = inputStreamSupplier.get()) { + utilGet(task, is, true, con.getContentLengthLong(), false); + } + tempFile.move(); + } finally { + task.setDataPath(dataFile); + } + } + if (task.getDataPath() != null) { + long lastModified = con.getLastModified(); + if (lastModified != 0) { + pathProcessor.setLastModified(task.getDataPath(), lastModified); + } + } + } + + @Override + protected void implPut(PutTask task) throws Exception { + throw new IOException("unsupported operation"); + } + + @Override + protected void implClose() { + // nothing + } + + private HttpURLConnection perform(String method, URI target, GetTask task) throws IOException { + String currAuth = preemptiveAuth ? auth : null; + String currProxyAuth = null; + if (cacheGetter.apply(authKey)) { + currAuth = this.auth; + } + if (cacheGetter.apply(proxyAuthKey)) { + currProxyAuth = this.proxyAuth; + } + return perform(method, new ArrayList<>(Collections.singletonList(target)), currAuth, currProxyAuth, task); + } + + private HttpURLConnection perform( + String method, ArrayList<URI> target, String currAuth, String currProxyAuth, GetTask task) + throws IOException { + if (target.size() > MAX_REDIRECTS) { + throw new IOException("Too many redirects"); + } + HttpURLConnection con = (HttpURLConnection) target.get(0).toURL().openConnection(proxy); + con.setConnectTimeout(connectTimeout); + con.setReadTimeout(requestTimeout); + con.setRequestMethod(method); + con.setUseCaches(false); + con.setInstanceFollowRedirects(false); + con.setRequestProperty(HttpConstants.ACCEPT_ENCODING, "gzip"); + con.setRequestProperty(HttpConstants.CACHE_CONTROL, "no-cache, no-store"); + con.setRequestProperty("Pragma", "no-cache"); + con.setRequestProperty(HttpConstants.USER_AGENT, userAgent); + headers.forEach(con::setRequestProperty); + if (currAuth != null) { + con.setRequestProperty(HEADER_AUTHORIZATION, basicAuthorization(auth)); + } Review Comment: The Authorization header is built from the field `auth` instead of the method parameter `currAuth`. This makes the `currAuth` parameter misleading and could break future changes where the credentials string differs from the field value (similar code for proxy auth correctly uses `currProxyAuth`). ########## maven-resolver-transport-url/README.md: ########## @@ -0,0 +1,19 @@ +# Maven Resolver URL Transport + +This is special Transport with limited HTTP capabilities. The main use case of this transport is outside of +Maven, but in case of other apps that are Java 8, and integrate Resolver mostly for **consumption purposes**. +Before this transport, the only option was to either up Java level to 11 and use JDK transport, or to use +the heavyweight Apache HttpClient transport, which is not always desirable. Review Comment: Minor grammar issues in the module README make the introduction harder to read (missing article, awkward phrasing). ########## maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java: ########## @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.transport.url; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.zip.DeflaterInputStream; +import java.util.zip.GZIPInputStream; + +import org.eclipse.aether.Keys; +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.http.ChecksumExtractor; +import org.eclipse.aether.spi.connector.transport.http.HttpConstants; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporter; +import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; +import org.eclipse.aether.spi.io.PathProcessor; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.connector.transport.http.HttpTransporterUtils; + +/** + * A special, "read only" and limited capability transport usable for bootstrapping. It provides HTTP with minimal + * support (only basic auth, only GET/HEAD). + * + * @since 2.0.21 + */ +public class UrlTransporter extends AbstractTransporter implements HttpTransporter { + + private static final int MAX_REDIRECTS = 5; + private static final String METHOD_GET = "GET"; + private static final String METHOD_HEAD = "HEAD"; + private static final String HEADER_LOCATION = "Location"; + private static final String HEADER_AUTHORIZATION = "Authorization"; + private static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization"; + private static final String AUTH_SCHEME_BASIC = "Basic"; + + private final ChecksumExtractor checksumExtractor; + private final PathProcessor pathProcessor; + private final URI baseUri; + private final Map<String, String> headers; + private final String userAgent; + private final int connectTimeout; + private final int requestTimeout; + private final boolean preemptiveAuth; + private final String auth; + private final Proxy proxy; + private final String proxyAuth; + + private final Object authKey; + private final Object proxyAuthKey; + private final Function<Object, Boolean> cacheGetter; + private final Consumer<Object> cacheSetter; + + @FunctionalInterface + private interface IOSupplier<T> { + T get() throws IOException; + } + + public UrlTransporter( + RemoteRepository repository, + RepositorySystemSession session, + ChecksumExtractor checksumExtractor, + PathProcessor pathProcessor) + throws NoTransporterException { + this.checksumExtractor = checksumExtractor; + this.pathProcessor = pathProcessor; + try { + this.baseUri = HttpTransporterUtils.getBaseUri(repository); + } catch (URISyntaxException e) { + throw new NoTransporterException(repository, e.getMessage(), e); + } + + this.headers = HttpTransporterUtils.getHttpHeaders(session, repository); + this.userAgent = HttpTransporterUtils.getUserAgent(session, repository); + this.connectTimeout = HttpTransporterUtils.getHttpConnectTimeout(session, repository); + this.requestTimeout = HttpTransporterUtils.getHttpRequestTimeout(session, repository); + String authString = null; + try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) { + if (repoAuthContext != null) { + String username = repoAuthContext.get(AuthenticationContext.USERNAME); + String password = repoAuthContext.get(AuthenticationContext.PASSWORD); + if (username != null && password != null) { + authString = username + ":" + password; + } + } + } + this.auth = authString; + this.preemptiveAuth = this.auth != null && HttpTransporterUtils.isHttpPreemptiveAuth(session, repository); + + org.eclipse.aether.repository.Proxy repoProxy = repository.getProxy(); + this.proxy = repoProxy != null + ? new Proxy(Proxy.Type.HTTP, new InetSocketAddress(repoProxy.getHost(), repoProxy.getPort())) + : Proxy.NO_PROXY; + String proxyAuthString = null; + try (AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy(session, repository)) { + if (proxyAuthContext != null) { + String username = proxyAuthContext.get(AuthenticationContext.USERNAME); + String password = proxyAuthContext.get(AuthenticationContext.PASSWORD); + if (username != null && password != null) { + proxyAuthString = username + ":" + password; + } + } + } + this.proxyAuth = proxyAuthString; + + this.authKey = Keys.of(UrlTransporter.class, repository, "auth"); + this.proxyAuthKey = Keys.of(UrlTransporter.class, repository, "proxyAuth"); + if (session.getCache() != null) { + this.cacheGetter = k -> { + Boolean ret = (Boolean) session.getCache().get(session, k); + if (ret == null) { + return false; + } else { + return ret; + } + }; + this.cacheSetter = k -> session.getCache().put(session, k, Boolean.TRUE); + } else { + this.cacheGetter = k -> false; + this.cacheSetter = k -> {}; + } + } + + @Override + protected void implPeek(PeekTask task) throws Exception { + HttpURLConnection con = perform(METHOD_HEAD, baseUri.resolve(task.getLocation()), null); + if (HttpURLConnection.HTTP_OK != con.getResponseCode()) { + throw new HttpTransporterException(con.getResponseCode()); + } + } + + @Override + protected void implGet(GetTask task) throws Exception { + HttpURLConnection con = perform(METHOD_GET, baseUri.resolve(task.getLocation()), task); + if (HttpURLConnection.HTTP_OK != con.getResponseCode()) { + throw new HttpTransporterException(con.getResponseCode()); + } + IOSupplier<InputStream> inputStreamSupplier = () -> { + String contentEncoding = con.getHeaderField("Content-Encoding"); + if (contentEncoding != null) { + if ("gzip".equals(contentEncoding)) { + return new GZIPInputStream(con.getInputStream()); + } else if ("deflate".equals(contentEncoding)) { + return new DeflaterInputStream(con.getInputStream()); + } + } + return con.getInputStream(); + }; + final Path dataFile = task.getDataPath(); + if (dataFile == null) { + try (InputStream is = inputStreamSupplier.get()) { + utilGet(task, is, true, con.getContentLengthLong(), false); + } + } else { + try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) { + task.setDataPath(tempFile.getPath(), false); + try (InputStream is = inputStreamSupplier.get()) { + utilGet(task, is, true, con.getContentLengthLong(), false); + } + tempFile.move(); + } finally { + task.setDataPath(dataFile); + } + } + if (task.getDataPath() != null) { + long lastModified = con.getLastModified(); + if (lastModified != 0) { + pathProcessor.setLastModified(task.getDataPath(), lastModified); + } + } + } + + @Override + protected void implPut(PutTask task) throws Exception { + throw new IOException("unsupported operation"); + } + + @Override + protected void implClose() { + // nothing + } + + private HttpURLConnection perform(String method, URI target, GetTask task) throws IOException { + String currAuth = preemptiveAuth ? auth : null; + String currProxyAuth = null; + if (cacheGetter.apply(authKey)) { + currAuth = this.auth; + } + if (cacheGetter.apply(proxyAuthKey)) { + currProxyAuth = this.proxyAuth; + } + return perform(method, new ArrayList<>(Collections.singletonList(target)), currAuth, currProxyAuth, task); + } + + private HttpURLConnection perform( + String method, ArrayList<URI> target, String currAuth, String currProxyAuth, GetTask task) + throws IOException { + if (target.size() > MAX_REDIRECTS) { + throw new IOException("Too many redirects"); + } + HttpURLConnection con = (HttpURLConnection) target.get(0).toURL().openConnection(proxy); + con.setConnectTimeout(connectTimeout); + con.setReadTimeout(requestTimeout); + con.setRequestMethod(method); + con.setUseCaches(false); + con.setInstanceFollowRedirects(false); + con.setRequestProperty(HttpConstants.ACCEPT_ENCODING, "gzip"); + con.setRequestProperty(HttpConstants.CACHE_CONTROL, "no-cache, no-store"); + con.setRequestProperty("Pragma", "no-cache"); + con.setRequestProperty(HttpConstants.USER_AGENT, userAgent); + headers.forEach(con::setRequestProperty); + if (currAuth != null) { + con.setRequestProperty(HEADER_AUTHORIZATION, basicAuthorization(auth)); + } + if (currProxyAuth != null) { + con.setRequestProperty(HEADER_PROXY_AUTHORIZATION, basicAuthorization(currProxyAuth)); + } + int responseCode = con.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + if (task != null) { + Map<String, String> checksums = checksumExtractor.extractChecksums(con::getHeaderField); + if (checksums != null && !checksums.isEmpty()) { + checksums.forEach(task::setChecksum); + } + } + } else if (responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { + target.add(0, URI.create(con.getHeaderField(HEADER_LOCATION))); + return perform(method, target, currAuth, currProxyAuth, task); Review Comment: Redirect handling assumes `Location` is always an absolute URI. If the server returns a relative Location (allowed by HTTP), `URI.toURL()` will fail later. Also, redirecting to a non-http(s) scheme will currently blow up with a `ClassCastException` when casting to `HttpURLConnection`. Resolve relative redirects against the current URL and reject non-http(s) schemes early. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
