This is an automated email from the ASF dual-hosted git repository. remm pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push: new ac14646dba Update with transfer-encoding support ac14646dba is described below commit ac14646dba3221e8cd99c920ddcf0cec809c1fbe 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 | 59 ++++++++++++---- java/org/apache/tomcat/util/http/parser/TE.java | 84 +++++++++++++++++++++++ test/org/apache/coyote/TestCompressionConfig.java | 69 ++++++++++++++----- webapps/docs/changelog.xml | 5 ++ 4 files changed, 187 insertions(+), 30 deletions(-) diff --git a/java/org/apache/coyote/CompressionConfig.java b/java/org/apache/coyote/CompressionConfig.java index c7fe685f29..7bd2c186db 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; @@ -219,6 +220,8 @@ public class CompressionConfig { return false; } + boolean useTE = false; + MimeHeaders responseHeaders = response.getMimeHeaders(); // Check if content is not already compressed @@ -267,32 +270,55 @@ public class CompressionConfig { } } - // 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; } } } + 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; } @@ -316,8 +342,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 71b1c00d93..86a6f702d9 100644 --- a/test/org/apache/coyote/TestCompressionConfig.java +++ b/test/org/apache/coyote/TestCompressionConfig.java @@ -29,24 +29,39 @@ import org.junit.runners.Parameterized.Parameter; @RunWith(Parameterized.class) public class TestCompressionConfig { - @Parameterized.Parameters(name = "{index}: accept-encoding[{0}], ETag [{1}], NoCompressionStrongETag[{2}], compress[{3}]") + @Parameterized.Parameters(name = "{index}: accept-encoding[{0}], ETag [{1}], NoCompressionStrongETag[{2}], compress[{3}], useTE[{4}]") public static Collection<Object[]> parameters() { List<Object[]> parameterSets = new ArrayList<>(); - parameterSets.add(new Object[] { new String[] { }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - 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[] { }, null, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE }); - 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.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.FALSE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.FALSE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.FALSE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.FALSE, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.FALSE, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.TRUE, Boolean.FALSE }); + + parameterSets.add(new Object[] { new String[] { }, null, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE }); + + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.FALSE, Boolean.TRUE }); + + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.FALSE, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.FALSE, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.TRUE, Boolean.TRUE }); return parameterSets; } @@ -59,6 +74,8 @@ public class TestCompressionConfig { public Boolean noCompressionStrongETag; @Parameter(3) public Boolean compress; + @Parameter(4) + public Boolean useTE; @SuppressWarnings("deprecation") @Test @@ -73,14 +90,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); } - - Assert.assertEquals(compress, Boolean.valueOf(compressionConfig.useCompression(request, response))); + boolean useCompression = compressionConfig.useCompression(request, response); + Assert.assertEquals(compress, Boolean.valueOf(useCompression)); + + 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 71ed6b28ba..75295317af 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