This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/11.0.x by this push: new c0df448d0d Fix BZ 69301 - Wrong headers in access log with HTTP/2 + trailers c0df448d0d is described below commit c0df448d0de13aa267c5dc7dd279c8e7ef4115ed Author: Mark Thomas <ma...@apache.org> AuthorDate: Fri Aug 30 19:26:31 2024 +0100 Fix BZ 69301 - Wrong headers in access log with HTTP/2 + trailers The non-trailer headers were not visible to the access log. They were replaced by the trailer headers. --- java/org/apache/coyote/http2/Stream.java | 9 +- .../apache/coyote/http2/TestHttp2AccessLogs.java | 172 +++++++++++++++++++++ webapps/docs/changelog.xml | 5 + 3 files changed, 182 insertions(+), 4 deletions(-) diff --git a/java/org/apache/coyote/http2/Stream.java b/java/org/apache/coyote/http2/Stream.java index 65a130892b..3db615827b 100644 --- a/java/org/apache/coyote/http2/Stream.java +++ b/java/org/apache/coyote/http2/Stream.java @@ -570,10 +570,11 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { return; } - // We can re-use the MimeHeaders from the response since they have - // already been processed by the encoder at this point - MimeHeaders mimeHeaders = coyoteResponse.getMimeHeaders(); - mimeHeaders.recycle(); + /* + * Need a dedicated MimeHeaders for trailers as the MimeHeaders from the response needs to be retained in case + * the access log needs to log header values. + */ + MimeHeaders mimeHeaders = new MimeHeaders(); Map<String,String> headerMap = supplier.get(); if (headerMap == null) { diff --git a/test/org/apache/coyote/http2/TestHttp2AccessLogs.java b/test/org/apache/coyote/http2/TestHttp2AccessLogs.java new file mode 100644 index 0000000000..ae9a9b5ace --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2AccessLogs.java @@ -0,0 +1,172 @@ +/* + * 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.CharArrayWriter; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.valves.AbstractAccessLogValve; + +public class TestHttp2AccessLogs extends Http2TestBase { + + private static final int COUNT_LIMIT = 20; + private static final long SLEEP = 50; + + private void configureAndStartWebApplication(CharArrayWriter writer) throws LifecycleException { + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Wrapper w = Tomcat.addServlet(ctxt, "trailers", new TrailersServlet()); + w.setAsyncSupported(true); + ctxt.addServletMappingDecoded("/trailers", "trailers"); + TesterAccessLogValve valve = new TesterAccessLogValve(writer); + valve.setPattern("%{header-key}o %{trailer-key}o"); + tomcat.getHost().getPipeline().addValve(valve); + tomcat.start(); + } + + + @Test + public void testHeadersAndTrailersInAccessLog() throws Exception { + CharArrayWriter writer = new CharArrayWriter(); + + enableHttp2(false); + configureAndStartWebApplication(writer); + openClientConnection(false); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + // Validate the access log is as expected after the initial requests + int count = 0; + String result = writer.toString(); + String expectedInitial = "- -- -"; + while (count < COUNT_LIMIT && !expectedInitial.equals(result)) { + try { + Thread.sleep(SLEEP); + } catch (InterruptedException ex) { + log.error("Exception during sleep", ex); + } + result = writer.toString(); + count++; + } + Assert.assertEquals(result, expectedInitial, result); + writer.reset(); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(256); + byte[] trailerFrameHeader = new byte[9]; + ByteBuffer trailerPayload = ByteBuffer.allocate(256); + + buildPostRequest(headersFrameHeader, headersPayload, false, null, -1, "/trailers", dataFrameHeader, dataPayload, + null, trailerFrameHeader, trailerPayload, 3); + + // Write the headers + writeFrame(headersFrameHeader, headersPayload); + // Body + writeFrame(dataFrameHeader, dataPayload); + + parser.readFrame(); + parser.readFrame(); + + String trace = output.getTrace(); + String[] lines = trace.split("\n"); + // Check the response code + Assert.assertEquals("3-HeadersStart", lines[0]); + Assert.assertEquals("3-Header-[:status]-[200]", lines[1]); + + // Validate the access log is as expected after the initial requests + count = 0; + result = writer.toString(); + String expected = "header-value -"; + while (count < COUNT_LIMIT && !expected.equals(result)) { + try { + Thread.sleep(SLEEP); + } catch (InterruptedException ex) { + log.error("Exception during sleep", ex); + } + result = writer.toString(); + count++; + } + Assert.assertEquals(result, expected, result); + } + + + /** + * Extend AbstractAccessLogValve to retrieve log output. + */ + private final class TesterAccessLogValve extends AbstractAccessLogValve { + + private final CharArrayWriter writer; + + TesterAccessLogValve(CharArrayWriter writer) { + this.writer = writer; + } + + /** + * Log the specified message to the log file, switching files if the date has changed since the previous log + * call. + * + * @param message Message to be logged + */ + @Override + public void log(CharArrayWriter message) { + try { + message.writeTo(writer); + } catch (IOException ex) { + log.error("Could not write to writer", ex); + } + } + } + + + private static class TrailersServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setTrailerFields(() -> { + Map<String,String> trailers = new HashMap<>(); + trailers.put("trailer-key", "trailer-value"); + return trailers; + }); + resp.addHeader("header-key", "header-value"); + resp.setStatus(HttpServletResponse.SC_OK); + resp.getOutputStream().close(); + } + } +} \ No newline at end of file diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 798065a138..f68afcda59 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -139,6 +139,11 @@ from the client when using the <code>OpenSSLImplementation</code>. (markt) </fix> + <fix> + <bug>69301</bug>: Fix trailer headers replacing non-trailer headers when + writing response headers to the access log. Based on a patch and test + case provided by hypnoce. (markt) + </fix> </changelog> </subsection> <subsection name="Jasper"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org