This is an automated email from the ASF dual-hosted git repository. remm pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push: new 47adb1053f Update with transfer-encoding support 47adb1053f is described below commit 47adb1053f137926bc62744021830978d6aa5bc7 Author: remm <r...@apache.org> AuthorDate: Thu Feb 20 18:57:39 2025 +0100 Update with transfer-encoding support Although there is no client browser support, and there may never be any, Transfer-Encoding is the right way to do compression the way Tomcat does it in GzipOutputFilter. This will be used if the client sends a TE: gzip header. --- java/org/apache/coyote/CompressionConfig.java | 75 ++++++++++++++------ java/org/apache/tomcat/util/http/parser/TE.java | 84 +++++++++++++++++++++++ test/org/apache/coyote/TestCompressionConfig.java | 59 ++++++++++++---- webapps/docs/changelog.xml | 5 ++ 4 files changed, 189 insertions(+), 34 deletions(-) diff --git a/java/org/apache/coyote/CompressionConfig.java b/java/org/apache/coyote/CompressionConfig.java index d4b27f3a5d..3545db1a52 100644 --- a/java/org/apache/coyote/CompressionConfig.java +++ b/java/org/apache/coyote/CompressionConfig.java @@ -32,6 +32,7 @@ import org.apache.tomcat.util.buf.MessageBytes; import org.apache.tomcat.util.http.MimeHeaders; import org.apache.tomcat.util.http.ResponseUtil; import org.apache.tomcat.util.http.parser.AcceptEncoding; +import org.apache.tomcat.util.http.parser.TE; import org.apache.tomcat.util.http.parser.TokenList; import org.apache.tomcat.util.res.StringManager; @@ -192,6 +193,8 @@ public class CompressionConfig { return false; } + boolean useTE = false; + MimeHeaders responseHeaders = response.getMimeHeaders(); // Check if content is not already compressed @@ -230,40 +233,63 @@ public class CompressionConfig { } } - // Check if the resource has a strong ETag - String eTag = responseHeaders.getHeader("ETag"); - if (eTag != null && !eTag.trim().startsWith("W/")) { - // Has an ETag that doesn't start with "W/..." so it must be a - // strong ETag - return false; - } - - // If processing reaches this far, the response might be compressed. - // Therefore, set the Vary header to keep proxies happy - ResponseUtil.addVaryFieldName(responseHeaders, "accept-encoding"); - - // Check if user-agent supports gzip encoding - // Only interested in whether gzip encoding is supported. Other - // encodings and weights can be ignored. - Enumeration<String> headerValues = request.getMimeHeaders().values("accept-encoding"); + Enumeration<String> headerValues = request.getMimeHeaders().values("TE"); boolean foundGzip = false; + // TE and accept-encoding seem to have equivalent syntax while (!foundGzip && headerValues.hasMoreElements()) { - List<AcceptEncoding> acceptEncodings = null; + List<TE> tes = null; try { - acceptEncodings = AcceptEncoding.parse(new StringReader(headerValues.nextElement())); + tes = TE.parse(new StringReader(headerValues.nextElement())); } catch (IOException ioe) { // If there is a problem reading the header, disable compression return false; } - for (AcceptEncoding acceptEncoding : acceptEncodings) { - if ("gzip".equalsIgnoreCase(acceptEncoding.getEncoding())) { + for (TE te : tes) { + if ("gzip".equalsIgnoreCase(te.getEncoding())) { + useTE = true; foundGzip = true; break; } } } + // Check if the resource has a strong ETag + String eTag = responseHeaders.getHeader("ETag"); + if (!useTE && eTag != null && !eTag.trim().startsWith("W/")) { + // Has an ETag that doesn't start with "W/..." so it must be a + // strong ETag + return false; + } + + if (!useTE) { + // If processing reaches this far, the response might be compressed. + // Therefore, set the Vary header to keep proxies happy + ResponseUtil.addVaryFieldName(responseHeaders, "accept-encoding"); + + // Check if user-agent supports gzip encoding + // Only interested in whether gzip encoding is supported. Other + // encodings and weights can be ignored. + headerValues = request.getMimeHeaders().values("accept-encoding"); + foundGzip = false; + while (!foundGzip && headerValues.hasMoreElements()) { + List<AcceptEncoding> acceptEncodings = null; + try { + acceptEncodings = AcceptEncoding.parse(new StringReader(headerValues.nextElement())); + } catch (IOException ioe) { + // If there is a problem reading the header, disable compression + return false; + } + + for (AcceptEncoding acceptEncoding : acceptEncodings) { + if ("gzip".equalsIgnoreCase(acceptEncoding.getEncoding())) { + foundGzip = true; + break; + } + } + } + } + if (!foundGzip) { return false; } @@ -287,8 +313,13 @@ public class CompressionConfig { // Compressed content length is unknown so mark it as such. response.setContentLength(-1); - // Configure the content encoding for compressed content - responseHeaders.setValue("Content-Encoding").setString("gzip"); + if (useTE) { + // Configure the transfer encoding for compressed content + responseHeaders.addValue("Transfer-Encoding").setString("gzip"); + } else { + // Configure the content encoding for compressed content + responseHeaders.setValue("Content-Encoding").setString("gzip"); + } return true; } diff --git a/java/org/apache/tomcat/util/http/parser/TE.java b/java/org/apache/tomcat/util/http/parser/TE.java new file mode 100644 index 0000000000..b2792448fa --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/TE.java @@ -0,0 +1,84 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TE { + + private final String encoding; + private final Map<String,String> parameters; + + protected TE(String encoding, Map<String,String> parameters) { + this.encoding = encoding; + this.parameters = parameters; + } + + public String getEncoding() { + return encoding; + } + + public Map<String,String> getParameters() { + return parameters; + } + + + public static List<TE> parse(StringReader input) throws IOException { + + List<TE> result = new ArrayList<>(); + + do { + String encoding = HttpParser.readToken(input); + if (encoding == null) { + // Invalid encoding, skip to the next one + HttpParser.skipUntil(input, 0, ','); + continue; + } + + if (encoding.length() == 0) { + // No more data to read + break; + } + + Map<String,String> parameters = null; + + // See if a quality has been provided + while (HttpParser.skipConstant(input, ";") == SkipResult.FOUND) { + String name = HttpParser.readToken(input); + String value = null; + if (HttpParser.skipConstant(input, "=") == SkipResult.FOUND) { + value = HttpParser.readTokenOrQuotedString(input, true); + } + if (name != null && value != null) { + if (parameters == null) { + parameters = new HashMap<>(); + } + parameters.put(name, value); + } + } + + result.add(new TE(encoding, parameters)); + } while (true); + + return result; + } +} diff --git a/test/org/apache/coyote/TestCompressionConfig.java b/test/org/apache/coyote/TestCompressionConfig.java index f5dc0379e0..ae16c02f8f 100644 --- a/test/org/apache/coyote/TestCompressionConfig.java +++ b/test/org/apache/coyote/TestCompressionConfig.java @@ -29,20 +29,33 @@ import org.junit.runners.Parameterized.Parameter; @RunWith(Parameterized.class) public class TestCompressionConfig { - @Parameterized.Parameters(name = "{index}: accept-encoding[{0}], ETag [{1}], compress[{2}]") + @Parameterized.Parameters(name = "{index}: accept-encoding[{0}], ETag [{1}], compress[{2}], useTE[{3}]") public static Collection<Object[]> parameters() { List<Object[]> parameterSets = new ArrayList<>(); - parameterSets.add(new Object[] { new String[] { }, null, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { }, null, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.FALSE }); + + parameterSets.add(new Object[] { new String[] { }, null, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.TRUE }); + + parameterSets.add(new Object[] { new String[] { "foobar;foo=bar, gzip;bla=\"quoted\"" }, "XX", Boolean.TRUE, Boolean.TRUE }); return parameterSets; } @@ -53,6 +66,8 @@ public class TestCompressionConfig { public String eTag; @Parameter(2) public Boolean compress; + @Parameter(3) + public Boolean useTE; @Test public void testUseCompression() throws Exception { @@ -65,14 +80,34 @@ public class TestCompressionConfig { Response response = new Response(); for (String header : headers) { - request.getMimeHeaders().addValue("accept-encoding").setString(header); + if (useTE.booleanValue()) { + request.getMimeHeaders().addValue("TE").setString(header); + } else { + request.getMimeHeaders().addValue("accept-encoding").setString(header); + } } if (eTag != null) { response.getMimeHeaders().addValue("ETag").setString(eTag); } + boolean useCompression = compressionConfig.useCompression(request, response); + Assert.assertEquals(compress, Boolean.valueOf(useCompression)); - Assert.assertEquals(compress, Boolean.valueOf(compressionConfig.useCompression(request, response))); + if (useTE.booleanValue()) { + Assert.assertNull(response.getMimeHeaders().getHeader("Content-Encoding")); + if (useCompression) { + Assert.assertEquals("gzip", response.getMimeHeaders().getHeader("Transfer-Encoding")); + } else { + Assert.assertNull(response.getMimeHeaders().getHeader("Transfer-Encoding")); + } + } else { + Assert.assertNull(response.getMimeHeaders().getHeader("Transfer-Encoding")); + if (useCompression) { + Assert.assertEquals("gzip", response.getMimeHeaders().getHeader("Content-Encoding")); + } else { + Assert.assertNull(response.getMimeHeaders().getHeader("Content-Encoding")); + } + } } } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 78c9b2db78..c1bf0a592d 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -134,6 +134,11 @@ compressed using <code>compress</code>, <code>deflate</code> or <code>zstd</code>. (remm) </fix> + <update> + Use <code>Transfer-Encoding</code> for compression rather than + <code>Content-Encoding</code> if the client submits a <code>TE</code> + header containing <code>gzip</code>. (remm) + </update> </changelog> </subsection> <subsection name="Other"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org