From 1984d2de11241508ac1bb622c79b34d1cb7fe824 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 6 Jan 2022 08:01:08 -0600 Subject: [PATCH] Issue #7277 - Allow `Request.getLocalName()` and `.getLocalPort()` to be overridden (#7357) * Issue #7277 - Allow `Request.getLocalName()` and `.getLocalPort()` to be overridden (#7316) * Introduce `HttpConfiguration.setServerAuthority(HostPort)` to influence `ServletRequest.getServerName()` and `ServletRequest.getServerPort()` * Introduce `HttpConfiguration.setLocalAddress(SocketAddress)` to influence `ServletRequest.getLocalName()`, `ServletRequest.getLocalPort()`, and `ServletRequest.getLocalAddr()` * Correcting Request URI logic on abs-uri without authority * Adding test cases Signed-off-by: Joakim Erdfelt --- .../jetty/server/AbstractConnector.java | 2 + .../org/eclipse/jetty/server/HttpChannel.java | 90 +- .../jetty/server/HttpConfiguration.java | 69 ++ .../org/eclipse/jetty/server/Request.java | 110 ++- .../jetty/server/DetectorConnectionTest.java | 3 + .../org/eclipse/jetty/server/DumpHandler.java | 2 + ...ttpConfigurationAuthorityOverrideTest.java | 766 ++++++++++++++++++ .../java/org/eclipse/jetty/util/HostPort.java | 14 + 8 files changed, 996 insertions(+), 60 deletions(-) create mode 100644 jetty-server/src/test/java/org/eclipse/jetty/server/HttpConfigurationAuthorityOverrideTest.java diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java index 0a020a44c01..8f9308bebb6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java @@ -156,6 +156,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co private long _shutdownIdleTimeout = 1000L; private String _defaultProtocol; private ConnectionFactory _defaultConnectionFactory; + /* The name used to link up virtual host configuration to named connectors */ private String _name; private int _acceptorPriorityDelta = -2; private boolean _accepting = true; @@ -348,6 +349,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co } _lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, _acceptors.length); + super.doStart(); for (int i = 0; i < _acceptors.length; i++) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 00303a36b32..27eb49ae20b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.EventListener; import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; @@ -97,7 +98,7 @@ public abstract class HttpChannel implements Runnable, HttpOutput.Interceptor public HttpChannel(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransport transport) { _connector = connector; - _configuration = configuration; + _configuration = Objects.requireNonNull(configuration); _endPoint = endPoint; _transport = transport; @@ -325,8 +326,81 @@ public abstract class HttpChannel implements Runnable, HttpOutput.Interceptor return _endPoint; } + /** + *

Return the local name of the connected channel.

+ * + *

+ * This is the host name after the connector is bound and the connection is accepted. + *

+ *

