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

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


The following commit(s) were added to refs/heads/main by this push:
     new 337d9f0171 Allow alternate protocols
337d9f0171 is described below

commit 337d9f0171c2c38922bdb1a86728a49113809317
Author: remm <[email protected]>
AuthorDate: Tue Sep 22 11:40:50 2026 +0200

    Allow alternate protocols
    
    Avoid conflict between TCP and UDP based protocols (name clash on the
    protocol JMX name, since it includes only the address and port, not the
    protocol name itself).
    Allow simple Alt-Svc header for discovery. It is possible to set a more
    complex one using a valve or rewrite (a value set there will not
    override any application set one).
    Co authored with OpenCode.
---
 java/org/apache/catalina/connector/Connector.java  |  3 ++
 java/org/apache/coyote/AbstractProtocol.java       |  3 ++
 java/org/apache/coyote/ProtocolHandler.java        | 12 +++++
 .../coyote/http11/AbstractHttp11Protocol.java      | 35 +++++++++++++
 java/org/apache/coyote/http11/Constants.java       |  4 ++
 java/org/apache/coyote/http11/Http11Processor.java | 10 ++++
 .../apache/coyote/http11/LocalStrings.properties   |  1 +
 java/org/apache/coyote/http2/StreamProcessor.java  | 12 +++++
 .../apache/coyote/http11/TestHttp11Processor.java  | 61 ++++++++++++++++++++++
 webapps/docs/changelog.xml                         |  6 +++
 webapps/docs/config/http.xml                       | 28 ++++++++++
 11 files changed, 175 insertions(+)

diff --git a/java/org/apache/catalina/connector/Connector.java 
b/java/org/apache/catalina/connector/Connector.java
index 0013db3d21..661ddb5d0a 100644
--- a/java/org/apache/catalina/connector/Connector.java
+++ b/java/org/apache/catalina/connector/Connector.java
@@ -1219,6 +1219,9 @@ public class Connector extends LifecycleMBeanBase {
                 sb.append(ObjectName.quote(address));
             }
         }
+        if (protocolHandler != null && protocolHandler.isUdp()) {
+            sb.append(",transport=UDP");
+        }
         return sb.toString();
     }
 
diff --git a/java/org/apache/coyote/AbstractProtocol.java 
b/java/org/apache/coyote/AbstractProtocol.java
index cb2a1e2a49..5e96689c06 100644
--- a/java/org/apache/coyote/AbstractProtocol.java
+++ b/java/org/apache/coyote/AbstractProtocol.java
@@ -915,6 +915,9 @@ public abstract class AbstractProtocol<S> implements 
ProtocolHandler, MBeanRegis
             name.append(",address=");
             name.append(ObjectName.quote(address.getHostAddress()));
         }
+        if (isUdp()) {
+            name.append(",transport=UDP");
+        }
         return new ObjectName(name.toString());
     }
 
diff --git a/java/org/apache/coyote/ProtocolHandler.java 
b/java/org/apache/coyote/ProtocolHandler.java
index cad042776c..017750e083 100644
--- a/java/org/apache/coyote/ProtocolHandler.java
+++ b/java/org/apache/coyote/ProtocolHandler.java
@@ -219,6 +219,18 @@ public interface ProtocolHandler {
     }
 
 
+    /**
+     * Indicates whether or not the transport used by this protocol is UDP 
based. This is used to disambiguate the JMX
+     * ObjectName of a connector that may share its port number with a TCP 
based connector (for example, a UDP based
+     * connector and a TCP based connector both listening on port 443).
+     *
+     * @return <code>true</code> if the transport is UDP based, otherwise 
<code>false</code>
+     */
+    default boolean isUdp() {
+        return false;
+    }
+
+
     /**
      * Create a new ProtocolHandler for the given protocol.
      *
diff --git a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java 
b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
index 3bc72c0c52..b71242e1e9 100644
--- a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
+++ b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
@@ -602,6 +602,41 @@ public abstract class AbstractHttp11Protocol<S> extends 
AbstractProtocol<S> {
     }
 
 
+    private String altService;
+
+    /**
+     * Get the alternative service that is announced to clients in the {@code 
Alt-Svc} response header.
+     *
+     * @return The alternative service protocol identifier or {@code null} if 
no alternative service is announced
+     */
+    public String getAltService() {
+        return altService;
+    }
+
+
+    /**
+     * Set the alternative service that is announced to clients in the {@code 
Alt-Svc} response header. The alternative
+     * service is assumed to be reachable at the same host and port as this 
connector. Announcing an alternative
+     * service allows clients to discover it from the first HTTP response 
without any client-side configuration. The
+     * value is used as the alternative service protocol identifier and must 
therefore be a valid HTTP token (RFC
+     * 7230).
+     *
+     * @param altService  The alternative service protocol identifier or 
{@code null}/empty to not announce an
+     *                        alternative service
+     *
+     * @throws IllegalArgumentException  If the value is not a valid 
alternative service protocol identifier
+     */
+    public void setAltService(String altService) {
+        if (altService == null || altService.isEmpty()) {
+            this.altService = null;
+        } else if (HttpParser.isToken(altService)) {
+            this.altService = altService;
+        } else {
+            throw new 
IllegalArgumentException(sm.getString("abstractHttp11Protocol.invalidAltSvc", 
altService));
+        }
+    }
+
+
     /**
      * Maximum size of trailing headers in bytes
      */
