From 077113e059af34a6377139274c1829eefd3616d3 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Tue, 7 Feb 2023 21:51:24 +0100 Subject: [PATCH] Added ViaRequest interceptor. --- .../hc/core5/http/protocol/ViaRequest.java | 127 ++++++++++++++++++ .../core5/http/protocol/ViaRequestTest.java | 117 ++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 httpcore5/src/main/java/org/apache/hc/core5/http/protocol/ViaRequest.java create mode 100644 httpcore5/src/test/java/org/apache/hc/core5/http/protocol/ViaRequestTest.java diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/ViaRequest.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/ViaRequest.java new file mode 100644 index 0000000000..ea95bed15a --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/ViaRequest.java @@ -0,0 +1,127 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http.protocol; + +import java.io.IOException; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.ProtocolVersion; +import org.apache.hc.core5.net.URIAuthority; +import org.apache.hc.core5.util.Args; + + +/** + * An Apache HttpComponents {@link HttpRequestInterceptor} to add the {@link HttpHeaders#VIA} HTTP + * header to requests. + *

The {@link HttpHeaders#VIA} header is used to indicate intermediate protocols and recipients + * between the user agent and the server (on requests) or between the origin server and the client + * (on responses). It can be used for tracking message forwards, avoiding request loops, and + * identifying the protocol capabilities of senders along the request/response chain. Each member of + * the {@link HttpHeaders#VIA} header field value represents a proxy or gateway that has forwarded + * the message. + *

A proxy MUST send an appropriate {@link HttpHeaders#VIA} header field, as described + * in + * the HTTP specification, in each message that it forwards. An HTTP-to-HTTP gateway MUST + * send an appropriate {@link HttpHeaders#VIA} header field in each inbound request message and + * MAY send a {@link HttpHeaders#VIA} header field in forwarded response messages. + *

This interceptor ensures that the {@link HttpHeaders#VIA} header is added to the request + * only + * if it has not been added previously, as per the HTTP specification. Additionally, it updates the + * values in the {@link HttpHeaders#VIA} header correctly in case of multiple intermediate protocols + * or recipients, by appending its own information about how the message was received to the end of + * the header field value. + * + * @since 5.3 + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class ViaRequest implements HttpRequestInterceptor { + + + /** + * Singleton instance. + */ + public static final HttpRequestInterceptor INSTANCE = new ViaRequest(); + + /** + * Constructs a new {@code ViaRequest}. + */ + public ViaRequest() { + } + + /** + * Adds the HTTP {@link HttpHeaders#VIA} header to the request if it does not already exist. + * + *

This method ensures that the version of the request is HTTP/1.1 or higher, and adds the + * Via header in the format {@code }, where {@code } is the protocol name, + * {@code } is the major and minor version of the request, and {@code } is the value of the Host header. + * + *

In case the {@link HttpHeaders#VIA} header already exists, this method updates its value by appending + * the new protocol information in the same format. + * + *

If the version of the request is lower than {@code HTTP/1.1} or the request authority not being specified, + * this method throws a {@link ProtocolException}. + * + * @param request the request object to modify + * @param entity the entity for the request, may be {@code null} + * @param context the context for the request + * @throws ProtocolException if there was a protocol error, such as the request version being lower than {@code HTTP/1.1}, + * or the request authority not being specified + * @throws IOException if there was an I/O error + */ + @Override + public void process(final HttpRequest request, final EntityDetails entity, final HttpContext context) throws ProtocolException, IOException { + Args.notNull(request, "HTTP request"); + Args.notNull(context, "HTTP context"); + final ProtocolVersion ver = context.getProtocolVersion() != null ? context.getProtocolVersion() : HttpVersion.HTTP_1_1; + + final URIAuthority authority = request.getAuthority(); + if (authority == null) { + throw new ProtocolException("Request authority not specified"); + } + + + if (!ver.greaterEquals(HttpVersion.HTTP_1_1)) { + throw new ProtocolException("Invalid protocol version: %s", ver); + } + if (!request.containsHeader(HttpHeaders.VIA)) { + String viaHeaderValue = ver.getProtocol() + " " + ver.getMajor() + "." + ver.getMinor() + " " + authority.getHostName(); + final int port = authority.getPort(); + if (port != -1) { + viaHeaderValue += ":" + port; + } + request.addHeader(HttpHeaders.VIA, viaHeaderValue); + } + } +} + diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/ViaRequestTest.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/ViaRequestTest.java new file mode 100644 index 0000000000..99cf74d52e --- /dev/null +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/ViaRequestTest.java @@ -0,0 +1,117 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http.protocol; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.message.BasicClassicHttpRequest; +import org.apache.hc.core5.net.URIAuthority; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +public class ViaRequestTest { + + @Test + public void testViaRequestGenerated() throws Exception { + + final HttpContext context = new BasicHttpContext(null); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + request.setAuthority(new URIAuthority("somehost", 8888)); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + final ViaRequest interceptor = new ViaRequest(); + interceptor.process(request, request.getEntity(), context); + + assertEquals(request.getHeader(HttpHeaders.VIA).getName(), HttpHeaders.VIA); + assertNotNull(request.getHeader(HttpHeaders.VIA)); + assertEquals(request.getHeader(HttpHeaders.VIA).getValue(), "HTTP 1.1 somehost:8888"); + + } + + @Test + public void testViaRequestGeneratedWithOutPort() throws Exception { + + final HttpContext context = new BasicHttpContext(null); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + request.setAuthority(new URIAuthority("somehost")); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + final ViaRequest interceptor = new ViaRequest(); + interceptor.process(request, request.getEntity(), context); + + assertEquals(request.getHeader(HttpHeaders.VIA).getName(), HttpHeaders.VIA); + assertNotNull(request.getHeader(HttpHeaders.VIA)); + assertEquals(request.getHeader(HttpHeaders.VIA).getValue(), "HTTP 1.1 somehost"); + } + + + @Test + public void testViaRequestInvalidHttpVersion() { + final HttpContext context = new BasicHttpContext(null); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_0_9); + request.setAuthority(new URIAuthority("somehost", 8888)); + + final HttpRequestInterceptor interceptor = ViaRequest.INSTANCE; + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + public void testViaRequestInvalidAuthority() { + final HttpContext context = new BasicHttpContext(null); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + final HttpRequestInterceptor interceptor = ViaRequest.INSTANCE; + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + public void testViaRequestNotCreatedAlreadyAdded() throws Exception { + + final HttpContext context = new BasicHttpContext(null); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + request.setAuthority(new URIAuthority("somehost", 8888)); + final String viaValue = "HTTP 1.1 host:8888"; + request.setHeader(HttpHeaders.VIA, viaValue); + final ViaRequest interceptor = new ViaRequest(); + interceptor.process(request, request.getEntity(), context); + + assertEquals(request.getHeader(HttpHeaders.VIA).getName(), HttpHeaders.VIA); + assertNotNull(request.getHeader(HttpHeaders.VIA)); + assertEquals(request.getHeader(HttpHeaders.VIA).getValue(), viaValue); + + } +} \ No newline at end of file