From 34138bf8b9a379c830860e4ee617e32a1e116ecf Mon Sep 17 00:00:00 2001 From: Konrad Windszus Date: Mon, 20 Apr 2026 16:49:01 +0200 Subject: [PATCH 1/4] Send "Accept" request header to indicate RFC 9457 compliance Also evaluate RFC 9457 responses for PUT requests Add tests to check for proper "Accept" headers for GET/HEAD/PUT This closes #1845 --- .../transport/http/HttpConstants.java | 2 ++ .../http/RFC9457/RFC9457Reporter.java | 15 +++++++-- .../test/util/http/HttpTransporterTest.java | 31 +++++++++++++++++++ .../apache/ApacheRFC9457Reporter.java | 9 +++++- .../transport/apache/ApacheTransporter.java | 3 +- .../transport/jdk/JdkRFC9457Reporter.java | 11 ++++++- .../aether/transport/jdk/JdkTransporter.java | 12 +++++-- .../transport/jetty/JettyRFC9457Reporter.java | 9 +++++- .../transport/jetty/JettyTransporter.java | 13 ++++++-- 9 files changed, 94 insertions(+), 11 deletions(-) diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpConstants.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpConstants.java index 6ae373d833..0f06ee5fee 100644 --- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpConstants.java +++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/HttpConstants.java @@ -36,6 +36,8 @@ private HttpConstants() {} public static final int PRECONDITION_FAILED = 412; + public static final String ACCEPT = "Accept"; + public static final String ACCEPT_ENCODING = "Accept-Encoding"; public static final String CACHE_CONTROL = "Cache-Control"; diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java index a91726ea51..fe349b0500 100644 --- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java +++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java @@ -31,8 +31,11 @@ * * @param The type of the response. * @param The base exception type to throw if the response is not a RFC9457 message. + * @param The type of the request or request builder (which allows to modify headers) */ -public abstract class RFC9457Reporter { +public abstract class RFC9457Reporter { + public static final String CONTENT_TYPE_PROBLEM_DETAILS_JSON = "application/problem+json"; + protected abstract boolean isRFC9457Message(T response); protected abstract int getStatusCode(T response); @@ -41,8 +44,16 @@ public abstract class RFC9457Reporter { protected abstract String getBody(T response) throws IOException; + /** + * Prepares the request to accept RFC 9457 responses. + * This involves setting/updating the "Accept" header to include "application/problem+json". + * @param request The request or request builder to prepare + * @see RFC 9457 section 3.2 + */ + public abstract void prepareRequest(R request); + protected boolean hasRFC9457ContentType(String contentType) { - return "application/problem+json".equals(contentType); + return CONTENT_TYPE_PROBLEM_DETAILS_JSON.equals(contentType); } /** 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 d1aab00d8c..8f745bb8c1 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 @@ -249,6 +249,14 @@ protected void testPeek() throws Exception { transporter.peek(new PeekTask(URI.create("repo/file.txt"))); } + @Test + protected void testPeek_DoesNotAcceptRfc9457() throws Exception { + // peek is HEAD request, therefore cannot support RFC 9457 + transporter.peek(new PeekTask(URI.create("repo/file.txt"))); + String accept = httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept"); + assertNull(accept, "No accept header expected for HEAD request, but was: " + accept); + } + @Test protected void testRetryHandler_defaultCount_positive() throws Exception { httpServer.setConnectionsToClose(3); @@ -451,6 +459,17 @@ protected void testGet_ToFileTimestamp() throws Exception { assertEquals(OLD_FILE_TIMESTAMP, file.lastModified()); } + @Test + protected void testGet_AcceptsRfc9457() throws Exception { + GetTask task = new GetTask(URI.create("repo/file.txt")); + transporter.get(task); + String accept = httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept"); + assertNotNull(accept, "Missing Accept header when retrieving artifact"); + assertTrue( + accept.contains("application/problem+json"), + "Expected Accept header to contain application/problem+json, but was: " + accept); + } + /** * Provides compression algorithms supported by the transporter implementation. * This should be the string value passed in the {@code Accept-Encoding} header. @@ -871,6 +890,18 @@ protected void testPut_FromMemory() throws Exception { httpServer.getLogEntries().get(0).getRequestHeaders().get("Content-Length")); } + @Test + protected void testPut_AcceptsRfc9457() throws Exception { + String payload = "upload"; + PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString(payload); + transporter.put(task); + String accept = httpServer.getLogEntries().get(0).getRequestHeaders().get("Accept"); + assertNotNull(accept, "Missing Accept header when retrieving artifact"); + assertTrue( + accept.contains("application/problem+json"), + "Expected Accept header to contain application/problem+json, but was: " + accept); + } + @Test protected void testPut_FromFile() throws Exception { File file = TestFileUtils.createTempFile("upload"); diff --git a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java index 3d9803ec10..444af1ad4f 100644 --- a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java +++ b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java @@ -23,16 +23,23 @@ import org.apache.http.Header; import org.apache.http.HttpHeaders; +import org.apache.http.HttpRequest; import org.apache.http.client.HttpResponseException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.util.EntityUtils; +import org.eclipse.aether.spi.connector.transport.http.HttpConstants; import org.eclipse.aether.spi.connector.transport.http.RFC9457.RFC9457Reporter; -public class ApacheRFC9457Reporter extends RFC9457Reporter { +public class ApacheRFC9457Reporter extends RFC9457Reporter { public static final ApacheRFC9457Reporter INSTANCE = new ApacheRFC9457Reporter(); private ApacheRFC9457Reporter() {} + @Override + public void prepareRequest(HttpRequest request) { + request.addHeader(HttpConstants.ACCEPT, CONTENT_TYPE_PROBLEM_DETAILS_JSON); + } + @Override protected boolean isRFC9457Message(final CloseableHttpResponse response) { Header[] headers = response.getHeaders(HttpHeaders.CONTENT_TYPE); diff --git a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java index 0374470c47..637ca130ce 100644 --- a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java +++ b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java @@ -354,6 +354,7 @@ protected void implGet(GetTask task) throws Exception { EntityGetter getter = new EntityGetter(task); HttpGet request = commonHeaders(new HttpGet(resolve(task))); + ApacheRFC9457Reporter.INSTANCE.prepareRequest(request); while (true) { try { if (resume) { @@ -378,6 +379,7 @@ protected void implGet(GetTask task) throws Exception { protected void implPut(PutTask task) throws Exception { PutTaskEntity entity = new PutTaskEntity(task); HttpPut request = commonHeaders(entity(new HttpPut(resolve(task)), entity)); + ApacheRFC9457Reporter.INSTANCE.prepareRequest(request); try { execute(request, null); } catch (HttpResponseException e) { @@ -507,7 +509,6 @@ private T commonHeaders(T request) { if (!state.isExpectContinue()) { request.removeHeaders(HttpHeaders.EXPECT); } - return request; } diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkRFC9457Reporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkRFC9457Reporter.java index be976875ed..221788644f 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkRFC9457Reporter.java +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkRFC9457Reporter.java @@ -20,18 +20,27 @@ import java.io.IOException; import java.io.InputStream; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.Optional; +import org.eclipse.aether.spi.connector.transport.http.HttpConstants; import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; import org.eclipse.aether.spi.connector.transport.http.RFC9457.RFC9457Reporter; -public class JdkRFC9457Reporter extends RFC9457Reporter, HttpTransporterException> { +public class JdkRFC9457Reporter + extends RFC9457Reporter, HttpTransporterException, HttpRequest.Builder> { public static final JdkRFC9457Reporter INSTANCE = new JdkRFC9457Reporter(); private JdkRFC9457Reporter() {} + @Override + public void prepareRequest(Builder requestBuilder) { + requestBuilder.header(HttpConstants.ACCEPT, CONTENT_TYPE_PROBLEM_DETAILS_JSON); + } + @Override protected boolean isRFC9457Message(final HttpResponse response) { Optional optionalContentType = response.headers().firstValue("Content-Type"); diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java index 499998920b..7dc0328988 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java @@ -276,6 +276,7 @@ protected void implGet(GetTask task) throws Exception { HttpRequest.Builder request = HttpRequest.newBuilder().uri(resolve(task)).GET(); headers.forEach(request::setHeader); + JdkRFC9457Reporter.INSTANCE.prepareRequest(request); if (resume) { long resumeOffset = task.getResumeOffset(); @@ -396,6 +397,7 @@ protected void implPut(PutTask task) throws Exception { request = request.expectContinue(expectContinue); } headers.forEach(request::setHeader); + JdkRFC9457Reporter.INSTANCE.prepareRequest(request); if (task.getDataLength() == 0L) { request.PUT(HttpRequest.BodyPublishers.noBody()); @@ -414,9 +416,15 @@ protected void implPut(PutTask task) throws Exception { } prepare(request); try { - HttpResponse response = send(request.build(), HttpResponse.BodyHandlers.discarding()); + HttpResponse response = send(request.build(), HttpResponse.BodyHandlers.ofInputStream()); if (response.statusCode() >= MULTIPLE_CHOICES) { - throw new HttpTransporterException(response.statusCode()); + try { + JdkRFC9457Reporter.INSTANCE.generateException(response, (statusCode, reasonPhrase) -> { + throw new HttpTransporterException(statusCode); + }); + } finally { + closeBody(response); + } } } catch (ConnectException e) { throw enhance(e); diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyRFC9457Reporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyRFC9457Reporter.java index 2c5e5ca398..eab38fb252 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyRFC9457Reporter.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyRFC9457Reporter.java @@ -28,14 +28,21 @@ import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException; import org.eclipse.aether.spi.connector.transport.http.RFC9457.RFC9457Reporter; import org.eclipse.jetty.client.InputStreamResponseListener; +import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.Response; import org.eclipse.jetty.http.HttpHeader; -public class JettyRFC9457Reporter extends RFC9457Reporter { +public class JettyRFC9457Reporter + extends RFC9457Reporter { public static final JettyRFC9457Reporter INSTANCE = new JettyRFC9457Reporter(); private JettyRFC9457Reporter() {} + @Override + public void prepareRequest(Request request) { + request.headers(h -> h.add(HttpHeader.ACCEPT.asString(), CONTENT_TYPE_PROBLEM_DETAILS_JSON)); + } + @Override protected boolean isRFC9457Message(final InputStreamResponseListener listener) { try { diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java index b1f37d12b1..57fe17855e 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -212,7 +212,7 @@ protected void implGet(GetTask task) throws Exception { if (preemptiveAuth) { mayApplyPreemptiveAuth(request); } - + JettyRFC9457Reporter.INSTANCE.prepareRequest(request); if (resume) { long resumeOffset = task.getResumeOffset(); long lastModified = @@ -309,14 +309,16 @@ private static Function headerGetter(Response response) { protected void implPut(PutTask task) throws Exception { Request request = client.newRequest(resolve(task)).method("PUT"); request.headers(m -> headers.forEach(m::add)); + JettyRFC9457Reporter.INSTANCE.prepareRequest(request); if (preemptiveAuth || preemptivePutAuth) { mayApplyPreemptiveAuth(request); } request.body(PutTaskRequestContent.from(task)); AtomicBoolean started = new AtomicBoolean(false); Response response; + InputStreamResponseListener listener = new InputStreamResponseListener(); try { - response = request.onRequestCommit(r -> { + request.onRequestCommit(r -> { if (task.getDataLength() == 0) { if (started.compareAndSet(false, true)) { try { @@ -342,7 +344,8 @@ protected void implPut(PutTask task) throws Exception { r.abort(e); } }) - .send(); + .send(listener); + response = listener.get(requestTimeout, TimeUnit.MILLISECONDS); } catch (ExecutionException e) { Throwable t = e.getCause(); if (t instanceof IOException ioex) { @@ -357,7 +360,11 @@ protected void implPut(PutTask task) throws Exception { throw new RuntimeException(t); } } + if (response.getStatus() >= MULTIPLE_CHOICES) { + JettyRFC9457Reporter.INSTANCE.generateException(listener, (statusCode, reasonPhrase) -> { + throw new HttpTransporterException(statusCode); + }); throw new HttpTransporterException(response.getStatus()); } } From e4521fa1164d0dc6c06875159568e1c180feddbe Mon Sep 17 00:00:00 2001 From: Konrad Windszus Date: Mon, 20 Apr 2026 17:57:51 +0200 Subject: [PATCH 2/4] Ignore content-type attributes for comparison Add IT against Cloudflare provided RFC 9457 endpoints (https://blog.cloudflare.com/rfc-9457-agent-error-pages/#how-to-use-it) --- .../transport/http/RFC9457/RFC9457Parser.java | 4 +- .../http/RFC9457/RFC9457Reporter.java | 8 + .../http/RFC9457/RFC9457ReporterTest.java | 56 +++++++ .../test/util/http/HttpTransporterTest.java | 158 +++++++++++++++--- 4 files changed, 202 insertions(+), 24 deletions(-) create mode 100644 maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457ReporterTest.java diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Parser.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Parser.java index 4e11a52af1..6d36f7fab5 100644 --- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Parser.java +++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Parser.java @@ -30,12 +30,12 @@ import com.google.gson.JsonParseException; public class RFC9457Parser { - private static Gson gson = new GsonBuilder() + private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(RFC9457Payload.class, new RFC9457PayloadAdapter()) .create(); public static RFC9457Payload parse(String data) { - return gson.fromJson(data, RFC9457Payload.class); + return GSON.fromJson(data, RFC9457Payload.class); } private static class RFC9457PayloadAdapter implements JsonDeserializer { diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java index fe349b0500..c7d928a071 100644 --- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java +++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java @@ -53,6 +53,14 @@ public abstract class RFC9457Reporter { public abstract void prepareRequest(R request); protected boolean hasRFC9457ContentType(String contentType) { + if (contentType == null) { + return false; + } + // strip off parameters + int idx = contentType.indexOf(';'); + if (idx > -1) { + contentType = contentType.substring(0, idx); + } return CONTENT_TYPE_PROBLEM_DETAILS_JSON.equals(contentType); } diff --git a/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457ReporterTest.java b/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457ReporterTest.java new file mode 100644 index 0000000000..95db2325a7 --- /dev/null +++ b/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457ReporterTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.spi.connector.transport.http.RFC9457; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RFC9457ReporterTest { + + @Test + void hasRFC9457ContentType() { + RFC9457Reporter reporter = new RFC9457Reporter() { + @Override + protected boolean isRFC9457Message(Object response) { + return false; + } + + @Override + protected int getStatusCode(Object response) { + return 0; + } + + @Override + protected String getReasonPhrase(Object response) { + return null; + } + + @Override + protected String getBody(Object response) { + return null; + } + + @Override + public void prepareRequest(Object request) {} + }; + assertTrue(reporter.hasRFC9457ContentType("application/problem+json")); + assertTrue(reporter.hasRFC9457ContentType("application/problem+json; charset=utf-8")); + } +} 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 8f745bb8c1..85a35cc7fa 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 @@ -18,6 +18,10 @@ */ package org.eclipse.aether.internal.test.util.http; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -31,6 +35,9 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -62,7 +69,9 @@ import org.eclipse.aether.transfer.NoTransporterException; import org.eclipse.aether.transfer.TransferCancelledException; import org.eclipse.aether.util.repository.AuthenticationBuilder; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -91,9 +100,11 @@ public abstract class HttpTransporterTest { protected static final Path TRUST_STORE_PATH = Paths.get("target/trustStore"); + protected static SSLContext defaultSslContext; + static { // Warning: "cross connected" with HttpServer! - System.setProperty( + /*System.setProperty( "javax.net.ssl.trustStore", KEY_STORE_PATH.toAbsolutePath().toString()); System.setProperty("javax.net.ssl.trustStorePassword", "server-pwd"); System.setProperty( @@ -101,10 +112,114 @@ public abstract class HttpTransporterTest { System.setProperty("javax.net.ssl.keyStorePassword", "client-pwd"); System.setProperty("javax.net.ssl.trustStoreType", "jks"); - System.setProperty("javax.net.ssl.keyStoreType", "jks"); + System.setProperty("javax.net.ssl.keyStoreType", "jks");*/ // System.setProperty("javax.net.debug", "all"); } + @BeforeAll + protected static void beforeAll() throws NoSuchAlgorithmException { + // initialize custom keystore and truststore files from classpath resources if not already present (e.g., from + // previous test run) + if (!Files.isRegularFile(KEY_STORE_PATH)) { + URL keyStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store"); + URL keyStoreSelfSignedUrl = + HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store-selfsigned"); + URL trustStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/client-store"); + + try { + try (InputStream keyStoreStream = keyStoreUrl.openStream(); + InputStream keyStoreSelfSignedStream = keyStoreSelfSignedUrl.openStream(); + InputStream trustStoreStream = trustStoreUrl.openStream()) { + Files.copy(keyStoreStream, KEY_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); + Files.copy( + keyStoreSelfSignedStream, KEY_STORE_SELF_SIGNED_PATH, StandardCopyOption.REPLACE_EXISTING); + Files.copy(trustStoreStream, TRUST_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + // override default SSLContext to include our custom keystore and truststore (which are "cross connected" with + // HttpServer) + defaultSslContext = SSLContext.getDefault(); + SSLContext.setDefault(createSSLContext()); + } + + @AfterAll + protected static void afterAll() { + if (defaultSslContext != null) { + SSLContext.setDefault(defaultSslContext); + } + } + + /** + * Creates an {@link SSLContext} that extends the default keystore and truststore with the entries + * from {@link #KEY_STORE_PATH} (password {@code "server-pwd"}) and {@link #TRUST_STORE_PATH} + * (password {@code "client-pwd"}). + * + * @return an {@link SSLContext} combining default and custom key/trust material + */ + protected static SSLContext createSSLContext() { + try { + // Load custom key store (KEY_STORE_PATH acts as truststore in "cross connected" setup) + KeyStore customTrustStore = KeyStore.getInstance("jks"); + try (InputStream is = Files.newInputStream(KEY_STORE_PATH)) { + customTrustStore.load(is, "server-pwd".toCharArray()); + } + + // Load custom trust store (TRUST_STORE_PATH acts as keystore in "cross connected" setup) + KeyStore customKeyStore = KeyStore.getInstance("jks"); + try (InputStream is = Files.newInputStream(TRUST_STORE_PATH)) { + customKeyStore.load(is, "client-pwd".toCharArray()); + } + + // Load default truststore and merge custom entries + KeyStore defaultTrustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + Path defaultTrustStorePath = Path.of(System.getProperty("java.home"), "lib", "security", "cacerts"); + if (Files.exists(defaultTrustStorePath)) { + try (InputStream is = Files.newInputStream(defaultTrustStorePath)) { + defaultTrustStore.load(is, "changeit".toCharArray()); + } + } else { + defaultTrustStore.load(null, null); + } + Enumeration aliases = customTrustStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + defaultTrustStore.setCertificateEntry("custom-trust-" + alias, customTrustStore.getCertificate(alias)); + } + + // Load default keystore and merge custom entries + KeyStore mergedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + mergedKeyStore.load(null, null); + Enumeration keyAliases = customKeyStore.aliases(); + while (keyAliases.hasMoreElements()) { + String alias = keyAliases.nextElement(); + if (customKeyStore.isKeyEntry(alias)) { + mergedKeyStore.setKeyEntry( + alias, + customKeyStore.getKey(alias, "client-pwd".toCharArray()), + "client-pwd".toCharArray(), + customKeyStore.getCertificateChain(alias)); + } else { + mergedKeyStore.setCertificateEntry(alias, customKeyStore.getCertificate(alias)); + } + } + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(defaultTrustStore); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(mergedKeyStore, "client-pwd".toCharArray()); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + return sslContext; + } catch (Exception e) { + throw new RuntimeException("Failed to create SSLContext", e); + } + } + private final Supplier transporterFactorySupplier; protected DefaultRepositorySystemSession session; @@ -125,26 +240,6 @@ public abstract class HttpTransporterTest { protected HttpTransporterTest(Supplier transporterFactorySupplier) { this.transporterFactorySupplier = requireNonNull(transporterFactorySupplier); - - if (!Files.isRegularFile(KEY_STORE_PATH)) { - URL keyStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store"); - URL keyStoreSelfSignedUrl = - HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store-selfsigned"); - URL trustStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/client-store"); - - try { - try (InputStream keyStoreStream = keyStoreUrl.openStream(); - InputStream keyStoreSelfSignedStream = keyStoreSelfSignedUrl.openStream(); - InputStream trustStoreStream = trustStoreUrl.openStream()) { - Files.copy(keyStoreStream, KEY_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); - Files.copy( - keyStoreSelfSignedStream, KEY_STORE_SELF_SIGNED_PATH, StandardCopyOption.REPLACE_EXISTING); - Files.copy(trustStoreStream, TRUST_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } } protected static ChecksumExtractor standardChecksumExtractor() { @@ -470,6 +565,25 @@ protected void testGet_AcceptsRfc9457() throws Exception { "Expected Accept header to contain application/problem+json, but was: " + accept); } + @Test + protected void testGet_ParseRfc9457() throws Exception { + // use Maven Central (Cloudflare CDN) as endpoints that return RFC 9457 responses + newTransporter("https://repo.maven.apache.org"); + try { + // https://blog.cloudflare.com/rfc-9457-agent-error-pages/#how-to-use-it + GetTask task = new GetTask(URI.create("cdn-cgi/error/1020")); + transporter.get(task); + fail("Should have throw HttpRFC9457Exception"); + } catch (HttpRFC9457Exception e) { + // Expected exception, verify the content of the RFC 9457 message. + assertEquals(403, e.getStatusCode()); + assertEquals("Error 1020: Access denied", e.getPayload().getTitle()); + assertEquals( + "The request was blocked by a Cloudflare firewall rule configured by the site owner.", + e.getPayload().getDetail()); + } + } + /** * Provides compression algorithms supported by the transporter implementation. * This should be the string value passed in the {@code Accept-Encoding} header. From 0ec3573cd17dada2879650941745279f2152439f Mon Sep 17 00:00:00 2001 From: Konrad Windszus Date: Mon, 20 Apr 2026 18:07:05 +0200 Subject: [PATCH 3/4] remove commented (now irrelevant) Java properties from static initializer --- .../test/util/http/HttpTransporterTest.java | 43 +++++++------------ 1 file changed, 15 insertions(+), 28 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 85a35cc7fa..00bbbc2425 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 @@ -103,41 +103,28 @@ public abstract class HttpTransporterTest { protected static SSLContext defaultSslContext; static { - // Warning: "cross connected" with HttpServer! - /*System.setProperty( - "javax.net.ssl.trustStore", KEY_STORE_PATH.toAbsolutePath().toString()); - System.setProperty("javax.net.ssl.trustStorePassword", "server-pwd"); - System.setProperty( - "javax.net.ssl.keyStore", TRUST_STORE_PATH.toAbsolutePath().toString()); - System.setProperty("javax.net.ssl.keyStorePassword", "client-pwd"); - - System.setProperty("javax.net.ssl.trustStoreType", "jks"); - System.setProperty("javax.net.ssl.keyStoreType", "jks");*/ + // uncomment to enable SSL debugging for easier troubleshooting of SSL related test failures // System.setProperty("javax.net.debug", "all"); } @BeforeAll protected static void beforeAll() throws NoSuchAlgorithmException { - // initialize custom keystore and truststore files from classpath resources if not already present (e.g., from - // previous test run) - if (!Files.isRegularFile(KEY_STORE_PATH)) { - URL keyStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store"); - URL keyStoreSelfSignedUrl = - HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store-selfsigned"); - URL trustStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/client-store"); + // populate custom keystore and truststore + URL keyStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store"); + URL keyStoreSelfSignedUrl = + HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store-selfsigned"); + URL trustStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/client-store"); - try { - try (InputStream keyStoreStream = keyStoreUrl.openStream(); - InputStream keyStoreSelfSignedStream = keyStoreSelfSignedUrl.openStream(); - InputStream trustStoreStream = trustStoreUrl.openStream()) { - Files.copy(keyStoreStream, KEY_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); - Files.copy( - keyStoreSelfSignedStream, KEY_STORE_SELF_SIGNED_PATH, StandardCopyOption.REPLACE_EXISTING); - Files.copy(trustStoreStream, TRUST_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); - } - } catch (IOException e) { - throw new UncheckedIOException(e); + try { + try (InputStream keyStoreStream = keyStoreUrl.openStream(); + InputStream keyStoreSelfSignedStream = keyStoreSelfSignedUrl.openStream(); + InputStream trustStoreStream = trustStoreUrl.openStream()) { + Files.copy(keyStoreStream, KEY_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); + Files.copy(keyStoreSelfSignedStream, KEY_STORE_SELF_SIGNED_PATH, StandardCopyOption.REPLACE_EXISTING); + Files.copy(trustStoreStream, TRUST_STORE_PATH, StandardCopyOption.REPLACE_EXISTING); } + } catch (IOException e) { + throw new UncheckedIOException(e); } // override default SSLContext to include our custom keystore and truststore (which are "cross connected" with // HttpServer) From de05b2fa0da023b595bfd022ee8184ebb79f38ec Mon Sep 17 00:00:00 2001 From: Konrad Windszus Date: Mon, 20 Apr 2026 20:05:19 +0200 Subject: [PATCH 4/4] Only emit non-null members of RFC 9457 payload Fix javadoc --- .../http/RFC9457/RFC9457Payload.java | 25 ++++++++++++++----- .../http/RFC9457/RFC9457Reporter.java | 5 ++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Payload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Payload.java index cf60a39603..501986ed72 100644 --- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Payload.java +++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Payload.java @@ -68,11 +68,24 @@ public URI getInstance() { @Override public String toString() { - return "RFC9457Payload {" + "type=" - + type + ", status=" - + status + ", title='" - + title + ", detail='" - + detail + ", instance=" - + instance + '}'; + StringBuilder builder = new StringBuilder(); + builder.append("RFC9457Payload ["); + if (type != null) { + builder.append("type=").append(type).append(", "); + } + if (status != null) { + builder.append("status=").append(status).append(", "); + } + if (title != null) { + builder.append("title=").append(title).append(", "); + } + if (detail != null) { + builder.append("detail=").append(detail).append(", "); + } + if (instance != null) { + builder.append("instance=").append(instance); + } + builder.append("]"); + return builder.toString(); } } diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java index c7d928a071..6c81e4f553 100644 --- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java +++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java @@ -24,14 +24,15 @@ * A reporter for RFC 9457 messages. * RFC 9457 is a standard for reporting problems in HTTP responses as a JSON object. * There are members specified in the RFC but none of those appear to be required, - * @see rfc9457 section 3.7 + * see rfc9457 section 3.7 * Given the JSON fields are not mandatory, this reporter simply extracts the body of the * response without validation. - * A RFC 9457 message is detected by the content type "application/problem+json". + * A RFC 9457 message is detected by the content type {@value #CONTENT_TYPE_PROBLEM_DETAILS_JSON} in the response header. * * @param The type of the response. * @param The base exception type to throw if the response is not a RFC9457 message. * @param The type of the request or request builder (which allows to modify headers) + * @see RFC 9457 */ public abstract class RFC9457Reporter { public static final String CONTENT_TYPE_PROBLEM_DETAILS_JSON = "application/problem+json";