Issue #880 Improve IPv6 support

For #880 added the HostPort class, which was used to fix #878, #879, #884 and #886
This commit is contained in:
Greg Wilkins 2016-08-31 16:43:42 +10:00
parent c07d842f06
commit 0155ae761c
15 changed files with 328 additions and 111 deletions

View File

@ -35,6 +35,7 @@ import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
@ -78,7 +79,7 @@ public abstract class HttpDestination implements Destination, Closeable, Dumpabl
}
this.connectionFactory = connectionFactory;
String host = getHost();
String host = HostPort.normalizeHost(getHost());
if (!client.isDefaultPort(getScheme(), getPort()))
host += ":" + getPort();
hostField = new HttpField(HttpHeader.HOST, host);

View File

@ -25,6 +25,7 @@ import java.util.List;
import java.util.Set;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.util.HostPort;
/**
* The configuration of the forward proxy to use with {@link org.eclipse.jetty.client.HttpClient}.
@ -58,6 +59,7 @@ public class ProxyConfiguration
public static abstract class Proxy
{
// TO use IPAddress Map
private final Set<String> included = new HashSet<>();
private final Set<String> excluded = new HashSet<>();
private final Origin.Address address;
@ -146,12 +148,10 @@ public class ProxyConfiguration
private boolean matches(Origin.Address address, String pattern)
{
// TODO: add support for CIDR notation like 192.168.0.0/24, see DoSFilter
int colon = pattern.indexOf(':');
if (colon < 0)
return pattern.equals(address.getHost());
String host = pattern.substring(0, colon);
String port = pattern.substring(colon + 1);
return host.equals(address.getHost()) && port.equals(String.valueOf(address.getPort()));
HostPort hostPort = new HostPort(pattern);
String host = hostPort.getHost();
int port = hostPort.getPort();
return host.equals(address.getHost()) && ( port<=0 || port==address.getPort() );
}
/**

View File

@ -81,6 +81,7 @@ import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Rule;
@ -1491,6 +1492,33 @@ public class HttpClientTest extends AbstractHttpClientServerTest
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void test_IPv6_Host() throws Exception
{
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
response.setContentType("text/plain");
response.getOutputStream().print(request.getHeader("Host"));
}
});
URI uri = URI.create(scheme + "://[::1]:" + connector.getLocalPort() + "/path");
ContentResponse response = client.newRequest(uri)
.method(HttpMethod.PUT)
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
Assert.assertThat(new String(response.getContent(), StandardCharsets.ISO_8859_1),Matchers.startsWith("[::1]:"));
}
private void consume(InputStream input) throws IOException
{
while (true)

View File

@ -63,4 +63,16 @@ public class ProxyConfigurationTest
Assert.assertTrue(proxy.matches(new Origin("http", "1.2.3.4", 0)));
Assert.assertFalse(proxy.matches(new Origin("http", "1.2.3.4", 5)));
}
@Test
public void testProxyMatchesWithIncludesAndExcludesIPv6() throws Exception
{
HttpProxy proxy = new HttpProxy("host", 0);
proxy.getIncludedAddresses().add("[1::2:3:4]");
proxy.getExcludedAddresses().add("[1::2:3:4]:5");
Assert.assertFalse(proxy.matches(new Origin("http", "any", 0)));
Assert.assertTrue(proxy.matches(new Origin("http", "[1::2:3:4]", 0)));
Assert.assertFalse(proxy.matches(new Origin("http", "[1::2:3:4]", 5)));
}
}

View File

@ -27,6 +27,7 @@ import org.eclipse.jetty.http.HttpTokens.EndOfContent;
import org.eclipse.jetty.util.ArrayTernaryTrie;
import org.eclipse.jetty.util.ArrayTrie;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.Trie;
import org.eclipse.jetty.util.TypeUtil;
@ -831,50 +832,22 @@ public class HttpParser
case HOST:
add_to_connection_trie=_connectionFields!=null && _field==null;
_host=true;
String host=_valueString;
int port=0;
if (host==null || host.length()==0)
if (_valueString==null || _valueString.length()==0)
{
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Bad Host header");
}
int len=host.length();
loop: for (int i = len; i-- > 0;)
try
{
char c2 = (char)(0xff & host.charAt(i));
switch (c2)
{
case ']':
break loop;
case ':':
try
{
len=i;
port = StringUtil.toInt(host.substring(i+1));
}
catch (NumberFormatException e)
{
if (DEBUG)
LOG.debug(e);
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Bad Host header");
}
break loop;
}
HostPort authority = new HostPort(_valueString);
if (_requestHandler!=null)
_requestHandler.parsedHostHeader(authority.getHost(),authority.getPort());
}
if (host.charAt(0)=='[')
catch (final Exception e)
{
if (host.charAt(len-1)!=']')
{
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Bad IPv6 Host header");
}
host = host.substring(0,len);
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Bad Host header")
{{initCause(e);}};
}
else if (len!=host.length())
host = host.substring(0,len);
if (_requestHandler!=null)
_requestHandler.parsedHostHeader(host,port);
break;

View File

@ -1346,7 +1346,7 @@ public class HttpParserTest
HttpParser.RequestHandler<ByteBuffer> handler = new Handler();
HttpParser parser= new HttpParser(handler);
parser.parseNext(buffer);
assertEquals("Bad IPv6 Host header",_bad);
assertEquals("Bad Host header",_bad);
}
@Test

View File

@ -32,6 +32,7 @@ import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -134,15 +135,10 @@ public class ConnectorServer extends AbstractLifeCycle
*/
private String startRegistry(String hostPath) throws Exception
{
int rmiPort = 1099; // default RMI registry port
String rmiHost = hostPath;
HostPort hostPort = new HostPort(hostPath);
int idx = hostPath.indexOf(':');
if (idx > 0)
{
rmiPort = Integer.parseInt(hostPath.substring(idx + 1));
rmiHost = hostPath.substring(0,idx);
}
String rmiHost = hostPort.getHost();
int rmiPort = hostPort.getPort(1099);
// Verify that local registry is being used
InetAddress hostAddress = InetAddress.getByName(rmiHost);
@ -171,7 +167,7 @@ public class ConnectorServer extends AbstractLifeCycle
_registry = LocateRegistry.createRegistry(rmiPort);
Thread.sleep(1000);
rmiHost = InetAddress.getLocalHost().getCanonicalHostName();
rmiHost = HostPort.normalizeHost(InetAddress.getLocalHost().getCanonicalHostName());
return rmiHost + ':' + Integer.toString(rmiPort);
}

