Merge remote-tracking branch 'origin/jetty-11.0.x' into jetty-12.0.x

This commit is contained in:
Joakim Erdfelt 2022-11-08 18:16:45 -06:00
commit aa9df2a402
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
9 changed files with 241 additions and 78 deletions

View File

@ -1028,6 +1028,8 @@ public class HttpParser
break;
case HOST:
if (_host)
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Host: multiple headers");
_host = true;
if (!(_field instanceof HostPortHttpField) && _valueString != null && !_valueString.isEmpty())
{

View File

@ -40,6 +40,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -2072,19 +2073,52 @@ public class HttpParserTest
assertEquals(8888, _port);
}
@Test
public void testHostBadPort()
@ParameterizedTest
@ValueSource(strings = {
"Host: whatever.com:xxxx",
"Host: myhost:testBadPort",
"Host: a b c d", // whitespace in reg-name
"Host: a\to\tz", // tabs in reg-name
"Host: hosta, hostb, hostc", // spaces in reg-name
"Host: [sd ajklf;d sajklf;d sajfkl;d]", // not a valid IPv6 address
"Host: hosta\nHost: hostb\nHost: hostc" // multi-line
})
public void testBadHost(String hostline)
{
ByteBuffer buffer = BufferUtil.toBuffer(
"GET / HTTP/1.1\r\n" +
"Host: myhost:testBadPort\r\n" +
"Connection: close\r\n" +
"\r\n");
"GET / HTTP/1.1\n" +
hostline + "\n" +
"Connection: close\n" +
"\n");
HttpParser.RequestHandler handler = new Handler();
HttpParser parser = new HttpParser(handler);
parser.parseNext(buffer);
assertThat(_bad, containsString("Bad Host"));
assertThat(_bad, startsWith("Bad"));
}
@ParameterizedTest
@ValueSource(strings = {
"Host: whatever.com:123",
"Host: myhost.com",
"Host: ::", // fake, no value, IPv6 (allowed)
"Host: a-b-c-d",
"Host: hosta,hostb,hostc", // commas are allowed
"Host: [fde3:827b:ea49:0:893:8016:e3ac:9778]:444", // IPv6 with port
"Host: [fde3:827b:ea49:0:893:8016:e3ac:9778]", // IPv6 without port
})
public void testGoodHost(String hostline)
{
ByteBuffer buffer = BufferUtil.toBuffer(
"GET / HTTP/1.1\n" +
hostline + "\n" +
"Connection: close\n" +
"\n");
HttpParser.RequestHandler handler = new Handler();
HttpParser parser = new HttpParser(handler);
parser.parseNext(buffer);
assertNull(_bad);
}
@Test

View File

