This is an automated email from the ASF dual-hosted git repository.
cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to refs/heads/master by this push:
new 21ab3aec5 Feat: new (limited) transport (#1966)
21ab3aec5 is described below
commit 21ab3aec5a75a65fcfed9ee9b388b8d5ae58db69
Author: Tamas Cservenak <[email protected]>
AuthorDate: Thu Jul 16 18:56:38 2026 +0200
Feat: new (limited) transport (#1966)
Many applications (outside of Maven) integrate Resolver and their use case
is almost always "consume" (use Resolver to resolve artifacts). Also, many of
those applications are still Java 8 level, hence, the JDK transport for them is
no-go. On the other hand, there is no lightweight replacement for them, except
to use some "heavyweight" transporter, but in case of CLI applications this is
usually undesirable, as they count every byte.
Transitive hull sizes of several existing transports:
* jdk - 1.5MB / Java 11+
* wagon - 1.9MB (without any provider; unusable like this) / Java 8+
* apache - 2.4MB / Java 8+
* jetty - 10.9MB / Java 11+
* minio - 23.5MB / Java 8+
The new transport has no dependencies and small size:
* url - 870 KB
This new transport, while is in Resolver, falls totally outside of "Maven
world", as nor Maven nor any other Maven-related thing will use it.
---
.../test/util/http/HttpTransporterTest.java | 22 +-
maven-resolver-transport-url/README.md | 37 ++
maven-resolver-transport-url/pom.xml | 84 +++++
.../aether/transport/url/UrlTransporter.java | 379 +++++++++++++++++++++
.../url/UrlTransporterConfigurationKeys.java | 72 ++++
.../transport/url/UrlTransporterFactory.java | 82 +++++
maven-resolver-transport-url/src/site/site.xml | 36 ++
.../aether/transport/url/UrlTransporterTest.java | 226 ++++++++++++
pom.xml | 7 +-
src/site/markdown/transporter-known-issues.md | 27 ++
10 files changed, 960 insertions(+), 12 deletions(-)
diff --git
a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
index 546a3ac9a..477b2aa19 100644
---
a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
+++
b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
@@ -513,9 +513,11 @@ public abstract class HttpTransporterTest {
RecordingTransportListener listener = new RecordingTransportListener();
PeekTask task = new
PeekTask(URI.create("repo/file.txt")).setListener(listener);
transporter.peek(task);
- assertEquals(
- HttpTransportProperty.SslProtocol.TLS_1_3,
-
listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
+ if (exposeContentCodingInTransportProperties()) {
+ assertEquals(
+ HttpTransportProperty.SslProtocol.TLS_1_3,
+
listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
+ }
}
@Test
@@ -785,12 +787,14 @@ public abstract class HttpTransporterTest {
assertEquals(1, listener.getStartedCount());
assertTrue(listener.getProgressedCount() > 0, "Count: " +
listener.getProgressedCount());
assertEquals(task.getDataString(),
listener.getBaos().toString(StandardCharsets.UTF_8));
- assertEquals(
- HttpTransportProperty.SslProtocol.TLS_1_3,
-
listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
- assertEquals(
- "TLS_AES_256_GCM_SHA384",
-
listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_CIPHER_SUITE));
+ if (exposeContentCodingInTransportProperties()) {
+ assertEquals(
+ HttpTransportProperty.SslProtocol.TLS_1_3,
+
listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_PROTOCOL));
+ assertEquals(
+ "TLS_AES_256_GCM_SHA384",
+
listener.getTransportProperties().get(HttpTransportProperty.Key.SSL_CIPHER_SUITE));
+ }
}
@Test
diff --git a/maven-resolver-transport-url/README.md
b/maven-resolver-transport-url/README.md
new file mode 100644
index 000000000..f50fc3f1f
--- /dev/null
+++ b/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, in 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 be 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 as
such. It is quite usable in
+artifact consumption scenarios, but it is not suitable for artifact deployment.
\ No newline at end of file
diff --git a/maven-resolver-transport-url/pom.xml
b/maven-resolver-transport-url/pom.xml
new file mode 100644
index 000000000..56b551808
--- /dev/null
+++ b/maven-resolver-transport-url/pom.xml
@@ -0,0 +1,84 @@
+<?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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver</artifactId>
+ <version>2.0.21-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>maven-resolver-transport-url</artifactId>
+
+ <name>Maven Artifact Resolver Transport URL</name>
+ <description>A transport implementation for read-only use
cases.</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-spi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-util</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-test-util</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-test-http</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.eclipse.sisu</groupId>
+ <artifactId>sisu-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java
b/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java
new file mode 100644
index 000000000..c65b78c48
--- /dev/null
+++
b/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java
@@ -0,0 +1,379 @@
+/*
+ * 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.ConfigUtils;
+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 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 enum RedirectMode {
+ /**
+ * No redirects allowed.
+ */
+ NONE,
+ /**
+ * Redirects only within same authority.
+ */
+ SAME_AUTHORITY,
+ /**
+ * Any redirect is followed.
+ */
+ ANY
+ }
+
+ 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 RedirectMode redirectMode;
+ private final boolean redirectAllowDowngrade;
+ private final int maxRedirects;
+
+ 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.redirectMode = RedirectMode.valueOf(ConfigUtils.getString(
+ session,
+ UrlTransporterConfigurationKeys.DEFAULT_REDIRECT_MODE,
+ UrlTransporterConfigurationKeys.CONFIG_PROP_REDIRECT_MODE +
"." + repository.getId(),
+ UrlTransporterConfigurationKeys.CONFIG_PROP_REDIRECT_MODE));
+ this.redirectAllowDowngrade = ConfigUtils.getBoolean(
+ session,
+
UrlTransporterConfigurationKeys.DEFAULT_REDIRECT_ALLOW_DOWNGRADE,
+
UrlTransporterConfigurationKeys.CONFIG_PROP_REDIRECT_ALLOW_DOWNGRADE + "." +
repository.getId(),
+
UrlTransporterConfigurationKeys.CONFIG_PROP_REDIRECT_ALLOW_DOWNGRADE);
+ this.maxRedirects = ConfigUtils.getInteger(
+ session,
+ UrlTransporterConfigurationKeys.DEFAULT_MAX_REDIRECT_COUNT,
+ UrlTransporterConfigurationKeys.CONFIG_PROP_MAX_REDIRECT_COUNT
+ "." + repository.getId(),
+
UrlTransporterConfigurationKeys.CONFIG_PROP_MAX_REDIRECT_COUNT);
+
+ 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);
+ try {
+ int responseCode = con.getResponseCode();
+ 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);
+ try {
+ int responseCode = con.getResponseCode();
+ if (HttpURLConnection.HTTP_OK != responseCode) {
+ 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);
+ }
+ }
+ } finally {
+ con.disconnect();
+ }
+ }
+
+ @Override
+ protected void implPut(PutTask task) {
+ throw new UnsupportedOperationException("PUT method unsupported");
+ }
+
+ @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() - 1 > maxRedirects) {
+ 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 == HttpURLConnection.HTTP_SEE_OTHER
+ || responseCode == HTTP_STATUS_TEMPORARY_REDIRECT
+ || responseCode == HTTP_STATUS_PERMANENT_REDIRECT) {
+ if (redirectMode == RedirectMode.NONE) {
+ con.disconnect();
+ throw new IOException("Refusing to follow redirects");
+ }
+ final String location = con.getHeaderField(HEADER_LOCATION);
+ if (location == null) {
+ con.disconnect();
+ throw new IOException("Redirect response missing Location
header");
+ }
+ final URI currentUri;
+ final URI redirectUri;
+ try {
+ currentUri = URI.create(con.getURL().toString());
+ redirectUri = currentUri.resolve(location);
+ } catch (IllegalArgumentException e) {
+ con.disconnect();
+ throw new IOException("Redirect response has invalid Location
header: " + location, e);
+ }
+ // ensure we are HTTP or HTTPS after redirect
+ if (!"http".equalsIgnoreCase(redirectUri.getScheme())
+ && !"https".equalsIgnoreCase(redirectUri.getScheme())) {
+ con.disconnect();
+ throw new IOException("Unsupported redirect protocol: " +
redirectUri.getScheme());
+ }
+ String currentAuthority = currentUri.getAuthority();
+ String redirectAuthority = redirectUri.getAuthority();
+ if (currentAuthority == null ||
!currentAuthority.equalsIgnoreCase(redirectAuthority)) {
+ if (redirectMode == RedirectMode.SAME_AUTHORITY) {
+ con.disconnect();
+ throw new IOException("Refusing to follow redirect to
different authority: " + redirectAuthority);
+ } else {
+ // reset auth if authority differs after redirect
+ currAuth = null;
+ }
+ }
+ if ("https".equalsIgnoreCase(currentUri.getScheme())
+ && "http".equalsIgnoreCase(redirectUri.getScheme())
+ && !redirectAllowDowngrade) {
+ // forbid HTTPS -> HTTP downgrade during redirect
+ con.disconnect();
+ throw new IOException("Refusing to downgrade from HTTPS to
HTTP protocol");
+ }
+ target.add(0, redirectUri);
+ con.disconnect();
+ return perform(method, target, currAuth, currProxyAuth, task);
+ } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED &&
currAuth == null && this.auth != null) {
+ con.disconnect();
+ return perform(method, target, this.auth, currProxyAuth, task);
+ } else if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH
+ && currProxyAuth == null
+ && this.proxyAuth != null) {
+ con.disconnect();
+ return perform(method, target, currAuth, this.proxyAuth, task);
+ }
+ if (currAuth != null) {
+ cacheSetter.accept(authKey);
+ }
+ if (currProxyAuth != null) {
+ cacheSetter.accept(proxyAuthKey);
+ }
+ return con;
+ }
+
+ private String basicAuthorization(String credentials) {
+ return AUTH_SCHEME_BASIC + " " +
Base64.getEncoder().encodeToString(credentials.getBytes(authEncoding));
+ }
+}
diff --git
a/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporterConfigurationKeys.java
b/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporterConfigurationKeys.java
new file mode 100644
index 000000000..cc335c546
--- /dev/null
+++
b/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporterConfigurationKeys.java
@@ -0,0 +1,72 @@
+/*
+ * 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 org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * Configuration for URL Transport.
+ *
+ * @since 2.0.21
+ */
+public final class UrlTransporterConfigurationKeys {
+ private UrlTransporterConfigurationKeys() {}
+
+ static final String CONFIG_PROPS_PREFIX =
+ ConfigurationProperties.PREFIX_TRANSPORT +
UrlTransporterFactory.NAME + ".";
+
+ /**
+ * The redirect mode of the transport. Accepted values are {@code NONE},
when any redirect response from server
+ * result in transport error, {@code SAME_AUTHORITY} when transport follow
redirects only happening within same
+ * URI authority (same server), and {@code ANY} when redirects are
followed everywhere.
+ *
+ * @configurationSource {@link
RepositorySystemSession#getConfigProperties()}
+ * @configurationType {@link String}
+ * @configurationDefaultValue {@link #DEFAULT_REDIRECT_MODE}
+ * @configurationRepoIdSuffix Yes
+ */
+ public static final String CONFIG_PROP_REDIRECT_MODE = CONFIG_PROPS_PREFIX
+ "redirectMode";
+
+ public static final String DEFAULT_REDIRECT_MODE = "ANY";
+
+ /**
+ * Maximum count of redirects that transport follows within one
transaction.
+ *
+ * @configurationSource {@link
RepositorySystemSession#getConfigProperties()}
+ * @configurationType {@link Integer}
+ * @configurationDefaultValue {@link #DEFAULT_MAX_REDIRECT_COUNT}
+ * @configurationRepoIdSuffix Yes
+ */
+ public static final String CONFIG_PROP_MAX_REDIRECT_COUNT =
CONFIG_PROPS_PREFIX + "maxRedirectCount";
+
+ public static final int DEFAULT_MAX_REDIRECT_COUNT = 5;
+
+ /**
+ * Whether protocol downgrade (HTTPS -> HTTP) is allowed during redirect
following.
+ *
+ * @configurationSource {@link
RepositorySystemSession#getConfigProperties()}
+ * @configurationType {@link java.lang.Boolean}
+ * @configurationDefaultValue {@link #DEFAULT_REDIRECT_ALLOW_DOWNGRADE}
+ * @configurationRepoIdSuffix Yes
+ */
+ public static final String CONFIG_PROP_REDIRECT_ALLOW_DOWNGRADE =
CONFIG_PROPS_PREFIX + "redirectAllowDowngrade";
+
+ public static final boolean DEFAULT_REDIRECT_ALLOW_DOWNGRADE = false;
+}
diff --git
a/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporterFactory.java
b/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporterFactory.java
new file mode 100644
index 000000000..86e1c82f8
--- /dev/null
+++
b/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporterFactory.java
@@ -0,0 +1,82 @@
+/*
+ * 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 javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
+import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory;
+import org.eclipse.aether.spi.io.PathProcessor;
+import org.eclipse.aether.transfer.NoTransporterException;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A bootstrapping transporter factory for repositories using the {@code
http:} or {@code https:} protocol.
+ *
+ * @since 2.0.21
+ */
+@Named(UrlTransporterFactory.NAME)
+public final class UrlTransporterFactory implements HttpTransporterFactory {
+ public static final String NAME = "url";
+
+ private float priority = -1.0f;
+
+ private final ChecksumExtractor checksumExtractor;
+
+ private final PathProcessor pathProcessor;
+
+ @Inject
+ public UrlTransporterFactory(ChecksumExtractor checksumExtractor,
PathProcessor pathProcessor) {
+ this.checksumExtractor = requireNonNull(checksumExtractor,
"checksumExtractor");
+ this.pathProcessor = requireNonNull(pathProcessor, "pathProcessor");
+ }
+
+ @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 UrlTransporterFactory setPriority(float priority) {
+ this.priority = priority;
+ return this;
+ }
+
+ @Override
+ public UrlTransporter 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 UrlTransporter(repository, session, checksumExtractor,
pathProcessor);
+ }
+}
diff --git a/maven-resolver-transport-url/src/site/site.xml
b/maven-resolver-transport-url/src/site/site.xml
new file mode 100644
index 000000000..0bd814bcd
--- /dev/null
+++ b/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 URL">
+ <body>
+ <menu name="Overview">
+ <item name="Introduction" href="index.html"/>
+ <item name="JavaDocs" href="apidocs/index.html"/>
+ <item name="Source Xref" href="xref/index.html"/>
+ <!--item name="FAQ" href="faq.html"/-->
+ </menu>
+
+ <menu ref="parent"/>
+ <menu ref="reports"/>
+ </body>
+</site>
\ No newline at end of file
diff --git
a/maven-resolver-transport-url/src/test/java/org/eclipse/aether/transport/url/UrlTransporterTest.java
b/maven-resolver-transport-url/src/test/java/org/eclipse/aether/transport/url/UrlTransporterTest.java
new file mode 100644
index 000000000..81b64e0ea
--- /dev/null
+++
b/maven-resolver-transport-url/src/test/java/org/eclipse/aether/transport/url/UrlTransporterTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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.util.stream.Stream;
+
+import org.eclipse.aether.internal.test.util.http.HttpTransporterTest;
+import org.eclipse.aether.spi.io.PathProcessorSupport;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * URL Transporter UT.
+ */
+class UrlTransporterTest extends HttpTransporterTest {
+
+ public UrlTransporterTest() {
+ super(() -> new UrlTransporterFactory(standardChecksumExtractor(), new
PathProcessorSupport()));
+ }
+
+ @Override
+ protected Stream<String> supportedCompressionAlgorithms() {
+ return Stream.of("gzip", "deflate");
+ }
+
+ @Override
+ protected boolean exposeContentCodingInTransportProperties() {
+ return false;
+ }
+
+ @Override
+ protected boolean supportsHttp3() {
+ return false;
+ }
+
+ @Override
+ protected boolean supportsHttp2() {
+ return false;
+ }
+
+ @Override
+ @Disabled("HTTP2 unsupported")
+ @Test
+ protected void testGet_HTTPS_HTTP2Only_Insecure_SecurityMode() {}
+
+ @Override
+ @Disabled("Controlled via Java system properties")
+ @Test
+ protected void testGet_HTTPS_Insecure_SecurityMode() {}
+
+ @Override
+ @Disabled("Controlled via Java system properties")
+ @Test
+ protected void testGet_HTTPS_Unknown_SecurityMode() {}
+
+ @Override
+ @Disabled("RFC9457 unsupported")
+ @Test
+ protected void testGet_AcceptsRfc9457() {}
+
+ @Override
+ @Disabled("RFC9457 unsupported")
+ @Test
+ protected void testGet_ParseRfc9457() {}
+
+ @Override
+ @Disabled("Resume unsupported")
+ @Test
+ protected void testGet_Resume() {}
+
+ @Override
+ @Disabled("RFC9457 unsupported")
+ @Test
+ protected void testGet_RFC9457Response() {}
+
+ @Override
+ @Disabled("RFC9457 unsupported")
+ @Test
+ protected void testGet_RFC9457Response_with_missing_fields() {}
+
+ @Override
+ @Disabled("RFC9457 unsupported")
+ @Test
+ protected void testPeek_DoesNotAcceptRfc9457() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testGetPut_AuthCache() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_AcceptsRfc9457() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_AuthCache() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_AuthCache_Preemptive() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_Authenticated_ExpectContinue() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_Authenticated_ExpectContinueBroken() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_Authenticated_ExpectContinueDisabled() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_Authenticated_ExpectContinueRejected() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void
testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_EmptyResource() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_EncodedResourcePath() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_FileHandleLeak() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_FromFile() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_FromMemory() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_PreemptiveIsDefault() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_ProgressCancelled() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_ProxyAuthenticated() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_ProxyUnauthenticated() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_SSL() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_StartCancelled() {}
+
+ @Override
+ @Disabled("PUT unsupported")
+ @Test
+ protected void testPut_Unauthenticated() {}
+
+ @Override
+ @Disabled
+ @Test
+ protected void testRetryHandler_defaultCount_positive() {}
+
+ @Override
+ @Disabled
+ @Test
+ protected void testRetryHandler_explicitCount_positive() {}
+
+ @Override
+ @Disabled
+ @Test
+ protected void testRetryHandler_tooManyRequests_explicitCount_negative() {}
+
+ @Override
+ @Disabled
+ @Test
+ protected void testRetryHandler_tooManyRequests_explicitCount_positive() {}
+}
diff --git a/pom.xml b/pom.xml
index f98c0bd9a..ee8a89355 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,13 +56,14 @@
<module>maven-resolver-test-util</module>
<module>maven-resolver-test-http</module>
<module>maven-resolver-connector-basic</module>
+ <module>maven-resolver-transport-apache</module>
<module>maven-resolver-transport-classpath</module>
<module>maven-resolver-transport-file</module>
- <module>maven-resolver-transport-jetty</module>
<module>maven-resolver-transport-jdk-parent</module>
- <module>maven-resolver-transport-apache</module>
- <module>maven-resolver-transport-wagon</module>
+ <module>maven-resolver-transport-jetty</module>
<module>maven-resolver-transport-minio</module>
+ <module>maven-resolver-transport-url</module>
+ <module>maven-resolver-transport-wagon</module>
<module>maven-resolver-generator-gnupg</module>
<module>maven-resolver-generator-sigstore</module>
<module>maven-resolver-supplier-mvn3</module>
diff --git a/src/site/markdown/transporter-known-issues.md
b/src/site/markdown/transporter-known-issues.md
index 43f50cf82..638347ea8 100644
--- a/src/site/markdown/transporter-known-issues.md
+++ b/src/site/markdown/transporter-known-issues.md
@@ -67,3 +67,30 @@ you need to add
`org.apache.maven.resolver.transport:transport-http-jetty` artif
probably improving with Jetty 13 (which is supposed to be shipping with a
Java QUIC implementation).
* When HTTP/3 is configured there is [no fallback to lower versions]
(https://github.com/jetty/jetty.project/issues/15423). The request will just
time out.
+
+## The `url` (Consumer Only) Transporter
+
+Special Transport with limited HTTP capabilities. The main use case of this
transport is outside of
+Maven, in 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 be 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 as
such. It is quite usable in
+artifact consumption scenarios, but it is not suitable for artifact deployment.
+
+### Known issues:
+
+* Does neither support HTTP/2 nor HTTP/3
+* Supports only GET/HEAD, usable only for artifact consumption
+* Is and will never be present in Maven; is meant for applications integrating
Resolver