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

markt-asf 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 54ff272b55 Various HTTP upgrade improvements
54ff272b55 is described below

commit 54ff272b5567392da9755bf745cf92f330b67cf6
Author: Mark Thomas <[email protected]>
AuthorDate: Tue Oct 27 19:15:53 2026 +0000

    Various HTTP upgrade improvements
    
    - validate the application does set the upgrade header
    - prevent start async and upgrade at the same time
---
 .../catalina/connector/LocalStrings.properties     |   3 +
 java/org/apache/catalina/connector/Request.java    |  26 +-
 java/org/apache/coyote/http11/Http11Processor.java |  55 +++
 .../apache/coyote/http11/LocalStrings.properties   |   1 +
 .../http11/upgrade/TestUpgradeInternalHandler.java |   2 +
 .../http11/upgrade/TestUpgradeValidation.java      | 468 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |  10 +
 7 files changed, 564 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/catalina/connector/LocalStrings.properties 
b/java/org/apache/catalina/connector/LocalStrings.properties
index 68a4fe198a..78fa47a270 100644
--- a/java/org/apache/catalina/connector/LocalStrings.properties
+++ b/java/org/apache/catalina/connector/LocalStrings.properties
@@ -48,6 +48,7 @@ coyoteOutputStream.nbNotready=In non-blocking mode you may 
not write to the Serv
 coyoteOutputStream.null=The output buffer object has been recycled and is no 
longer associated with this facade
 
 coyoteRequest.alreadyAuthenticated=This request has already been authenticated
+coyoteRequest.asyncAfterUpgrade=Unable to start async because the request has 
been upgraded
 coyoteRequest.attributeEvent=Exception thrown by attributes event listener
 coyoteRequest.authenticate.ise=Cannot call authenticate() after the response 
has been committed
 coyoteRequest.changeSessionId=Cannot change session ID. There is no session 
associated with this request.
@@ -70,6 +71,8 @@ coyoteRequest.sessionEndAccessFail=Exception triggered ending 
access to session
 coyoteRequest.setAttribute.namenull=Cannot call setAttribute with a null name
 coyoteRequest.tooManyLocales=More than the maximum allowed number of locales, 
[{0}], were detected. Use the maxLocaleCount attribute of the connector to 
resolve this if the application should accept more locales.
 coyoteRequest.trailersNotReady=It is illegal to call getTrailerFields() before 
isTrailerFieldsReady() has returned true
+coyoteRequest.upgradeAfterAsync=Unable to upgrade the request because async 
processing has been started
+coyoteRequest.upgradeAfterCommit=Unable to upgrade the request because the 
response has been committed
 coyoteRequest.uploadCreate=Creating the temporary upload location [{0}] as it 
is required by the servlet [{1}]
 coyoteRequest.uploadCreateFail=Failed to create the upload location [{0}]
 coyoteRequest.uploadLocationInvalid=The temporary upload location [{0}] is not 
valid
diff --git a/java/org/apache/catalina/connector/Request.java 
b/java/org/apache/catalina/connector/Request.java
index 822ab9a73e..e7d8d38b23 100644
--- a/java/org/apache/catalina/connector/Request.java
+++ b/java/org/apache/catalina/connector/Request.java
@@ -426,6 +426,11 @@ public class Request implements HttpServletRequest {
      */
     protected Boolean asyncSupported = null;
 
+    /**
+     * Has HttpServletRequest.upgrade() been called for this request?
+     */
+    private boolean upgradeRequested = false;
+
     private HttpServletRequest applicationRequest = null;
 
     /**
@@ -559,6 +564,7 @@ public class Request implements HttpServletRequest {
         }
 
         asyncSupported = null;
+        upgradeRequested = false;
         if (asyncContext != null) {
             asyncContext.recycle();
             asyncContext = null;
@@ -1640,6 +1646,10 @@ public class Request implements HttpServletRequest {
             throw ise;
         }
 
+        if (upgradeRequested) {
+            throw new 
IllegalStateException(sm.getString("coyoteRequest.asyncAfterUpgrade"));
+        }
+
         if (asyncContext == null) {
             asyncContext = new AsyncContextImpl(this);
         }
@@ -2010,6 +2020,13 @@ public class Request implements HttpServletRequest {
     @Override
     public <T extends HttpUpgradeHandler> T upgrade(Class<T> 
httpUpgradeHandlerClass)
             throws IOException, ServletException {
+        if (isAsyncStarted()) {
+            throw new 
IllegalStateException(sm.getString("coyoteRequest.upgradeAfterAsync"));
+        }
+        if (response.isCommitted()) {
+            throw new 
IllegalStateException(sm.getString("coyoteRequest.upgradeAfterCommit"));
+        }
+
         T handler;
         InstanceManager instanceManager = null;
         try {
@@ -2028,6 +2045,7 @@ public class Request implements HttpServletRequest {
                 getUpgradeProtocolName(httpUpgradeHandlerClass));
 
         coyoteRequest.action(ActionCode.UPGRADE, upgradeToken);
+        upgradeRequested = true;
 
         // Output required by RFC2616. Protocol specific headers should have
         // already been set.
@@ -2052,9 +2070,15 @@ public class Request implements HttpServletRequest {
         }
 
         if (result == null) {
-            // Ugly but use the class name - it is better than nothing
+            /*
+             * Use the class name temporarily. It will be replaced at the end 
of Servlet.service() by the upgrade header
+             * field value.
+             */
             result = httpUpgradeHandlerClass.getName();
+        } else {
+            result = result.trim();
         }
+
         return result;
     }
 
diff --git a/java/org/apache/coyote/http11/Http11Processor.java 
b/java/org/apache/coyote/http11/Http11Processor.java
index ee617fe42b..82408ade87 100644
--- a/java/org/apache/coyote/http11/Http11Processor.java
+++ b/java/org/apache/coyote/http11/Http11Processor.java
@@ -504,6 +504,48 @@ public class Http11Processor extends AbstractProcessor {
     }
 
 
+    private boolean validateUpgradeToken() {
+        // Can't upgrade in an error state
+        if (getErrorState().isError()) {
+            return false;
+        }
+        // Make sure the application hasn't changed the status
+        if (response.getStatus() != 
HttpServletResponse.SC_SWITCHING_PROTOCOLS) {
+            return false;
+        }
+        // Make sure the application has set the Upgrade header
+        String upgrade = response.getMimeHeaders().getHeader("upgrade");
+        if (upgrade == null) {
+            return false;
+        } else {
+            upgrade = upgrade.trim();
+        }
+
+        if (!upgradeToken.getProtocol().equalsIgnoreCase(upgrade)) {
+            // The application set/updated the upgrade protocol name. Update 
the token.
+            upgradeToken = new 
UpgradeToken(upgradeToken.getHttpUpgradeHandler(), 
upgradeToken.getContextBind(),
+                    upgradeToken.getInstanceManager(), upgrade);
+        }
+        return true;
+    }
+
+
+    private void cleanUpgradeToken() {
+        if (upgradeToken.getInstanceManager() != null) {
+            ClassLoader oldCL = upgradeToken.getContextBind().bind(false, 
null);
+            try {
+                
upgradeToken.getInstanceManager().destroyInstance(upgradeToken.getHttpUpgradeHandler());
+            } catch (Throwable t) {
+                ExceptionUtils.handleThrowable(t);
+                log.warn(sm.getString("http11processor.upgrade.clean.fail"), 
t);
+            } finally {
+                upgradeToken.getContextBind().unbind(false, oldCL);
+            }
+        }
+        upgradeToken = null;
+    }
+
+
     @Override
     protected final void setSocketWrapper(SocketWrapperBase<?> socketWrapper) {
         super.setSocketWrapper(socketWrapper);
@@ -1077,6 +1119,19 @@ public class Http11Processor extends AbstractProcessor {
             headers.setValue("Server").setString(server);
         }
 
+        if (upgradeToken != null && !validateUpgradeToken()) {
+            /*
+             * doHttpUpgrade() stopped further HTTP output. The upgrade has 
failed so the HTTP response must be
+             * completed.
+             */
+            outputBuffer.responseFinished = false;
+            cleanUpgradeToken();
+            // Don't overwrite any existing error
+            if (!getErrorState().isError()) {
+                setErrorState(ErrorState.CLOSE_CLEAN, null);
+            }
+        }
+
         writeHeaders(response.getStatus(), headers);
 
         outputBuffer.commit();
diff --git a/java/org/apache/coyote/http11/LocalStrings.properties 
b/java/org/apache/coyote/http11/LocalStrings.properties
index 91a3c18bd8..3d1e1d3035 100644
--- a/java/org/apache/coyote/http11/LocalStrings.properties
+++ b/java/org/apache/coyote/http11/LocalStrings.properties
@@ -47,6 +47,7 @@ http11processor.response.finish=Error finishing response
 http11processor.response.invalidHeader=The HTTP response header [{0}] with 
value [{1}] has been removed from the response because it is invalid
 http11processor.sendfile.error=Error sending data using sendfile. May be 
caused by invalid request attributes for start/end points
 http11processor.socket.info=Exception getting socket information
+http11processor.upgrade.clean.fail=Unable to clean-up HttpUpgradeHandler 
instance after failed upgrade
 
 iib.available.readFail=A non-blocking read failed while attempting to 
determine if data was available
 iib.eof.error=Unexpected EOF read on the socket
diff --git 
a/test/org/apache/coyote/http11/upgrade/TestUpgradeInternalHandler.java 
b/test/org/apache/coyote/http11/upgrade/TestUpgradeInternalHandler.java
index d98f2c9525..fd1dfd2ee2 100644
--- a/test/org/apache/coyote/http11/upgrade/TestUpgradeInternalHandler.java
+++ b/test/org/apache/coyote/http11/upgrade/TestUpgradeInternalHandler.java
@@ -137,6 +137,8 @@ public class TestUpgradeInternalHandler extends 
TomcatBaseTest {
         protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                 throws ServletException, IOException {
 
+            // No request Upgrade header is sent in these tests so set a fixed 
value on the response
+            resp.setHeader("upgrade", "test");
             req.upgrade(upgradeHandlerClass);
         }
     }
diff --git a/test/org/apache/coyote/http11/upgrade/TestUpgradeValidation.java 
b/test/org/apache/coyote/http11/upgrade/TestUpgradeValidation.java
new file mode 100644
index 0000000000..60b08591c9
--- /dev/null
+++ b/test/org/apache/coyote/http11/upgrade/TestUpgradeValidation.java
@@ -0,0 +1,468 @@
+/*
+ *  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.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.net.SocketFactory;
+
+import jakarta.annotation.PreDestroy;
+import jakarta.servlet.AsyncContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpUpgradeHandler;
+import jakarta.servlet.http.WebConnection;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
+import org.apache.catalina.Context;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.coyote.http11.AbstractHttp11Protocol;
+
+/**
+ * Tests for the validation of the response Upgrade header when an application 
calls
+ * {@link HttpServletRequest#upgrade(Class)}.
+ */
+public class TestUpgradeValidation extends TomcatBaseTest {
+
+    private static final String MESSAGE = "This is a test.";
+
+    private static final AtomicBoolean handlerInitCalled = new AtomicBoolean();
+    private static final AtomicBoolean handlerDestroyed = new AtomicBoolean();
+    private static final AtomicBoolean handlerInstantiated = new 
AtomicBoolean();
+    private static final AtomicBoolean iseThrown = new AtomicBoolean();
+    private static volatile CountDownLatch upgradeCalled = new 
CountDownLatch(1);
+
+
+    @Before
+    public void resetFlags() {
+        handlerInitCalled.set(false);
+        handlerDestroyed.set(false);
+        handlerInstantiated.set(false);
+        iseThrown.set(false);
+        upgradeCalled = new CountDownLatch(1);
+    }
+
+
+    /*
+     * The application does not set the Upgrade header on the response. The 
upgrade must be abandoned, the handler must
+     * be cleaned up and the connection closed.
+     */
+    @Test
+    public void testNoResponseHeader() throws Exception {
+        Response response = doRequest(Mode.NO_RESPONSE_HEADER, "user-token");
+
+        assertConnectionClosed(response);
+        assertCleanedUp();
+        Assert.assertFalse(getUpgradeGroupInfos().containsKey("user-token"));
+        Assert.assertTrue(getUpgradeGroupInfos().isEmpty());
+    }
+
+
+    /*
+     * As above but the client should be told the upgrade failed rather than 
being sent a 101 response that has no
+     * Upgrade header. This requires the validation to take place before the 
response is committed.
+     */
+    @Test
+    public void testNoResponseHeaderStatus() throws Exception {
+        Response response = doRequest(Mode.NO_RESPONSE_HEADER, "user-token");
+
+        Assert.assertEquals("500", response.status);
+        assertConnectionClosed(response);
+    }
+
+
+    /*
+     * The response header differs from the request header. The response 
header (the application's choice) must be used
+     * and the request header must be ignored.
+     */
+    @Test
+    public void testResponseHeaderDiffersFromRequest() throws Exception {
+        Response response = doRequest(Mode.DIFFERENT_RESPONSE_HEADER, 
"client-token");
+
+        Assert.assertEquals("101", response.status);
+        Assert.assertEquals("app-token", response.upgradeHeader);
+
+        // Make sure the upgraded connection is actually working (this also 
means the upgrade processor and its
+        // per-protocol state have been created)
+        response.writer.write(MESSAGE + CRLF);
+        response.writer.flush();
+        Assert.assertEquals(MESSAGE, response.reader.readLine());
+
+        Map<String,?> infos = getUpgradeGroupInfos();
+        Assert.assertTrue(infos.toString(), infos.containsKey("app-token"));
+        Assert.assertFalse(infos.toString(), 
infos.containsKey("client-token"));
+
+        response.socket.close();
+    }
+
+
+    /*
+     * The application sets a response header that matches the request header. 
The upgrade must succeed.
+     */
+    @Test
+    public void testResponseHeaderMatchesRequest() throws Exception {
+        Response response = doRequest(Mode.ECHO_RESPONSE_HEADER, "test");
+
+        Assert.assertEquals("101", response.status);
+        Assert.assertEquals("test", response.upgradeHeader);
+
+        response.writer.write(MESSAGE + CRLF);
+        response.writer.flush();
+        Assert.assertEquals(MESSAGE, response.reader.readLine());
+        Assert.assertTrue(getUpgradeGroupInfos().containsKey("test"));
+
+        response.socket.close();
+    }
+
+
+    /*
+     * Upgrade and async can't be mixed. Calling startAsync() after upgrade() 
must fail but must not affect the upgrade.
+     */
+    @Test
+    public void testStartAsyncAfterUpgrade() throws Exception {
+        Response response = doRequest(Mode.UPGRADE_THEN_ASYNC, "test");
+
+        Assert.assertTrue("startAsync() must throw IllegalStateException", 
iseThrown.get());
+        Assert.assertEquals("101", response.status);
+
+        response.writer.write(MESSAGE + CRLF);
+        response.writer.flush();
+        Assert.assertEquals(MESSAGE, response.reader.readLine());
+
+        response.socket.close();
+    }
+
+
+    /*
+     * Calling upgrade() after startAsync() must fail and must not affect the 
async processing.
+     */
+    @Test
+    public void testUpgradeAfterStartAsync() throws Exception {
+        Response response = doRequest(Mode.ASYNC_THEN_UPGRADE, "test");
+
+        Assert.assertTrue("upgrade() must throw IllegalStateException", 
iseThrown.get());
+        Assert.assertEquals("200", response.status);
+        Assert.assertFalse("HttpUpgradeHandler must not be created", 
handlerInstantiated.get());
+        Assert.assertTrue(getUpgradeGroupInfos().isEmpty());
+
+        response.socket.close();
+    }
+
+
+    /*
+     * The application fails after calling upgrade(). Even though it set the 
Upgrade header, the upgrade must not
+     * proceed and the handler must be cleaned up.
+     */
+    @Test
+    public void testExceptionAfterUpgrade() throws Exception {
+        Response response = doRequest(Mode.EXCEPTION_AFTER_UPGRADE, "test");
+
+        Assert.assertNotEquals("101", response.status);
+        assertConnectionClosed(response);
+        assertCleanedUp();
+        Assert.assertTrue(getUpgradeGroupInfos().isEmpty());
+    }
+
+
+    /*
+     * The application calls upgrade() but then deliberately sets a different 
status. The upgrade must be abandoned and
+     * the handler cleaned up but the status chosen by the application must 
not be replaced with a 500.
+     */
+    @Test
+    public void testStatusChangedAfterUpgrade() throws Exception {
+        Response response = doRequest(Mode.STATUS_CHANGED_AFTER_UPGRADE, 
"test");
+
+        Assert.assertEquals("400", response.status);
+        assertConnectionClosed(response);
+        assertCleanedUp();
+        Assert.assertTrue(getUpgradeGroupInfos().isEmpty());
+    }
+
+
+    /*
+     * The response is committed before upgrade() is called so the Upgrade 
header validation that takes place when the
+     * response is committed never sees the upgrade. Either upgrade() must be 
rejected or the (client controlled)
+     * request header must not be used as the protocol name.
+     */
+    @Test
+    public void testCommitThenUpgrade() throws Exception {
+        Response response = doRequest(Mode.COMMIT_THEN_UPGRADE, "user-token");
+
+        Assert.assertEquals("200", response.status);
+
+        // The response headers are sent before upgrade() is called so wait 
for the server to get that far
+        Assert.assertTrue(upgradeCalled.await(5, TimeUnit.SECONDS));
+
+        if (!iseThrown.get()) {
+            // The upgrade went ahead. Make sure the upgrade processor (and 
any per-protocol state) has been created.
+            response.writer.write(MESSAGE + CRLF);
+            response.writer.flush();
+            // The flushed response was chunked so skip any chunk framing that 
precedes the echoed message
+            String line = response.reader.readLine();
+            while (line != null && (line.isEmpty() || "0".equals(line))) {
+                line = response.reader.readLine();
+            }
+            Assert.assertEquals(MESSAGE, line);
+        }
+
+        Map<String,?> infos = getUpgradeGroupInfos();
+        Assert.assertFalse(infos.toString(), infos.containsKey("user-token"));
+
+        response.socket.close();
+    }
+
+
+    private void assertCleanedUp() {
+        Assert.assertFalse("HttpUpgradeHandler.init() must not be called", 
handlerInitCalled.get());
+        Assert.assertTrue("The HttpUpgradeHandler instance must be destroyed", 
handlerDestroyed.get());
+    }
+
+
+    private void assertConnectionClosed(Response response) throws IOException {
+        try {
+            // Consume any error page body then expect EOF
+            while (response.reader.read() != -1) {
+                // Keep reading
+            }
+        } catch (SocketTimeoutException ste) {
+            Assert.fail("The connection was left open");
+        } catch (IOException ioe) {
+            // Connection reset is also a closed connection
+        } finally {
+            response.socket.close();
+        }
+    }
+
+
+    @SuppressWarnings("unchecked")
+    private Map<String,?> getUpgradeGroupInfos() throws Exception {
+        AbstractHttp11Protocol<?> protocol =
+                (AbstractHttp11Protocol<?>) 
getTomcatInstance().getConnector().getProtocolHandler();
+        Field field = 
AbstractHttp11Protocol.class.getDeclaredField("upgradeProtocolGroupInfos");
+        field.setAccessible(true);
+        return (Map<String,?>) field.get(protocol);
+    }
+
+
+    private Response doRequest(Mode mode, String requestUpgrade) throws 
Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        // No file system docBase required
+        Context ctx = getProgrammaticRootContext();
+
+        Tomcat.addServlet(ctx, "servlet", new 
UpgradeServlet(mode)).setAsyncSupported(true);
+        ctx.addServletMapping("/", "servlet");
+
+        tomcat.start();
+
+        Socket socket = SocketFactory.getDefault().createSocket("localhost", 
getPort());
+        socket.setSoTimeout(5000);
+
+        Writer writer = new OutputStreamWriter(socket.getOutputStream(), 
StandardCharsets.UTF_8);
+        BufferedReader reader = new BufferedReader(new 
InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
+
+        writer.write("GET / HTTP/1.1" + CRLF);
+        writer.write("Host: whatever" + CRLF);
+        writer.write("Connection: Upgrade" + CRLF);
+        writer.write("Upgrade: " + requestUpgrade + CRLF);
+        writer.write(CRLF);
+        writer.flush();
+
+        String statusLine = reader.readLine();
+        if (statusLine == null) {
+            // Connection closed without a response
+            return new Response(socket, writer, reader, null, null);
+        }
+
+        String upgradeHeader = null;
+        String line = reader.readLine();
+        while (line != null && line.length() > 0) {
+            if (line.regionMatches(true, 0, "Upgrade:", 0, 8)) {
+                upgradeHeader = line.substring(8).trim();
+            }
+            line = reader.readLine();
+        }
+
+        return new Response(socket, writer, reader, getStatusCode(statusLine), 
upgradeHeader);
+    }
+
+
+    private static final class Response {
+        private final Socket socket;
+        private final Writer writer;
+        private final BufferedReader reader;
+        private final String status;
+        private final String upgradeHeader;
+
+        Response(Socket socket, Writer writer, BufferedReader reader, String 
status, String upgradeHeader) {
+            this.socket = socket;
+            this.writer = writer;
+            this.reader = reader;
+            this.status = status;
+            this.upgradeHeader = upgradeHeader;
+        }
+    }
+
+
+    private enum Mode {
+        NO_RESPONSE_HEADER,
+        ECHO_RESPONSE_HEADER,
+        DIFFERENT_RESPONSE_HEADER,
+        UPGRADE_THEN_ASYNC,
+        ASYNC_THEN_UPGRADE,
+        EXCEPTION_AFTER_UPGRADE,
+        COMMIT_THEN_UPGRADE,
+        STATUS_CHANGED_AFTER_UPGRADE
+    }
+
+
+    private static class UpgradeServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        private final Mode mode;
+
+        UpgradeServlet(Mode mode) {
+            this.mode = mode;
+        }
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+            switch (mode) {
+                case NO_RESPONSE_HEADER:
+                    req.upgrade(TestHandler.class);
+                    break;
+                case ECHO_RESPONSE_HEADER:
+                    // Only a single protocol is requested so it is safe to 
echo it
+                    resp.setHeader("Upgrade", req.getHeader("Upgrade"));
+                    req.upgrade(EchoHandler.class);
+                    break;
+                case DIFFERENT_RESPONSE_HEADER:
+                    resp.setHeader("Upgrade", "app-token");
+                    req.upgrade(EchoHandler.class);
+                    break;
+                case UPGRADE_THEN_ASYNC:
+                    resp.setHeader("Upgrade", req.getHeader("Upgrade"));
+                    req.upgrade(EchoHandler.class);
+                    try {
+                        req.startAsync();
+                    } catch (IllegalStateException ise) {
+                        iseThrown.set(true);
+                    }
+                    break;
+                case ASYNC_THEN_UPGRADE:
+                    AsyncContext ac = req.startAsync();
+                    try {
+                        req.upgrade(TestHandler.class);
+                    } catch (IllegalStateException ise) {
+                        iseThrown.set(true);
+                    }
+                    ac.complete();
+                    break;
+                case COMMIT_THEN_UPGRADE:
+                    // Once committed, the Upgrade header can no longer be set 
so the validation at commit time has
+                    // already been missed
+                    resp.flushBuffer();
+                    try {
+                        req.upgrade(EchoHandler.class);
+                    } catch (IllegalStateException ise) {
+                        iseThrown.set(true);
+                    }
+                    upgradeCalled.countDown();
+                    break;
+                case EXCEPTION_AFTER_UPGRADE:
+                    resp.setHeader("Upgrade", req.getHeader("Upgrade"));
+                    req.upgrade(TestHandler.class);
+                    throw new ServletException("Deliberate failure after 
upgrade");
+                case STATUS_CHANGED_AFTER_UPGRADE:
+                    resp.setHeader("Upgrade", req.getHeader("Upgrade"));
+                    req.upgrade(TestHandler.class);
+                    // The application changes its mind
+                    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+                    break;
+            }
+        }
+    }
+
+
+    public static class TestHandler implements HttpUpgradeHandler {
+
+        public TestHandler() {
+            handlerInstantiated.set(true);
+        }
+
+        @Override
+        public void init(WebConnection connection) {
+            handlerInitCalled.set(true);
+        }
+
+        @Override
+        public void destroy() {
+            // NO-OP
+        }
+
+        @PreDestroy
+        public void preDestroy() {
+            handlerDestroyed.set(true);
+        }
+    }
+
+
+    public static class EchoHandler implements HttpUpgradeHandler {
+
+        @Override
+        public void init(WebConnection connection) {
+            try (ServletInputStream sis = connection.getInputStream();
+                    ServletOutputStream sos = connection.getOutputStream()) {
+                byte[] buffer = new byte[8192];
+                int read;
+                while ((read = sis.read(buffer)) >= 0) {
+                    sos.write(buffer, 0, read);
+                    sos.flush();
+                }
+            } catch (IOException ioe) {
+                throw new IllegalStateException(ioe);
+            }
+        }
+
+        @Override
+        public void destroy() {
+            // NO-OP
+        }
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 371c60aad1..3ec2fc2405 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -278,6 +278,16 @@
         same last two digits. Based on pull request <pr>1104</pr> by Arturo
         Bernal. (markt)
       </fix>
+      <fix>
+        Validate that applications calling
+        <code>HttpServletRequest.upgrade</code> are also setting the
+        <code>upgrade</code> header on the response. Attempted upgrades without
+        this header will now fail. (markt)
+      </fix>
+      <fix>
+        Ensure that requests cannot try to start asynchronous processing and 
the
+        HTTP upgrade process on the same request. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to