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:
parent
497fbf7137
commit
87324a70b6
|
@ -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`
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue