Skip to content

Commit 24f8135

Browse files
committed
Backport of MRESOLVER-600 to 1.9.x release
1 parent cb69b1d commit 24f8135

File tree

9 files changed

+450
-1
lines changed

9 files changed

+450
-1
lines changed

maven-resolver-transport-http/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
<groupId>org.apache.maven.resolver</groupId>
4949
<artifactId>maven-resolver-util</artifactId>
5050
</dependency>
51+
<dependency>
52+
<groupId>com.google.code.gson</groupId>
53+
<artifactId>gson</artifactId>
54+
<version>2.13.1</version>
55+
</dependency>
5156
<dependency>
5257
<groupId>org.apache.httpcomponents</groupId>
5358
<artifactId>httpclient</artifactId>

maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595
import org.eclipse.aether.spi.connector.transport.TransportTask;
9696
import org.eclipse.aether.transfer.NoTransporterException;
9797
import org.eclipse.aether.transfer.TransferCancelledException;
98+
import org.eclipse.aether.transport.http.RFC9457.HttpRFC9457Exception;
99+
import org.eclipse.aether.transport.http.RFC9457.RFC9457Reporter;
98100
import org.eclipse.aether.util.ConfigUtils;
99101
import org.eclipse.aether.util.FileUtils;
100102
import org.slf4j.Logger;
@@ -624,9 +626,12 @@ private <T extends HttpUriRequest> void resume(T request, GetTask task) {
624626
}
625627
}
626628