+ * Value can be overridden by {@link HttpConfiguration#setLocalAddress(SocketAddress)}. + *

+ *

+ * Note: some connectors are not based on IP networking, and default behavior here will + * result in a null return. Use {@link HttpConfiguration#setLocalAddress(SocketAddress)} + * to set the value to an acceptable host name. + *

+ * + * @return the local name, or null + */ + public String getLocalName() + { + HttpConfiguration httpConfiguration = getHttpConfiguration(); + if (httpConfiguration != null) + { + SocketAddress localAddress = httpConfiguration.getLocalAddress(); + if (localAddress instanceof InetSocketAddress) + return ((InetSocketAddress)localAddress).getHostName(); + } + + InetSocketAddress local = getLocalAddress(); + if (local != null) + return local.getHostString(); + + return null; + } + + /** + *

Return the Local Port of the connected channel.

+ * + *

+ * This is the port the connector is bound to and is accepting connections on. + *

+ *

+ * Value can be overridden by {@link HttpConfiguration#setLocalAddress(SocketAddress)}. + *

+ *

+ * Note: some connectors are not based on IP networking, and default behavior here will + * result in a value of 0 returned. Use {@link HttpConfiguration#setLocalAddress(SocketAddress)} + * to set the value to an acceptable port. + *

+ * + * @return the local port, or 0 if unspecified + */ + public int getLocalPort() + { + HttpConfiguration httpConfiguration = getHttpConfiguration(); + if (httpConfiguration != null) + { + SocketAddress localAddress = httpConfiguration.getLocalAddress(); + if (localAddress instanceof InetSocketAddress) + return ((InetSocketAddress)localAddress).getPort(); + } + + InetSocketAddress local = getLocalAddress(); + return local == null ? 0 : local.getPort(); + } + public InetSocketAddress getLocalAddress() { + HttpConfiguration httpConfiguration = getHttpConfiguration(); + if (httpConfiguration != null) + { + SocketAddress localAddress = httpConfiguration.getLocalAddress(); + if (localAddress instanceof InetSocketAddress) + return ((InetSocketAddress)localAddress); + } + SocketAddress local = _endPoint.getLocalSocketAddress(); if (local instanceof InetSocketAddress) return (InetSocketAddress)local; @@ -341,6 +415,18 @@ public abstract class HttpChannel implements Runnable, HttpOutput.Interceptor return null; } + /** + * @return return the HttpConfiguration server authority override + */ + public HostPort getServerAuthority() + { + HttpConfiguration httpConfiguration = getHttpConfiguration(); + if (httpConfiguration != null) + return httpConfiguration.getServerAuthority(); + + return null; + } + /** * If the associated response has the Expect header set to 100 Continue, * then accessing the input stream indicates that the handler/servlet @@ -551,7 +637,7 @@ public abstract class HttpChannel implements Runnable, HttpOutput.Interceptor // If send error is called we need to break. if (checkAndPrepareUpgrade()) break; - + // Set a close callback on the HttpOutput to make it an async callback _response.completeOutput(Callback.from(NON_BLOCKING, () -> _state.completed(null), _state::completed)); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index 2c73ff993c8..9adc05c2018 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.server; import java.io.IOException; +import java.net.SocketAddress; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; @@ -23,6 +24,7 @@ import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.util.HostPort; import org.eclipse.jetty.util.Index; import org.eclipse.jetty.util.Jetty; import org.eclipse.jetty.util.annotation.ManagedAttribute; @@ -75,6 +77,8 @@ public class HttpConfiguration implements Dumpable private CookieCompliance _responseCookieCompliance = CookieCompliance.RFC6265; private boolean _notifyRemoteAsyncErrors = true; private boolean _relativeRedirectAllowed; + private HostPort _serverAuthority; + private SocketAddress _localAddress; /** *

An interface that allows a request object to be customized @@ -145,6 +149,8 @@ public class HttpConfiguration implements Dumpable _notifyRemoteAsyncErrors = config._notifyRemoteAsyncErrors; _relativeRedirectAllowed = config._relativeRedirectAllowed; _uriCompliance = config._uriCompliance; + _serverAuthority = config._serverAuthority; + _localAddress = config._localAddress; } /** @@ -651,6 +657,69 @@ public class HttpConfiguration implements Dumpable return _relativeRedirectAllowed; } + /** + * Get the SocketAddress override to be reported as the local address of all connections + * + * @return Returns the connection local address override or null. + */ + @ManagedAttribute("Local SocketAddress override") + public SocketAddress getLocalAddress() + { + return _localAddress; + } + + /** + *

+ * Specify the connection local address used within application API layer + * when identifying the local host name/port of a connected endpoint. + *

+ *

+ * This allows an override of higher level APIs, such as + * {@code ServletRequest.getLocalName()}, {@code ServletRequest.getLocalAddr()}, + * and {@code ServletRequest.getLocalPort()}. + *

+ * + * @param localAddress the address to use for host/addr/port, or null to reset to default behavior + */ + public void setLocalAddress(SocketAddress localAddress) + { + _localAddress = localAddress; + } + + /** + * Get the Server authority override to be used if no authority is provided by a request. + * + * @return Returns the connection server authority (name/port) or null + */ + @ManagedAttribute("The server authority if none provided by requests") + public HostPort getServerAuthority() + { + return _serverAuthority; + } + + /** + *

+ * Specify the connection server authority (name/port) used within application API layer + * when identifying the server host name/port of a connected endpoint. + *

+ * + *

+ * This allows an override of higher level APIs, such as + * {@code ServletRequest.getServerName()}, and {@code ServletRequest.getServerPort()}. + *

+ * + * @param authority the authority host (and optional port), or null to reset to default behavior + */ + public void setServerAuthority(HostPort authority) + { + if (authority == null) + _serverAuthority = null; + else if (!authority.hasHost()) + throw new IllegalStateException("Server Authority must have host declared"); + else + _serverAuthority = authority; + } + @Override public String dump() { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 64d7a1c03fc..7239eae8b58 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -22,7 +22,6 @@ import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.UnknownHostException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; @@ -420,6 +419,7 @@ public class Request implements HttpServletRequest * A response is being committed for a session, * potentially write the session out before the * client receives the response. + * * @param session the session */ private void commitSession(Session session) @@ -978,63 +978,47 @@ public class Request implements HttpServletRequest @Override public String getLocalAddr() { - if (_channel == null) + if (_channel != null) { - try - { - String name = InetAddress.getLocalHost().getHostAddress(); - if (StringUtil.ALL_INTERFACES.equals(name)) - return null; - return formatAddrOrHost(name); - } - catch (UnknownHostException e) - { - LOG.trace("IGNORED", e); - return null; - } + InetSocketAddress local = _channel.getLocalAddress(); + if (local == null) + return ""; + InetAddress address = local.getAddress(); + String result = address == null + ? local.getHostString() + : address.getHostAddress(); + + return formatAddrOrHost(result); } - InetSocketAddress local = _channel.getLocalAddress(); - if (local == null) - return ""; - InetAddress address = local.getAddress(); - String result = address == null - ? local.getHostString() - : address.getHostAddress(); - return formatAddrOrHost(result); + return ""; } + /* + * @see javax.servlet.ServletRequest#getLocalName() + */ @Override public String getLocalName() { if (_channel != null) { - InetSocketAddress local = _channel.getLocalAddress(); - if (local != null) - return formatAddrOrHost(local.getHostString()); + String localName = _channel.getLocalName(); + return formatAddrOrHost(localName); } - try - { - String name = InetAddress.getLocalHost().getHostName(); - if (StringUtil.ALL_INTERFACES.equals(name)) - return null; - return formatAddrOrHost(name); - } - catch (UnknownHostException e) - { - LOG.trace("IGNORED", e); - } - return null; + return ""; // not allowed to be null } @Override public int getLocalPort() { - if (_channel == null) - return 0; - InetSocketAddress local = _channel.getLocalAddress(); - return local == null ? 0 : local.getPort(); + if (_channel != null) + { + int localPort = _channel.getLocalPort(); + if (localPort > 0) + return localPort; + } + return 0; } @Override @@ -1321,32 +1305,38 @@ public class Request implements HttpServletRequest @Override public String getServerName() { - return _uri == null ? findServerName() : formatAddrOrHost(_uri.getHost()); + if ((_uri != null) && StringUtil.isNotBlank(_uri.getAuthority())) + return formatAddrOrHost(_uri.getHost()); + else + return findServerName(); } private String findServerName() { + if (_channel != null) + { + HostPort serverAuthority = _channel.getServerAuthority(); + if (serverAuthority != null) + return formatAddrOrHost(serverAuthority.getHost()); + } + // Return host from connection String name = getLocalName(); if (name != null) return formatAddrOrHost(name); - // Return the local host - try - { - return formatAddrOrHost(InetAddress.getLocalHost().getHostAddress()); - } - catch (UnknownHostException e) - { - LOG.trace("IGNORED", e); - } - return null; + return ""; // not allowed to be null } @Override public int getServerPort() { - int port = _uri == null ? -1 : _uri.getPort(); + int port = -1; + + if ((_uri != null) && StringUtil.isNotBlank(_uri.getAuthority())) + port = _uri.getPort(); + else + port = findServerPort(); // If no port specified, return the default port for the scheme if (port <= 0) @@ -1358,11 +1348,15 @@ public class Request implements HttpServletRequest private int findServerPort() { - // Return host from connection if (_channel != null) - return getLocalPort(); + { + HostPort serverAuthority = _channel.getServerAuthority(); + if (serverAuthority != null) + return serverAuthority.getPort(); + } - return -1; + // Return host from connection + return getLocalPort(); } @Override @@ -1532,7 +1526,7 @@ public class Request implements HttpServletRequest _session = _sessionHandler.newHttpSession(this); if (_session == null) throw new IllegalStateException("Create session failed"); - + HttpCookie cookie = _sessionHandler.getSessionCookie(_session, getContextPath(), isSecure()); if (cookie != null) _channel.getResponse().replaceCookie(cookie); @@ -2563,7 +2557,7 @@ public class Request implements HttpServletRequest // which we recover from the IncludeAttributes wrapper. return findServletPathMapping(); } - + private String formatAddrOrHost(String name) { return _channel == null ? HostPort.normalizeHost(name) : _channel.formatAddrOrHost(name); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/DetectorConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/DetectorConnectionTest.java index 5c353f4155a..bba525eb360 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/DetectorConnectionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/DetectorConnectionTest.java @@ -237,6 +237,9 @@ public class DetectorConnectionTest assertThat(response, Matchers.containsString("HTTP/1.1 200")); assertThat(response, Matchers.containsString("pathInfo=/path")); + assertThat(response, Matchers.containsString("servername=server")); + assertThat(response, Matchers.containsString("serverport=80")); + assertThat(response, Matchers.containsString("localname=5.6.7.8")); assertThat(response, Matchers.containsString("local=5.6.7.8:222")); assertThat(response, Matchers.containsString("remote=1.2.3.4:111")); } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java b/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java index 33241a2345c..d15bdb204e7 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java @@ -105,6 +105,8 @@ public class DumpHandler extends AbstractHandler writer.write("
\ncontentType=" + request.getContentType() + "\n
\n"); writer.write("
\nencoding=" + request.getCharacterEncoding() + "\n
\n"); writer.write("
\nservername=" + request.getServerName() + "\n
\n"); + writer.write("
\nserverport=" + request.getServerPort() + "\n
\n"); + writer.write("
\nlocalname=" + request.getLocalName() + "\n
\n"); writer.write("
\nlocal=" + request.getLocalAddr() + ":" + request.getLocalPort() + "\n
\n"); writer.write("
\nremote=" + request.getRemoteAddr() + ":" + request.getRemotePort() + "\n
\n"); writer.write("

Header:

");
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConfigurationAuthorityOverrideTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConfigurationAuthorityOverrideTest.java
new file mode 100644
index 00000000000..5ba9338e7cd
--- /dev/null
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConfigurationAuthorityOverrideTest.java
@@ -0,0 +1,766 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 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.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.util.HostPort;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class HttpConfigurationAuthorityOverrideTest
+{
+    @Test
+    public void testLocalAuthorityHttp10NoHostDump() throws Exception
+    {
+        InetSocketAddress localAddress = InetSocketAddress.createUnresolved("foo.local.name", 80);
+
+        try (CloseableServer server = startServer(null, localAddress))
+        {
+            String rawRequest = "GET /dump HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[foo.local.name]"),
+                containsString("ServerPort=[80]"),
+                containsString("LocalAddr=[foo.local.name]"),
+                containsString("LocalName=[foo.local.name]"),
+                containsString("LocalPort=[80]"),
+                containsString("RequestURL=[http://foo.local.name/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testLocalAuthorityHttp10NoHostRedirect() throws Exception
+    {
+        InetSocketAddress localAddress = InetSocketAddress.createUnresolved("foo.local.name", 80);
+
+        try (CloseableServer server = startServer(null, localAddress))
+        {
+            String rawRequest = "GET /redirect HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://foo.local.name/dump"));
+        }
+    }
+
+    @Test
+    public void testLocalAuthorityHttp10NotFound() throws Exception
+    {
+        InetSocketAddress localAddress = InetSocketAddress.createUnresolved("foo.local.name", 777);
+
+        try (CloseableServer server = startServer(null, localAddress))
+        {
+            String rawRequest = "GET /bogus HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            // because of the custom error handler, we actually expect a redirect
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://foo.local.name:777/error"));
+        }
+    }
+
+    @Test
+    public void testLocalAuthorityHttp11EmptyHostDump() throws Exception
+    {
+        InetSocketAddress localAddress = InetSocketAddress.createUnresolved("foo.local.name", 80);
+
+        try (CloseableServer server = startServer(null, localAddress))
+        {
+            String rawRequest = "GET /dump HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[foo.local.name]"),
+                containsString("ServerPort=[80]"),
+                containsString("LocalAddr=[foo.local.name]"),
+                containsString("LocalName=[foo.local.name]"),
+                containsString("LocalPort=[80]"),
+                containsString("RequestURL=[http://foo.local.name/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testLocalAuthorityHttp11EmptyHostRedirect() throws Exception
+    {
+        InetSocketAddress localAddress = InetSocketAddress.createUnresolved("foo.local.name", 80);
+
+        try (CloseableServer server = startServer(null, localAddress))
+        {
+            String rawRequest = "GET /redirect HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connect: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://foo.local.name/dump"));
+        }
+    }
+
+    @Test
+    public void testLocalAuthorityHttp11EmptyHostAbsUriDump() throws Exception
+    {
+        InetSocketAddress localAddress = InetSocketAddress.createUnresolved("bar.local.name", 9999);
+
+        try (CloseableServer server = startServer(null, localAddress))
+        {
+            String rawRequest = "GET mobile:///dump HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[bar.local.name]"),
+                containsString("ServerPort=[9999]"),
+                containsString("LocalAddr=[bar.local.name]"),
+                containsString("LocalName=[bar.local.name]"),
+                containsString("LocalPort=[9999]"),
+                containsString("RequestURL=[mobile://bar.local.name:9999/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testLocalAuthorityHttp11ValidHostDump() throws Exception
+    {
+        InetSocketAddress localAddress = InetSocketAddress.createUnresolved("zed.local.name", 9999);
+
+        try (CloseableServer server = startServer(null, localAddress))
+        {
+            String rawRequest = "GET /dump HTTP/1.1\r\n" +
+                "Host: jetty.eclipse.org:8888\r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[jetty.eclipse.org]"),
+                containsString("ServerPort=[8888]"),
+                containsString("LocalAddr=[zed.local.name]"),
+                containsString("LocalName=[zed.local.name]"),
+                containsString("LocalPort=[9999]"),
+                containsString("RequestURL=[http://jetty.eclipse.org:8888/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testLocalAuthorityHttp11ValidHostRedirect() throws Exception
+    {
+        InetSocketAddress localAddress = InetSocketAddress.createUnresolved("zed.local.name", 9999);
+
+        try (CloseableServer server = startServer(null, localAddress))
+        {
+            String rawRequest = "GET /redirect HTTP/1.1\r\n" +
+                "Host: jetty.eclipse.org:8888\r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://jetty.eclipse.org:8888/dump"));
+        }
+    }
+
+    @Test
+    public void testServerAuthorityNoPortHttp11EmptyHostDump() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("foo.server.authority");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /dump HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[foo.server.authority]"),
+                containsString("ServerPort=[80]"),
+                // expect default locals
+                containsString("LocalAddr=[" + server.getConnectorLocalAddr() + "]"),
+                containsString("LocalName=[" + server.getConnectorLocalName() + "]"),
+                containsString("LocalPort=[" + server.getConnectorLocalPort() + "]"),
+                containsString("RequestURL=[http://foo.server.authority/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testServerAuthorityNoPortHttp11EmptyHostRedirect() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("foo.server.authority");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /redirect HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connect: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://foo.server.authority/dump"));
+        }
+    }
+
+    @Test
+    public void testServerAuthorityWithPortHttp11EmptyHostDump() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("foo.server.authority:7777");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /dump HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[foo.server.authority]"),
+                containsString("ServerPort=[7777]"),
+                // expect default locals
+                containsString("LocalAddr=[" + server.getConnectorLocalAddr() + "]"),
+                containsString("LocalName=[" + server.getConnectorLocalName() + "]"),
+                containsString("LocalPort=[" + server.getConnectorLocalPort() + "]"),
+                containsString("RequestURL=[http://foo.server.authority:7777/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testServerAuthorityWithPortHttp11EmptyHostRedirect() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("foo.server.authority:7777");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /redirect HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connect: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://foo.server.authority:7777/dump"));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityNoPortHttp10NoHostDump() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("foo.server.authority");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /dump HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[foo.server.authority]"),
+                containsString("ServerPort=[80]"),
+                // expect default locals
+                containsString("LocalAddr=[" + server.getConnectorLocalAddr() + "]"),
+                containsString("LocalName=[" + server.getConnectorLocalName() + "]"),
+                containsString("LocalPort=[" + server.getConnectorLocalPort() + "]"),
+                containsString("RequestURL=[http://foo.server.authority/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityNoPortHttp10NoHostRedirect() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("foo.server.authority");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /redirect HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://foo.server.authority/dump"));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityNoPortHttp10NotFound() throws Exception
+    {
+        HostPort severUriAuthority = new HostPort("foo.server.authority");
+
+        try (CloseableServer server = startServer(severUriAuthority, null))
+        {
+            String rawRequest = "GET /bogus HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            // because of the custom error handler, we actually expect a redirect
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://foo.server.authority/error"));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityNoPortHttp10PathError() throws Exception
+    {
+        HostPort severUriAuthority = new HostPort("foo.server.authority");
+
+        try (CloseableServer server = startServer(severUriAuthority, null))
+        {
+            String rawRequest = "GET /%00 HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.BAD_REQUEST_400));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityNoPortHttp11ValidHostDump() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("zed.server.authority");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /dump HTTP/1.1\r\n" +
+                "Host: jetty.eclipse.org:8888\r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[jetty.eclipse.org]"),
+                containsString("ServerPort=[8888]"),
+                // expect default locals
+                containsString("LocalAddr=[" + server.getConnectorLocalAddr() + "]"),
+                containsString("LocalName=[" + server.getConnectorLocalName() + "]"),
+                containsString("LocalPort=[" + server.getConnectorLocalPort() + "]"),
+                containsString("RequestURL=[http://jetty.eclipse.org:8888/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityNoPortHttp11ValidHostRedirect() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("zed.local.name");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /redirect HTTP/1.1\r\n" +
+                "Host: jetty.eclipse.org:8888\r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://jetty.eclipse.org:8888/dump"));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityWithPortHttp10NoHostDump() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("bar.server.authority:9999");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /dump HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[bar.server.authority]"),
+                containsString("ServerPort=[9999]"),
+                // expect default locals
+                containsString("LocalAddr=[" + server.getConnectorLocalAddr() + "]"),
+                containsString("LocalName=[" + server.getConnectorLocalName() + "]"),
+                containsString("LocalPort=[" + server.getConnectorLocalPort() + "]"),
+                containsString("RequestURL=[http://bar.server.authority:9999/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityWithPortHttp10NoHostRedirect() throws Exception
+    {
+        HostPort severUriAuthority = new HostPort("foo.server.authority:9999");
+
+        try (CloseableServer server = startServer(severUriAuthority, null))
+        {
+            String rawRequest = "GET /redirect HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://foo.server.authority:9999/dump"));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityWithPortHttp10NotFound() throws Exception
+    {
+        HostPort severUriAuthority = new HostPort("foo.server.authority:7777");
+
+        try (CloseableServer server = startServer(severUriAuthority, null))
+        {
+            String rawRequest = "GET /bogus HTTP/1.0\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            // because of the custom error handler, we actually expect a redirect
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://foo.server.authority:7777/error"));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityWithPortHttp11ValidHostDump() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("zed.server.authority:7777");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /dump HTTP/1.1\r\n" +
+                "Host: jetty.eclipse.org:8888\r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[jetty.eclipse.org]"),
+                containsString("ServerPort=[8888]"),
+                // expect default locals
+                containsString("LocalAddr=[" + server.getConnectorLocalAddr() + "]"),
+                containsString("LocalName=[" + server.getConnectorLocalName() + "]"),
+                containsString("LocalPort=[" + server.getConnectorLocalPort() + "]"),
+                containsString("RequestURL=[http://jetty.eclipse.org:8888/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityWithPortHttp11EmptyHostAbsUriDump() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("zed.server.authority:7777");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET mobile:///dump HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[zed.server.authority]"),
+                containsString("ServerPort=[7777]"),
+                // expect default locals
+                containsString("LocalAddr=[" + server.getConnectorLocalAddr() + "]"),
+                containsString("LocalName=[" + server.getConnectorLocalName() + "]"),
+                containsString("LocalPort=[" + server.getConnectorLocalPort() + "]"),
+                containsString("RequestURL=[mobile://zed.server.authority:7777/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testServerUriAuthorityWithPortHttp11ValidHostRedirect() throws Exception
+    {
+        HostPort serverUriAuthority = new HostPort("zed.local.name:7777");
+
+        try (CloseableServer server = startServer(serverUriAuthority, null))
+        {
+            String rawRequest = "GET /redirect HTTP/1.1\r\n" +
+                "Host: jetty.eclipse.org:8888\r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://jetty.eclipse.org:8888/dump"));
+        }
+    }
+
+    @Test
+    public void testUnsetAuthoritiesHttp11EmptyHostDump() throws Exception
+    {
+        try (CloseableServer server = startServer(null, null))
+        {
+            String rawRequest = "GET /dump HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat("response.status", response.getStatus(), is(200));
+            String responseContent = response.getContent();
+            assertThat("response content", responseContent, allOf(
+                containsString("ServerName=[" + server.getConnectorLocalName() + "]"),
+                containsString("ServerPort=[" + server.getConnectorLocalPort() + "]"),
+                // expect default locals
+                containsString("LocalAddr=[" + server.getConnectorLocalAddr() + "]"),
+                containsString("LocalName=[" + server.getConnectorLocalName() + "]"),
+                containsString("LocalPort=[" + server.getConnectorLocalPort() + "]"),
+                containsString("RequestURL=[http://" + server.getConnectorLocalName() + ":" + server.getConnectorLocalPort() + "/dump]")
+            ));
+        }
+    }
+
+    @Test
+    public void testUnsetAuthoritiesHttp11EmptyHostRedirect() throws Exception
+    {
+        try (CloseableServer server = startServer(null, null))
+        {
+            String rawRequest = "GET /redirect HTTP/1.1\r\n" +
+                "Host: \r\n" +
+                "Connection: close\r\n" +
+                "\r\n";
+
+            HttpTester.Response response = issueRequest(server, rawRequest);
+
+            assertThat(response.getStatus(), is(HttpStatus.MOVED_TEMPORARILY_302));
+            String location = response.get(HttpHeader.LOCATION);
+            assertThat(location, is("http://" + server.getConnectorLocalName() + ":" + server.getConnectorLocalPort() + "/dump"));
+        }
+    }
+
+    private HttpTester.Response issueRequest(CloseableServer server, String rawRequest) throws Exception
+    {
+        try (Socket socket = new Socket("localhost", server.getConnectorLocalPort());
+             OutputStream output = socket.getOutputStream();
+             InputStream input = socket.getInputStream())
+        {
+            output.write(rawRequest.getBytes(StandardCharsets.UTF_8));
+            output.flush();
+
+            HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(input));
+            assertNotNull(response, "response");
+            return response;
+        }
+    }
+
+    private CloseableServer startServer(HostPort serverUriAuthority, InetSocketAddress localAddress) throws Exception
+    {
+        Server server = new Server();
+
+        HttpConfiguration httpConfiguration = new HttpConfiguration();
+        if (serverUriAuthority != null)
+            httpConfiguration.setServerAuthority(serverUriAuthority);
+        if (localAddress != null)
+            httpConfiguration.setLocalAddress(localAddress);
+
+        ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
+        connector.setPort(0);
+        server.addConnector(connector);
+
+        HandlerList handlers = new HandlerList();
+        handlers.addHandler(new RedirectHandler());
+        handlers.addHandler(new DumpHandler());
+        handlers.addHandler(new ErrorMsgHandler());
+        server.setHandler(handlers);
+
+        server.setErrorHandler(new RedirectErrorHandler());
+        server.start();
+
+        return new CloseableServer(server, connector);
+    }
+
+    private static class DumpHandler extends AbstractHandler
+    {
+        @Override
+        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+        {
+            if (target.startsWith("/dump"))
+            {
+                baseRequest.setHandled(true);
+                response.setCharacterEncoding("utf-8");
+                response.setContentType("text/plain");
+                PrintWriter out = response.getWriter();
+                out.printf("ServerName=[%s]%n", request.getServerName());
+                out.printf("ServerPort=[%d]%n", request.getServerPort());
+                out.printf("LocalName=[%s]%n", request.getLocalName());
+                out.printf("LocalAddr=[%s]%n", request.getLocalAddr());
+                out.printf("LocalPort=[%s]%n", request.getLocalPort());
+                out.printf("RequestURL=[%s]%n", request.getRequestURL());
+            }
+        }
+    }
+
+    private static class RedirectHandler extends AbstractHandler
+    {
+        @Override
+        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+        {
+            if (target.startsWith("/redirect"))
+            {
+                baseRequest.setHandled(true);
+                response.sendRedirect("/dump");
+            }
+        }
+    }
+
+    private static class ErrorMsgHandler extends AbstractHandler
+    {
+        @Override
+        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+        {
+            if (target.startsWith("/error"))
+            {
+                baseRequest.setHandled(true);
+                response.setCharacterEncoding("utf-8");
+                response.setContentType("text/plain");
+                response.getWriter().println("Generic Error Page.");
+            }
+        }
+    }
+
+    public static class RedirectErrorHandler extends ErrorHandler
+    {
+        @Override
+        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+        {
+            response.sendRedirect("/error");
+        }
+    }
+
+    private static class CloseableServer implements AutoCloseable
+    {
+        private final Server server;
+        private final ServerConnector connector;
+
+        public CloseableServer(Server server, ServerConnector connector)
+        {
+            this.server = Objects.requireNonNull(server, "Server");
+            this.connector = Objects.requireNonNull(connector, "Connector");
+        }
+
+        @Override
+        public void close() throws Exception
+        {
+            LifeCycle.stop(this.server);
+        }
+
+        public String getConnectorLocalAddr()
+        {
+            return "127.0.0.1";
+        }
+
+        public String getConnectorLocalName()
+        {
+            return HostPort.normalizeHost(getConnectorLocalAddr());
+        }
+
+        public int getConnectorLocalPort()
+        {
+            return this.connector.getLocalPort();
+        }
+    }
+}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java b/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java
index b0980c9617f..6ded2ffa1e1 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java
@@ -13,6 +13,8 @@
 
 package org.eclipse.jetty.util;
 
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+
 /**
  * 

Parse an authority string (in the form {@code host:port}) into * {@code host} and {@code port}, handling IPv4 and IPv6 host formats @@ -99,6 +101,7 @@ public class HostPort * * @return the host */ + @ManagedAttribute("host") public String getHost() { return _host; @@ -109,6 +112,7 @@ public class HostPort * * @return the port */ + @ManagedAttribute("port") public int getPort() { return _port; @@ -125,6 +129,16 @@ public class HostPort return _port > 0 ? _port : defaultPort; } + public boolean hasHost() + { + return StringUtil.isNotBlank(_host); + } + + public boolean hasPort() + { + return _port > 0; + } + @Override public String toString() {