diff --git a/java/org/apache/coyote/http11/Constants.java 
b/java/org/apache/coyote/http11/Constants.java
index b6343dc552..07be9cd25f 100644
--- a/java/org/apache/coyote/http11/Constants.java
+++ b/java/org/apache/coyote/http11/Constants.java
@@ -106,6 +106,10 @@ public final class Constants {
      * Keep-Alive header name.
      */
     public static final String KEEP_ALIVE_HEADER_NAME = "Keep-Alive";
+    /**
+     * Alt-Svc header name.
+     */
+    public static final String ALT_SVC_HEADER_NAME = "Alt-Svc";
     /**
      * HTTP 200 OK response bytes.
      */
diff --git a/java/org/apache/coyote/http11/Http11Processor.java 
b/java/org/apache/coyote/http11/Http11Processor.java
index 874caa2250..d583fafe6c 100644
--- a/java/org/apache/coyote/http11/Http11Processor.java
+++ b/java/org/apache/coyote/http11/Http11Processor.java
@@ -1035,6 +1035,16 @@ public class Http11Processor extends AbstractProcessor {
             headers.setValue("Server").setString(server);
         }
 
+        // Announce the configured alternative service so clients can
+        // discover it without any pre-configuration. The alternative
+        // service is assumed to listen on the same port as this request. An
+        // Alt-Svc header set by the application takes precedence.
+        String altService = protocol.getAltService();
+        if (altService != null && 
headers.getValue(Constants.ALT_SVC_HEADER_NAME) == null) {
+            headers.addValue(Constants.ALT_SVC_HEADER_NAME)
+                    .setString(altService + "=\":" + request.getServerPort() + 
"\"");
+        }
+
         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 642b02f971..21b29ef1a5 100644
--- a/java/org/apache/coyote/http11/LocalStrings.properties
+++ b/java/org/apache/coyote/http11/LocalStrings.properties
@@ -16,6 +16,7 @@
 abstractHttp11Protocol.alpnConfigured=The [{0}] connector has been configured 
to support negotiation to [{1}] via ALPN
 abstractHttp11Protocol.alpnWithNoAlpn=The upgrade handler [{0}] for [{1}] only 
supports upgrade via ALPN but has been configured for the [{2}] connector that 
does not support ALPN.
 abstractHttp11Protocol.httpUpgradeConfigured=The [{0}] connector has been 
configured to support HTTP upgrade to [{1}]
+abstractHttp11Protocol.invalidAltSvc=The alternative service [{0}] is invalid. 
The value must be a valid HTTP token.
 abstractHttp11Protocol.upgradeJmxNameFail=Failed to create ObjectName with 
which to register upgrade protocol in JMX
 abstractHttp11Protocol.upgradeJmxRegistrationFail=Failed to register upgrade 
protocol in JMX
 
diff --git a/java/org/apache/coyote/http2/StreamProcessor.java 
b/java/org/apache/coyote/http2/StreamProcessor.java
index 279ac32cfc..3a218a7b60 100644
--- a/java/org/apache/coyote/http2/StreamProcessor.java
+++ b/java/org/apache/coyote/http2/StreamProcessor.java
@@ -261,6 +261,18 @@ class StreamProcessor extends AbstractProcessor implements 
NonPipeliningProcesso
                 // server always overrides anything the app might set
                 headers.setValue("Server").setString(server);
             }
+
+            // Announce the configured alternative service so clients can
+            // discover it without any pre-configuration. The alternative
+            // service is assumed to listen on the same port as this
+            // request. An Alt-Svc header set by the application takes
+            // precedence.
+            String altService = protocol.getHttp11Protocol().getAltService();
+            if (altService != null &&
+                    
headers.getValue(org.apache.coyote.http11.Constants.ALT_SVC_HEADER_NAME) == 
null) {
+                
headers.addValue(org.apache.coyote.http11.Constants.ALT_SVC_HEADER_NAME)
+                        .setString(altService + "=\":" + 
coyoteRequest.getServerPort() + "\"");
+            }
         }
     }
 
