This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 744333edb6de7fb71ff4fb4159d7674712325f55
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Mon Dec 6 23:15:08 2021 +0000

    Fix BZ 65726 - handle HTTP/1.1 upgrade with request body
    
    https://bz.apache.org/bugzilla/show_bug.cgi?id=65726
---
 java/org/apache/coyote/http11/Http11Processor.java | 116 +++++++++++++++------
 .../upgrade/UpgradeApplicationBufferHandler.java   |  45 ++++++++
 java/org/apache/coyote/http2/Stream.java           |   2 +-
 test/org/apache/coyote/http2/Http2TestBase.java    |  27 +++++
 .../coyote/http2/TestHttp2UpgradeHandler.java      |  80 ++++++++++++++
 webapps/docs/changelog.xml                         |   9 ++
 webapps/docs/config/http.xml                       |  24 +++--
 webapps/docs/security-howto.xml                    |   8 +-
 8 files changed, 262 insertions(+), 49 deletions(-)

diff --git a/java/org/apache/coyote/http11/Http11Processor.java 
b/java/org/apache/coyote/http11/Http11Processor.java
index 4f27473..623631a 100644
--- a/java/org/apache/coyote/http11/Http11Processor.java
+++ b/java/org/apache/coyote/http11/Http11Processor.java
@@ -47,6 +47,7 @@ import 
org.apache.coyote.http11.filters.SavedRequestInputFilter;
 import org.apache.coyote.http11.filters.VoidInputFilter;
 import org.apache.coyote.http11.filters.VoidOutputFilter;
 import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
+import org.apache.coyote.http11.upgrade.UpgradeApplicationBufferHandler;
 import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.util.ExceptionUtils;
@@ -58,6 +59,7 @@ import org.apache.tomcat.util.http.parser.HttpParser;
 import org.apache.tomcat.util.http.parser.TokenList;
 import org.apache.tomcat.util.log.UserDataHelper;
 import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
+import org.apache.tomcat.util.net.ApplicationBufferHandler;
 import org.apache.tomcat.util.net.SSLSupport;
 import org.apache.tomcat.util.net.SendfileDataBase;
 import org.apache.tomcat.util.net.SendfileKeepAliveState;
@@ -336,18 +338,33 @@ public class Http11Processor extends AbstractProcessor {
                 UpgradeProtocol upgradeProtocol = 
protocol.getUpgradeProtocol(requestedProtocol);
                 if (upgradeProtocol != null) {
                     if (upgradeProtocol.accept(request)) {
-                        
response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS);
-                        response.setHeader("Connection", "Upgrade");
-                        response.setHeader("Upgrade", requestedProtocol);
-                        action(ActionCode.CLOSE,  null);
-                        getAdapter().log(request, response, 0);
-
-                        InternalHttpUpgradeHandler upgradeHandler =
-                                upgradeProtocol.getInternalUpgradeHandler(
-                                        socketWrapper, getAdapter(), 
cloneRequest(request));
-                        UpgradeToken upgradeToken = new 
UpgradeToken(upgradeHandler, null, null, requestedProtocol);
-                        action(ActionCode.UPGRADE, upgradeToken);
-                        return SocketState.UPGRADING;
+                        // Create clone of request for upgraded protocol
+                        Request upgradeRequest = null;
+                        try {
+                            upgradeRequest = cloneRequest(request);
+                        } catch (ByteChunk.BufferOverflowException ioe) {
+                            
response.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
+                            setErrorState(ErrorState.CLOSE_CLEAN, null);
+                        } catch (IOException ioe) {
+                            
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                            setErrorState(ErrorState.CLOSE_CLEAN, ioe);
+                        }
+
+                        if (upgradeRequest != null) {
+                            // Complete the HTTP/1.1 upgrade process
+                            
response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS);
+                            response.setHeader("Connection", "Upgrade");
+                            response.setHeader("Upgrade", requestedProtocol);
+                            action(ActionCode.CLOSE,  null);
+                            getAdapter().log(request, response, 0);
+
+                            // Continue processing using new protocol
+                            InternalHttpUpgradeHandler upgradeHandler =
+                                    
upgradeProtocol.getInternalUpgradeHandler(socketWrapper, getAdapter(), 
upgradeRequest);
+                            UpgradeToken upgradeToken = new 
UpgradeToken(upgradeHandler, null, null, requestedProtocol);
+                            action(ActionCode.UPGRADE, upgradeToken);
+                            return SocketState.UPGRADING;
+                        }
                     }
                 }
             }