View File

@ -51,6 +51,7 @@ import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
@ -221,14 +222,9 @@ public class ConnectHandler extends HandlerWrapper
return;
}
String host = serverAddress;
int port = 80;
int colon = serverAddress.indexOf(':');
if (colon > 0)
{
host = serverAddress.substring(0, colon);
port = Integer.parseInt(serverAddress.substring(colon + 1));
}
HostPort hostPort = new HostPort(serverAddress);
String host = hostPort.getHost();
int port = hostPort.getPort(80);
if (!validateDestination(host, port))
{

View File

@ -66,7 +66,7 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest
@Test
public void testCONNECT() throws Exception
{
{
String hostPort = "localhost:" + serverConnector.getLocalPort();
String request = "" +
"CONNECT " + hostPort + " HTTP/1.1\r\n" +
@ -86,6 +86,28 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest
}
}
@Test
public void testCONNECTwithIPv6() throws Exception
{
String hostPort = "[::1]:" + serverConnector.getLocalPort();
String request = "" +
"CONNECT " + hostPort + " HTTP/1.1\r\n" +
"Host: " + hostPort + "\r\n" +
"\r\n";
try (Socket socket = newSocket())
{
OutputStream output = socket.getOutputStream();
BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
// Expect 200 OK from the CONNECT request
SimpleHttpResponse response = readResponse(input);
Assert.assertEquals("200", response.getCode());
}
}
@Test
public void testCONNECTAndGET() throws Exception
{

View File

@ -75,6 +75,7 @@ import org.eclipse.jetty.server.handler.ContextHandler.Context;
import org.eclipse.jetty.server.session.AbstractSession;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.AttributesMap;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiException;
import org.eclipse.jetty.util.MultiMap;
@ -1212,46 +1213,19 @@ public class Request implements HttpServletRequest
_port=0;
if (hostPort != null)
{
int len=hostPort.length();
loop: for (int i = len; i-- > 0;)
try
{
char c2 = (char)(0xff & hostPort.charAt(i));
switch (c2)
{
case ']':
break loop;
case ':':
try
{
len=i;
_port = StringUtil.toInt(hostPort.substring(i+1));
}
catch (NumberFormatException e)
{
LOG.warn(e);
_serverName=hostPort;
_port=0;
return _serverName;
}
break loop;
}
HostPort authority = new HostPort(hostPort);
_serverName=authority.getHost();
_port=authority.getPort();
}
if (hostPort.charAt(0)=='[')
catch (Exception e)
{
if (hostPort.charAt(len-1)!=']')
{
LOG.warn("Bad IPv6 "+hostPort);
_serverName=hostPort;
_port=0;
return _serverName;
}
_serverName = hostPort.substring(0,len);
}
else if (len==hostPort.length())
LOG.warn(e);
_serverName=hostPort;
else
_serverName = hostPort.substring(0,len);
_port=0;
return _serverName;
}
return _serverName;
}

View File

@ -0,0 +1,129 @@
//
// ========================================================================
// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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.util;
/**
* Parse an authority string into Host and Port
* <p>Parse a string in the form "host:port", handling IPv4 an IPv6 hosts</p>
*
*/
public class HostPort
{
private final String _host;
private final int _port;
public HostPort(String authority) throws IllegalArgumentException
{
if (authority==null || authority.length()==0)
throw new IllegalArgumentException("No Authority");
try
{
if (authority.charAt(0)=='[')
{
// ipv6reference
int close=authority.lastIndexOf(']');
if (close<0)
throw new IllegalArgumentException("Bad IPv6 host");
_host=authority.substring(0,close+1);
if (authority.length()>close+1)
{
if (authority.charAt(close+1)!=':')
throw new IllegalArgumentException("Bad IPv6 port");
_port=StringUtil.toInt(authority,close+2);
}
else
_port=0;
}
else
{
// ipv4address or hostname
int c = authority.lastIndexOf(':');
if (c>=0)
{
_host=authority.substring(0,c);
_port=StringUtil.toInt(authority,c+1);
}
else
{
_host=authority;
_port=0;
}
}
}
catch (IllegalArgumentException iae)
{
throw iae;
}
catch(final Exception ex)
{
throw new IllegalArgumentException("Bad HostPort")
{
{initCause(ex);}
};
}
if(_host.isEmpty())
throw new IllegalArgumentException("Bad host");
if(_port<0)
throw new IllegalArgumentException("Bad port");
}
/* ------------------------------------------------------------ */
/** Get the host.
* @return the host
*/
public String getHost()
{
return _host;
}
/* ------------------------------------------------------------ */
/** Get the port.
* @return the port
*/
public int getPort()
{
return _port;
}
/* ------------------------------------------------------------ */
/** Get the port.
* @param defaultPort, the default port to return if a port is not specified
* @return the port
*/
public int getPort(int defaultPort)
{
return _port>0?_port:defaultPort;
}
/* ------------------------------------------------------------ */
/** Normalize IPv6 address as per https://www.ietf.org/rfc/rfc2732.txt
* @param host A host name
* @return Host name surrounded by '[' and ']' as needed.
*/
public static String normalizeHost(String host)
{
// if it is normalized IPv6 or could not be IPv6, return
if (host.isEmpty() || host.charAt(0)=='[' || host.indexOf(':')<0)
return host;
// normalize with [ ]
return "["+host+"]";
}
}

View File

@ -40,6 +40,7 @@ import java.util.StringTokenizer;
* - => 0-255
* a,b,... - a list of wildcard specifications
* </pre>
* @deprecated
*/
@SuppressWarnings("serial")
public class IPAddressMap<TYPE> extends HashMap<String, TYPE>

View File

@ -668,22 +668,21 @@ public class StringUtil
return sidBytes;
}
/**
* Convert String to an integer. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown
*
* @param string
* A String containing an integer.
* @param string A String containing an integer.
* @param from The index to start parsing from
* @return an int
*/
public static int toInt(String string)
public static int toInt(String string,int from)
{
int val = 0;
boolean started = false;
boolean minus = false;
for (int i = 0; i < string.length(); i++)
for (int i = from; i < string.length(); i++)
{
char b = string.charAt(i);
if (b <= ' ')

View File

@ -708,10 +708,7 @@ public class URIUtil
*/
public static void appendSchemeHostPort(StringBuilder url,String scheme,String server, int port)
{
if (server.indexOf(':')>=0&&server.charAt(0)!='[')
url.append(scheme).append("://").append('[').append(server).append(']');
else
url.append(scheme).append("://").append(server);
url.append(scheme).append("://").append(HostPort.normalizeHost(server));
if (port > 0)
{
@ -745,10 +742,7 @@ public class URIUtil
{
synchronized (url)
{
if (server.indexOf(':')>=0&&server.charAt(0)!='[')
url.append(scheme).append("://").append('[').append(server).append(']');
else
url.append(scheme).append("://").append(server);
url.append(scheme).append("://").append(HostPort.normalizeHost(server));
if (port > 0)
{

View File

@ -0,0 +1,92 @@
//
// ========================================================================
// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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.util;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class HostPortTest
{
@Parameters(name="{0}")
public static List<String[]> testCases()
{
String data[][] = new String[][] {
{"host","host",null},
{"host:80","host","80"},
{"10.10.10.1","10.10.10.1",null},
{"10.10.10.1:80","10.10.10.1","80"},
{"[0::0::0::1]","[0::0::0::1]",null},
{"[0::0::0::1]:80","[0::0::0::1]","80"},
{null,null,null},
{"host:",null,null},
{"",null,null},
{":80",null,"80"},
{"127.0.0.1:",null,null},
{"[0::0::0::0::1]:",null,null},
{"host:xxx",null,null},
{"127.0.0.1:xxx",null,null},
{"[0::0::0::0::1]:xxx",null,null},
{"host:-80",null,null},
{"127.0.0.1:-80",null,null},
{"[0::0::0::0::1]:-80",null,null},
};
return Arrays.asList(data);
}
@Parameter(0)
public String _authority;
@Parameter(1)
public String _expectedHost;
@Parameter(2)
public String _expectedPort;
@Test
public void test()
{
try
{
HostPort hostPort = new HostPort(_authority);
assertThat(hostPort.getHost(),is(_expectedHost));
if (_expectedPort==null)
assertThat(hostPort.getPort(),is(0));
else
assertThat(hostPort.getPort(),is(Integer.valueOf(_expectedPort)));
}
catch (Exception e)
{
assertNull(_expectedHost);
}
}
}