Copilot commented on code in PR #1966:
URL: https://github.com/apache/maven-resolver/pull/1966#discussion_r3595561889


##########
maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java:
##########
@@ -0,0 +1,323 @@
+/*
+ * 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.Charset;
+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.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+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). It is implemented using {@link 
java.net.HttpURLConnection} class.
+ *
+ * @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 static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
+    private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
+
+    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 Charset authEncoding;
+    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.authEncoding = 
HttpTransporterUtils.getHttpCredentialsEncoding(session, repository);
+        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);
+        int responseCode = con.getResponseCode();
+        try {
+            if (HttpURLConnection.HTTP_OK != responseCode) {
+                throw new HttpTransporterException(responseCode);
+            }
+        } finally {
+            con.disconnect();
+        }
+    }
+
+    @Override
+    protected void implGet(GetTask task) throws Exception {
+        HttpURLConnection con = perform(METHOD_GET, 
baseUri.resolve(task.getLocation()), task);
+        int responseCode = con.getResponseCode();
+        if (HttpURLConnection.HTTP_OK != responseCode) {
+            con.disconnect();
+            throw new HttpTransporterException(responseCode);
+        }
+        IOSupplier<InputStream> inputStreamSupplier = () -> {
+            String contentEncoding = con.getHeaderField("Content-Encoding");
+            if (contentEncoding != null) {
+                if ("gzip".equalsIgnoreCase(contentEncoding)) {
+                    return new GZIPInputStream(con.getInputStream());
+                } else if ("deflate".equalsIgnoreCase(contentEncoding)) {
+                    return new InflaterInputStream(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);
+            }
+        }
+    }

Review Comment:
   `implGet` disconnects the `HttpURLConnection` only on the non-200 branch; on 
the success path (and on exceptions during download/last-modified handling) the 
connection is never disconnected. This can retain sockets/resources longer than 
necessary. Consider mirroring `implPeek` by ensuring `con.disconnect()` runs in 
a `finally` after the download logic.



##########
maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java:
##########
@@ -0,0 +1,323 @@
+/*
+ * 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.Charset;
+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.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+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). It is implemented using {@link 
java.net.HttpURLConnection} class.
+ *
+ * @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 static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
+    private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
+
+    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 Charset authEncoding;
+    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.authEncoding = 
HttpTransporterUtils.getHttpCredentialsEncoding(session, repository);
+        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);
+        int responseCode = con.getResponseCode();
+        try {
+            if (HttpURLConnection.HTTP_OK != responseCode) {
+                throw new HttpTransporterException(responseCode);
+            }
+        } finally {
+            con.disconnect();
+        }
+    }
+
+    @Override
+    protected void implGet(GetTask task) throws Exception {
+        HttpURLConnection con = perform(METHOD_GET, 
baseUri.resolve(task.getLocation()), task);
+        int responseCode = con.getResponseCode();
+        if (HttpURLConnection.HTTP_OK != responseCode) {
+            con.disconnect();
+            throw new HttpTransporterException(responseCode);
+        }
+        IOSupplier<InputStream> inputStreamSupplier = () -> {
+            String contentEncoding = con.getHeaderField("Content-Encoding");
+            if (contentEncoding != null) {
+                if ("gzip".equalsIgnoreCase(contentEncoding)) {
+                    return new GZIPInputStream(con.getInputStream());
+                } else if ("deflate".equalsIgnoreCase(contentEncoding)) {
+                    return new InflaterInputStream(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");
+        }

Review Comment:
   Redirect limit is off by one: `target` contains the current URI plus the 
redirect chain, so `target.size() > MAX_REDIRECTS` only allows 4 redirects when 
`MAX_REDIRECTS` is 5 (after 5 redirects the size is 6). Compare against 
`target.size() - 1` (number of redirects) instead.



##########
maven-resolver-transport-url/README.md:
##########
@@ -0,0 +1,37 @@
+<!---
+ 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.
+-->
+
+# Maven Resolver URL Transport
+
+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 only 
for **consumption purposes**.
+Before this transport, the only option was to either bump Java level to 11 and 
use JDK transport, or to use
+the heavyweight Apache HttpClient transport, which is not always desirable.
+
+Supported features:
+* Implemented using `java.net.HttpURLConnection` class
+* 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)

Review Comment:
   Grammar: the phrase "known to needed" is unclear/incorrect; it reads like a 
typo. Consider rephrasing to "known-to-be-needed" (or similar) to convey that 
auth caching avoids the initial 401 challenge round-trip once auth is known to 
be required.



##########
maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java:
##########
@@ -0,0 +1,323 @@
+/*
+ * 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.Charset;
+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.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+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). It is implemented using {@link 
java.net.HttpURLConnection} class.
+ *
+ * @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 static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
+    private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
+
+    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 Charset authEncoding;
+    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.authEncoding = 
HttpTransporterUtils.getHttpCredentialsEncoding(session, repository);
+        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);
+        int responseCode = con.getResponseCode();
+        try {
+            if (HttpURLConnection.HTTP_OK != responseCode) {
+                throw new HttpTransporterException(responseCode);
+            }
+        } finally {
+            con.disconnect();
+        }
+    }
+
+    @Override
+    protected void implGet(GetTask task) throws Exception {
+        HttpURLConnection con = perform(METHOD_GET, 
baseUri.resolve(task.getLocation()), task);
+        int responseCode = con.getResponseCode();
+        if (HttpURLConnection.HTTP_OK != responseCode) {
+            con.disconnect();
+            throw new HttpTransporterException(responseCode);
+        }
+        IOSupplier<InputStream> inputStreamSupplier = () -> {
+            String contentEncoding = con.getHeaderField("Content-Encoding");
+            if (contentEncoding != null) {
+                if ("gzip".equalsIgnoreCase(contentEncoding)) {
+                    return new GZIPInputStream(con.getInputStream());
+                } else if ("deflate".equalsIgnoreCase(contentEncoding)) {
+                    return new InflaterInputStream(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,deflate");
+        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(currAuth));
+        }
+        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
+                || responseCode == HTTP_STATUS_TEMPORARY_REDIRECT
+                || responseCode == HTTP_STATUS_PERMANENT_REDIRECT) {

Review Comment:
   Redirect handling omits HTTP 303 (See Other). Since README advertises 
redirect support, a 303 with `Location` should be followed the same way as 
301/302/307/308 for GET/HEAD; currently it will fall through and be treated as 
an error by callers.



-- 
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]


Reply via email to