@@ -491,16 +508,39 @@ public class Http11Processor extends AbstractProcessor {
 
         // Transfer the minimal information required for the copy of the 
Request
         // that is passed to the HTTP upgrade process
-
         dest.decodedURI().duplicate(source.decodedURI());
         dest.method().duplicate(source.method());
         dest.getMimeHeaders().duplicate(source.getMimeHeaders());
         dest.requestURI().duplicate(source.requestURI());
         dest.queryString().duplicate(source.queryString());
 
-        return dest;
+        // Preparation for reading the request body
+        MimeHeaders headers = source.getMimeHeaders();
+        prepareExpectation(headers);
+        prepareInputFilters(headers);
+        ack(ContinueResponseTiming.ALWAYS);
+
+        // Need to read and buffer the request body, if any. RFC 7230 requires
+        // that the request is fully read before the upgrade takes place.
+        ByteChunk body = new ByteChunk();
+        int maxSavePostSize = protocol.getMaxSavePostSize();
+        if (maxSavePostSize != 0) {
+            body.setLimit(maxSavePostSize);
+            ApplicationBufferHandler buffer = new 
UpgradeApplicationBufferHandler();
+
+            while (source.getInputBuffer().doRead(buffer) >= 0) {
+                body.append(buffer.getByteBuffer());
+            }
+        }
+
+        // Make the buffered request body available to the upgraded protocol.
+        SavedRequestInputFilter srif = new SavedRequestInputFilter(body);
+        dest.setInputBuffer(srif);
 
+        return dest;
     }
+
+
     private boolean handleIncompleteRequestLineRead() {
         // Haven't finished reading the request so keep the socket
         // open
@@ -594,8 +634,6 @@ public class Http11Processor extends AbstractProcessor {
      */
     private void prepareRequest() throws IOException {
 
-        contentDelimitation = false;
-
         if (protocol.isSSLEnabled()) {
             request.scheme().setString("https");
         }
@@ -615,16 +653,7 @@ public class Http11Processor extends AbstractProcessor {
         }
 
         if (http11) {
-            MessageBytes expectMB = headers.getValue("expect");
-            if (expectMB != null && !expectMB.isNull()) {
-                if 
(expectMB.toString().trim().equalsIgnoreCase("100-continue")) {
-                    inputBuffer.setSwallowInput(false);
-                    request.setExpectation(true);
-                } else {
-                    
response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED);
-                    setErrorState(ErrorState.CLOSE_CLEAN, null);
-                }
-            }
+            prepareExpectation(headers);
         }
 
         // Check user-agent header
@@ -759,6 +788,34 @@ public class Http11Processor extends AbstractProcessor {
         }
 
         // Input filter setup
+        prepareInputFilters(headers);
+
+        // Validate host name and extract port if present
+        parseHost(hostValueMB);
+
+        if (!getErrorState().isIoAllowed()) {
+            getAdapter().log(request, response, 0);
+        }
+    }
+
+
+    private void prepareExpectation(MimeHeaders headers) {
+        MessageBytes expectMB = headers.getValue("expect");
+        if (expectMB != null && !expectMB.isNull()) {
+            if (expectMB.toString().trim().equalsIgnoreCase("100-continue")) {
+                inputBuffer.setSwallowInput(false);
+                request.setExpectation(true);
+            } else {
+                response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED);
+                setErrorState(ErrorState.CLOSE_CLEAN, null);
+            }
+        }
+    }
+
+    private void prepareInputFilters(MimeHeaders headers) throws IOException {
+
+        contentDelimitation = false;
+
         InputFilter[] inputFilters = inputBuffer.getFilters();
 
         // Parse transfer-encoding header
@@ -804,9 +861,6 @@ public class Http11Processor extends AbstractProcessor {
             }
         }
 
-        // Validate host name and extract port if present
-        parseHost(hostValueMB);
-
         if (!contentDelimitation) {
             // If there's no content length
             // (broken HTTP/1.0 or HTTP/1.1), assume
@@ -814,10 +868,6 @@ public class Http11Processor extends AbstractProcessor {
             inputBuffer.addActiveFilter(inputFilters[Constants.VOID_FILTER]);
             contentDelimitation = true;
         }
-
-        if (!getErrorState().isIoAllowed()) {
-            getAdapter().log(request, response, 0);
-        }
     }
 
 