627-
private void handleStatus(CloseableHttpResponse response) throws HttpResponseException {
629+
private void handleStatus(CloseableHttpResponse response) throws HttpResponseException, HttpRFC9457Exception {
628630
int status = response.getStatusLine().getStatusCode();
629631
if (status >= 300) {
632+
if (RFC9457Reporter.INSTANCE.isRFC9457Message(response)) {
633+
RFC9457Reporter.INSTANCE.generateException(response);
634+
}
630635
throw new HttpResponseException(status, response.getStatusLine().getReasonPhrase() + " (" + status + ")");
631636
}
632637
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.eclipse.aether.transport.http.RFC9457;
20+
21+
import java.io.IOException;
22+
23+
/**
24+
* Exception thrown by {@link HttpTransporter} in case of errors.
25+
*
26+
* @since 1.9.24
27+
*/
28+
public class HttpRFC9457Exception extends IOException {
29+
private final int statusCode;
30+
31+
private final String reasonPhrase;
32+
33+
private final RFC9457Payload payload;
34+
35+
public HttpRFC9457Exception(int statusCode, String reasonPhrase, RFC9457Payload payload) {
36+
super(payload.toString());
37+
this.statusCode = statusCode;
38+
this.reasonPhrase = reasonPhrase;
39+
this.payload = payload;
40+
}
41+
42+
public int getStatusCode() {
43+
return statusCode;
44+
}
45+
46+
public String getReasonPhrase() {
47+
return reasonPhrase;
48+
}
49+
50+
public RFC9457Payload getPayload() {
51+
return payload;
52+
}
53+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.eclipse.aether.transport.http.RFC9457;
20+
21+
import java.lang.reflect.Type;
22+
import java.net.URI;
23+
24+
import com.google.gson.Gson;
25+
import com.google.gson.GsonBuilder;
26+
import com.google.gson.JsonDeserializationContext;
27+
import com.google.gson.JsonDeserializer;
28+
import com.google.gson.JsonElement;
29+
import com.google.gson.JsonObject;
30+
import com.google.gson.JsonParseException;
31+
32+
public class RFC9457Parser {
33+
private static final Gson GSON = new GsonBuilder()
34+
.registerTypeAdapter(RFC9457Payload.class, new RFC9457PayloadAdapter())
35+
.create();
36+
37+
public static RFC9457Payload parse(String data) {
38+
return GSON.fromJson(data, RFC9457Payload.class);
39+
}
40+
41+
private static class RFC9457PayloadAdapter implements JsonDeserializer<RFC9457Payload> {
42+
@Override
43+
public RFC9457Payload deserialize(
44+
final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
45+
throws JsonParseException {
46+
JsonObject asJsonObject = json.getAsJsonObject();
47+
URI type = parseNullableURI(asJsonObject, "type", "about:blank");
48+
Integer status = parseStatus(asJsonObject);
49+
String title = parseNullableString(asJsonObject, "title");
50+
String detail = parseNullableString(asJsonObject, "detail");
51+
URI instance = parseNullableURI(asJsonObject, "instance", null);
52+
return new RFC9457Payload(type, status, title, detail, instance);
53+
}
54+
}
55+
56+
private static Integer parseStatus(JsonObject jsonObject) {
57+
return jsonObject.get("status") == null || jsonObject.get("status").isJsonNull()
58+
? null
59+
: jsonObject.get("status").getAsInt();
60+
}
61+
62+
private static String parseNullableString(JsonObject jsonObject, String key) {
63+
return jsonObject.get(key) == null || jsonObject.get(key).isJsonNull()
64+
? null
65+
: jsonObject.get(key).getAsString();
66+
}
67+
68+
private static URI parseNullableURI(JsonObject jsonObject, String key, String defaultValue) {
69+
return !jsonObject.has(key) || jsonObject.get(key).isJsonNull()
70+
? defaultValue != null ? URI.create(defaultValue) : null
71+
: URI.create(jsonObject.get(key).getAsString());
72+
}
73+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.eclipse.aether.transport.http.RFC9457;
20+
21+
import java.net.URI;
22+
23+
public class RFC9457Payload {
24+
public static final RFC9457Payload INSTANCE = new RFC9457Payload();
25+
26+
private final URI type;
27+
28+
private final Integer status;
29+
30+
private final String title;
31+
32+
private final String detail;
33+
34+
private final URI instance;
35+
36+
private RFC9457Payload() {
37+
this(null, null, null, null, null);
38+
}
39+
40+
public RFC9457Payload(
41+
final URI type, final Integer status, final String title, final String detail, final URI instance) {
42+
this.type = type;
43+
this.status = status;
44+
this.title = title;
45+
this.detail = detail;
46+
this.instance = instance;
47+
}
48+
49+
public URI getType() {
50+
return type;
51+
}
52+
53+
public Integer getStatus() {
54+
return status;
55+
}
56+
57+
public String getTitle() {
58+
return title;
59+
}
60+
61+
public String getDetail() {
62+
return detail;
63+
}
64+
65+
public URI getInstance() {
66+
return instance;
67+
}
68+
69+
@Override
70+
public String toString() {
71+
return "RFC9457Payload {" + "type="
72+
+ type + ", status="
73+
+ status + ", title='"
74+
+ title + ", detail='"
75+
+ detail + ", instance="
76+
+ instance + '}';
77+
}
78+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.eclipse.aether.transport.http.RFC9457;
20+
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
24+
import org.apache.http.Header;
25+
import org.apache.http.HttpHeaders;
26+
import org.apache.http.client.methods.CloseableHttpResponse;
27+
import org.apache.http.util.EntityUtils;
28+
29+
public class RFC9457Reporter {
30+
public static final RFC9457Reporter INSTANCE = new RFC9457Reporter();
31+
32+
public boolean isRFC9457Message(CloseableHttpResponse response) {
33+
Header[] headers = response.getHeaders(HttpHeaders.CONTENT_TYPE);
34+
if (headers.length > 0) {
35+
String contentType = headers[0].getValue();
36+
return hasRFC9457ContentType(contentType);
37+
}
38+
return false;
39+
}
40+
41+
public void generateException(CloseableHttpResponse response) throws HttpRFC9457Exception {
42+
int statusCode = getStatusCode(response);
43+
String reasonPhrase = getReasonPhrase(response);
44+
45+
String body;
46+
try {
47+
body = getBody(response);
48+
} catch (IOException ignore) {
49+
// No body found but it is representing a RFC 9457 message due to the content type.
50+
throw new HttpRFC9457Exception(statusCode, reasonPhrase, RFC9457Payload.INSTANCE);
51+
}
52+
53+
if (body != null && !body.isEmpty()) {
54+
RFC9457Payload rfc9457Payload = RFC9457Parser.parse(body);
55+
throw new HttpRFC9457Exception(statusCode, reasonPhrase, rfc9457Payload);
56+
}
57+
throw new HttpRFC9457Exception(statusCode, reasonPhrase, RFC9457Payload.INSTANCE);
58+
}
59+
60+
private String getBody(final CloseableHttpResponse response) throws IOException {
61+
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
62+
}
63+
64+
private int getStatusCode(final CloseableHttpResponse response) {
65+
return response.getStatusLine().getStatusCode();
66+
}
67+
68+
private String getReasonPhrase(final CloseableHttpResponse response) {
69+
String reasonPhrase = response.getStatusLine().getReasonPhrase();
70+
if (reasonPhrase == null || reasonPhrase.isEmpty()) {
71+
return "";
72+
}
73+
int statusCode = getStatusCode(response);
74+
return reasonPhrase + " (" + statusCode + ")";
75+
}
76+
77+
private boolean hasRFC9457ContentType(String contentType) {
78+
return "application/problem+json".equals(contentType);
79+
}
80+
}

maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpServer.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import java.io.FileInputStream;
2626
import java.io.FileOutputStream;
2727
import java.io.IOException;
28+
import java.io.OutputStream;
29+
import java.net.URI;
30+
import java.nio.charset.StandardCharsets;
2831
import java.util.ArrayList;
2932
import java.util.Collections;
3033
import java.util.Enumeration;
@@ -35,6 +38,8 @@
3538
import java.util.regex.Matcher;
3639
import java.util.regex.Pattern;
3740

41+
import com.google.gson.Gson;
42+
import org.eclipse.aether.transport.http.RFC9457.RFC9457Payload;
3843
import org.eclipse.aether.util.ChecksumUtils;
3944
import org.eclipse.jetty.http.HttpHeader;
4045
import org.eclipse.jetty.http.HttpMethod;
@@ -229,6 +234,7 @@ public HttpServer start() throws Exception {
229234
handlers.addHandler(new AuthHandler());
230235
handlers.addHandler(new RedirectHandler());
231236
handlers.addHandler(new RepoHandler());
237+
handlers.addHandler(new RFC9457Handler());
232238

233239
server = new Server();
234240
httpConnector = new ServerConnector(server);
@@ -478,6 +484,46 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS
478484
}
479485
}
480486

487+
private class RFC9457Handler extends AbstractHandler {
488+
@Override
489+
public void handle(
490+
final String target,
491+
final Request req,
492+
final HttpServletRequest request,
493+
final HttpServletResponse response)
494+
throws IOException {
495+
String path = req.getPathInfo().substring(1);
496+
497+
if (!path.startsWith("rfc9457/")) {
498+
return;
499+
}
500+
req.setHandled(true);
501+
502+
if (HttpMethod.GET.is(req.getMethod())) {
503+
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
504+
response.setHeader(HttpHeader.CONTENT_TYPE.asString(), "application/problem+json");
505+
RFC9457Payload rfc9457Payload;
506+
if (path.endsWith("missing_fields.txt")) {
507+
rfc9457Payload = new RFC9457Payload(null, null, null, null, null);
508+
} else {
509+
rfc9457Payload = new RFC9457Payload(
510+
URI.create("https://example.com/probs/out-of-credit"),
511+
HttpServletResponse.SC_FORBIDDEN,
512+
"You do not have enough credit.",
513+
"Your current balance is 30, but that costs 50.",
514+
URI.create("/account/12345/msgs/abc"));
515+
}
516+
try (OutputStream outputStream = response.getOutputStream()) {
517+
outputStream.write(buildRFC9457Message(rfc9457Payload).getBytes(StandardCharsets.UTF_8));
518+
}
519+
}
520+
}
521+
}
522+
523+
private String buildRFC9457Message(RFC9457Payload payload) {
524+
return new Gson().toJson(payload, RFC9457Payload.class);
525+
}
526+
481527
static boolean checkBasicAuth(String credentials, String username, String password) {
482528
if (credentials != null) {
483529
int space = credentials.indexOf(' ');

0 commit comments

Comments
 (0)