diff --git a/test/org/apache/coyote/http11/TestHttp11Processor.java 
b/test/org/apache/coyote/http11/TestHttp11Processor.java
index 0d264ca476..977c59ceb9 100644
--- a/test/org/apache/coyote/http11/TestHttp11Processor.java
+++ b/test/org/apache/coyote/http11/TestHttp11Processor.java
@@ -2200,4 +2200,65 @@ public class TestHttp11Processor extends TomcatBaseTest {
         Assert.assertTrue(newEncodings.contains("br"));
         Assert.assertFalse(newEncodings.contains("gzip"));
     }
+
+
+    private static final class AltSvcClient extends SimpleHttpClient {
+
+        AltSvcClient(int port) {
+            setPort(port);
+        }
+
+        @Override
+        public boolean isResponseBodyOK() {
+            return true;
+        }
+    }
+
+
+    @Test
+    public void testAltServiceHeader() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+        Context root = getProgrammaticRootContext();
+        Tomcat.addServlet(root, "AltSvc", new TesterServlet());
+        root.addServletMapping("/test", "AltSvc");
+
+        AbstractHttp11Protocol<?> protocol =
+                (AbstractHttp11Protocol<?>) 
tomcat.getConnector().getProtocolHandler();
+        protocol.setAltService("h2");
+
+        tomcat.start();
+        int port = tomcat.getConnector().getLocalPort();
+
+        AltSvcClient client = new AltSvcClient(port);
+        String request = "GET /test HTTP/1.1" + CRLF + "Host: localhost:" + 
port + CRLF + "Connection: close" +
+                CRLF + CRLF;
+        client.setRequest(new String[] { request });
+        client.connect();
+        client.processRequest();
+
+        Assert.assertTrue(client.getResponseLine(), client.isResponse200());
+        String expected = "Alt-Svc: h2=\"" + ":" + port + "\"";
+        Assert.assertTrue(client.getResponseHeaders().toString(), 
client.getResponseHeaders().contains(expected));
+    }
+
+
+    @Test
+    public void testAltServiceInvalidValue() {
+        Http11NioProtocol protocol = new Http11NioProtocol();
+
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> protocol.setAltService("not a token"));
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> protocol.setAltService("h2=\""));
+
+        // Any valid token identifier is accepted
+        protocol.setAltService("h2");
+        Assert.assertEquals("h2", protocol.getAltService());
+
+        protocol.setAltService("");
+        Assert.assertNull(protocol.getAltService());
+
+        protocol.setAltService(null);
+        Assert.assertNull(protocol.getAltService());
+    }
 }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 3b7bfdcea5..6a01bb7fc2 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -309,6 +309,12 @@
         (remm)
       </fix>
       <!-- Entries for backport and removal before 12.0.0-M1 below this line 
-->
+      <update>
+        Allow UDP based protocols (without conflicting with a TCP based
+        protocol on the same port), and setting a <code>Alt-Svc</code>
+        header using the <code>altService</code> attribute of the
+        <code>Connector</code> element. (remm)
+      </update>
     </changelog>
   </subsection>
   <subsection name="Jasper">
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 6340474b42..119883db87 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -409,6 +409,34 @@
       comma-separated list of header names.</p>
     </attribute>
 
+    <attribute name="altService" required="false">
+      <p>The protocol identifier of an alternative service that is announced
+      to clients in the <code>Alt-Svc</code> response header as described by
+      <a href="https://tools.ietf.org/rfc/rfc9239.txt";>RFC 9239</a>. This
+      allows clients to discover the alternative service from the first
+      response without any client-side configuration.
+      The value is used as the alternative service protocol identifier and
+      must therefore be a valid HTTP token as defined by
+      <a href="https://tools.ietf.org/rfc/rfc7230.txt";>RFC 7230</a>, for
+      example <code>h2</code>. If the value is not valid,
+      the connector fails to start.</p>
+
+      <p>The alternative service is assumed to be reachable at the same host
+      and port as this connector. The header value is generated from the
+      protocol identifier and the port on which the request was received and
+      takes a form such as <code>Alt-Svc: h2=":8443"</code>.</p>
+
+      <p>If this attribute is not set, no <code>Alt-Svc</code> header is
+      added by Tomcat. If a web application sets its own
+      <code>Alt-Svc</code> header for a response, the application provided
+      value is used and this attribute is ignored for that response.</p>
+
+      <p>The header is added to HTTP/1.1 responses and to HTTP/2 responses
+      served via the HTTP/2
+      <a href="http2.html">UpgradeProtocol</a> associated with this
+      connector.</p>
+    </attribute>
+
     <attribute name="bindOnInit" required="false">
       <p>Controls when the socket used by the connector is bound. If set to
       <code>true</code> it is bound when the connector is initiated and unbound


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

Reply via email to