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 28353a0681d7..0b3cb1f173e4 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 @@ -161,6 +161,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co private long _idleTimeout = 30000; 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; @@ -318,6 +319,7 @@ protected void doStart() throws Exception } _lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, _acceptors.length); + super.doStart(); _stopping = new CountDownLatch(_acceptors.length); 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 492c23cf5ab1..5187082c66b9 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,10 +20,12 @@ import java.io.IOException; import java.net.InetSocketAddress; +import java.net.SocketAddress; 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; @@ -106,7 +108,7 @@ public 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; @@ -298,8 +300,81 @@ public EndPoint getEndPoint() 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); + } + return _endPoint.getLocalAddress(); } @@ -308,6 +383,18 @@ public InetSocketAddress getRemoteAddress() return _endPoint.getRemoteAddress(); } + /** + * @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 @@ -545,7 +632,7 @@ public boolean handle() 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)); @@ -911,7 +998,7 @@ public boolean sendResponse(MetaData.Response info, ByteBuffer content, boolean commit(info); _combinedListener.onResponseBegin(_request); _request.onResponseCommit(); - + // wrap callback to process 100 responses final int status = info.getStatus(); final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100) 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 e932f787a544..5fc41bb2f6d0 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 @@ -19,6 +19,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; @@ -26,6 +27,7 @@ import org.eclipse.jetty.http.CookieCompliance; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.util.HostPort; import org.eclipse.jetty.util.Jetty; import org.eclipse.jetty.util.TreeTrie; import org.eclipse.jetty.util.Trie; @@ -77,6 +79,8 @@ public class HttpConfiguration implements Dumpable private MultiPartFormDataCompliance _multiPartCompliance = MultiPartFormDataCompliance.LEGACY; // TODO change default in jetty-10 private boolean _notifyRemoteAsyncErrors = true; private boolean _relativeRedirectAllowed; + private HostPort _serverAuthority; + private SocketAddress _localAddress; /** *

An interface that allows a request object to be customized @@ -144,6 +148,8 @@ public HttpConfiguration(HttpConfiguration config) _multiPartCompliance = config._multiPartCompliance; _notifyRemoteAsyncErrors = config._notifyRemoteAsyncErrors; _relativeRedirectAllowed = config._relativeRedirectAllowed; + _serverAuthority = config._serverAuthority; + _localAddress = config._localAddress; } /** @@ -662,6 +668,69 @@ public boolean isRelativeRedirectAllowed() 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 7e7820cb4e10..e056a517040b 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 @@ -27,7 +27,6 @@ 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; @@ -373,10 +372,10 @@ public void addEventListener(final EventListener listener) if (listener instanceof AsyncListener) throw new IllegalArgumentException(listener.getClass().toString()); } - + /** * Remember a session that this request has just entered. - * + * * @param s the session */ public void enterSession(HttpSession s) @@ -404,9 +403,10 @@ private void leaveSession(Session session) } /** - * A response is being committed for a session, + * 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) @@ -991,30 +991,20 @@ public Enumeration getLocales() @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.ignore(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 ""; } /* @@ -1025,23 +1015,11 @@ 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.ignore(e); - } - return null; + return ""; // not allowed to be null } /* @@ -1050,10 +1028,13 @@ public String getLocalName() @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; } /* @@ -1428,21 +1409,19 @@ 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.ignore(e); - } - return null; + return ""; // not allowed to be null } /* @@ -1458,9 +1437,10 @@ public int getServerPort() // If no port specified, return the default port for the scheme if (port <= 0) { - if (getScheme().equalsIgnoreCase(URIUtil.HTTPS)) + if (HttpScheme.HTTPS.is(getScheme())) return 443; - return 80; + else + return 80; } // return a specific port @@ -1474,19 +1454,25 @@ private int findServerPort() HttpField host = metadata == null ? null : metadata.getFields().getField(HttpHeader.HOST); if (host != null) { - // TODO is this needed now? HostPortHttpField authority = (host instanceof HostPortHttpField) ? ((HostPortHttpField)host) : new HostPortHttpField(host.getValue()); - metadata.getURI().setAuthority(authority.getHost(), authority.getPort()); - return authority.getPort(); + if (authority.getHostPort().hasHost() && authority.getHostPort().hasPort()) + { + metadata.getURI().setAuthority(authority.getHost(), authority.getPort()); + return authority.getPort(); + } } - // 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 @@ -1551,7 +1537,7 @@ public void onCompleted() leaveSession(s); } } - + /** * Called when a response is about to be committed, ie sent * back to the client @@ -1567,7 +1553,7 @@ public void onResponseCommit() /** * Find a session that this request has already entered for the - * given SessionHandler + * given SessionHandler * * @param sessionHandler the SessionHandler (ie context) to check * @return @@ -1576,9 +1562,9 @@ public HttpSession getSession(SessionHandler sessionHandler) { if (_sessions == null || _sessions.size() == 0 || sessionHandler == null) return null; - + HttpSession session = null; - + for (HttpSession s:_sessions) { Session ss = Session.class.cast(s); @@ -1591,7 +1577,7 @@ public HttpSession getSession(SessionHandler sessionHandler) } return session; } - + /* * @see javax.servlet.http.HttpServletRequest#getSession() */ @@ -1627,7 +1613,7 @@ public HttpSession getSession(boolean create) _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); 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 fdfa5cceb923..d1b20aa06622 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 @@ -244,6 +244,9 @@ public void testDetectingSslProxyToHttpNoSslWithProxy() throws Exception 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 d7da2576f530..bc88c4c3093e 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 @@ -110,6 +110,8 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques 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 000000000000..47e4a9bcfb09
--- /dev/null
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConfigurationAuthorityOverrideTest.java
@@ -0,0 +1,771 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+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 9888e2e9d646..5b9fa16ce1c4 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
@@ -18,6 +18,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 @@ -104,6 +106,7 @@ else if (authority.charAt(0) == '[') * * @return the host */ + @ManagedAttribute("host") public String getHost() { return _host; @@ -114,6 +117,7 @@ public String getHost() * * @return the port */ + @ManagedAttribute("port") public int getPort() { return _port; @@ -130,6 +134,16 @@ public int getPort(int defaultPort) return _port > 0 ? _port : defaultPort; } + public boolean hasHost() + { + return StringUtil.isNotBlank(_host); + } + + public boolean hasPort() + { + return _port > 0; + } + @Override public String toString() {