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\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("
"); 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() {