## Ports the `ipaccess` being added as a module from jetty 10

you can now do `java -jar start.jar --add-to-start=inetaccess` to add
the inetaccess handler to your jetty config.

## Allows you to specify a list of connector names that the
`InetAccessHandler` applies to

This is important for those who run jetty using `start.jar` to make it
possible to do things like have an open HTTPS connector but a whitelist
restricted HTTP connector.

Example:

`etc/jetty-inetaccess.xml`

```
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN"
"http://www.eclipse.org/jetty/configure_9_3.dtd">

<Configure id="Server" class="org.eclipse.jetty.server.Server">
    <Call name="insertHandler">
        <Arg>
            <New id="InetAccessHandler"
class="org.eclipse.jetty.server.handler.InetAccessHandler">
                <Call name="exclude"><Arg>127.0.0.128-127.0.0.129</Arg></Call>
                <Call name="includeConnectorName"><Arg>http</Arg></Call>
            </New>
        </Arg>
    </Call>
</Configure>
```

You can now
`java -jar start.jar --add-to-start=https`
and
`java -jar start.jar --add-to-start=inetaccess`

And you can now choose what inetaccess handler rules apply to http
versus https.

## Adds a basic `InetAccessHandler` Unit Test

Cover a few of the basic features of InetAccessHandler so it can have
some coverage.

Signed-off-by: Nicholas DiPiazza <nicholas.dipiazza@lucidworks.com>
This commit is contained in:
Nicholas DiPiazza 2019-04-18 23:18:19 -05:00 committed by Greg Wilkins
parent 205c4dc498
commit 1f4189eb19
5 changed files with 410 additions and 47 deletions

View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="insertHandler">
<Arg>
<New id="InetAccessHandler" class="org.eclipse.jetty.server.handler.InetAccessHandler">
<!-- include - Inclusive list of Inet address range to allow through. -->
<!--<Call name="include"><Arg>127.0.0.1-127.0.0.255</Arg></Call>-->
<!-- exclude - Exclusive list of Inet address ranges to block. -->
<!--<Call name="exclude"><Arg>127.0.0.128-127.0.0.129</Arg></Call>-->
<!-- includeConnectorName - This access handler will only apply to these connector names. -->
<!--<Call name="includeConnectorName"><Arg>http</Arg></Call>-->
<!-- excludeConnectorName - This access handler will not apply to these connector names. -->
<!--<Call name="excludeConnectorName"><Arg>tls</Arg></Call>-->
</New>
</Arg>
</Call>
</Configure>

View File

@ -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

View File

@ -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<IPAddressMap<Boolean>> 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));
}
}

View File

@ -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
* <p>
* 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.
* <p>
* 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
* <b>connectorNames</b> 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<String, InetAddress> _set = new IncludeExcludeSet<>(InetAddressSet.class);
private final IncludeExclude<String> _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()));
DumpableCollection.from("included", _set.getIncluded()),
DumpableCollection.from("excluded", _set.getExcluded()),
DumpableCollection.from("includedConnectorNames", _connectorNames.getIncluded()),
DumpableCollection.from("excludedConnectorNames", _connectorNames.getExcluded()));
}
}

View File

@ -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<String, String> 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<String, String> headers;
private final String body;
/* ------------------------------------------------------------ */
private Response(String code, Map<String, String> headers, String body)
{
this.code = code;
this.headers = headers;
this.body = body;
}
/* ------------------------------------------------------------ */
public String getCode()
{
return code;
}
/* ------------------------------------------------------------ */
public Map<String, String> 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<String, String> 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<Arguments> 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);
}
}