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 <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2024-07-30 17:52:55 +03:00 committed by GitHub
parent 497fbf7137
commit 87324a70b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 176 additions and 5 deletions

View File

@ -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]. 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 <<request-customizer-forwarded,`Forwarded` HTTP header>> and the <<connector-protocol-proxy-http11,PROXY protocol>>.
The synthesized authority will be exposed as the `Request` URI authority via `Request.getHttpURI().getAuthority()`.
[[request-customizer-proxy]] [[request-customizer-proxy]]
=== `ProxyCustomizer` === `ProxyCustomizer`

View File

@ -402,9 +402,6 @@ public final class HttpCompliance implements ComplianceViolation.Mode
for (String hostValue: hostValues) for (String hostValue: hostValues)
if (StringUtil.isBlank(hostValue)) if (StringUtil.isBlank(hostValue))
assertAllowed(Violation.UNSAFE_HOST_HEADER, mode, listener); 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; seenHostHeader = true;
} }
} }

View File

@ -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;
/**
* <p>A {@link HttpConfiguration.Customizer} that synthesizes the authority when the
* {@link HttpHeader#C_AUTHORITY} header is missing.</p>
* <p>After customization, the synthesized authority is accessible via
* {@link HttpURI#getAuthority()} from the {@link Request} object.</p>
* <p>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)}.</p>
*/
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;
}
};
}
}

View File

@ -41,6 +41,7 @@ import org.junit.jupiter.api.AfterEach;
public class AbstractServerTest public class AbstractServerTest
{ {
protected HttpConfiguration httpConfig = new HttpConfiguration();
protected ServerConnector connector; protected ServerConnector connector;
protected ByteBufferPool bufferPool; protected ByteBufferPool bufferPool;
protected Generator generator; protected Generator generator;
@ -49,14 +50,14 @@ public class AbstractServerTest
protected void startServer(Handler handler) throws Exception protected void startServer(Handler handler) throws Exception
{ {
prepareServer(new HTTP2ServerConnectionFactory(new HttpConfiguration())); prepareServer(new HTTP2ServerConnectionFactory(httpConfig));
server.setHandler(handler); server.setHandler(handler);
server.start(); server.start();
} }
protected void startServer(ServerSessionListener listener) throws Exception protected void startServer(ServerSessionListener listener) throws Exception
{ {
prepareServer(new RawHTTP2ServerConnectionFactory(new HttpConfiguration(), listener)); prepareServer(new RawHTTP2ServerConnectionFactory(httpConfig, listener));
server.start(); server.start();
} }

View File

@ -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<HeadersFrame> 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());
}
}
}