@ -13,6 +13,8 @@
package org.eclipse.jetty.util;
import java.net.InetAddress;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
/**
@ -49,9 +51,12 @@ public class HostPort
if (close < 0)
throw new IllegalArgumentException("Bad IPv6 host");
_host = authority.substring(0, close + 1);
if (!isValidIpAddress(_host))
throw new IllegalArgumentException("Bad IPv6 host");
if (authority.length() > close + 1)
{
// ipv6 with port
if (authority.charAt(close + 1) != ':')
throw new IllegalArgumentException("Bad IPv6 port");
_port = parsePort(authority.substring(close + 2));
@ -67,21 +72,29 @@ public class HostPort
int c = authority.lastIndexOf(':');
if (c >= 0)
{
// ipv6address
if (c != authority.indexOf(':'))
{
// ipv6address no port
_host = "[" + authority + "]";
if (!isValidIpAddress(_host))
throw new IllegalArgumentException("Bad IPv6 host");
_port = 0;
}
else
{
// host/ipv4 with port
_host = authority.substring(0, c);
if (StringUtil.isBlank(_host) || !isValidHostName(_host))
throw new IllegalArgumentException("Bad Authority");
_port = parsePort(authority.substring(c + 1));
}
}
else
{
// host/ipv4 without port
_host = authority;
if (StringUtil.isBlank(_host) || !isValidHostName(_host))
throw new IllegalArgumentException("Bad Authority");
_port = 0;
}
}
@ -96,6 +109,26 @@ public class HostPort
}
}
protected boolean isValidIpAddress(String ip)
{
try
{
// Per javadoc, If a literal IP address is supplied, only the validity of the
// address format is checked.
InetAddress.getByName(ip);
return true;
}
catch (Throwable ignore)
{
return false;
}
}
protected boolean isValidHostName(String name)
{
return URIUtil.isValidHostRegisteredName(name);
}
/**
* Get the host.
*

View File

@ -23,6 +23,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
@ -54,6 +55,24 @@ public final class URIUtil
.with("jar:")
.build();
// From https://www.rfc-editor.org/rfc/rfc3986
private static final String UNRESERVED = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~";
private static final String SUBDELIMS = "!$&'()*+,;=";
private static final String REGNAME = UNRESERVED + SUBDELIMS;
// Allowed characters in https://www.rfc-editor.org/rfc/rfc3986 reg-name
private static final boolean[] REGNAME_ALLOWED;
static
{
REGNAME_ALLOWED = new boolean[128];
Arrays.fill(REGNAME_ALLOWED, false);
for (char c : REGNAME.toCharArray())
{
REGNAME_ALLOWED[c] = true;
}
}
/**
* The characters that are supported by the URI class and that can be decoded by {@link #canonicalPath(String)}
*/
@ -1362,6 +1381,50 @@ public final class URIUtil
return false;
}
/**
* True if token is a <a href="https://www.rfc-editor.org/rfc/rfc3986">RFC3986</a> {@code reg-name} (Registered Name)
*
* @param token the to test
* @return true if the token passes as a valid Host Registered Name
*/
public static boolean isValidHostRegisteredName(String token)
{
/* reg-name ABNF is defined as :
* reg-name = *( unreserved / pct-encoded / sub-delims )
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* pct-encoded = "%" HEXDIG HEXDIG
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
*/
if (token == null)
return true; // null token is considered valid
int length = token.length();
for (int i = 0; i < length; i++)
{
char c = token.charAt(i);
if (c > 127)
return false;
if (REGNAME_ALLOWED[c])
continue;
if (c == '%')
{
if (StringUtil.isHex(token, i + 1, 2))
{
i += 2;
continue;
}
else
{
return false;
}
}
return false;
}
return true;
}
/**
* Create a new URI from the arguments, handling IPv6 host encoding and default ports
*

View File

@ -31,14 +31,10 @@ public class HostPortTest
return Stream.of(
Arguments.of("", "", null),
Arguments.of(":80", "", "80"),
Arguments.of("host", "host", null),
Arguments.of("host:80", "host", "80"),
Arguments.of("10.10.10.1", "10.10.10.1", null),
Arguments.of("10.10.10.1:80", "10.10.10.1", "80"),
Arguments.of("[0::0::0::1]", "[0::0::0::1]", null),
Arguments.of("[0::0::0::1]:80", "[0::0::0::1]", "80"),
Arguments.of("0:1:2:3:4:5:6", "[0:1:2:3:4:5:6]", null),
Arguments.of("127.0.0.1:65535", "127.0.0.1", "65535"),
// Localhost tests
Arguments.of("localhost:80", "localhost", "80"),
@ -68,18 +64,21 @@ public class HostPortTest
{
return Stream.of(
null,
"host:",
"127.0.0.1:",
"[0::0::0::0::1]:",
"host:xxx",
"127.0.0.1:xxx",
"[0::0::0::0::1]:xxx",
"host:-80",
"127.0.0.1:-80",
"[0::0::0::0::1]:-80",
"127.0.0.1:65536"
)
.map(Arguments::of);
":80", // no host, port only
"host:", // no port
"127.0.0.1:", // no port
"[0::0::0::0::1]:", // no port
"[0::0::0::1]", // not valid to Java (InetAddress, InetSocketAddress, or URI) : "Expected hex digits or IPv4 address"
"[0::0::0::1]:80", // not valid to Java (InetAddress, InetSocketAddress, or URI) : "Expected hex digits or IPv4 address"
"0:1:2:3:4:5:6", // not valid to Java (InetAddress, InetSocketAddress, or URI) : "IPv6 address too short"
"host:xxx", // invalid port
"127.0.0.1:xxx", // host + invalid port
"[0::0::0::0::1]:xxx", // ipv6 + invalid port
"host:-80", // host + invalid port
"127.0.0.1:-80", // ipv4 + invalid port
"[0::0::0::0::1]:-80", // ipv6 + invalid port
"127.0.0.1:65536" // ipv4 + port value too high
).map(Arguments::of);
}
@ParameterizedTest

View File

@ -36,6 +36,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
@ -897,6 +900,42 @@ public class URIUtilTest
assertEquals(path, decoded);
}
@ParameterizedTest
@ValueSource(strings = {
"a",
"deadbeef",
"321zzz123",
"pct%25encoded",
"a,b,c",
"*",
"_-_-_",
"192.168.1.22",
"192.168.1.com"
})
public void testIsValidHostRegisteredNameTrue(String token)
{
assertTrue(URIUtil.isValidHostRegisteredName(token), "Token [" + token + "] should be a valid reg-name");
}
@ParameterizedTest
@ValueSource(strings = {
" ",
"tab\tchar",
"a long name with spaces",
"8-bit-\u00dd", // 8-bit characters
"пример.рф", // unicode - raw IDN (not normalized to punycode)
// Invalid pct-encoding
"%XX",
"%%",
"abc%d",
"100%",
"[brackets]"
})
public void testIsValidHostRegisteredNameFalse(String token)
{
assertFalse(URIUtil.isValidHostRegisteredName(token), "Token [" + token + "] should be an invalid reg-name");
}
public static Stream<Arguments> uriJarPrefixCasesGood()
{
return Stream.of(

View File

@ -89,6 +89,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -710,8 +712,16 @@ public class RequestTest
assertThat(responses, startsWith("HTTP/1.1 200"));
}
@Test
public void testInvalidHostHeader() throws Exception
@ParameterizedTest
@ValueSource(strings = {
"Host: whatever.com:xxxx", // invalid port
"Host: myhost:testBadPort", // invalid port
"Host: a b c d", // spaces
"Host: a\to\tz", // control characters
"Host: hosta, hostb, hostc", // spaces (commas are ok)
"Host: hosta\nHost: hostb\nHost: hostc" // multi-line
})
public void testInvalidHostHeader(String hostline) throws Exception
{
// Use a contextHandler with vhosts to force call to Request.getServerName()
_server.stop();
@ -720,7 +730,7 @@ public class RequestTest
// Request with illegal Host header
String request = "GET / HTTP/1.1\n" +
"Host: whatever.com:xxxx\n" +
hostline + "\n" +
"Content-Type: text/html;charset=utf8\n" +
"Connection: close\n" +
"\n";

View File

@ -1135,7 +1135,6 @@ public class ConstraintTest
response = _connector.getResponse("POST /ctx/auth/info HTTP/1.1\r\n" +
"Host: test\r\n" +
"Host: localhost\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Length: 10\r\n" +
"\r\n" +

View File

@ -13,11 +13,9 @@
package org.eclipse.jetty.ee9.test;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -29,33 +27,26 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.IO;
import org.junit.jupiter.api.AfterAll;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.PathResource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class AllowedResourceAliasCheckerTest
{
private static Server _server;
private static ServerConnector _connector;
private static HttpClient _client;
private static ServletContextHandler _context;
private static File _baseDir;
private static Path getResourceDir() throws Exception
{
URL url = AllowedResourceAliasCheckerTest.class.getClassLoader().getResource(".");
assertNotNull(url);
return new File(url.toURI()).toPath();
}
private Server _server;
private ServerConnector _connector;
private HttpClient _client;
private ServletContextHandler _context;
private Path _baseDir;
public void start() throws Exception
{
@ -63,8 +54,8 @@ public class AllowedResourceAliasCheckerTest
_client.start();
}
@BeforeAll
public static void beforeAll() throws Exception
@BeforeEach
public void prepare(WorkDir workDir)
{
_client = new HttpClient();
_server = new Server();
@ -76,46 +67,39 @@ public class AllowedResourceAliasCheckerTest
_context.addServlet(DefaultServlet.class, "/");
_server.setHandler(_context);
_baseDir = getResourceDir().resolve("baseDir").toFile();
_baseDir.deleteOnExit();
assertFalse(_baseDir.exists());
_context.setResourceBase(_baseDir.getAbsolutePath());
}
@AfterAll
public static void afterAll() throws Exception
{
_client.stop();
_server.stop();
_baseDir = workDir.getEmptyPathDir().resolve("baseDir");
_context.setBaseResource(new PathResource(_baseDir));
}
@AfterEach
public void afterEach()
public void dispose()
{
IO.delete(_baseDir);
LifeCycle.stop(_client);
LifeCycle.stop(_server);
}
public void createBaseDir() throws IOException
{
assertFalse(_baseDir.exists());
assertTrue(_baseDir.mkdir());
FS.ensureDirExists(_baseDir);
// Create a file in the baseDir.
File file = _baseDir.toPath().resolve("file.txt").toFile();
file.deleteOnExit();
assertTrue(file.createNewFile());
try (FileWriter fileWriter = new FileWriter(file))
Path file = Files.writeString(_baseDir.resolve("file.txt"), "this is a file in the baseDir");
boolean symlinkSupported;
try
{
fileWriter.write("this is a file in the baseDir");
// Create a symlink to that file.
// Symlink to a directory inside the webroot.
Path symlink = _baseDir.resolve("symlink");
Files.createSymbolicLink(symlink, file);
symlinkSupported = true;
}
catch (UnsupportedOperationException | FileSystemException e)
{
symlinkSupported = false;
}
// Create a symlink to that file.
// Symlink to a directory inside of the webroot.
File symlink = _baseDir.toPath().resolve("symlink").toFile();
symlink.deleteOnExit();
Files.createSymbolicLink(symlink.toPath(), file.toPath());
assertTrue(symlink.exists());
assumeTrue(symlinkSupported, "Symlink not supported");
}
@Test