From 9f39787a14c50192a3a61c9391750e670cc6c300 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Wed, 16 Oct 2024 21:32:32 +0200 Subject: [PATCH] Add HTTP/2 and HTTP/1.1 TE header validation interceptors Implemented `H2RequestTE` and `RequestTE` interceptors to ensure proper validation of the `TE` header for both HTTP/2 and HTTP/1.1 requests. --- .../hc/core5/http2/impl/H2Processors.java | 2 + .../hc/core5/http2/protocol/H2RequestTE.java | 130 ++++++++++++ .../core5/http2/protocol/TestH2RequestTE.java | 101 +++++++++ .../classic/ClassicIntegrationTest.java | 4 +- .../hc/core5/http/impl/HttpProcessors.java | 2 + .../hc/core5/http/protocol/RequestTE.java | 190 +++++++++++++++++ .../hc/core5/http/protocol/TestRequestTE.java | 199 ++++++++++++++++++ .../protocol/TestStandardInterceptors.java | 19 ++ 8 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestTE.java create mode 100644 httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestTE.java create mode 100644 httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java create mode 100644 httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/H2Processors.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/H2Processors.java index b6cf55270..6d3227f08 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/H2Processors.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/H2Processors.java @@ -38,6 +38,7 @@ import org.apache.hc.core5.http2.protocol.H2RequestConformance; import org.apache.hc.core5.http2.protocol.H2RequestConnControl; import org.apache.hc.core5.http2.protocol.H2RequestContent; +import org.apache.hc.core5.http2.protocol.H2RequestTE; import org.apache.hc.core5.http2.protocol.H2RequestTargetHost; import org.apache.hc.core5.http2.protocol.H2RequestValidateHost; import org.apache.hc.core5.http2.protocol.H2ResponseConformance; @@ -86,6 +87,7 @@ public static HttpProcessorBuilder customClient(final String agentInfo) { H2RequestTargetHost.INSTANCE, H2RequestContent.INSTANCE, H2RequestConnControl.INSTANCE, + H2RequestTE.INSTANCE, new RequestUserAgent(!TextUtils.isBlank(agentInfo) ? agentInfo : VersionInfo.getSoftwareInfo(SOFTWARE, "org.apache.hc.core5", HttpProcessors.class)), RequestExpectContinue.INSTANCE); diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestTE.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestTE.java new file mode 100644 index 000000000..11c6248f6 --- /dev/null +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestTE.java @@ -0,0 +1,130 @@ +/* + * ==================================================================== + * 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.http2.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.Header; +import org.apache.hc.core5.http.HttpException; +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.ProtocolException; +import org.apache.hc.core5.http.ProtocolVersion; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.protocol.RequestTE; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.Tokenizer; + +/** + * HTTP request interceptor responsible for validating the {@code TE} header in HTTP/2 requests. + *

+ * The {@code TE} header in HTTP/2 is restricted to containing only the {@code trailers} directive. + * This interceptor ensures compliance by validating that the {@code TE} header does not include + * any other directives or transfer codings. If any value other than {@code trailers} is present, + * a {@link ProtocolException} is thrown. + *

+ * For HTTP/1.x requests, this interceptor falls back to the behavior of {@link RequestTE}, + * where other transfer codings may be allowed. + * + * @since 5.5 + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class H2RequestTE extends RequestTE { + + /** + * Singleton instance of the {@code H2RequestTE} interceptor. + */ + public static final HttpRequestInterceptor INSTANCE = new H2RequestTE(); + + /** + * Processes the {@code TE} header for HTTP/2 compliance. + *

+ * If the protocol version is HTTP/2, this method checks if the {@code TE} header contains + * only the {@code trailers} directive. If any other value is found, it throws a {@link ProtocolException}. + * For HTTP/1.x requests, it delegates processing to the parent {@link RequestTE} class. + * + * @param request the HTTP request to validate + * @param entity the entity associated with the request (may be {@code null}) + * @param context the execution context for the request + * @throws HttpException if the {@code TE} header contains invalid values for HTTP/2 + * @throws IOException in case of an I/O error + */ + @Override + public void process(final HttpRequest request, final EntityDetails entity, final HttpContext context) + throws HttpException, IOException { + + Args.notNull(context, "HTTP context"); + final ProtocolVersion ver = context.getProtocolVersion(); + + // If the protocol version is HTTP/2 + if (ver.getMajor() >= 2) { + // Check if TE header is present + final Header teHeader = request.getFirstHeader(HttpHeaders.TE); + if (teHeader != null) { + final String teValue = teHeader.getValue(); + validateTEHeaderForHttp2(teValue); + } + } else { + // For HTTP/1.x, fall back to the parent TE logic + super.process(request, entity, context); + } + } + + /** + * Validates that the {@code TE} header for HTTP/2 contains only the {@code trailers} directive. + *

+ * This method parses the {@code TE} header and ensures that only the {@code trailers} directive is present. + * If any other value is found, a {@link ProtocolException} is thrown. + * + * @param teValue the value of the {@code TE} header to validate + * @throws HttpException if the {@code TE} header contains invalid values for HTTP/2 + */ + private void validateTEHeaderForHttp2(final String teValue) throws HttpException { + final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, teValue.length()); + + while (!cursor.atEnd()) { + final String member = Tokenizer.INSTANCE.parseToken(teValue, cursor, DELIMITER).trim(); + + // Only 'trailers' is allowed in HTTP/2 + if (!"trailers".equalsIgnoreCase(member)) { + throw new ProtocolException("In HTTP/2, the TE header must only contain 'trailers'. Found: " + member); + } + + // Skip any whitespace and delimiter before moving to the next value + if (!cursor.atEnd()) { + Tokenizer.INSTANCE.skipWhiteSpace(teValue, cursor); + cursor.updatePos(cursor.getPos() + 1); + } + } + } +} + diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestTE.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestTE.java new file mode 100644 index 000000000..15c557e84 --- /dev/null +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestTE.java @@ -0,0 +1,101 @@ +/* + * ==================================================================== + * 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.http2.protocol; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +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.http.protocol.HttpCoreContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class TestH2RequestTE { + + private HttpCoreContext context; + + @BeforeEach + void setUp() { + context = HttpCoreContext.create(); + context.setProtocolVersion(HttpVersion.HTTP_2); // Set the protocol to HTTP/2 for tests + } + + @Test + void testValidTEHeaderForHttp2() throws Exception { + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + request.setHeader(HttpHeaders.TE, "trailers"); + + final HttpRequestInterceptor interceptor = new H2RequestTE(); + interceptor.process(request, request.getEntity(), context); + + // Assertions + assertNotNull(request.getHeader(HttpHeaders.TE)); + assertEquals("trailers", request.getHeader(HttpHeaders.TE).getValue()); + } + + @Test + void testInvalidTEHeaderForHttp2() { + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + request.setHeader(HttpHeaders.TE, "trailers, deflate;q=0.5"); + + final H2RequestTE interceptor = new H2RequestTE(); + // Expect a ProtocolException due to invalid value in the TE header for HTTP/2 + assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testTEHeaderWithoutTrailersForHttp2() { + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + request.setHeader(HttpHeaders.TE, "gzip;q=0.5"); + + final H2RequestTE interceptor = new H2RequestTE(); + // Expect a ProtocolException because 'trailers' is not present + assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testNoTEHeaderForHttp2() throws Exception { + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + + final H2RequestTE interceptor = new H2RequestTE(); + interceptor.process(request, request.getEntity(), context); + + // Ensure that no TE header is present, which is valid + assertNull(request.getHeader(HttpHeaders.TE)); + } + +} \ No newline at end of file diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java index e92582efd..22f27425c 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java @@ -65,6 +65,7 @@ import org.apache.hc.core5.http.protocol.RequestConnControl; import org.apache.hc.core5.http.protocol.RequestContent; import org.apache.hc.core5.http.protocol.RequestExpectContinue; +import org.apache.hc.core5.http.protocol.RequestTE; import org.apache.hc.core5.http.protocol.RequestTargetHost; import org.apache.hc.core5.http.protocol.RequestUserAgent; import org.apache.hc.core5.testing.extension.classic.ClassicTestResources; @@ -638,7 +639,8 @@ void testHttpPostNoContentLength() throws Exception { RequestTargetHost.INSTANCE, RequestConnControl.INSTANCE, RequestUserAgent.INSTANCE, - RequestExpectContinue.INSTANCE)); + RequestExpectContinue.INSTANCE, + RequestTE.INSTANCE)); client.start(); final HttpCoreContext context = HttpCoreContext.create(); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java index 3da9d11dd..6753cd47d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java @@ -32,6 +32,7 @@ import org.apache.hc.core5.http.protocol.RequestConnControl; import org.apache.hc.core5.http.protocol.RequestContent; import org.apache.hc.core5.http.protocol.RequestExpectContinue; +import org.apache.hc.core5.http.protocol.RequestTE; import org.apache.hc.core5.http.protocol.RequestTargetHost; import org.apache.hc.core5.http.protocol.RequestUserAgent; import org.apache.hc.core5.http.protocol.RequestValidateHost; @@ -69,6 +70,7 @@ public static HttpProcessorBuilder customServer(final String serverInfo) { ResponseContent.INSTANCE, ResponseConnControl.INSTANCE) .addAll( + RequestTE.INSTANCE, RequestValidateHost.INSTANCE, RequestConformance.INSTANCE); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java new file mode 100644 index 000000000..bf2d542a6 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java @@ -0,0 +1,190 @@ +/* + * ==================================================================== + * 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.Header; +import org.apache.hc.core5.http.HeaderElements; +import org.apache.hc.core5.http.HttpException; +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.ProtocolException; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.Tokenizer; + +/** + * HTTP request interceptor responsible for validating and processing the {@code TE} header field in HTTP/1.1 requests. + *

+ * The {@code TE} header is used to indicate transfer codings the client is willing to accept and, in some cases, whether + * the client is willing to accept trailer fields. This interceptor ensures that the {@code TE} header does not include + * the {@code chunked} transfer coding and validates the presence of the {@code Connection: TE} header. + *

+ * For HTTP/1.1 requests, the {@code TE} header can contain multiple values separated by commas and may include quality + * values (denoted by {@code q=}) separated by semicolons. + *

+ * In case of HTTP/2, this validation is skipped, and another layer of logic handles the specifics of HTTP/2 compliance. + * + * @since 5.5 + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class RequestTE implements HttpRequestInterceptor { + + /** + * Singleton instance of the {@code RequestTE} interceptor. + */ + public static final HttpRequestInterceptor INSTANCE = new RequestTE(); + + /** + * Delimiter used to parse the {@code TE} header, recognizing both commas (',') and semicolons (';') as delimiters. + */ + public static final Tokenizer.Delimiter DELIMITER = Tokenizer.delimiters(',', ';'); + + /** + * Default constructor. + */ + public RequestTE() { + super(); + } + + /** + * Processes the {@code TE} header of the given HTTP request and ensures compliance with HTTP/1.1 requirements. + *

+ * If the {@code TE} header is present, this method validates that: + *

+ * + * @param request the HTTP request containing the headers to validate + * @param entity the entity associated with the request (may be {@code null}) + * @param context the execution context for the request + * @throws HttpException if the {@code TE} header contains invalid values or the {@code Connection} header is missing + * @throws IOException in case of an I/O error + */ + @Override + public void process(final HttpRequest request, final EntityDetails entity, final HttpContext context) + throws HttpException, IOException { + Args.notNull(request, "HTTP request"); + + // Fetch the TE header + final Header teHeader = request.getFirstHeader(HttpHeaders.TE); + + if (teHeader == null) { + return; // No further validation needed + } + + final String teValue = teHeader.getValue(); + validateTEField(teValue); + + validateConnectionHeader(request); + } + + /** + * Validates the {@code TE} header values for compliance with HTTP/1.1. + *

+ * Specifically, this method ensures that: + *

+ * + * @param teValue the value of the {@code TE} header + * @throws HttpException if the {@code TE} header contains invalid values + */ + private void validateTEField(final String teValue) throws HttpException { + final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, teValue.length()); + + while (!cursor.atEnd()) { + Tokenizer.INSTANCE.skipWhiteSpace(teValue, cursor); + + final String member = Tokenizer.INSTANCE.parseToken(teValue, cursor, DELIMITER); + + if (member.isEmpty()) { + if (!cursor.atEnd()) { + Tokenizer.INSTANCE.skipWhiteSpace(teValue, cursor); + cursor.updatePos(cursor.getPos() + 1); + } + continue; + } + + if ("trailers".equalsIgnoreCase(member)) { + continue; + } + + if (HeaderElements.CHUNKED_ENCODING.equalsIgnoreCase(member)) { + throw new ProtocolException("'chunked' transfer coding must not be listed in the TE header for HTTP/1.1."); + } + + if (!cursor.atEnd()) { + Tokenizer.INSTANCE.skipWhiteSpace(teValue, cursor); + cursor.updatePos(cursor.getPos() + 1); + } + } + } + + /** + * Validates the presence of the {@code Connection: TE} header when the {@code TE} header is present. + *

+ * If the {@code TE} header is used, the HTTP/1.1 protocol requires that the {@code Connection} header includes the {@code TE} directive to prevent forwarding by intermediaries. + * + * @param request the HTTP request to validate + * @throws HttpException if the {@code Connection: TE} header is missing + */ + private void validateConnectionHeader(final HttpRequest request) throws HttpException { + final Header connectionHeader = request.getFirstHeader(HttpHeaders.CONNECTION); + if (connectionHeader == null) { + throw new ProtocolException("The 'TE' header is present, but the 'Connection' header is missing."); + } + final String connectionValue = connectionHeader.getValue(); + final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, connectionValue.length()); + + boolean hasTE = false; + + while (!cursor.atEnd()) { + final String directive = Tokenizer.INSTANCE.parseToken(connectionValue, cursor, DELIMITER).trim(); + + if ("TE".equalsIgnoreCase(directive)) { + hasTE = true; + break; + } + + if (!cursor.atEnd()) { + cursor.updatePos(cursor.getPos() + 1); + } + } + + if (!hasTE) { + throw new ProtocolException("The 'Connection' header must include the 'TE' directive when the 'TE' header is present."); + } + } +} diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java new file mode 100644 index 000000000..990c4909b --- /dev/null +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java @@ -0,0 +1,199 @@ +/* + * ==================================================================== + * 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 static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TestRequestTE { + + @Test + void testValidTEHeader() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + // Set the TE header and Connection header + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "TE"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertNotNull(request.getHeader(HttpHeaders.TE)); + assertEquals("trailers", request.getHeader(HttpHeaders.TE).getValue()); + } + + + @Test + void testMultipleValidTEHeaders() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + // Set both the TE header and the Connection header + request.setHeader(HttpHeaders.TE, "trailers, deflate;q=0.5"); + request.setHeader(HttpHeaders.CONNECTION, "TE"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertNotNull(request.getHeader(HttpHeaders.TE)); + assertEquals("trailers, deflate;q=0.5", request.getHeader(HttpHeaders.TE).getValue()); + } + + + @Test + void testTEHeaderNotPresent() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + // No TE header, no validation should occur + assertNull(request.getHeader(HttpHeaders.TE)); + } + + @Test + void testTEHeaderContainsChunked() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + request.setHeader(HttpHeaders.TE, "chunked"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testTEHeaderInvalidTransferCoding() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + request.setHeader(HttpHeaders.TE, "invalid;q=abc"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testTEHeaderAlreadySet() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + final String teValue = "trailers"; + request.setHeader(HttpHeaders.TE, teValue); + request.setHeader(HttpHeaders.CONNECTION, "TE"); // Add the Connection header as required + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertEquals(HttpHeaders.TE, request.getHeader(HttpHeaders.TE).getName()); + assertNotNull(request.getHeader(HttpHeaders.TE)); + assertEquals(teValue, request.getHeader(HttpHeaders.TE).getValue()); + } + + + @Test + void testTEHeaderWithConnectionHeaderValidation() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "TE"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertEquals(HttpHeaders.TE, request.getHeader(HttpHeaders.TE).getName()); + assertNotNull(request.getHeader(HttpHeaders.TE)); + assertEquals("trailers", request.getHeader(HttpHeaders.TE).getValue()); + } + + @Test + void testTEHeaderWithoutConnectionHeaderThrowsException() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + request.setHeader(HttpHeaders.TE, "trailers"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testTEHeaderWithoutTEInConnectionHeaderThrowsException() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + // Set TE header but Connection header does not include "TE" + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "keep-alive"); // Missing "TE" + + final HttpRequestInterceptor interceptor = new RequestTE(); + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testTEHeaderWithMultipleDirectivesInConnectionHeader() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + // Set TE header and a Connection header with multiple directives + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "keep-alive, close, TE"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertNotNull(request.getHeader(HttpHeaders.CONNECTION)); + assertTrue(request.getHeader(HttpHeaders.CONNECTION).getValue().contains("TE")); + } + + +} + diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java index bdf336a0d..e9f66b744 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java @@ -442,6 +442,25 @@ void testRequestTargetHostConnectHttp10() throws Exception { Assertions.assertNull(header); } + @Test + void testTEHeaderWithConnectionTE() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + // Set both TE and Connection headers as per the requirement + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "TE"); + + final RequestTE interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + final Header connectionHeader = request.getFirstHeader(HttpHeaders.CONNECTION); + Assertions.assertNotNull(connectionHeader); + Assertions.assertEquals("TE", connectionHeader.getValue()); + } + + @Test void testRequestUserAgentGenerated() throws Exception { final HttpCoreContext context = HttpCoreContext.create();