diff --git a/jetty-server/src/main/config/etc/jetty-inetaccess.xml b/jetty-server/src/main/config/etc/jetty-inetaccess.xml new file mode 100644 index 00000000000..d68009c246e --- /dev/null +++ b/jetty-server/src/main/config/etc/jetty-inetaccess.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-server/src/main/config/modules/inetaccess.mod b/jetty-server/src/main/config/modules/inetaccess.mod new file mode 100644 index 00000000000..64eb5856df4 --- /dev/null +++ b/jetty-server/src/main/config/modules/inetaccess.mod @@ -0,0 +1,14 @@ +DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html + +[description] +Enable the InetAccessHandler to apply a include/exclude +control of the remote IP of requests. + +[tags] +handler + +[depend] +server + +[xml] +etc/jetty-inetaccess.xml diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IPAccessHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IPAccessHandler.java index c8119014e57..3498572a63e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IPAccessHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IPAccessHandler.java @@ -32,6 +32,7 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.IPAddressMap; +import org.eclipse.jetty.util.component.DumpableCollection; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -347,40 +348,11 @@ public class IPAccessHandler extends HandlerWrapper /** * Dump the handler configuration */ - @Override - public String dump() + public void dump(Appendable out, String indent) throws IOException { - StringBuilder buf = new StringBuilder(); - - buf.append(toString()); - buf.append(" WHITELIST:\n"); - dump(buf, _white); - buf.append(toString()); - buf.append(" BLACKLIST:\n"); - dump(buf, _black); - - return buf.toString(); - } - - /* ------------------------------------------------------------ */ - /** - * Dump a pattern map into a StringBuilder buffer - * - * @param buf buffer - * @param patternMap pattern map to dump - */ - protected void dump(StringBuilder buf, PathMap> patternMap) - { - for (String path: patternMap.keySet()) - { - for (String addr: patternMap.get(path).keySet()) - { - buf.append("# "); - buf.append(addr); - buf.append("|"); - buf.append(path); - buf.append("\n"); - } - } + dumpObjects(out, indent, + DumpableCollection.from("white", _white), + DumpableCollection.from("black", _black), + DumpableCollection.from("whiteListByPath", _whiteListByPath)); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java index 0573f237286..0bd6def29a6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java @@ -30,6 +30,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.IncludeExclude; import org.eclipse.jetty.util.IncludeExcludeSet; import org.eclipse.jetty.util.InetAddressSet; import org.eclipse.jetty.util.component.DumpableCollection; @@ -39,16 +40,32 @@ import org.eclipse.jetty.util.log.Logger; /** * InetAddress Access Handler *

- * Controls access to the wrapped handler using the real remote IP. Control is provided - * by and {@link IncludeExcludeSet} over a {@link InetAddressSet}. This handler - * uses the real internet address of the connection, not one reported in the forwarded - * for headers, as this cannot be as easily forged. + * Controls access to the wrapped handler using the real remote IP. Control is + * provided by and {@link IncludeExcludeSet} over a {@link InetAddressSet}. This + * handler uses the real internet address of the connection, not one reported in + * the forwarded for headers, as this cannot be as easily forged. + *

+ * Additionally, there may be times when you want to only apply this handler to + * a subset of your connectors. In this situation you can use + * connectorNames to specify the connector names that you want this IP + * access filter to apply to. */ public class InetAccessHandler extends HandlerWrapper { private static final Logger LOG = Log.getLogger(InetAccessHandler.class); private final IncludeExcludeSet _set = new IncludeExcludeSet<>(InetAddressSet.class); + private final IncludeExclude _connectorNames = new IncludeExclude<>(); + + /** + * Clears all the includes, excludes, included connector names and excluded + * connector names. + */ + public void clear() + { + _set.clear(); + _connectorNames.clear(); + } /** * Includes an InetAddress pattern @@ -94,11 +111,52 @@ public class InetAccessHandler extends HandlerWrapper _set.exclude(patterns); } + /** + * Includes a connector name. + * + * @param name Connector name to include in this handler. + */ + public void includeConnectorName(String name) + { + _connectorNames.include(name); + } + + /** + * Excludes a connector name. + * + * @param name Connector name to exclude in this handler. + */ + public void excludeConnectorName(String name) + { + _connectorNames.exclude(name); + } + + /** + * Includes connector names. + * + * @param names Connector names to include in this handler. + */ + public void includeConnectorNames(String... names) + { + _connectorNames.include(names); + } + + /** + * Excludes connector names. + * + * @param names Connector names to exclude in this handler. + */ + public void excludeConnectorNames(String... names) + { + _connectorNames.exclude(names); + } + /** * Checks the incoming request against the whitelist and blacklist */ @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { // Get the real remote IP (not the one set by the forwarded headers (which may be forged)) HttpChannel channel = baseRequest.getHttpChannel(); @@ -108,7 +166,7 @@ public class InetAccessHandler extends HandlerWrapper if (endp != null) { InetSocketAddress address = endp.getRemoteAddress(); - if (address != null && !isAllowed(address.getAddress(), request)) + if (address != null && !isAllowed(address.getAddress(), baseRequest, request)) { response.sendError(HttpStatus.FORBIDDEN_403); baseRequest.setHandled(true); @@ -123,23 +181,46 @@ public class InetAccessHandler extends HandlerWrapper /** * Checks if specified address and request are allowed by current InetAddress rules. * - * @param address the inetAddress to check - * @param request the request to check + * @param address the inetAddress to check + * @param baseRequest the base request to check + * @param request the HttpServletRequest request to check * @return true if inetAddress and request are allowed */ - protected boolean isAllowed(InetAddress address, HttpServletRequest request) + protected boolean isAllowed(InetAddress address, Request baseRequest, HttpServletRequest request) { - boolean allowed = _set.test(address); + String connectorName = baseRequest.getHttpChannel().getConnector().getName(); + boolean allowed = !isMatchingConnectorName(connectorName) || _set.test(address); if (LOG.isDebugEnabled()) LOG.debug("{} {} {} for {}", this, allowed ? "allowed" : "denied", address, request); return allowed; } + /** + * Checks if this is a connector name that applies to this access handler. + * + * @return true if connector name is applicable given connectorNames property + */ + protected boolean isMatchingConnectorName(String connectorName) + { + boolean hasConnectorNames = !_connectorNames.getIncluded().isEmpty(); + if (connectorName == null) + { + return !hasConnectorNames; + } + if (hasConnectorNames && !_connectorNames.getIncluded().contains(connectorName)) + { + return false; + } + return !_connectorNames.getExcluded().contains(connectorName); + } + @Override public void dump(Appendable out, String indent) throws IOException { - dumpObjects(out, indent, - DumpableCollection.from("included",_set.getIncluded()), - DumpableCollection.from("excluded",_set.getExcluded())); + dumpObjects(out, indent, + DumpableCollection.from("included", _set.getIncluded()), + DumpableCollection.from("excluded", _set.getExcluded()), + DumpableCollection.from("includedConnectorNames", _connectorNames.getIncluded()), + DumpableCollection.from("excludedConnectorNames", _connectorNames.getExcluded())); } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetHandlerTest.java new file mode 100644 index 00000000000..e23d89688d6 --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetHandlerTest.java @@ -0,0 +1,273 @@ +package org.eclipse.jetty.server.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class InetHandlerTest +{ + private static Server _server; + private static ServerConnector _connector; + private static InetAccessHandler _handler; + + @BeforeAll + public static void setUp() throws Exception + { + _server = new Server(); + _connector = new ServerConnector(_server); + _connector.setName("http"); + _server.setConnectors(new Connector[] { _connector }); + + _handler = new InetAccessHandler(); + _handler.setHandler(new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + response.setStatus(HttpStatus.OK_200); + } + }); + _server.setHandler(_handler); + _server.start(); + } + + /* ------------------------------------------------------------ */ + @AfterAll + public static void tearDown() throws Exception + { + _server.stop(); + } + + /* ------------------------------------------------------------ */ + @ParameterizedTest + @MethodSource("data") + public void testHandler(String include, String exclude, String includeConnectors, String excludeConnectors, + String host, String uri, String code) throws Exception + { + _handler.clear(); + for (String inc : include.split(";", -1)) { + if (inc.length() > 0) { + _handler.include(inc); + } + } + for (String exc : exclude.split(";", -1)) { + if (exc.length() > 0) { + _handler.exclude(exc); + } + } + for (String inc : includeConnectors.split(";", -1)) { + if (inc.length() > 0) { + _handler.includeConnectorName(inc); + } + } + for (String exc : excludeConnectors.split(";", -1)) { + if (exc.length() > 0) { + _handler.excludeConnectorName(exc); + } + } + String request = "GET " + uri + " HTTP/1.1\n" + "Host: " + host + "\n\n"; + Socket socket = new Socket("127.0.0.1", _connector.getLocalPort()); + socket.setSoTimeout(5000); + try { + OutputStream output = socket.getOutputStream(); + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + Response response = readResponse(input); + Object[] params = new Object[] { "Request WBHUC", include, exclude, includeConnectors, excludeConnectors, + host, uri, code, "Response", response.getCode() }; + assertEquals(code, response.getCode(), Arrays.deepToString(params)); + } finally { + socket.close(); + } + } + + /* ------------------------------------------------------------ */ + protected Response readResponse(BufferedReader reader) throws IOException + { + // Simplified parser for HTTP responses + String line = reader.readLine(); + if (line == null) + throw new EOFException(); + Matcher responseLine = Pattern.compile("HTTP/1\\.1\\s+(\\d+)").matcher(line); + assertTrue(responseLine.lookingAt()); + String code = responseLine.group(1); + + Map headers = new LinkedHashMap<>(); + while ((line = reader.readLine()) != null) { + if (line.trim().length() == 0) + break; + + Matcher header = Pattern.compile("([^:]+):\\s*(.*)").matcher(line); + assertTrue(header.lookingAt()); + String headerName = header.group(1); + String headerValue = header.group(2); + headers.put(headerName.toLowerCase(Locale.ENGLISH), headerValue.toLowerCase(Locale.ENGLISH)); + } + + StringBuilder body = new StringBuilder(); + if (headers.containsKey("content-length")) { + int length = Integer.parseInt(headers.get("content-length")); + for (int i = 0; i < length; ++i) { + char c = (char) reader.read(); + body.append(c); + } + } else if ("chunked".equals(headers.get("transfer-encoding"))) { + while ((line = reader.readLine()) != null) { + if ("0".equals(line)) { + line = reader.readLine(); + assertEquals("", line); + break; + } + + int length = Integer.parseInt(line, 16); + for (int i = 0; i < length; ++i) { + char c = (char) reader.read(); + body.append(c); + } + line = reader.readLine(); + assertEquals("", line); + } + } + + return new Response(code, headers, body.toString().trim()); + } + + /* ------------------------------------------------------------ */ + protected class Response + { + private final String code; + private final Map headers; + private final String body; + + /* ------------------------------------------------------------ */ + private Response(String code, Map headers, String body) + { + this.code = code; + this.headers = headers; + this.body = body; + } + + /* ------------------------------------------------------------ */ + public String getCode() + { + return code; + } + + /* ------------------------------------------------------------ */ + public Map getHeaders() + { + return headers; + } + + /* ------------------------------------------------------------ */ + public String getBody() + { + return body; + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append(code).append("\r\n"); + for (Map.Entry entry : headers.entrySet()) + builder.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); + builder.append("\r\n"); + builder.append(body); + return builder.toString(); + } + } + + /* ------------------------------------------------------------ */ + public static Stream data() + { + Object[][] data = new Object[][] { + // Empty lists + { "", "", "", "", "127.0.0.1", "/", "200" }, { "", "", "", "", "127.0.0.1", "/dump/info", "200" }, + + // test simple filters + { "127.0.0.1", "", "", "", "127.0.0.1", "/", "200" }, + { "127.0.0.1", "", "", "", "127.0.0.1", "/dump/info", "200" }, + { "127.0.0.1-127.0.0.254", "", "", "", "127.0.0.1", "/", "200" }, + { "127.0.0.1-127.0.0.254", "", "", "", "127.0.0.1", "/dump/info", "200" }, + { "192.0.0.1", "", "", "", "127.0.0.1", "/", "403" }, + { "192.0.0.1", "", "", "", "127.0.0.1", "/dump/info", "403" }, + { "192.0.0.1-192.0.0.254", "", "", "", "127.0.0.1", "/", "403" }, + { "192.0.0.1-192.0.0.254", "", "", "", "127.0.0.1", "/dump/info", "403" }, + + // test connector name filters + { "127.0.0.1", "", "http", "", "127.0.0.1", "/", "200" }, + { "127.0.0.1", "", "http", "", "127.0.0.1", "/dump/info", "200" }, + { "127.0.0.1-127.0.0.254", "", "http", "", "127.0.0.1", "/", "200" }, + { "127.0.0.1-127.0.0.254", "", "http", "", "127.0.0.1", "/dump/info", "200" }, + { "192.0.0.1", "", "http", "", "127.0.0.1", "/", "403" }, + { "192.0.0.1", "", "http", "", "127.0.0.1", "/dump/info", "403" }, + { "192.0.0.1-192.0.0.254", "", "http", "", "127.0.0.1", "/", "403" }, + { "192.0.0.1-192.0.0.254", "", "http", "", "127.0.0.1", "/dump/info", "403" }, + + { "127.0.0.1", "", "nothttp", "", "127.0.0.1", "/", "200" }, + { "127.0.0.1", "", "nothttp", "", "127.0.0.1", "/dump/info", "200" }, + { "127.0.0.1-127.0.0.254", "", "nothttp", "", "127.0.0.1", "/", "200" }, + { "127.0.0.1-127.0.0.254", "", "nothttp", "", "127.0.0.1", "/dump/info", "200" }, + { "192.0.0.1", "", "nothttp", "", "127.0.0.1", "/", "200" }, + { "192.0.0.1", "", "nothttp", "", "127.0.0.1", "/dump/info", "200" }, + { "192.0.0.1-192.0.0.254", "", "nothttp", "", "127.0.0.1", "/", "200" }, + { "192.0.0.1-192.0.0.254", "", "nothttp", "", "127.0.0.1", "/dump/info", "200" }, + + { "127.0.0.1", "", "", "http", "127.0.0.1", "/", "200" }, + { "127.0.0.1", "", "", "http", "127.0.0.1", "/dump/info", "200" }, + { "127.0.0.1-127.0.0.254", "", "", "http", "127.0.0.1", "/", "200" }, + { "127.0.0.1-127.0.0.254", "", "", "http", "127.0.0.1", "/dump/info", "200" }, + { "192.0.0.1", "", "", "http", "127.0.0.1", "/", "200" }, + { "192.0.0.1", "", "", "http", "127.0.0.1", "/dump/info", "200" }, + { "192.0.0.1-192.0.0.254", "", "", "http", "127.0.0.1", "/", "200" }, + { "192.0.0.1-192.0.0.254", "", "", "http", "127.0.0.1", "/dump/info", "200" }, + + { "127.0.0.1", "", "", "nothttp", "127.0.0.1", "/", "200" }, + { "127.0.0.1", "", "", "nothttp", "127.0.0.1", "/dump/info", "200" }, + { "127.0.0.1-127.0.0.254", "", "", "nothttp", "127.0.0.1", "/", "200" }, + { "127.0.0.1-127.0.0.254", "", "", "nothttp", "127.0.0.1", "/dump/info", "200" }, + { "192.0.0.1", "", "", "nothttp", "127.0.0.1", "/", "403" }, + { "192.0.0.1", "", "", "nothttp", "127.0.0.1", "/dump/info", "403" }, + { "192.0.0.1-192.0.0.254", "", "", "nothttp", "127.0.0.1", "/", "403" }, + { "192.0.0.1-192.0.0.254", "", "", "nothttp", "127.0.0.1", "/dump/info", "403" }, + + }; + return Arrays.asList(data).stream().map(Arguments::of); + } +}