From 87324a70b67da0d5edb217e2dd18a28fb8fd1dbb Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 30 Jul 2024 17:52:55 +0300 Subject: [PATCH] Fixes #11926 - Authority Customizer. (#12066) Fixes #11926 - Authority Customizer. Introduced AuthorityCustomizer to synthesize the authority from the Host header (or serverName:serverPort), and related documentation. Removed additional check on authority's host in `HttpCompliance`, as it was too strict and in the wrong place (authority checks should be factored out elsewhere for all HTTP protocol versions). Signed-off-by: Simone Bordet --- .../programming-guide/pages/server/http.adoc | 12 +++ .../eclipse/jetty/http/HttpCompliance.java | 3 - .../http2/server/AuthorityCustomizer.java | 63 ++++++++++++ .../jetty/http2/tests/AbstractServerTest.java | 5 +- .../http2/tests/AuthorityCustomizerTest.java | 98 +++++++++++++++++++ 5 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/AuthorityCustomizer.java create mode 100644 jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AuthorityCustomizerTest.java diff --git a/documentation/jetty/modules/programming-guide/pages/server/http.adoc b/documentation/jetty/modules/programming-guide/pages/server/http.adoc index 840a2d76b199..659428005167 100644 --- a/documentation/jetty/modules/programming-guide/pages/server/http.adoc +++ b/documentation/jetty/modules/programming-guide/pages/server/http.adoc @@ -239,6 +239,18 @@ WebSocket over HTTP/2 or over HTTP/3 initiate the WebSocket communication with a For more information about how to configure `HostHeaderCustomizer`, see also link:{javadoc-url}/org/eclipse/jetty/server/HostHeaderCustomizer.html[the javadocs]. +[[request-customizer-authority]] +=== `AuthorityCustomizer` + +`AuthorityCustomizer` should be added when Jetty receives HTTP/2 or HTTP/3 requests that lack the `:authority` pseudo-header, and web applications have logic that depends on this value, exposed through the `Request` URI authority via `Request.getHttpURI().getAuthority()`. + +The `:authority` pseudo-header may be missing if the request arrived to a proxy in HTTP/1.1 format, and the proxy is converting it to HTTP/2 or HTTP/3 before sending it to the backend server. + +`AuthorityCustomizer` will synthesize the authority using the `Host` header field, if present. +If the `Host` header is also missing, it will use the request _server name_ and _server port_, values that may be influenced by the <> and the <>. + +The synthesized authority will be exposed as the `Request` URI authority via `Request.getHttpURI().getAuthority()`. + [[request-customizer-proxy]] === `ProxyCustomizer` diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java index 02b514935626..41cebbf55d0f 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java @@ -402,9 +402,6 @@ public static void checkHttpCompliance(MetaData.Request request, HttpCompliance for (String hostValue: hostValues) if (StringUtil.isBlank(hostValue)) assertAllowed(Violation.UNSAFE_HOST_HEADER, mode, listener); - String authority = request.getHttpURI().getHost(); - if (StringUtil.isBlank(authority)) - assertAllowed(Violation.UNSAFE_HOST_HEADER, mode, listener); seenHostHeader = true; } } diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/AuthorityCustomizer.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/AuthorityCustomizer.java new file mode 100644 index 000000000000..7587a0488289 --- /dev/null +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/AuthorityCustomizer.java @@ -0,0 +1,63 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http2.server; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.HostPort; +import org.eclipse.jetty.util.URIUtil; + +/** + *

A {@link HttpConfiguration.Customizer} that synthesizes the authority when the + * {@link HttpHeader#C_AUTHORITY} header is missing.

+ *

After customization, the synthesized authority is accessible via + * {@link HttpURI#getAuthority()} from the {@link Request} object.

+ *

The authority is synthesized from the {@code Host} header. + * If the {@code Host} header is also missing, it is synthesized using + * {@link Request#getServerName(Request)} and {@link Request#getServerPort(Request)}.

+ */ +public class AuthorityCustomizer implements HttpConfiguration.Customizer +{ + @Override + public Request customize(Request request, HttpFields.Mutable responseHeaders) + { + if (request.getConnectionMetaData().getHttpVersion().getVersion() < 20) + return request; + + HttpURI httpURI = request.getHttpURI(); + if (httpURI.hasAuthority() && !httpURI.getAuthority().isEmpty()) + return request; + + String hostPort = request.getHeaders().get(HttpHeader.HOST); + if (hostPort == null) + { + String host = Request.getServerName(request); + int port = URIUtil.normalizePortForScheme(httpURI.getScheme(), Request.getServerPort(request)); + hostPort = new HostPort(host, port).toString(); + } + + HttpURI newHttpURI = HttpURI.build(httpURI).authority(hostPort).asImmutable(); + return new Request.Wrapper(request) + { + @Override + public HttpURI getHttpURI() + { + return newHttpURI; + } + }; + } +} diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractServerTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractServerTest.java index 61b599c513f5..b3ea38d23b98 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractServerTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractServerTest.java @@ -41,6 +41,7 @@ public class AbstractServerTest { + protected HttpConfiguration httpConfig = new HttpConfiguration(); protected ServerConnector connector; protected ByteBufferPool bufferPool; protected Generator generator; @@ -49,14 +50,14 @@ public class AbstractServerTest protected void startServer(Handler handler) throws Exception { - prepareServer(new HTTP2ServerConnectionFactory(new HttpConfiguration())); + prepareServer(new HTTP2ServerConnectionFactory(httpConfig)); server.setHandler(handler); server.start(); } protected void startServer(ServerSessionListener listener) throws Exception { - prepareServer(new RawHTTP2ServerConnectionFactory(new HttpConfiguration(), listener)); + prepareServer(new RawHTTP2ServerConnectionFactory(httpConfig, listener)); server.start(); } diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AuthorityCustomizerTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AuthorityCustomizerTest.java new file mode 100644 index 000000000000..1518e3397f84 --- /dev/null +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AuthorityCustomizerTest.java @@ -0,0 +1,98 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http2.tests; + +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.PrefaceFrame; +import org.eclipse.jetty.http2.frames.SettingsFrame; +import org.eclipse.jetty.http2.parser.Parser; +import org.eclipse.jetty.http2.server.AuthorityCustomizer; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AuthorityCustomizerTest extends AbstractServerTest +{ + @Test + public void testSynthesizeAuthorityFromHost() throws Exception + { + startServer(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + int status = request.getHttpURI().hasAuthority() ? HttpStatus.OK_200 : HttpStatus.BAD_REQUEST_400; + response.setStatus(status); + callback.succeeded(); + return true; + } + }); + httpConfig.addCustomizer(new AuthorityCustomizer()); + + ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator(); + generator.control(accumulator, new PrefaceFrame()); + generator.control(accumulator, new SettingsFrame(new HashMap<>(), false)); + MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP.asString(), null, path, HttpVersion.HTTP_2, HttpFields.EMPTY, -1); + generator.control(accumulator, new HeadersFrame(1, metaData, null, true)); + + try (Socket client = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = client.getOutputStream(); + for (ByteBuffer buffer : accumulator.getByteBuffers()) + { + output.write(BufferUtil.toArray(buffer)); + } + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference frameRef = new AtomicReference<>(); + Parser parser = new Parser(bufferPool, 8192); + parser.init(new Parser.Listener() + { + @Override + public void onHeaders(HeadersFrame frame) + { + frameRef.set(frame); + latch.countDown(); + } + }); + parseResponse(client, parser); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + + HeadersFrame frame = frameRef.get(); + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + } +}