From 3d24929287d56e21544fe9910f2154c9cc619f6c Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 27 Jul 2023 18:28:29 +0200 Subject: [PATCH] Fix #10155 include writer outputstream (#10157) * Fix #10155 Include mixed writers and input streams --------- Signed-off-by: Ludovic Orban Co-authored-by: Ludovic Orban --- .../src/test/webapp/top.jsp | 4 +- .../jetty/ee10/servlet/Dispatcher.java | 117 ++++++++++++-- .../jetty/ee10/servlet/DispatcherTest.java | 146 +++++++++++++++--- .../ee10/servlet/IncludedServletTest.java | 3 + .../test/resources/contextResources/test.txt | 2 +- 5 files changed, 240 insertions(+), 32 deletions(-) diff --git a/jetty-ee10/jetty-ee10-glassfish-jstl/src/test/webapp/top.jsp b/jetty-ee10/jetty-ee10-glassfish-jstl/src/test/webapp/top.jsp index 53d8d0c3c16a..6feebaea4eb7 100644 --- a/jetty-ee10/jetty-ee10-glassfish-jstl/src/test/webapp/top.jsp +++ b/jetty-ee10/jetty-ee10-glassfish-jstl/src/test/webapp/top.jsp @@ -1,5 +1,5 @@ <% - application.getRequestDispatcher("/included.jsp").include(request,response); - response.setHeader("main-page-key", "main-page-value"); + application.getRequestDispatcher("/included.jsp").include(request,response); + response.setHeader("main-page-key", "main-page-value"); %>