diff --git 
a/java/org/apache/coyote/http11/upgrade/UpgradeApplicationBufferHandler.java 
b/java/org/apache/coyote/http11/upgrade/UpgradeApplicationBufferHandler.java
new file mode 100644
index 0000000..b551aab
--- /dev/null
+++ b/java/org/apache/coyote/http11/upgrade/UpgradeApplicationBufferHandler.java
@@ -0,0 +1,45 @@
+/*
+ * 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.http11.upgrade;
+
+import java.nio.ByteBuffer;
+
+import org.apache.tomcat.util.net.ApplicationBufferHandler;
+
+/**
+ * Trivial implementation of {@link ApplicationBufferHandler} to support saving
+ * of HTTP request bodies during an HTTP/1.1 upgrade.
+ */
+public class UpgradeApplicationBufferHandler implements 
ApplicationBufferHandler {
+
+    private ByteBuffer byteBuffer;
+
+    @Override
+    public void setByteBuffer(ByteBuffer byteBuffer) {
+        this.byteBuffer = byteBuffer;
+    }
+
+    @Override
+    public ByteBuffer getByteBuffer() {
+        return byteBuffer;
+    }
+
+    @Override
+    public void expand(int size) {
+        // NO-OP
+    }
+}
diff --git a/java/org/apache/coyote/http2/Stream.java 
b/java/org/apache/coyote/http2/Stream.java
index e3d6fa8..ac3dd72 100644
--- a/java/org/apache/coyote/http2/Stream.java
+++ b/java/org/apache/coyote/http2/Stream.java
@@ -121,7 +121,7 @@ class Stream extends AbstractNonZeroStream implements 
HeaderEmitter {
                     coyoteResponse.setError();
                 }
             }
-            // TODO Assuming the body has been read at this point is not valid
+            // Request body, if any, has been read and buffered
             state.receivedEndOfStream();
         }
         this.coyoteRequest.setSendfile(handler.hasAsyncIO() && 
handler.getProtocol().getUseSendfile());
diff --git a/test/org/apache/coyote/http2/Http2TestBase.java 
b/test/org/apache/coyote/http2/Http2TestBase.java
index f677719..6e97e5a 100644
--- a/test/org/apache/coyote/http2/Http2TestBase.java
+++ b/test/org/apache/coyote/http2/Http2TestBase.java
@@ -21,6 +21,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.net.Socket;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
@@ -1384,6 +1385,32 @@ public abstract class Http2TestBase extends 
TomcatBaseTest {
     }
 
 
+    static class ReadRequestBodyServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+                throws ServletException, IOException {
+            // Request bodies are unusal with GET but not illegal
+
+            long total = 0;
+            long read = 0;
+            byte[] buffer = new byte[1024];
+            try (InputStream is = req.getInputStream()) {
+                while ((read = is.read(buffer)) > 0) {
+                    total += read;
+                }
+            }
+
+            resp.setContentType("text/plain");
+            resp.setCharacterEncoding("UTF-8");
+            PrintWriter pw = resp.getWriter();
+            pw.print("Total bytes read from request body [" + total + "]");
+        }
+    }
+
+
     static class SettingValue {
         private final int setting;
         private final long value;
diff --git a/test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java 
b/test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java
index 383b486..ebdb14a 100644
--- a/test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java
+++ b/test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java
@@ -17,6 +17,7 @@
 package org.apache.coyote.http2;
 
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 
 import org.junit.Assert;
 import org.junit.Test;
@@ -69,4 +70,83 @@ public class TestHttp2UpgradeHandler extends Http2TestBase {
                 "3-EndOfStream\n", output.getTrace());
     }
 
+
+    @Test
+    public void testUpgradeWithRequestBody() throws Exception {
+        doTestUpgradeWithRequestBody(false);
+    }
+
+
+    @Test
+    public void testUpgradeWithRequestBodyTooBig() throws Exception {
+        doTestUpgradeWithRequestBody(true);
+    }
+
+
+    private void doTestUpgradeWithRequestBody(boolean tooBig) throws Exception 
{
+        enableHttp2();
+
+        Tomcat tomcat = getTomcatInstance();
+
+        Context ctxt = tomcat.addContext("", null);
+        Tomcat.addServlet(ctxt, "ReadRequestBodyServlet", new 
ReadRequestBodyServlet());
+        ctxt.addServletMappingDecoded("/", "ReadRequestBodyServlet");
+
+        if (tooBig) {
+            // Reduce maxSavePostSize rather than use a larger request body
+            tomcat.getConnector().setProperty("maxSavePostSize", "10");
+        }
+        tomcat.start();
+
+        openClientConnection();
+
+        byte[] upgradeRequest = ("GET / HTTP/1.1\r\n" +
+                "Host: localhost:" + getPort() + "\r\n" +
+                "Content-Length: 18\r\n" +
+                "Connection: Upgrade,HTTP2-Settings\r\n" +
+                "Upgrade: h2c\r\n" +
+                EMPTY_HTTP2_SETTINGS_HEADER +
+                "\r\n" +
+                "Small request body").getBytes(StandardCharsets.ISO_8859_1);
+        os.write(upgradeRequest);
+        os.flush();
+
+        if (tooBig) {
+            String[] responseHeaders = readHttpResponseHeaders();
+            Assert.assertNotNull(responseHeaders);
+            Assert.assertNotEquals(0, responseHeaders.length);
+            Assert.assertEquals("HTTP/1.1 413 ", responseHeaders[0]);
+        } else {
+            Assert.assertTrue("Failed to read HTTP Upgrade response", 
readHttpUpgradeResponse());
+
+            sendClientPreface();
+
+            // - 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.assertEquals("0-Settings-[3]-[200]\n" +
+                    "0-Settings-End\n" +
+                    "0-Settings-Ack\n" +
+                    "0-Ping-[0,0,0,0,0,0,0,1]\n" +
+                    "1-HeadersStart\n" +
+                    "1-Header-[:status]-[200]\n" +
+                    "1-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
+                    "1-Header-[content-length]-[39]\n" +
+                    "1-Header-[date]-[" + DEFAULT_DATE + "]\n" +
+                    "1-HeadersEnd\n" +
+                    "1-Body-39\n" +
+                    "1-EndOfStream\n"
+                    , output.getTrace());
+        }
+    }
 }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 3c5631a..192470c 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -114,6 +114,15 @@
       </fix>
     </changelog>
   </subsection>
+  <subsection name="Coyote">
+    <changelog>
+      <fix>
+        <bug>65726</bug>: Implement support for HTTP/1.1 upgrade when the
+        request includes a body. The maximum permitted size of the body is
+        controlled by <code>maxSavePostSize</code>. (markt)
+      </fix>
+    </changelog>
+  </subsection>
   <subsection name="Jasper">
     <changelog>
       <fix>
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index f6e8d08..6b2fa5f 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -185,18 +185,20 @@
     </attribute>
 
     <attribute name="maxSavePostSize" required="false">
-      <p>The maximum size in bytes of the POST which will be saved/buffered by
-      the container during FORM or CLIENT-CERT authentication. For both types
-      of authentication, the POST will be saved/buffered before the user is
-      authenticated. For CLIENT-CERT authentication, the POST is buffered for
-      the duration of the SSL handshake and the buffer emptied when the request
-      is processed. For FORM authentication the POST is saved whilst the user
-      is re-directed to the login form and is retained until the user
-      successfully authenticates or the session associated with the
-      authentication request expires. The limit can be disabled by setting this
+      <p>The maximum size in bytes of the request body which will be
+      saved/buffered by the container during FORM or CLIENT-CERT authentication
+      or during HTTP/1.1 upgarde. For both types of authentication, the request
+      body will be saved/buffered before the user is authenticated. For
+      CLIENT-CERT authentication, the request body is buffered for the duration
+      of the SSL handshake and the buffer emptied when the request is 
processed.
+      For FORM authentication the POST is saved whilst the user is re-directed
+      to the login form and is retained until the user successfully
+      authenticates or the session associated with the authentication request
+      expires. For HTTP/1.1 upgrade, the request body is buffered for the
+      duration of the upgrade process. The limit can be disabled by setting 
this
       attribute to -1. Setting the attribute to zero will disable the saving of
-      POST data during authentication. If not specified, this attribute is set
-      to 4096 (4 kilobytes).</p>
+      the requets body data during authentication and HTTP/1.1 upgrade. If not
+      specified, this attribute is set to 4096 (4 kilobytes).</p>
     </attribute>
 
     <attribute name="parseBodyMethods" required="false">
diff --git a/webapps/docs/security-howto.xml b/webapps/docs/security-howto.xml
index 197cbe9..bd60acb 100644
--- a/webapps/docs/security-howto.xml
+++ b/webapps/docs/security-howto.xml
@@ -294,10 +294,10 @@
       default to reduce exposure to a DOS attack.</p>
 
       <p>The <strong>maxSavePostSize</strong> attribute controls the saving of
-      POST requests during FORM and CLIENT-CERT authentication. The parameters
-      are cached for the duration of the authentication (which may be many
-      minutes) so this is limited to 4KB by default to reduce exposure to a DOS
-      attack.</p>
+      the request body during FORM and CLIENT-CERT authentication and HTTP/1.1
+      upgrade. For FORM authentication, the request body is cached for the
+      duration of the authentication (which may be many minutes) so this is
+      limited to 4KB by default to reduce exposure to a DOS attack.</p>
 
       <p>The <strong>maxParameterCount</strong> attribute controls the
       maximum number of parameter and value pairs (GET plus POST) that can

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to