diff --git a/java/org/apache/coyote/LocalStrings.properties b/java/org/apache/coyote/LocalStrings.properties index 6acb6be07c21..11eb66a2b2fa 100644 --- a/java/org/apache/coyote/LocalStrings.properties +++ b/java/org/apache/coyote/LocalStrings.properties @@ -58,6 +58,7 @@ request.nullReadListener=The listener passed to setReadListener() may not be nul request.readListenerSet=The non-blocking read listener has already been set response.encoding.invalid=The encoding [{0}] is not recognised by the JRE +response.invalidHeader=Invalid header [{0}: {1}] response.noTrailers.notSupported=A trailer fields supplier may not be set for this response. Either the underlying protocol does not support trailer fields or the protocol requires that the supplier is set before the response is committed response.notAsync=It is only valid to switch to non-blocking IO within async processing or HTTP upgrade processing response.notNonBlocking=It is invalid to call isReady() when the response has not been put into non-blocking mode diff --git a/java/org/apache/coyote/LocalStrings_fr.properties b/java/org/apache/coyote/LocalStrings_fr.properties index 30e2465ace3f..1126370f8cb9 100644 --- a/java/org/apache/coyote/LocalStrings_fr.properties +++ b/java/org/apache/coyote/LocalStrings_fr.properties @@ -57,6 +57,7 @@ request.nullReadListener=L'écouteur passé à setReadListener() ne peut pas êt request.readListenerSet=L'écouteur des lectures non bloquantes a déjà été défini response.encoding.invalid=L''encodage [{0}] n''est pas reconnu par le JRE +response.invalidHeader=En-tête invalide [{0}: {1}] response.noTrailers.notSupported=Un fournisseur d'en-tête de fin ne peut pas être mis sur cette réponse, soit le protocole ne supporte pas ces en-têtes, soit le protocole requiert que le fournisseur soit fourni avant le début de l'envoi de la réponse response.notAsync=Il n'est possible de passer en mode d'entrée-sorties non bloquantes que lors de traitements asynchrones ou après mise à niveau depuis HTTP response.notNonBlocking=Il n'est pas permis d'appeler isReady() quand la réponse n'a pas été mise en mode non-bloquant diff --git a/java/org/apache/coyote/Response.java b/java/org/apache/coyote/Response.java index 91473a1b770e..85b664c3c540 100644 --- a/java/org/apache/coyote/Response.java +++ b/java/org/apache/coyote/Response.java @@ -30,6 +30,7 @@ import jakarta.servlet.WriteListener; +import org.apache.coyote.http2.Http2OutputBuffer; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.buf.B2CConverter; @@ -61,7 +62,6 @@ public final class Response { */ private static final Locale DEFAULT_LOCALE = Locale.getDefault(); - // ----------------------------------------------------- Instance Variables /** @@ -435,6 +435,20 @@ private boolean checkSpecialHeader( String name, String value) { return false; } } + + if (outputBuffer instanceof Http2OutputBuffer && name.equalsIgnoreCase("Connection") ) { + + /* + * Connection headers are invalid in HTTP/2, and some clients (like Safari or curl) + * are very touchy about it. Most probably, an application component has added the + * typical HTTP/1.x "Connection: keep-alive" header, but despite the component's + * good intention, the header is faulty in HTTP/2 and *should* be refused. + * . + * @see https://tools.ietf.org/html/rfc7540#section-8.1.2.2 + */ + + throw new IllegalArgumentException(sm.getString("response.invalidHeader", "Connection", value)); + } return false; } diff --git a/test/org/apache/coyote/http2/Http2TestBase.java b/test/org/apache/coyote/http2/Http2TestBase.java index 3f65876c9930..eb486078a219 100644 --- a/test/org/apache/coyote/http2/Http2TestBase.java +++ b/test/org/apache/coyote/http2/Http2TestBase.java @@ -149,6 +149,25 @@ protected void validateHttp2InitialResponse() throws Exception { output.clearTrace(); } + protected void validateHttp2InitialErrorResponse() throws Exception { + // - 101 response acts as acknowledgement of the HTTP2-Settings header + // Need to read 5 frames + // - settings (server settings - must be first) + // - settings ack (for the settings frame in the client preface) + // - ping + // - headers (for response) + // - data (for response body) + parser.readFrame(true); + parser.readFrame(true); + parser.readFrame(true); + parser.readFrame(true); + parser.readFrame(true); + + Assert.assertTrue(output.getTrace().contains("1-Header-[:status]-[500]")); + output.clearTrace(); + } + + protected void sendEmptyGetRequest(int streamId) throws IOException { byte[] frameHeader = new byte[9]; diff --git a/test/org/apache/coyote/http2/TestInvalidHeader.java b/test/org/apache/coyote/http2/TestInvalidHeader.java new file mode 100644 index 000000000000..8bd10a270529 --- /dev/null +++ b/test/org/apache/coyote/http2/TestInvalidHeader.java @@ -0,0 +1,70 @@ +/* + * 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.apache.coyote.http2; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; + +public class TestInvalidHeader extends Http2TestBase { + + /* + * @see org.apache.coyote.Response#checkSpecialHeaders() + */ + protected static class FaultyServlet extends SimpleServlet { + + private static final long serialVersionUID = 1L; + + public static final int CONTENT_LENGTH = 8192; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Add faulty header + resp.addHeader("Connection", "keep-alive"); + super.doGet(req, resp); + } + } + + + @Test + public void testInvalidHeader() throws Exception { + + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = tomcat.addContext("", null); + Tomcat.addServlet(ctxt, "simple", new FaultyServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialErrorResponse(); + } + +}