Hello, this is the top page. diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java index 9062e4e8fc82..767dc2a657aa 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java @@ -14,10 +14,14 @@ package org.eclipse.jetty.ee10.servlet; import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -27,6 +31,7 @@ import jakarta.servlet.ServletOutputStream; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.WriteListener; import jakarta.servlet.http.HttpServletMapping; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; @@ -35,6 +40,7 @@ import org.eclipse.jetty.ee10.servlet.util.ServletOutputStreamWrapper; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.pathmap.MatchedResource; +import org.eclipse.jetty.io.WriterOutputStream; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.UrlEncoded; @@ -136,12 +142,14 @@ public void include(ServletRequest request, ServletResponse response) throws Ser HttpServletResponse httpResponse = (response instanceof HttpServletResponse) ? (HttpServletResponse)response : new ServletResponseHttpWrapper(response); ServletContextResponse servletContextResponse = ServletContextResponse.getServletContextResponse(response); + IncludeResponse includeResponse = new IncludeResponse(httpResponse); try { - _mappedServlet.handle(_servletHandler, _decodedPathInContext, new IncludeRequest(httpRequest), new IncludeResponse(httpResponse)); + _mappedServlet.handle(_servletHandler, _decodedPathInContext, new IncludeRequest(httpRequest), includeResponse); } finally { + includeResponse.onIncluded(); servletContextResponse.included(); } } @@ -423,23 +431,108 @@ public String getQueryString() private static class IncludeResponse extends HttpServletResponseWrapper { public static final String JETTY_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include."; + ServletOutputStream _servletOutputStream; + PrintWriter _printWriter; + PrintWriter _mustFlush; public IncludeResponse(HttpServletResponse response) { super(response); } + public void onIncluded() + { + if (_mustFlush != null) + _mustFlush.flush(); + } + @Override public ServletOutputStream getOutputStream() throws IOException { - return new ServletOutputStreamWrapper(getResponse().getOutputStream()) + if (_printWriter != null) + throw new IllegalStateException("getWriter() called"); + if (_servletOutputStream == null) { - @Override - public void close() + try { - // NOOP for include. + _servletOutputStream = new ServletOutputStreamWrapper(getResponse().getOutputStream()) + { + @Override + public void close() + { + // NOOP for include. + } + }; } - }; + catch (IllegalStateException ise) + { + OutputStream os = new WriterOutputStream(getResponse().getWriter(), getResponse().getCharacterEncoding()); + _servletOutputStream = new ServletOutputStream() + { + @Override + public boolean isReady() + { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) + { + throw new UnsupportedOperationException(); + } + + @Override + public void write(int b) throws IOException + { + os.write(b); + } + + @Override + public void write(byte[] b) throws IOException + { + os.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException + { + os.write(b, off, len); + } + + @Override + public void flush() throws IOException + { + os.flush(); + } + + @Override + public void close() + { + // NOOP for include. + } + }; + } + } + return _servletOutputStream; + } + + @Override + public PrintWriter getWriter() throws IOException + { + if (_servletOutputStream != null) + throw new IllegalStateException("getOutputStream called"); + if (_printWriter == null) + { + try + { + _printWriter = super.getWriter(); + } + catch (IllegalStateException ise) + { + _printWriter = _mustFlush = new PrintWriter(new OutputStreamWriter(super.getOutputStream(), super.getCharacterEncoding())); + } + } + return _printWriter; } @Override @@ -448,6 +541,12 @@ public void setCharacterEncoding(String charset) // NOOP for include. } + @Override + public void setLocale(Locale loc) + { + // NOOP for include. + } + @Override public void setContentLength(int len) { @@ -469,15 +568,13 @@ public void setContentType(String type) @Override public void reset() { - // TODO can include do this? - super.reset(); + // NOOP for include. } @Override public void resetBuffer() { - // TODO can include do this? - super.resetBuffer(); + // NOOP for include. } @Override diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherTest.java index 70c6f36952dc..c234b06a21b7 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherTest.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.List; +import java.util.stream.Stream; import jakarta.servlet.AsyncContext; import jakarta.servlet.DispatcherType; @@ -61,6 +62,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -380,10 +384,81 @@ public void testInclude() throws Exception HTTP/1.1 200 OK\r specialSetHeader: specialSetHeader\r specialAddHeader: specialAddHeader\r - Content-Length: 7\r + Content-Length: 20\r + Connection: close\r + \r + Include: + INCLUDE--- + """; + + assertEquals(expected, responses); + } + + public static Stream includeTests() + { + return Stream.of( + Arguments.of(false, false), + Arguments.of(false, true), + Arguments.of(true, false), + Arguments.of(true, true) + ); + } + + @ParameterizedTest + @MethodSource("includeTests") + public void testIncludeOutputStreamWriter(boolean includeWriter, boolean helloWriter) throws Exception + { + _contextHandler.addServlet(new ServletHolder(new IncludeServlet(includeWriter)), "/IncludeServlet/*"); + _contextHandler.addServlet(new ServletHolder(new HelloServlet(helloWriter)), "/Hello"); + + //test include, along with special extension to include that allows headers to + //be set during an include + String responses = _connector.getResponse(""" + GET /context/IncludeServlet?do=hello HTTP/1.1\r + Host: local\r + Connection: close\r + \r + """); + + responses = responses.replaceFirst("Content-Length: .*\r\n", ""); + + String expected = """ + HTTP/1.1 200 OK\r + Connection: close\r + \r + Include: + Hello + --- + """; + + assertEquals(expected, responses); + } + + @Test + public void testIncludeWriterOutputStream() throws Exception + { + _contextHandler.addServlet(IncludeServlet.class, "/IncludeServlet/*"); + _contextHandler.addServlet(AssertIncludeServlet.class, "/AssertIncludeServlet/*"); + + //test include, along with special extension to include that allows headers to + //be set during an include + String responses = _connector.getResponse(""" + GET /context/IncludeServlet?do=assertinclude&do=more&test=1&headers=true HTTP/1.1\r + Host: local\r Connection: close\r \r - INCLUDE"""; + """); + + String expected = """ + HTTP/1.1 200 OK\r + specialSetHeader: specialSetHeader\r + specialAddHeader: specialAddHeader\r + Content-Length: 20\r + Connection: close\r + \r + Include: + INCLUDE--- + """; assertEquals(expected, responses); } @@ -404,17 +479,18 @@ public void testIncludeStatic() throws Exception String expected = """ HTTP/1.1 200 OK\r - Content-Length: 26\r + Content-Length: 31\r Connection: close\r \r Include: - Test 2 to too two"""; + Test 2 to too two + --- + """; assertEquals(expected, responses); } @Test - @Disabled("Bug #10155 - response misses the Content-Length header") public void testIncludeStaticWithWriter() throws Exception { _contextHandler.addServlet(new ServletHolder(new IncludeServlet(true)), "/IncludeServlet/*"); @@ -430,11 +506,12 @@ public void testIncludeStaticWithWriter() throws Exception String expected = """ HTTP/1.1 200 OK\r - Content-Length: 26\r Connection: close\r \r Include: - Test 2 to too two"""; + Test 2 to too two + --- + """; assertEquals(expected, responses); } @@ -460,10 +537,11 @@ public void testForwardStatic() throws Exception Last-Modified: xxx\r Content-Type: text/plain\r Accept-Ranges: bytes\r - Content-Length: 17\r + Content-Length: 18\r Connection: close\r \r - Test 2 to too two"""; + Test 2 to too two + """; assertEquals(expected, responses); @@ -558,10 +636,12 @@ public void testForwardThenInclude() throws Exception String expected = """ HTTP/1.1 200 OK\r - Content-Length: 7\r + Content-Length: 20\r Connection: close\r \r - INCLUDE"""; + Include: + INCLUDE--- + """; assertEquals(expected, rawResponse); } @@ -582,10 +662,11 @@ public void testIncludeThenForward() throws Exception String expected = """ HTTP/1.1 200 OK\r - Content-Length: 7\r + Content-Length: 11\r Connection: close\r \r - FORWARD"""; + FORWARD--- + """; assertEquals(expected, rawResponse); } @@ -1078,6 +1159,11 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t RequestDispatcher dispatcher = null; boolean headers = Boolean.parseBoolean(request.getParameter("headers")); + if (useWriter) + response.getWriter().println("Include:"); + else + response.getOutputStream().write("Include:\n".getBytes(StandardCharsets.US_ASCII)); + if (request.getParameter("do").equals("forward")) dispatcher = getServletContext().getRequestDispatcher("/ForwardServlet/forwardpath?do=assertincludeforward"); else if (request.getParameter("do").equals("assertforwardinclude")) @@ -1085,15 +1171,37 @@ else if (request.getParameter("do").equals("assertforwardinclude")) else if (request.getParameter("do").equals("assertinclude")) dispatcher = getServletContext().getRequestDispatcher("/AssertIncludeServlet?do=end&do=the&headers=" + headers); else if (request.getParameter("do").equals("static")) - { - if (useWriter) - response.getWriter().println("Include:"); - else - response.getOutputStream().write("Include:\n".getBytes(StandardCharsets.US_ASCII)); dispatcher = getServletContext().getRequestDispatcher("/test.txt"); - } + else if (request.getParameter("do").equals("hello")) + dispatcher = getServletContext().getRequestDispatcher("/Hello"); + assert dispatcher != null; + dispatcher.include(request, response); + + if (useWriter) + response.getWriter().println("---"); + else + response.getOutputStream().write("---\n".getBytes(StandardCharsets.US_ASCII)); + } + } + + public static class HelloServlet extends HttpServlet implements Servlet + { + private final boolean useWriter; + + public HelloServlet(boolean useWriter) + { + this.useWriter = useWriter; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + if (useWriter) + response.getWriter().println("Hello"); + else + response.getOutputStream().write("Hello\n".getBytes(StandardCharsets.US_ASCII)); } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/IncludedServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/IncludedServletTest.java index 943908cb6364..d0cec5642b26 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/IncludedServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/IncludedServletTest.java @@ -172,6 +172,9 @@ public void testTopWithIncludedHeader() throws IOException IO.copy(reader, writer); String response = writer.toString(); + connection.getHeaderFields().entrySet().forEach(System.err::println); + System.err.println(); + System.err.println(response); // System.out.printf("Response%n%s",response); assertThat("Response", response, containsString("

Hello, this is the top page.")); assertThat("Response", response, containsString("

This is the included page")); diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/resources/contextResources/test.txt b/jetty-ee10/jetty-ee10-servlet/src/test/resources/contextResources/test.txt index cf3582b72034..4518b58d2aaa 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/resources/contextResources/test.txt +++ b/jetty-ee10/jetty-ee10-servlet/src/test/resources/contextResources/test.txt @@ -1 +1 @@ -Test 2 to too two \ No newline at end of file +Test 2 to too two