Merge remote-tracking branch 'origin/jetty-11.0.x' into jetty-12.0.x
This commit is contained in:
commit
aa9df2a402
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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" +
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue