Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-9.4.x-5492-java-features-start-properties

This commit is contained in:
Joakim Erdfelt 2021-02-18 10:55:34 -06:00
commit 2aa1a68079
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
42 changed files with 2521 additions and 320 deletions

View File

@ -18,25 +18,39 @@
package org.eclipse.jetty.http;
import java.util.Objects;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil;
public class CompressedContentFormat
{
/**
* The separator within an etag used to indicate a compressed variant. By default the separator is "--"
* So etag for compressed resource that normally has an etag of <code>W/"28c772d6"</code>
* is <code>W/"28c772d6--gzip"</code>. The separator may be changed by the
* "org.eclipse.jetty.http.CompressedContentFormat.ETAG_SEPARATOR" System property. If changed, it should be changed to a string
* that will not be found in a normal etag or at least is very unlikely to be a substring of a normal etag.
*/
public static final String ETAG_SEPARATOR = System.getProperty(CompressedContentFormat.class.getName() + ".ETAG_SEPARATOR", "--");
public static final CompressedContentFormat GZIP = new CompressedContentFormat("gzip", ".gz");
public static final CompressedContentFormat BR = new CompressedContentFormat("br", ".br");
public static final CompressedContentFormat[] NONE = new CompressedContentFormat[0];
public final String _encoding;
public final String _extension;
public final String _etag;
public final String _etagQuote;
public final PreEncodedHttpField _contentEncoding;
private final String _encoding;
private final String _extension;
private final String _etagSuffix;
private final String _etagSuffixQuote;
private final PreEncodedHttpField _contentEncoding;
public CompressedContentFormat(String encoding, String extension)
{
_encoding = encoding;
_extension = extension;
_etag = "--" + encoding;
_etagQuote = _etag + "\"";
_contentEncoding = new PreEncodedHttpField(HttpHeader.CONTENT_ENCODING, encoding);
_encoding = StringUtil.asciiToLowerCase(encoding);
_extension = StringUtil.asciiToLowerCase(extension);
_etagSuffix = StringUtil.isEmpty(ETAG_SEPARATOR) ? "" : (ETAG_SEPARATOR + _encoding);
_etagSuffixQuote = _etagSuffix + "\"";
_contentEncoding = new PreEncodedHttpField(HttpHeader.CONTENT_ENCODING, _encoding);
}
@Override
@ -45,22 +59,107 @@ public class CompressedContentFormat
if (!(o instanceof CompressedContentFormat))
return false;
CompressedContentFormat ccf = (CompressedContentFormat)o;
if (_encoding == null && ccf._encoding != null)
return false;
if (_extension == null && ccf._extension != null)
return false;
return _encoding.equalsIgnoreCase(ccf._encoding) && _extension.equalsIgnoreCase(ccf._extension);
return Objects.equals(_encoding, ccf._encoding) && Objects.equals(_extension, ccf._extension);
}
public static boolean tagEquals(String etag, String tag)
public String getEncoding()
{
if (etag.equals(tag))
return _encoding;
}
public String getExtension()
{
return _extension;
}
public String getEtagSuffix()
{
return _etagSuffix;
}
public HttpField getContentEncoding()
{
return _contentEncoding;
}
/** Get an etag with suffix that represents this compressed type.
* @param etag An etag
* @return An etag with compression suffix, or the etag itself if no suffix is configured.
*/
public String etag(String etag)
{
if (StringUtil.isEmpty(ETAG_SEPARATOR))
return etag;
int end = etag.length() - 1;
if (etag.charAt(end) == '"')
return etag.substring(0, end) + _etagSuffixQuote;
return etag + _etagSuffix;
}
@Override
public int hashCode()
{
return Objects.hash(_encoding, _extension);
}
/** Check etags for equality, accounting for quoting and compression suffixes.
* @param etag An etag without a compression suffix
* @param etagWithSuffix An etag optionally with a compression suffix.
* @return True if the tags are equal.
*/
public static boolean tagEquals(String etag, String etagWithSuffix)
{
// Handle simple equality
if (etag.equals(etagWithSuffix))
return true;
int dashdash = tag.indexOf("--");
if (dashdash > 0 && dashdash == etag.length() - 1)
return etag.regionMatches(0, tag, 0, dashdash);
return false;
// If no separator defined, then simple equality is only possible positive
if (StringUtil.isEmpty(ETAG_SEPARATOR))
return false;
// Are both tags quoted?
boolean etagQuoted = etag.endsWith("\"");
boolean etagSuffixQuoted = etagWithSuffix.endsWith("\"");
// Look for a separator
int separator = etagWithSuffix.lastIndexOf(ETAG_SEPARATOR);
// If both tags are quoted the same (the norm) then any difference must be the suffix
if (etagQuoted == etagSuffixQuoted)
return separator > 0 && etag.regionMatches(0, etagWithSuffix, 0, separator);
// If either tag is weak then we can't match because weak tags must be quoted
if (etagWithSuffix.startsWith("W/") || etag.startsWith("W/"))
return false;
// compare unquoted strong etags
etag = etagQuoted ? QuotedStringTokenizer.unquote(etag) : etag;
etagWithSuffix = etagSuffixQuoted ? QuotedStringTokenizer.unquote(etagWithSuffix) : etagWithSuffix;
separator = etagWithSuffix.lastIndexOf(ETAG_SEPARATOR);
if (separator > 0)
return etag.regionMatches(0, etagWithSuffix, 0, separator);
return Objects.equals(etag, etagWithSuffix);
}
public String stripSuffixes(String etagsList)
{
if (StringUtil.isEmpty(ETAG_SEPARATOR))
return etagsList;
// This is a poor implementation that ignores list and tag structure
while (true)
{
int i = etagsList.lastIndexOf(_etagSuffix);
if (i < 0)
return etagsList;
etagsList = etagsList.substring(0, i) + etagsList.substring(i + _etagSuffix.length());
}
}
@Override
public String toString()
{
return _encoding;
}
}

View File

@ -56,13 +56,14 @@ public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so t
LEGACY(sectionsBySpec("0,METHOD_CASE_SENSITIVE")),
/**
* The legacy RFC2616 support, which incorrectly excludes
* The legacy RFC2616 support, which excludes
* {@link HttpComplianceSection#METHOD_CASE_SENSITIVE},
* {@link HttpComplianceSection#FIELD_COLON},
* {@link HttpComplianceSection#TRANSFER_ENCODING_WITH_CONTENT_LENGTH},
* {@link HttpComplianceSection#MULTIPLE_CONTENT_LENGTHS},
* {@link HttpComplianceSection#MULTIPLE_CONTENT_LENGTHS} and
* {@link HttpComplianceSection#NO_AMBIGUOUS_PATH_SEGMENTS}.
*/
RFC2616_LEGACY(sectionsBySpec("RFC2616,-FIELD_COLON,-METHOD_CASE_SENSITIVE,-TRANSFER_ENCODING_WITH_CONTENT_LENGTH,-MULTIPLE_CONTENT_LENGTHS")),
RFC2616_LEGACY(sectionsBySpec("RFC2616,-FIELD_COLON,-METHOD_CASE_SENSITIVE,-TRANSFER_ENCODING_WITH_CONTENT_LENGTH,-MULTIPLE_CONTENT_LENGTHS,-NO_AMBIGUOUS_PATH_SEGMENTS")),
/**
* The strict RFC2616 support mode
@ -70,9 +71,11 @@ public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so t
RFC2616(sectionsBySpec("RFC2616")),
/**
* Jetty's current RFC7230 support, which incorrectly excludes {@link HttpComplianceSection#METHOD_CASE_SENSITIVE}
* Jetty's current RFC7230 support, which excludes
* {@link HttpComplianceSection#METHOD_CASE_SENSITIVE} and
* {@link HttpComplianceSection#NO_AMBIGUOUS_PATH_SEGMENTS}.
*/
RFC7230_LEGACY(sectionsBySpec("RFC7230,-METHOD_CASE_SENSITIVE")),
RFC7230_LEGACY(sectionsBySpec("RFC7230,-METHOD_CASE_SENSITIVE,-NO_AMBIGUOUS_PATH_SEGMENTS")),
/**
* The RFC7230 support mode
@ -123,11 +126,6 @@ public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so t
i++;
break;
case "*":
i++;
sections = EnumSet.allOf(HttpComplianceSection.class);
break;
case "RFC2616":
sections = EnumSet.complementOf(EnumSet.of(
HttpComplianceSection.NO_FIELD_FOLDING,
@ -135,6 +133,7 @@ public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so t
i++;
break;
case "*":
case "RFC7230":
i++;
sections = EnumSet.allOf(HttpComplianceSection.class);
@ -152,11 +151,6 @@ public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so t
if (exclude)
element = element.substring(1);
HttpComplianceSection section = HttpComplianceSection.valueOf(element);
if (section == null)
{
LOG.warn("Unknown section '" + element + "' in HttpCompliance spec: " + spec);
continue;
}
if (exclude)
sections.remove(section);
else

View File

@ -31,7 +31,8 @@ public enum HttpComplianceSection
NO_FIELD_FOLDING("https://tools.ietf.org/html/rfc7230#section-3.2.4", "No line Folding"),
NO_HTTP_0_9("https://tools.ietf.org/html/rfc7230#appendix-A.2", "No HTTP/0.9"),
TRANSFER_ENCODING_WITH_CONTENT_LENGTH("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Transfer-Encoding and Content-Length"),
MULTIPLE_CONTENT_LENGTHS("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Multiple Content-Lengths");
MULTIPLE_CONTENT_LENGTHS("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Multiple Content-Lengths"),
NO_AMBIGUOUS_PATH_SEGMENTS("https://tools.ietf.org/html/rfc3986#section-3.3", "No ambiguous URI path segments");
final String url;
final String description;

View File

@ -311,6 +311,11 @@ public class HttpParser
return _handler;
}
public HttpCompliance getHttpCompliance()
{
return _compliance;
}
/**
* Check RFC compliance violation
*

View File

@ -23,8 +23,11 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import org.eclipse.jetty.util.ArrayTrie;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.Trie;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
@ -65,6 +68,30 @@ public class HttpURI
ASTERISK
}
/**
* The concept of URI path parameters was originally specified in
* <a href="https://tools.ietf.org/html/rfc2396#section-3.3">RFC2396</a>, but that was
* obsoleted by
* <a href="https://tools.ietf.org/html/rfc3986#section-3.3">RFC3986</a> which removed
* a normative definition of path parameters. Specifically it excluded them from the
* <a href="https://tools.ietf.org/html/rfc3986#section-5.2.4">Remove Dot Segments</a>
* algorithm. This results in some ambiguity as dot segments can result from later
* parameter removal or % encoding expansion, that are not removed from the URI
* by {@link URIUtil#canonicalPath(String)}. Thus this class flags such ambiguous
* path segments, so that they may be rejected by the server if so configured.
*/
private static final Trie<Boolean> __ambiguousSegments = new ArrayTrie<>();
static
{
__ambiguousSegments.put("%2e", Boolean.TRUE);
__ambiguousSegments.put("%2e%2e", Boolean.TRUE);
__ambiguousSegments.put(".%2e", Boolean.TRUE);
__ambiguousSegments.put("%2e.", Boolean.TRUE);
__ambiguousSegments.put("..", Boolean.FALSE);
__ambiguousSegments.put(".", Boolean.FALSE);
}
private String _scheme;
private String _user;
private String _host;
@ -73,9 +100,9 @@ public class HttpURI
private String _param;
private String _query;
private String _fragment;
String _uri;
String _decodedPath;
private String _uri;
private String _decodedPath;
private boolean _ambiguousSegment;
/**
* Construct a normalized URI.
@ -108,16 +135,29 @@ public class HttpURI
_scheme = scheme;
_host = host;
_port = port;
_path = path;
_param = param;
_query = query;
_fragment = fragment;
if (path != null)
parse(State.PATH, path, 0, path.length());
if (param != null)
_param = param;
if (query != null)
_query = query;
if (fragment != null)
_fragment = fragment;
}
public HttpURI(HttpURI uri)
{
this(uri._scheme, uri._host, uri._port, uri._path, uri._param, uri._query, uri._fragment);
_scheme = uri._scheme;
_user = uri._user;
_host = uri._host;
_port = uri._port;
_path = uri._path;
_param = uri._param;
_query = uri._query;
_fragment = uri._fragment;
_uri = uri._uri;
_decodedPath = uri._decodedPath;
_ambiguousSegment = uri._ambiguousSegment;
}
public HttpURI(String uri)
@ -129,40 +169,44 @@ public class HttpURI
public HttpURI(URI uri)
{
_uri = null;
_scheme = uri.getScheme();
_host = uri.getHost();
if (_host == null && uri.getRawSchemeSpecificPart().startsWith("//"))
_host = "";
_port = uri.getPort();
_user = uri.getUserInfo();
_path = uri.getRawPath();
_decodedPath = uri.getPath();
if (_decodedPath != null)
{
int p = _decodedPath.lastIndexOf(';');
if (p >= 0)
_param = _decodedPath.substring(p + 1);
}
String path = uri.getRawPath();
if (path != null)
parse(State.PATH, path, 0, path.length());
_query = uri.getRawQuery();
_fragment = uri.getFragment();
_decodedPath = null;
}
public HttpURI(String scheme, String host, int port, String pathQuery)
{
_uri = null;
_scheme = scheme;
_host = host;
_port = port;
if (pathQuery != null)
parse(State.PATH, pathQuery, 0, pathQuery.length());
}
public void clear()
{
_uri = null;
_scheme = null;
_user = null;
_host = null;
_port = -1;
_path = null;
_param = null;
_query = null;
_fragment = null;
_decodedPath = null;
_ambiguousSegment = false;
}
public void parse(String uri)
{
clear();
@ -205,9 +249,12 @@ public class HttpURI
private void parse(State state, final String uri, final int offset, final int end)
{
boolean encoded = false;
int mark = offset;
int pathMark = 0;
int mark = offset; // the start of the current section being parsed
int pathMark = 0; // the start of the path section
int segment = 0; // the start of the current segment within the path
boolean encoded = false; // set to true if the path contains % encoded characters
boolean dot = false; // set to true if the path containers . or .. segments
int escapedSlash = 0; // state of parsing a %2f
for (int i = offset; i < end; i++)
{
@ -241,21 +288,30 @@ public class HttpURI
_path = "*";
state = State.ASTERISK;
break;
case '%':
encoded = true;
escapedSlash = 1;
mark = pathMark = segment = i;
state = State.PATH;
break;
case '.' :
dot = true;
pathMark = segment = i;
state = State.PATH;
break;
default:
mark = i;
if (_scheme == null)
state = State.SCHEME_OR_PATH;
else
{
pathMark = i;
pathMark = segment = i;
state = State.PATH;
}
}
continue;
}
case SCHEME_OR_PATH:
{
switch (c)
@ -266,40 +322,38 @@ public class HttpURI
// Start again with scheme set
state = State.START;
break;
case '/':
// must have been in a path and still are
segment = i + 1;
state = State.PATH;
break;
case ';':
// must have been in a path
mark = i + 1;
state = State.PARAM;
break;
case '?':
// must have been in a path
_path = uri.substring(mark, i);
mark = i + 1;
state = State.QUERY;
break;
case '%':
// must have be in an encoded path
encoded = true;
escapedSlash = 1;
state = State.PATH;
break;
case '#':
// must have been in a path
_path = uri.substring(mark, i);
state = State.FRAGMENT;
break;
default:
break;
}
continue;
}
case HOST_OR_PATH:
{
switch (c)
@ -310,23 +364,26 @@ public class HttpURI
state = State.HOST;
break;
case '%':
case '@':
case ';':
case '?':
case '#':
case '.':
// was a path, look again
i--;
pathMark = mark;
segment = mark + 1;
state = State.PATH;
break;
default:
// it is a path
pathMark = mark;
segment = mark + 1;
state = State.PATH;
}
continue;
}
case HOST:
{
switch (c)
@ -334,6 +391,7 @@ public class HttpURI
case '/':
_host = uri.substring(mark, i);
pathMark = mark = i;
segment = mark + 1;
state = State.PATH;
break;
case ':':
@ -348,14 +406,14 @@ public class HttpURI
_user = uri.substring(mark, i);
mark = i + 1;
break;
case '[':
state = State.IPV6;
break;
default:
break;
}
continue;
}
case IPV6:
{
switch (c)
@ -376,11 +434,11 @@ public class HttpURI
state = State.PATH;
}
break;
default:
break;
}
continue;
}
case PORT:
{
if (c == '@')
@ -396,36 +454,57 @@ public class HttpURI
{
_port = TypeUtil.parseInt(uri, mark, i - mark, 10);
pathMark = mark = i;
segment = i + 1;
state = State.PATH;
}
continue;
}
case PATH:
{
switch (c)
{
case ';':
checkSegment(uri, segment, i, true);
mark = i + 1;
state = State.PARAM;
break;
case '?':
checkSegment(uri, segment, i, false);
_path = uri.substring(pathMark, i);
mark = i + 1;
state = State.QUERY;
break;
case '#':
checkSegment(uri, segment, i, false);
_path = uri.substring(pathMark, i);
mark = i + 1;
state = State.FRAGMENT;
break;
case '/':
checkSegment(uri, segment, i, false);
segment = i + 1;
break;
case '.':
dot |= segment == i;
break;
case '%':
encoded = true;
escapedSlash = 1;
break;
case '2':
escapedSlash = escapedSlash == 1 ? 2 : 0;
break;
case 'f':
case 'F':
_ambiguousSegment |= (escapedSlash == 2);
escapedSlash = 0;
break;
default:
escapedSlash = 0;
break;
}
continue;
}
case PARAM:
{
switch (c)
@ -444,17 +523,18 @@ public class HttpURI
break;
case '/':
encoded = true;
// ignore internal params
segment = i + 1;
state = State.PATH;
break;
case ';':
// multiple parameters
mark = i + 1;
break;
default:
break;
}
continue;
}
case QUERY:
{
if (c == '#')
@ -465,17 +545,18 @@ public class HttpURI
}
continue;
}
case ASTERISK:
{
throw new IllegalArgumentException("Bad character '*'");
}
case FRAGMENT:
{
_fragment = uri.substring(mark, end);
i = end;
break;
}
default:
break;
}
}
@ -486,51 +567,78 @@ public class HttpURI
case SCHEME_OR_PATH:
_path = uri.substring(mark, end);
break;
case HOST_OR_PATH:
_path = uri.substring(mark, end);
break;
case HOST:
if (end > mark)
_host = uri.substring(mark, end);
break;
case IPV6:
throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri);
case PORT:
_port = TypeUtil.parseInt(uri, mark, end - mark, 10);
break;
case ASTERISK:
break;
case FRAGMENT:
_fragment = uri.substring(mark, end);
break;
case PARAM:
_path = uri.substring(pathMark, end);
_param = uri.substring(mark, end);
break;
case PATH:
checkSegment(uri, segment, end, false);
_path = uri.substring(pathMark, end);
break;
case QUERY:
_query = uri.substring(mark, end);
break;
default:
break;
}
if (!encoded)
if (!encoded && !dot)
{
if (_param == null)
_decodedPath = _path;
else
_decodedPath = _path.substring(0, _path.length() - _param.length() - 1);
}
else if (_path != null)
{
String canonical = URIUtil.canonicalPath(_path);
if (canonical == null)
throw new BadMessageException("Bad URI");
_decodedPath = URIUtil.decodePath(canonical);
}
}
/**
* Check for ambiguous path segments.
*
* An ambiguous path segment is one that is perhaps technically legal, but is considered undesirable to handle
* due to possible ambiguity. Examples include segments like '..;', '%2e', '%2e%2e' etc.
* @param uri The URI string
* @param segment The inclusive starting index of the segment (excluding any '/')
* @param end The exclusive end index of the segment
*/
private void checkSegment(String uri, int segment, int end, boolean param)
{
if (!_ambiguousSegment)
{
Boolean ambiguous = __ambiguousSegments.get(uri, segment, end - segment);
_ambiguousSegment |= ambiguous == Boolean.TRUE || (param && ambiguous == Boolean.FALSE);
}
}
/**
* @return True if the URI has a possibly ambiguous segment like '..;' or '%2e%2e'
*/
public boolean hasAmbiguousSegment()
{
return _ambiguousSegment;
}
public String getScheme()
@ -561,10 +669,12 @@ public class HttpURI
return _path;
}
/**
* @return The decoded canonical path.
* @see URIUtil#canonicalPath(String)
*/
public String getDecodedPath()
{
if (_decodedPath == null && _path != null)
_decodedPath = URIUtil.decodePath(_path);
return _decodedPath;
}
@ -575,10 +685,14 @@ public class HttpURI
public void setParam(String param)
{
_param = param;
if (_path != null && !_path.contains(_param))
if (!Objects.equals(_param, param))
{
_path += ";" + _param;
if (_param != null && _path.endsWith(";" + _param))
_path = _path.substring(0, _path.length() - 1 - _param.length());
_param = param;
if (_param != null)
_path = (_path == null ? "" : _path) + ";" + _param;
_uri = null;
}
}
@ -620,21 +734,6 @@ public class HttpURI
UrlEncoded.decodeTo(_query, parameters, encoding);
}
public void clear()
{
_uri = null;
_scheme = null;
_host = null;
_port = -1;
_path = null;
_param = null;
_query = null;
_fragment = null;
_decodedPath = null;
}
public boolean isAbsolute()
{
return _scheme != null && !_scheme.isEmpty();
@ -688,6 +787,12 @@ public class HttpURI
return toString().equals(o.toString());
}
@Override
public int hashCode()
{
return toString().hashCode();
}
public void setScheme(String scheme)
{
_scheme = scheme;
@ -711,19 +816,24 @@ public class HttpURI
public void setPath(String path)
{
_uri = null;
_path = path;
_decodedPath = null;
_path = null;
if (path != null)
parse(State.PATH, path, 0, path.length());
}
public void setPathQuery(String path)
public void setPathQuery(String pathQuery)
{
_uri = null;
_path = null;
_decodedPath = null;
_param = null;
_fragment = null;
if (path != null)
parse(State.PATH, path, 0, path.length());
/*
* The query is not cleared here and old values may be retained if there is no query in
* the pathQuery. This has been fixed in 10, but left as is here to preserve behaviour in 9.
*/
if (pathQuery != null)
parse(State.PATH, pathQuery, 0, pathQuery.length());
}
public void setQuery(String query)

View File

@ -71,7 +71,7 @@ public class PrecompressedHttpContent implements HttpContent
@Override
public String getETagValue()
{
return _content.getResource().getWeakETag(_format._etag);
return _content.getResource().getWeakETag(_format.getEtagSuffix());
}
@Override
@ -101,13 +101,13 @@ public class PrecompressedHttpContent implements HttpContent
@Override
public HttpField getContentEncoding()
{
return _format._contentEncoding;
return _format.getContentEncoding();
}
@Override
public String getContentEncodingValue()
{
return _format._contentEncoding.getValue();
return _format.getContentEncoding().getValue();
}
@Override
@ -167,7 +167,9 @@ public class PrecompressedHttpContent implements HttpContent
@Override
public String toString()
{
return String.format("PrecompressedHttpContent@%x{e=%s,r=%s|%s,lm=%s|%s,ct=%s}", hashCode(), _format._encoding,
return String.format("%s@%x{e=%s,r=%s|%s,lm=%s|%s,ct=%s}",
this.getClass().getSimpleName(), hashCode(),
_format,
_content.getResource(), _precompressedContent.getResource(),
_content.getResource().lastModified(), _precompressedContent.getResource().lastModified(),
getContentType());

View File

@ -21,12 +21,12 @@ package org.eclipse.jetty.http;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.log.Log;
import static java.lang.Integer.MIN_VALUE;
/**
* Implements a quoted comma separated list of quality values
* in accordance with RFC7230 and RFC7231.
@ -55,7 +55,8 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
return 3;
};
private final List<Double> _quality = new ArrayList<>();
private final List<QualityValue> _qualities = new ArrayList<>();
private QualityValue _lastQuality;
private boolean _sorted = false;
private final ToIntFunction<String> _secondaryOrdering;
@ -64,7 +65,7 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
*/
public QuotedQualityCSV()
{
this((ToIntFunction)null);
this((ToIntFunction<String>)null);
}
/**
@ -85,32 +86,48 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
if ("*".equals(s))
return preferredOrder.length;
return MIN_VALUE;
return 0;
});
}
/**
* Orders values with equal quality with the given function.
*
* @param secondaryOrdering Function to apply an ordering other than specified by quality
* @param secondaryOrdering Function to apply an ordering other than specified by quality, highest values are sorted first.
*/
public QuotedQualityCSV(ToIntFunction<String> secondaryOrdering)
{
this._secondaryOrdering = secondaryOrdering == null ? s -> 0 : secondaryOrdering;
}
@Override
protected void parsedValueAndParams(StringBuffer buffer)
{
super.parsedValueAndParams(buffer);
// Collect full value with parameters
_lastQuality = new QualityValue(_lastQuality._quality, buffer.toString(), _lastQuality._index);
_qualities.set(_lastQuality._index, _lastQuality);
}
@Override
protected void parsedValue(StringBuffer buffer)
{
super.parsedValue(buffer);
_sorted = false;
// This is the just the value, without parameters.
// Assume a quality of ONE
_quality.add(1.0D);
_lastQuality = new QualityValue(1.0D, buffer.toString(), _qualities.size());
_qualities.add(_lastQuality);
}
@Override
protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
{
_sorted = false;
if (paramName < 0)
{
if (buffer.charAt(buffer.length() - 1) == ';')
@ -120,7 +137,7 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
buffer.charAt(paramName) == 'q' && paramValue > paramName &&
buffer.length() >= paramName && buffer.charAt(paramName + 1) == '=')
{
Double q;
double q;
try
{
q = (_keepQuotes && buffer.charAt(paramValue) == '"')
@ -135,8 +152,10 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
buffer.setLength(Math.max(0, paramName - 1));
if (q != 1.0D)
// replace assumed quality
_quality.set(_quality.size() - 1, q);
{
_lastQuality = new QualityValue(q, buffer.toString(), _lastQuality._index);
_qualities.set(_lastQuality._index, _lastQuality);
}
}
}
@ -158,38 +177,73 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
protected void sort()
{
_values.clear();
_qualities.stream()
.filter((qv) -> qv._quality != 0.0D)
.sorted()
.map(QualityValue::getValue)
.collect(Collectors.toCollection(() -> _values));
_sorted = true;
}
Double last = 0.0D;
int lastSecondaryOrder = Integer.MIN_VALUE;
private class QualityValue implements Comparable<QualityValue>
{
private final double _quality;
private final String _value;
private final int _index;
for (int i = _values.size(); i-- > 0; )
private QualityValue(double quality, String value, int index)
{
String v = _values.get(i);
Double q = _quality.get(i);
int compare = last.compareTo(q);
if (compare > 0 || (compare == 0 && _secondaryOrdering.applyAsInt(v) < lastSecondaryOrder))
{
_values.set(i, _values.get(i + 1));
_values.set(i + 1, v);
_quality.set(i, _quality.get(i + 1));
_quality.set(i + 1, q);
last = 0.0D;
lastSecondaryOrder = 0;
i = _values.size();
continue;
}
last = q;
lastSecondaryOrder = _secondaryOrdering.applyAsInt(v);
_quality = quality;
_value = value;
_index = index;
}
int lastElement = _quality.size();
while (lastElement > 0 && _quality.get(--lastElement).equals(0.0D))
@Override
public int hashCode()
{
_quality.remove(lastElement);
_values.remove(lastElement);
return Double.hashCode(_quality) ^ Objects.hash(_value, _index);
}
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof QualityValue))
return false;
QualityValue qv = (QualityValue)obj;
return _quality == qv._quality && Objects.equals(_value, qv._value) && Objects.equals(_index, qv._index);
}
private String getValue()
{
return _value;
}
@Override
public int compareTo(QualityValue o)
{
// sort highest quality first
int compare = Double.compare(o._quality, _quality);
if (compare == 0)
{
// then sort secondary order highest first
compare = Integer.compare(_secondaryOrdering.applyAsInt(o._value), _secondaryOrdering.applyAsInt(_value));
if (compare == 0)
// then sort index lowest first
compare = -Integer.compare(o._index, _index);
}
return compare;
}
@Override
public String toString()
{
return String.format("%s@%x[%s,q=%f,i=%d]",
getClass().getSimpleName(),
hashCode(),
_value,
_quality,
_index);
}
}
}

View File

@ -35,6 +35,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.eclipse.jetty.http.CompressedContentFormat.BR;
import static org.eclipse.jetty.http.CompressedContentFormat.GZIP;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -80,9 +82,25 @@ public class GZIPContentDecoderTest
{
assertTrue(CompressedContentFormat.tagEquals("tag", "tag"));
assertTrue(CompressedContentFormat.tagEquals("\"tag\"", "\"tag\""));
assertTrue(CompressedContentFormat.tagEquals("\"tag\"", "\"tag--gzip\""));
assertFalse(CompressedContentFormat.tagEquals("Zag", "Xag--gzip"));
assertTrue(CompressedContentFormat.tagEquals("\"tag\"", "\"tag" + GZIP.getEtagSuffix() + "\""));
assertTrue(CompressedContentFormat.tagEquals("\"tag\"", "\"tag" + BR.getEtagSuffix() + "\""));
assertTrue(CompressedContentFormat.tagEquals("W/\"1234567\"", "W/\"1234567\""));
assertTrue(CompressedContentFormat.tagEquals("W/\"1234567\"", "W/\"1234567" + GZIP.getEtagSuffix() + "\""));
assertFalse(CompressedContentFormat.tagEquals("Zag", "Xag" + GZIP.getEtagSuffix()));
assertFalse(CompressedContentFormat.tagEquals("xtag", "tag"));
assertFalse(CompressedContentFormat.tagEquals("W/\"1234567\"", "W/\"1234111\""));
assertFalse(CompressedContentFormat.tagEquals("W/\"1234567\"", "W/\"1234111" + GZIP.getEtagSuffix() + "\""));
assertTrue(CompressedContentFormat.tagEquals("12345", "\"12345\""));
assertTrue(CompressedContentFormat.tagEquals("\"12345\"", "12345"));
assertTrue(CompressedContentFormat.tagEquals("12345", "\"12345" + GZIP.getEtagSuffix() + "\""));
assertTrue(CompressedContentFormat.tagEquals("\"12345\"", "12345" + GZIP.getEtagSuffix()));
assertThat(GZIP.stripSuffixes("12345"), is("12345"));
assertThat(GZIP.stripSuffixes("12345, 666" + GZIP.getEtagSuffix()), is("12345, 666"));
assertThat(GZIP.stripSuffixes("12345, 666" + GZIP.getEtagSuffix() + ",W/\"9999" + GZIP.getEtagSuffix() + "\""),
is("12345, 666,W/\"9999\""));
}
@Test

View File

@ -20,9 +20,14 @@ package org.eclipse.jetty.http;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.stream.Stream;
import org.eclipse.jetty.util.MultiMap;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@ -214,11 +219,140 @@ public class HttpURITest
}
@Test
public void testBasicAuthCredentials() throws Exception
public void testSetters() throws Exception
{
HttpURI uri = new HttpURI("http://user:password@example.com:8888/blah");
assertEquals("http://user:password@example.com:8888/blah", uri.toString());
assertEquals(uri.getAuthority(), "example.com:8888");
assertEquals(uri.getUser(), "user:password");
HttpURI uri = new HttpURI();
assertEquals("", uri.toString());
uri = new HttpURI(null, null, 0, null, null, null, null);
assertEquals("", uri.toString());
uri.setPath("/path/info");
assertEquals("/path/info", uri.toString());
uri.setAuthority("host", 8080);
assertEquals("//host:8080/path/info", uri.toString());
uri.setParam("param");
assertEquals("//host:8080/path/info;param", uri.toString());
uri.setQuery("a=b");
assertEquals("//host:8080/path/info;param?a=b", uri.toString());
uri.setScheme("http");
assertEquals("http://host:8080/path/info;param?a=b", uri.toString());
uri.setPathQuery("/other;xxx/path;ppp?query");
assertEquals("http://host:8080/other;xxx/path;ppp?query", uri.toString());
assertThat(uri.getScheme(), is("http"));
assertThat(uri.getAuthority(), is("host:8080"));
assertThat(uri.getHost(), is("host"));
assertThat(uri.getPort(), is(8080));
assertThat(uri.getPath(), is("/other;xxx/path;ppp"));
assertThat(uri.getDecodedPath(), is("/other/path"));
assertThat(uri.getParam(), is("ppp"));
assertThat(uri.getQuery(), is("query"));
assertThat(uri.getPathQuery(), is("/other;xxx/path;ppp?query"));
uri.setPathQuery(null);
assertEquals("http://host:8080?query", uri.toString()); // Yes silly result!
uri.setQuery(null);
assertEquals("http://host:8080", uri.toString());
uri.setPathQuery("/other;xxx/path;ppp?query");
assertEquals("http://host:8080/other;xxx/path;ppp?query", uri.toString());
uri.setScheme(null);
assertEquals("//host:8080/other;xxx/path;ppp?query", uri.toString());
uri.setAuthority(null, -1);
assertEquals("/other;xxx/path;ppp?query", uri.toString());
uri.setParam(null);
assertEquals("/other;xxx/path?query", uri.toString());
uri.setQuery(null);
assertEquals("/other;xxx/path", uri.toString());
uri.setPath(null);
assertEquals("", uri.toString());
}
public static Stream<Arguments> decodePathTests()
{
return Arrays.stream(new Object[][]
{
// Simple path example
{"http://host/path/info", "/path/info", false},
{"//host/path/info", "/path/info", false},
{"/path/info", "/path/info", false},
// legal non ambiguous relative paths
{"http://host/../path/info", null, false},
{"http://host/path/../info", "/info", false},
{"http://host/path/./info", "/path/info", false},
{"//host/path/../info", "/info", false},
{"//host/path/./info", "/path/info", false},
{"/path/../info", "/info", false},
{"/path/./info", "/path/info", false},
{"path/../info", "info", false},
{"path/./info", "path/info", false},
// illegal paths
{"//host/../path/info", null, false},
{"/../path/info", null, false},
{"../path/info", null, false},
{"/path/%XX/info", null, false},
{"/path/%2/F/info", null, false},
// ambiguous dot encodings or parameter inclusions
{"scheme://host/path/%2e/info", "/path/./info", true},
{"scheme:/path/%2e/info", "/path/./info", true},
{"/path/%2e/info", "/path/./info", true},
{"path/%2e/info/", "path/./info/", true},
{"/path/%2e%2e/info", "/path/../info", true},
{"/path/%2e%2e;/info", "/path/../info", true},
{"/path/%2e%2e;param/info", "/path/../info", true},
{"/path/%2e%2e;param;other/info;other", "/path/../info", true},
{"/path/.;/info", "/path/./info", true},
{"/path/.;param/info", "/path/./info", true},
{"/path/..;/info", "/path/../info", true},
{"/path/..;param/info", "/path/../info", true},
{"%2e/info", "./info", true},
{"%2e%2e/info", "../info", true},
{"%2e%2e;/info", "../info", true},
{".;/info", "./info", true},
{".;param/info", "./info", true},
{"..;/info", "../info", true},
{"..;param/info", "../info", true},
{"%2e", ".", true},
{"%2e.", "..", true},
{".%2e", "..", true},
{"%2e%2e", "..", true},
// ambiguous segment separators
{"/path/%2f/info", "/path///info", true},
{"%2f/info", "//info", true},
{"%2F/info", "//info", true},
}).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("decodePathTests")
public void testDecodedPath(String input, String decodedPath, boolean ambiguous)
{
try
{
HttpURI uri = new HttpURI(input);
assertThat(uri.getDecodedPath(), is(decodedPath));
assertThat(uri.hasAmbiguousSegment(), is(ambiguous));
}
catch (Exception e)
{
assertThat(decodedPath, nullValue());
}
}
}

View File

@ -34,6 +34,9 @@ import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
/**
* <p>A ClientConnectionFactory that creates client-side {@link SslConnection} instances.</p>
*/
public class SslClientConnectionFactory implements ClientConnectionFactory
{
public static final String SSL_CONTEXT_FACTORY_CONTEXT_KEY = "ssl.context.factory";
@ -120,7 +123,10 @@ public class SslClientConnectionFactory implements ClientConnectionFactory
{
String host = (String)context.get(SSL_PEER_HOST_CONTEXT_KEY);
int port = (Integer)context.get(SSL_PEER_PORT_CONTEXT_KEY);
SSLEngine engine = sslContextFactory.newSSLEngine(host, port);
SSLEngine engine = sslContextFactory instanceof SslEngineFactory
? ((SslEngineFactory)sslContextFactory).newSslEngine(host, port, context)
: sslContextFactory.newSSLEngine(host, port);
engine.setUseClientMode(true);
context.put(SSL_ENGINE_CONTEXT_KEY, engine);
@ -155,6 +161,25 @@ public class SslClientConnectionFactory implements ClientConnectionFactory
return ClientConnectionFactory.super.customize(connection, context);
}
/**
* <p>A factory for {@link SSLEngine} objects.</p>
* <p>Typically implemented by {@link SslContextFactory.Client}
* to support more flexible creation of SSLEngine instances.</p>
*/
public interface SslEngineFactory
{
/**
* <p>Creates a new {@link SSLEngine} instance for the given peer host and port,
* and with the given context to help the creation of the SSLEngine.</p>
*
* @param host the peer host
* @param port the peer port
* @param context the {@link ClientConnectionFactory} context
* @return a new SSLEngine instance
*/
public SSLEngine newSslEngine(String host, int port, Map<String, Object> context);
}
private class HTTPSHandshakeListener implements SslHandshakeListener
{
private final Map<String, Object> context;

View File

@ -0,0 +1,549 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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.proxy;
import java.io.IOException;
import java.security.KeyStore;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.security.auth.x500.X500Principal;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* <p>Tests for client and proxy authentication using certificates.</p>
* <p>There are 3 KeyStores:</p>
* <dl>
* <dt>{@code client_keystore.p12}</dt>
* <dd>{@code server} -> the server certificate with CN=server</dd>
* <dd>{@code user1_client} -> the client certificate for user1, signed with the server certificate</dd>
* <dd>{@code user2_client} -> the client certificate for user2, signed with the server certificate</dd>
* </dl>
* <dl>
* <dt>{@code proxy_keystore.p12}</dt>
* <dd>{@code proxy} -> the proxy domain private key and certificate with CN=proxy</dd>
* <dd>{@code server} -> the server domain certificate with CN=server</dd>
* <dd>{@code user1_proxy} -> the proxy client certificate for user1, signed with the server certificate</dd>
* <dd>{@code user2_proxy} -> the proxy client certificate for user2, signed with the server certificate</dd>
* </dl>
* <dl>
* <dt>{@code server_keystore.p12}</dt>
* <dd>{@code server} -> the server domain private key and certificate with CN=server,
* with extension ca:true to sign client and proxy certificates.</dd>
* </dl>
* <p>In this way, a remote client can connect to the proxy and be authenticated,
* and the proxy can connect to the server on behalf of that remote client, since
* the proxy has a certificate correspondent to the one of the remote client.</p>
* <p>The main problem is to make sure that the {@code HttpClient} in the proxy uses different connections
* to connect to the same server, and that those connections are authenticated via TLS client certificate
* with the correct certificate, avoiding that requests made by {@code user2} are sent over connections
* that are authenticated with {@code user1} certificates.</p>
*/
public class ClientAuthProxyTest
{
private Server server;
private ServerConnector serverConnector;
private Server proxy;
private ServerConnector proxyConnector;
private HttpClient client;
private void startServer(Handler handler) throws Exception
{
QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server");
server = new Server(serverThreads);
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
SslContextFactory.Server serverTLS = new SslContextFactory.Server();
serverTLS.setNeedClientAuth(true);
// The KeyStore is also a TrustStore.
serverTLS.setKeyStorePath(MavenTestingUtils.getTestResourceFile("client_auth/server_keystore.p12").getAbsolutePath());
serverTLS.setKeyStorePassword("storepwd");
serverTLS.setKeyStoreType("PKCS12");
SslConnectionFactory ssl = new SslConnectionFactory(serverTLS, http.getProtocol());
serverConnector = new ServerConnector(server, 1, 1, ssl, http);
server.addConnector(serverConnector);
server.setHandler(handler);
server.start();
System.err.println("SERVER = localhost:" + serverConnector.getLocalPort());
}
private void startServer() throws Exception
{
startServer(new EmptyServerHandler()
{
@Override
protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.JAVAX_SERVLET_REQUEST_X_509_CERTIFICATE);
Assertions.assertNotNull(certificates);
X509Certificate certificate = certificates[0];
X500Principal principal = certificate.getSubjectX500Principal();
ServletOutputStream output = response.getOutputStream();
output.println(principal.toString());
output.println(request.getRemotePort());
}
});
}
private void startProxy(AbstractProxyServlet servlet) throws Exception
{
QueuedThreadPool proxyThreads = new QueuedThreadPool();
proxyThreads.setName("proxy");
proxy = new Server();
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
SslContextFactory.Server proxyTLS = new SslContextFactory.Server();
proxyTLS.setNeedClientAuth(true);
// The KeyStore is also a TrustStore.
proxyTLS.setKeyStorePath(MavenTestingUtils.getTestResourceFile("client_auth/proxy_keystore.p12").getAbsolutePath());
proxyTLS.setKeyStorePassword("storepwd");
proxyTLS.setKeyStoreType("PKCS12");
SslConnectionFactory ssl = new SslConnectionFactory(proxyTLS, http.getProtocol());
proxyConnector = new ServerConnector(proxy, 1, 1, ssl, http);
proxy.addConnector(proxyConnector);
ServletContextHandler context = new ServletContextHandler(proxy, "/");
context.addServlet(new ServletHolder(servlet), "/*");
proxy.setHandler(context);
proxy.start();
System.err.println("PROXY = localhost:" + proxyConnector.getLocalPort());
}
private void startClient() throws Exception
{
SslContextFactory.Client clientTLS = new SslContextFactory.Client();
// Disable TLS-level hostname verification.
clientTLS.setEndpointIdentificationAlgorithm(null);
clientTLS.setKeyStorePath(MavenTestingUtils.getTestResourceFile("client_auth/client_keystore.p12").getAbsolutePath());
clientTLS.setKeyStorePassword("storepwd");
clientTLS.setKeyStoreType("PKCS12");
client = new HttpClient(clientTLS);
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
client.setExecutor(clientThreads);
client.start();
}
@AfterEach
public void dispose() throws Exception
{
LifeCycle.stop(client);
LifeCycle.stop(proxy);
LifeCycle.stop(server);
}
private static String retrieveUser(HttpServletRequest request)
{
X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.JAVAX_SERVLET_REQUEST_X_509_CERTIFICATE);
String clientName = certificates[0].getSubjectX500Principal().getName();
Matcher matcher = Pattern.compile("CN=([^,]+)").matcher(clientName);
if (matcher.find())
{
// Retain only "userN".
return matcher.group(1).split("_")[0];
}
return null;
}
@Test
public void testClientAuthProxyingWithMultipleHttpClients() throws Exception
{
// Using a different HttpClient (with a different SslContextFactory.Client)
// per user works, but there is a lot of duplicated state in the HttpClients:
// Executors and Schedulers (although they can be shared), but also CookieManagers
// ProtocolHandlers, etc.
// The proxy has different SslContextFactory.Client statically configured
// for different users.
startServer();
startProxy(new AsyncProxyServlet()
{
private final Map<String, HttpClient> httpClients = new ConcurrentHashMap<>();
@Override
protected Request newProxyRequest(HttpServletRequest request, String rewrittenTarget)
{
String user = retrieveUser(request);
HttpClient httpClient = getOrCreateHttpClient(user);
Request proxyRequest = httpClient.newRequest(rewrittenTarget)
.method(request.getMethod())
.attribute(CLIENT_REQUEST_ATTRIBUTE, request);
// Send the request to the server.
proxyRequest.port(serverConnector.getLocalPort());
// No need to tag the request when using different HttpClients.
return proxyRequest;
}
private HttpClient getOrCreateHttpClient(String user)
{
if (user == null)
return getHttpClient();
return httpClients.computeIfAbsent(user, key ->
{
SslContextFactory.Client clientTLS = new SslContextFactory.Client();
// Disable TLS-level hostname verification for this test.
clientTLS.setEndpointIdentificationAlgorithm(null);
clientTLS.setKeyStorePath(MavenTestingUtils.getTestResourceFile("client_auth/proxy_keystore.p12").getAbsolutePath());
clientTLS.setKeyStorePassword("storepwd");
clientTLS.setKeyStoreType("PKCS12");
clientTLS.setCertAlias(key + "_proxy");
// TODO: httpClients should share Executor and Scheduler at least.
HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP(1), clientTLS);
LifeCycle.start(httpClient);
return httpClient;
});
}
});
startClient();
testRequestsFromRemoteClients();
}
@Test
public void testClientAuthProxyingWithMultipleServerSubDomains() throws Exception
{
// Another idea is to use multiple subdomains for the server,
// such as user1.server.com, user2.server.com, with the server
// providing a *.server.com certificate.
// The proxy must pick the right alias dynamically based on the
// remote client request.
// For this test we use 127.0.0.N addresses.
startServer();
startProxy(new AsyncProxyServlet()
{
private final AtomicInteger userIds = new AtomicInteger();
private final Map<String, String> subDomains = new ConcurrentHashMap<>();
@Override
protected Request newProxyRequest(HttpServletRequest request, String rewrittenTarget)
{
String user = retrieveUser(request);
// Obviously not fool proof, but for the 2 users in this test it does the job.
String subDomain = subDomains.computeIfAbsent(user, key -> "127.0.0." + userIds.incrementAndGet());
Request proxyRequest = super.newProxyRequest(request, rewrittenTarget);
proxyRequest.host(subDomain).port(serverConnector.getLocalPort());
// Tag the request.
proxyRequest.tag(new AliasTLSTag(user));
return proxyRequest;
}
@Override
protected HttpClient newHttpClient()
{
SslContextFactory.Client clientTLS = new SslContextFactory.Client()
{
@Override
protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception
{
KeyManager[] keyManagers = super.getKeyManagers(keyStore);
for (int i = 0; i < keyManagers.length; i++)
{
keyManagers[i] = new ProxyAliasX509ExtendedKeyManager(keyManagers[i]);
}
return keyManagers;
}
};
// Disable TLS-level hostname verification for this test.
clientTLS.setEndpointIdentificationAlgorithm(null);
clientTLS.setKeyStorePath(MavenTestingUtils.getTestResourceFile("client_auth/proxy_keystore.p12").getAbsolutePath());
clientTLS.setKeyStorePassword("storepwd");
clientTLS.setKeyStoreType("PKCS12");
return new HttpClient(new HttpClientTransportOverHTTP(1), clientTLS);
}
});
startClient();
testRequestsFromRemoteClients();
}
@Test
public void testClientAuthProxyingWithSSLSessionResumptionDisabled() throws Exception
{
// To user the same HttpClient and server hostName, we need to disable
// SSLSession caching, which is only possible by creating SSLEngine
// without peer host information.
// This is more CPU intensive because TLS sessions can never be resumed.
startServer();
startProxy(new AsyncProxyServlet()
{
@Override
protected Request newProxyRequest(HttpServletRequest request, String rewrittenTarget)
{
String user = retrieveUser(request);
Request proxyRequest = super.newProxyRequest(request, rewrittenTarget);
proxyRequest.port(serverConnector.getLocalPort());
// Tag the request.
proxyRequest.tag(new AliasTLSTag(user));
return proxyRequest;
}
@Override
protected HttpClient newHttpClient()
{
SslContextFactory.Client clientTLS = new SslContextFactory.Client()
{
@Override
public SSLEngine newSSLEngine(String host, int port)
{
// This disable TLS session resumption and requires
// endpointIdentificationAlgorithm=null because the TLS implementation
// does not have the peer host to verify the server certificate.
return newSSLEngine();
}
@Override
protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception
{
KeyManager[] keyManagers = super.getKeyManagers(keyStore);
for (int i = 0; i < keyManagers.length; i++)
{
keyManagers[i] = new ProxyAliasX509ExtendedKeyManager(keyManagers[i]);
}
return keyManagers;
}
};
// Disable hostname verification is required.
clientTLS.setEndpointIdentificationAlgorithm(null);
clientTLS.setKeyStorePath(MavenTestingUtils.getTestResourceFile("client_auth/proxy_keystore.p12").getAbsolutePath());
clientTLS.setKeyStorePassword("storepwd");
clientTLS.setKeyStoreType("PKCS12");
return new HttpClient(new HttpClientTransportOverHTTP(1), clientTLS);
}
});
startClient();
testRequestsFromRemoteClients();
}
@Test
public void testClientAuthProxyingWithCompositeSslContextFactory() throws Exception
{
// The idea here is to have a composite SslContextFactory that holds one for each user.
// It requires a change in SslClientConnectionFactory to "sniff" for the composite.
startServer();
startProxy(new AsyncProxyServlet()
{
@Override
protected Request newProxyRequest(HttpServletRequest request, String rewrittenTarget)
{
String user = retrieveUser(request);
Request proxyRequest = super.newProxyRequest(request, rewrittenTarget);
proxyRequest.port(serverConnector.getLocalPort());
proxyRequest.tag(user);
return proxyRequest;
}
@Override
protected HttpClient newHttpClient()
{
ProxyAliasClientSslContextFactory clientTLS = configure(new ProxyAliasClientSslContextFactory(), null);
// Statically add SslContextFactory.Client instances per each user.
clientTLS.factories.put("user1", configure(new SslContextFactory.Client(), "user1"));
clientTLS.factories.put("user2", configure(new SslContextFactory.Client(), "user2"));
return new HttpClient(new HttpClientTransportOverHTTP(1), clientTLS);
}
private <T extends SslContextFactory.Client> T configure(T tls, String user)
{
// Disable TLS-level hostname verification for this test.
tls.setEndpointIdentificationAlgorithm(null);
tls.setKeyStorePath(MavenTestingUtils.getTestResourceFile("client_auth/proxy_keystore.p12").getAbsolutePath());
tls.setKeyStorePassword("storepwd");
tls.setKeyStoreType("PKCS12");
if (user != null)
{
tls.setCertAlias(user + "_proxy");
LifeCycle.start(tls);
}
return tls;
}
});
startClient();
testRequestsFromRemoteClients();
}
private void testRequestsFromRemoteClients() throws Exception
{
// User1 makes a request to the proxy using its own certificate.
SslContextFactory clientTLS = client.getSslContextFactory();
clientTLS.reload(ssl -> ssl.setCertAlias("user1_client"));
ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort())
.scheme(HttpScheme.HTTPS.asString())
.timeout(5, TimeUnit.SECONDS)
.tag("user1")
.send();
Assertions.assertEquals(HttpStatus.OK_200, response.getStatus());
String[] parts = response.getContentAsString().split("\n");
String proxyClientSubject1 = parts[0];
String proxyClientPort1 = parts[1];
// User2 makes a request to the proxy using its own certificate.
clientTLS.reload(ssl -> ssl.setCertAlias("user2_client"));
response = client.newRequest("localhost", proxyConnector.getLocalPort())
.scheme(HttpScheme.HTTPS.asString())
.timeout(5, TimeUnit.SECONDS)
.tag("user2")
.send();
Assertions.assertEquals(HttpStatus.OK_200, response.getStatus());
parts = response.getContentAsString().split("\n");
String proxyClientSubject2 = parts[0];
String proxyClientPort2 = parts[1];
Assertions.assertNotEquals(proxyClientSubject1, proxyClientSubject2);
Assertions.assertNotEquals(proxyClientPort1, proxyClientPort2);
}
private static class AliasTLSTag implements ClientConnectionFactory.Decorator
{
private final String user;
private AliasTLSTag(String user)
{
this.user = user;
}
@Override
public ClientConnectionFactory apply(ClientConnectionFactory factory)
{
return (endPoint, context) ->
{
Connection connection = factory.newConnection(endPoint, context);
SSLEngine sslEngine = (SSLEngine)context.get(SslClientConnectionFactory.SSL_ENGINE_CONTEXT_KEY);
sslEngine.getSession().putValue("user", user);
return connection;
};
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
AliasTLSTag that = (AliasTLSTag)obj;
return user.equals(that.user);
}
@Override
public int hashCode()
{
return Objects.hash(user);
}
}
private static class ProxyAliasX509ExtendedKeyManager extends SslContextFactory.X509ExtendedKeyManagerWrapper
{
private ProxyAliasX509ExtendedKeyManager(KeyManager keyManager)
{
super((X509ExtendedKeyManager)keyManager);
}
@Override
public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine engine)
{
for (String keyType : keyTypes)
{
String[] aliases = getClientAliases(keyType, issuers);
if (aliases != null)
{
SSLSession sslSession = engine.getSession();
String user = (String)sslSession.getValue("user");
String alias = user + "_proxy";
if (Arrays.asList(aliases).contains(alias))
return alias;
}
}
return super.chooseEngineClientAlias(keyTypes, issuers, engine);
}
}
private static class ProxyAliasClientSslContextFactory extends SslContextFactory.Client implements SslClientConnectionFactory.SslEngineFactory
{
private final Map<String, SslContextFactory.Client> factories = new ConcurrentHashMap<>();
@Override
public SSLEngine newSslEngine(String host, int port, Map<String, Object> context)
{
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
String user = (String)destination.getOrigin().getTag();
return factories.compute(user, (key, value) -> value != null ? value : this).newSSLEngine(host, port);
}
}
}

View File

@ -0,0 +1,73 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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.rewrite.handler;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
public class ForceRequestHeaderValueRule extends Rule
{
private String headerName;
private String forcedValue;
public String getHeaderName()
{
return headerName;
}
public void setHeaderName(String headerName)
{
this.headerName = headerName;
}
public String getForcedValue()
{
return forcedValue;
}
public void setForcedValue(String forcedValue)
{
this.forcedValue = forcedValue;
}
@Override
public String matchAndApply(String target, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException
{
String existingValue = httpServletRequest.getHeader(headerName);
if (existingValue == null)
{
// no hit, skip this rule.
return null;
}
if (existingValue.equals(forcedValue))
{
// already what we expect, skip this rule.
return null;
}
Request baseRequest = Request.getBaseRequest(httpServletRequest);
baseRequest.getHttpFields().remove(headerName);
baseRequest.getHttpFields().add(headerName, forcedValue);
return target;
}
}

View File

@ -0,0 +1,187 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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.rewrite.handler;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Collections;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.util.component.LifeCycle;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ForceRequestHeaderValueRuleTest
{
private Server server;
private LocalConnector connector;
private ForceRequestHeaderValueRule rule;
@BeforeEach
public void setup() throws Exception
{
server = new Server();
connector = new LocalConnector(server);
server.addConnector(connector);
HandlerList handlers = new HandlerList();
RewriteHandler rewriteHandler = new RewriteHandler();
rule = new ForceRequestHeaderValueRule();
rewriteHandler.addRule(rule);
Handler handler = new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
OutputStream stream = response.getOutputStream();
OutputStreamWriter out = new OutputStreamWriter(stream);
out.append("Echo\n");
for (String headerName : Collections.list(request.getHeaderNames()))
{
// Combine all values for header into single output on response body
out.append("Request Header[").append(headerName).append("]: [")
.append(request.getHeader(headerName)).append("]\n");
}
out.flush();
baseRequest.setHandled(true);
}
};
handlers.addHandler(rewriteHandler);
handlers.addHandler(handler);
server.setHandler(handlers);
server.start();
}
@AfterEach
public void teardown()
{
LifeCycle.stop(server);
}
@Test
public void testNormalRequest() throws Exception
{
rule.setHeaderName("Accept");
rule.setForcedValue("*/*");
StringBuilder request = new StringBuilder();
request.append("GET /echo/foo HTTP/1.1\r\n");
request.append("Host: local\r\n");
request.append("Connection: closed\r\n");
request.append("\r\n");
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request.toString()));
assertEquals(200, response.getStatus());
assertThat(response.getContent(), not(containsString("[Accept]")));
assertThat(response.getContent(), containsString("[Host]: [local]"));
assertThat(response.getContent(), containsString("[Connection]: [closed]"));
}
@Test
public void testOneAcceptHeaderRequest() throws Exception
{
rule.setHeaderName("Accept");
rule.setForcedValue("*/*");
StringBuilder request = new StringBuilder();
request.append("GET /echo/foo HTTP/1.1\r\n");
request.append("Host: local\r\n");
request.append("Accept: */*\r\n");
request.append("Connection: closed\r\n");
request.append("\r\n");
String rawResponse = connector.getResponse(request.toString());
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertEquals(200, response.getStatus());
assertThat(response.getContent(), containsString("[Accept]: [*/*]"));
assertThat(response.getContent(), containsString("[Host]: [local]"));
assertThat(response.getContent(), containsString("[Connection]: [closed]"));
}
@Test
public void testThreeAcceptHeadersRequest() throws Exception
{
rule.setHeaderName("Accept");
rule.setForcedValue("*/*");
StringBuilder request = new StringBuilder();
request.append("GET /echo/foo HTTP/1.1\r\n");
request.append("Host: local\r\n");
request.append("Accept: images/jpeg\r\n");
request.append("Accept: text/plain\r\n");
request.append("Accept: */*\r\n");
request.append("Connection: closed\r\n");
request.append("\r\n");
String rawResponse = connector.getResponse(request.toString());
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertEquals(200, response.getStatus());
assertThat(response.getContent(), containsString("[Accept]: [*/*]"));
assertThat(response.getContent(), containsString("[Host]: [local]"));
assertThat(response.getContent(), containsString("[Connection]: [closed]"));
}
@Test
public void testInterleavedAcceptHeadersRequest() throws Exception
{
rule.setHeaderName("Accept");
rule.setForcedValue("*/*");
StringBuilder request = new StringBuilder();
request.append("GET /echo/foo HTTP/1.1\r\n");
request.append("Host: local\r\n");
request.append("Accept: images/jpeg\r\n"); // not value intended to be forced
request.append("Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0\r\n");
request.append("accept: text/plain\r\n"); // interleaved with other headers shouldn't matter
request.append("Accept-Charset: iso-8859-5, unicode-1-1;q=0.8\r\n");
request.append("ACCEPT: */*\r\n"); // case shouldn't matter
request.append("Connection: closed\r\n");
request.append("\r\n");
String rawResponse = connector.getResponse(request.toString());
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertEquals(200, response.getStatus());
assertThat(response.getContent(), containsString("[Accept]: [*/*]"));
assertThat(response.getContent(), containsString("[Accept-Charset]: [iso-8859-5, unicode-1-1;q=0.8]"));
assertThat(response.getContent(), containsString("[Accept-Encoding]: [gzip;q=1.0, identity; q=0.5, *;q=0]"));
assertThat(response.getContent(), containsString("[Host]: [local]"));
assertThat(response.getContent(), containsString("[Connection]: [closed]"));
}
}

View File

@ -29,7 +29,7 @@ import static org.hamcrest.Matchers.is;
public class RewritePatternRuleTest extends AbstractRuleTestCase
{
// TODO: Parameterize
private String[][] _tests =
private final String[][] _tests =
{
{"/foo/bar", "/", "/replace"},
{"/foo/bar", "/*", "/replace/foo/bar"},
@ -98,8 +98,8 @@ public class RewritePatternRuleTest extends AbstractRuleTestCase
assertThat("result matches expected", result, is(replacement));
rewritePatternRule.applyURI(_request, null, result);
assertThat("queryString matches expected", _request.getQueryString(), is(queryString));
assertThat("request URI matches expected", _request.getRequestURI(), is(replacement));
assertThat("queryString matches expected", _request.getQueryString(), is(queryString));
}
@Test

View File

@ -235,7 +235,7 @@ public class CachedContentFactory implements HttpContent.ContentFactory
Map<CompressedContentFormat, CachedHttpContent> precompresssedContents = new HashMap<>(_precompressedFormats.length);
for (CompressedContentFormat format : _precompressedFormats)
{
String compressedPathInContext = pathInContext + format._extension;
String compressedPathInContext = pathInContext + format.getExtension();
CachedHttpContent compressedContent = _cache.get(compressedPathInContext);
if (compressedContent == null || compressedContent.isValid())
{
@ -280,7 +280,7 @@ public class CachedContentFactory implements HttpContent.ContentFactory
Map<CompressedContentFormat, HttpContent> compressedContents = new HashMap<>();
for (CompressedContentFormat format : _precompressedFormats)
{
String compressedPathInContext = pathInContext + format._extension;
String compressedPathInContext = pathInContext + format.getExtension();
CachedHttpContent compressedContent = _cache.get(compressedPathInContext);
if (compressedContent != null && compressedContent.isValid() && compressedContent.getResource().lastModified() >= resource.lastModified())
compressedContents.put(format, compressedContent);
@ -693,7 +693,7 @@ public class CachedContentFactory implements HttpContent.ContentFactory
_content = content;
_precompressedContent = precompressedContent;
_etag = (CachedContentFactory.this._etags) ? new PreEncodedHttpField(HttpHeader.ETAG, _content.getResource().getWeakETag(format._etag)) : null;
_etag = (CachedContentFactory.this._etags) ? new PreEncodedHttpField(HttpHeader.ETAG, _content.getResource().getWeakETag(format.getEtagSuffix())) : null;
}
public boolean isValid()

View File

@ -114,6 +114,12 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
LOG.debug("New HTTP Connection {}", this);
}
@Deprecated
public HttpCompliance getHttpCompliance()
{
return _parser.getHttpCompliance();
}
public HttpConfiguration getHttpConfiguration()
{
return _config;
@ -376,8 +382,15 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
@Override
public void onCompleted()
{
// If we are fill interested, then a read is pending and we must abort
if (isFillInterested())
{
LOG.warn("Pending read in onCompleted {} {}", this, getEndPoint());
_channel.abort(new IOException("Pending read in onCompleted"));
}
// Handle connection upgrades
if (_channel.getResponse().getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101)
else if (_channel.getResponse().getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101)
{
Connection connection = (Connection)_channel.getRequest().getAttribute(UPGRADE_CONNECTION_ATTRIBUTE);
if (connection != null)
@ -401,6 +414,9 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
}
}
// Drive to EOF, EarlyEOF or Error
boolean complete = _input.consumeAll();
// Finish consuming the request
// If we are still expecting
if (_channel.isExpecting100Continue())
@ -409,7 +425,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
_parser.close();
}
// else abort if we can't consume all
else if (_generator.isPersistent() && !_input.consumeAll())
else if (_generator.isPersistent() && !complete)
{
if (LOG.isDebugEnabled())
LOG.debug("unconsumed input {} {}", this, _parser);

View File

@ -720,12 +720,17 @@ public class HttpInput extends ServletInputStream implements Runnable
{
produceContent();
if (_content == null && _intercepted == null && _inputQ.isEmpty())
{
_state = EARLY_EOF;
_inputQ.notify();
return false;
}
}
catch (Throwable e)
{
LOG.debug(e);
_state = new ErrorState(e);
_inputQ.notify();
return false;
}
}

View File

@ -29,6 +29,7 @@ import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.ResourceBundle;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletOutputStream;
@ -69,7 +70,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
enum State
{
OPEN, // Open
CLOSE, // Close needed from onWriteCompletion
CLOSE, // Close needed from onWriteComplete
CLOSING, // Close in progress after close API called
CLOSED // Closed
}
@ -308,7 +309,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
{
// Somebody called close or complete while we were writing.
// We can now send a (probably empty) last buffer and then when it completes
// onWriteCompletion will be called again to actually execute the _completeCallback
// onWriteComplete will be called again to actually execute the _completeCallback
_state = State.CLOSING;
closeContent = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
}
@ -411,53 +412,87 @@ public class HttpOutput extends ServletOutputStream implements Runnable
ByteBuffer content = null;
synchronized (_channelState)
{
switch (_state)
// First check the API state for any unrecoverable situations
switch (_apiState)
{
case CLOSED:
succeeded = true;
case UNREADY: // isReady() has returned false so a call to onWritePossible may happen at any time
error = new CancellationException("Completed whilst write unready");
break;
case CLOSE:
case CLOSING:
_closedCallback = Callback.combine(_closedCallback, callback);
case PENDING: // an async write is pending and may complete at any time
// If this is not the last write, then we must abort
if (!_channel.getResponse().isContentComplete(_written))
error = new CancellationException("Completed whilst write pending");
break;
case OPEN:
if (_onError != null)
{
error = _onError;
case BLOCKED: // another thread is blocked in a write or a close
error = new CancellationException("Completed whilst write blocked");
break;
default:
break;
}
// If we can't complete due to the API state, then abort
if (error != null)
{
_channel.abort(error);
_writeBlocker.fail(error);
_state = State.CLOSED;
}
else
{
// Otherwise check the output state to determine how to complete
switch (_state)
{
case CLOSED:
succeeded = true;
break;
}
_closedCallback = Callback.combine(_closedCallback, callback);
case CLOSE:
case CLOSING:
_closedCallback = Callback.combine(_closedCallback, callback);
break;
switch (_apiState)
{
case BLOCKING:
// Output is idle blocking state, but we still do an async close
_apiState = ApiState.BLOCKED;
_state = State.CLOSING;
content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
case OPEN:
if (_onError != null)
{
error = _onError;
break;
}
case ASYNC:
case READY:
// Output is idle in async state, so we can do an async close
_apiState = ApiState.PENDING;
_state = State.CLOSING;
content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
break;
_closedCallback = Callback.combine(_closedCallback, callback);
case BLOCKED:
case UNREADY:
case PENDING:
// An operation is in progress, so we soft close now
_softClose = true;
// then trigger a close from onWriteComplete
_state = State.CLOSE;
break;
}
break;
switch (_apiState)
{
case BLOCKING:
// Output is idle blocking state, but we still do an async close
_apiState = ApiState.BLOCKED;
_state = State.CLOSING;
content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
break;
case ASYNC:
case READY:
// Output is idle in async state, so we can do an async close
_apiState = ApiState.PENDING;
_state = State.CLOSING;
content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER;
break;
case UNREADY:
case PENDING:
// An operation is in progress, so we soft close now
_softClose = true;
// then trigger a close from onWriteComplete
_state = State.CLOSE;
break;
default:
throw new IllegalStateException();
}
break;
}
}
}
@ -1399,7 +1434,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
{
_state = State.OPEN;
_apiState = ApiState.BLOCKING;
_softClose = false;
_softClose = true; // Stay closed until next request
_interceptor = _channel;
HttpConfiguration config = _channel.getHttpConfiguration();
_bufferSize = config.getOutputBufferSize();

View File

@ -65,6 +65,8 @@ import javax.servlet.http.Part;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpComplianceSection;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
@ -77,6 +79,7 @@ import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandler.Context;
@ -1811,10 +1814,28 @@ public class Request implements HttpServletRequest
*/
public void setMetaData(org.eclipse.jetty.http.MetaData.Request request)
{
if (_metaData == null && _input != null && _channel != null)
{
_input.recycle();
_channel.getResponse().getHttpOutput().reopen();
}
_metaData = request;
setMethod(request.getMethod());
HttpURI uri = request.getURI();
if (uri.hasAmbiguousSegment())
{
// TODO replace in jetty-10 with HttpCompliance from the HttpConfiguration
Connection connection = _channel.getConnection();
HttpCompliance compliance = connection instanceof HttpConnection
? ((HttpConnection)connection).getHttpCompliance()
: _channel.getConnector().getBean(HttpCompliance.class);
boolean allow = compliance != null && !compliance.sections().contains(HttpComplianceSection.NO_AMBIGUOUS_PATH_SEGMENTS);
if (!allow)
throw new BadMessageException("Ambiguous segment in URI");
}
_originalURI = uri.isAbsolute() && request.getHttpVersion() != HttpVersion.HTTP_2 ? uri.toString() : uri.getPathQuery();
String encoded = uri.getPath();
@ -1826,7 +1847,7 @@ public class Request implements HttpServletRequest
}
else if (encoded.startsWith("/"))
{
path = (encoded.length() == 1) ? "/" : URIUtil.canonicalPath(uri.getDecodedPath());
path = (encoded.length() == 1) ? "/" : uri.getDecodedPath();
}
else if ("*".equals(encoded) || HttpMethod.CONNECT.is(getMethod()))
{
@ -1879,7 +1900,7 @@ public class Request implements HttpServletRequest
getHttpChannelState().recycle();
_requestAttributeListeners.clear();
_input.recycle();
// Defer _input.recycle() until setMetaData on next request, TODO replace with recycle and reopen in 10
_metaData = null;
_originalURI = null;
_contextPath = null;

View File

@ -84,7 +84,7 @@ public class ResourceContentFactory implements ContentFactory
Map<CompressedContentFormat, HttpContent> compressedContents = new HashMap<>(_precompressedFormats.length);
for (CompressedContentFormat format : _precompressedFormats)
{
String compressedPathInContext = pathInContext + format._extension;
String compressedPathInContext = pathInContext + format.getExtension();
Resource compressedResource = _factory.getResource(compressedPathInContext);
if (compressedResource != null && compressedResource.exists() && compressedResource.lastModified() >= resource.lastModified() &&
compressedResource.length() < resource.length())

View File

@ -141,7 +141,7 @@ public class ResourceService
public void setPrecompressedFormats(CompressedContentFormat[] precompressedFormats)
{
_precompressedFormats = precompressedFormats;
_preferredEncodingOrder = stream(_precompressedFormats).map(f -> f._encoding).toArray(String[]::new);
_preferredEncodingOrder = stream(_precompressedFormats).map(f -> f.getEncoding()).toArray(String[]::new);
}
public void setEncodingCacheSize(int encodingCacheSize)
@ -282,7 +282,7 @@ public class ResourceService
if (LOG.isDebugEnabled())
LOG.debug("precompressed={}", precompressedContent);
content = precompressedContent;
response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), precompressedContentEncoding._encoding);
response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), precompressedContentEncoding.getEncoding());
}
}
@ -355,7 +355,7 @@ public class ResourceService
{
for (CompressedContentFormat format : availableFormats)
{
if (format._encoding.equals(encoding))
if (format.getEncoding().equals(encoding))
return format;
}
@ -531,9 +531,9 @@ public class ResourceService
if (etag != null)
{
QuotedCSV quoted = new QuotedCSV(true, ifm);
for (String tag : quoted)
for (String etagWithSuffix : quoted)
{
if (CompressedContentFormat.tagEquals(etag, tag))
if (CompressedContentFormat.tagEquals(etag, etagWithSuffix))
{
match = true;
break;
@ -829,19 +829,19 @@ public class ResourceService
Response r = (Response)response;
r.putHeaders(content, contentLength, _etags);
HttpFields f = r.getHttpFields();
if (_acceptRanges)
if (_acceptRanges && !response.containsHeader(HttpHeader.ACCEPT_RANGES.asString()))
f.put(ACCEPT_RANGES);
if (_cacheControl != null)
if (_cacheControl != null && !response.containsHeader(HttpHeader.CACHE_CONTROL.asString()))
f.put(_cacheControl);
}
else
{
Response.putHeaders(response, content, contentLength, _etags);
if (_acceptRanges)
if (_acceptRanges && !response.containsHeader(HttpHeader.ACCEPT_RANGES.name()))
response.setHeader(ACCEPT_RANGES.getName(), ACCEPT_RANGES.getValue());
if (_cacheControl != null)
if (_cacheControl != null && !response.containsHeader(HttpHeader.CACHE_CONTROL.name()))
response.setHeader(_cacheControl.getName(), _cacheControl.getValue());
}
}

View File

@ -307,7 +307,7 @@ public class ResourceHandler extends HandlerWrapper implements ResourceFactory,
{
for (CompressedContentFormat formats : _resourceService.getPrecompressedFormats())
{
if (CompressedContentFormat.GZIP._encoding.equals(formats._encoding))
if (CompressedContentFormat.GZIP.getEncoding().equals(formats.getEncoding()))
return true;
}
return false;

View File

@ -36,7 +36,6 @@ import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PreEncodedHttpField;
@ -123,6 +122,7 @@ import org.eclipse.jetty.util.log.Logger;
* If a ETag is present in the Response headers, and GzipHandler is compressing the
* contents, it will add the {@code --gzip} suffix before the Response headers are committed
* and sent to the User Agent.
* Note that the suffix used is determined by {@link CompressedContentFormat#ETAG_SEPARATOR}
* </p>
* <p>
* This implementation relies on an Jetty internal {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
@ -152,13 +152,13 @@ import org.eclipse.jetty.util.log.Logger;
*/
public class GzipHandler extends HandlerWrapper implements GzipFactory
{
public static final String GZIP_HANDLER_ETAGS = "o.e.j.s.h.gzip.GzipHandler.etag";
public static final String GZIP = "gzip";
public static final String DEFLATE = "deflate";
public static final int DEFAULT_MIN_GZIP_SIZE = 32;
public static final int BREAK_EVEN_GZIP_SIZE = 23;
private static final Logger LOG = Log.getLogger(GzipHandler.class);
private static final HttpField X_CE_GZIP = new PreEncodedHttpField("X-Content-Encoding", "gzip");
private static final HttpField TE_CHUNKED = new PreEncodedHttpField(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED.asString());
private static final Pattern COMMA_GZIP = Pattern.compile(".*, *gzip");
private int _poolCapacity = -1;
@ -476,7 +476,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
public String[] getExcludedAgentPatterns()
{
Set<String> excluded = _agentPatterns.getExcluded();
return excluded.toArray(new String[excluded.size()]);
return excluded.toArray(new String[0]);
}
/**
@ -488,7 +488,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
public String[] getExcludedMethods()
{
Set<String> excluded = _methods.getExcluded();
return excluded.toArray(new String[excluded.size()]);
return excluded.toArray(new String[0]);
}
/**
@ -500,7 +500,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
public String[] getExcludedMimeTypes()
{
Set<String> excluded = _mimeTypes.getExcluded();
return excluded.toArray(new String[excluded.size()]);
return excluded.toArray(new String[0]);
}
/**
@ -512,7 +512,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
public String[] getExcludedPaths()
{
Set<String> excluded = _paths.getExcluded();
return excluded.toArray(new String[excluded.size()]);
return excluded.toArray(new String[0]);
}
/**
@ -524,7 +524,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
public String[] getIncludedAgentPatterns()
{
Set<String> includes = _agentPatterns.getIncluded();
return includes.toArray(new String[includes.size()]);
return includes.toArray(new String[0]);
}
/**
@ -536,7 +536,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
public String[] getIncludedMethods()
{
Set<String> includes = _methods.getIncluded();
return includes.toArray(new String[includes.size()]);
return includes.toArray(new String[0]);
}
/**
@ -548,7 +548,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
public String[] getIncludedMimeTypes()
{
Set<String> includes = _mimeTypes.getIncluded();
return includes.toArray(new String[includes.size()]);
return includes.toArray(new String[0]);
}
/**
@ -560,7 +560,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
public String[] getIncludedPaths()
{
Set<String> includes = _paths.getIncluded();
return includes.toArray(new String[includes.size()]);
return includes.toArray(new String[0]);
}
/**
@ -688,23 +688,20 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
}
// Special handling for etags
for (ListIterator<HttpField> fields = baseRequest.getHttpFields().listIterator(); fields.hasNext(); )
if (!StringUtil.isEmpty(CompressedContentFormat.ETAG_SEPARATOR))
{
HttpField field = fields.next();
if (field.getHeader() == HttpHeader.IF_NONE_MATCH || field.getHeader() == HttpHeader.IF_MATCH)
for (ListIterator<HttpField> fields = baseRequest.getHttpFields().listIterator(); fields.hasNext(); )
{
String etag = field.getValue();
int i = etag.indexOf(CompressedContentFormat.GZIP._etagQuote);
if (i > 0)
HttpField field = fields.next();
if (field.getHeader() == HttpHeader.IF_NONE_MATCH || field.getHeader() == HttpHeader.IF_MATCH)
{
baseRequest.setAttribute("o.e.j.s.h.gzip.GzipHandler.etag", etag);
while (i >= 0)
String etags = field.getValue();
String etagsNoSuffix = CompressedContentFormat.GZIP.stripSuffixes(etags);
if (!etagsNoSuffix.equals(etags))
{
etag = etag.substring(0, i) + etag.substring(i + CompressedContentFormat.GZIP._etag.length());
i = etag.indexOf(CompressedContentFormat.GZIP._etagQuote, i);
fields.set(new HttpField(field.getHeader(), etagsNoSuffix));
baseRequest.setAttribute(GZIP_HANDLER_ETAGS, etags);
}
fields.set(new HttpField(field.getHeader(), etag));
}
}
}

View File

@ -27,6 +27,7 @@ import java.util.zip.Deflater;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.server.HttpChannel;
@ -150,9 +151,9 @@ public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor
LOG.debug("{} exclude by status {}", this, sc);
noCompression();
if (sc == 304)
if (sc == HttpStatus.NOT_MODIFIED_304)
{
String requestEtags = (String)_channel.getRequest().getAttribute("o.e.j.s.h.gzip.GzipHandler.etag");
String requestEtags = (String)_channel.getRequest().getAttribute(GzipHandler.GZIP_HANDLER_ETAGS);
String responseEtag = response.getHttpFields().get(HttpHeader.ETAG);
if (requestEtags != null && responseEtag != null)
{
@ -217,7 +218,7 @@ public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor
return;
}
fields.put(GZIP._contentEncoding);
fields.put(GZIP.getContentEncoding());
_crc.reset();
// Adjust headers
@ -245,8 +246,7 @@ public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor
private String etagGzip(String etag)
{
int end = etag.length() - 1;
return (etag.charAt(end) == '"') ? etag.substring(0, end) + GZIP._etag + '"' : etag + GZIP._etag;
return GZIP.etag(etag);
}
public void noCompression()

View File

@ -0,0 +1,504 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class BlockingTest
{
private Server server;
private ServerConnector connector;
private ContextHandler context;
@BeforeEach
public void setUp()
{
server = new Server();
connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
context = new ContextHandler("/ctx");
HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[]{context, new DefaultHandler()});
server.setHandler(handlers);
}
@AfterEach
public void tearDown() throws Exception
{
server.stop();
}
@Test
public void testBlockingReadThenNormalComplete() throws Exception
{
CountDownLatch started = new CountDownLatch(1);
CountDownLatch stopped = new CountDownLatch(1);
AtomicReference<Throwable> readException = new AtomicReference<>();
AbstractHandler handler = new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
new Thread(() ->
{
try
{
int b = baseRequest.getHttpInput().read();
if (b == '1')
{
started.countDown();
if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
throw new IllegalStateException();
}
}
catch (Throwable t)
{
readException.set(t);
stopped.countDown();
}
}).start();
try
{
// wait for thread to start and read first byte
started.await(10, TimeUnit.SECONDS);
// give it time to block on second byte
Thread.sleep(1000);
}
catch (Throwable e)
{
throw new ServletException(e);
}
response.setStatus(200);
response.setContentType("text/plain");
response.getOutputStream().print("OK\r\n");
}
};
context.setHandler(handler);
server.start();
StringBuilder request = new StringBuilder();
request.append("POST /ctx/path/info HTTP/1.1\r\n")
.append("Host: localhost\r\n")
.append("Content-Type: test/data\r\n")
.append("Content-Length: 2\r\n")
.append("\r\n")
.append("1");
int port = connector.getLocalPort();
try (Socket socket = new Socket("localhost", port))
{
socket.setSoTimeout(10000);
OutputStream out = socket.getOutputStream();
out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
assertThat(response, notNullValue());
assertThat(response.getStatus(), is(200));
assertThat(response.getContent(), containsString("OK"));
// Async thread should have stopped
assertTrue(stopped.await(10, TimeUnit.SECONDS));
assertThat(readException.get(), instanceOf(IOException.class));
}
}
@Test
public void testNormalCompleteThenBlockingRead() throws Exception
{
CountDownLatch started = new CountDownLatch(1);
CountDownLatch completed = new CountDownLatch(1);
CountDownLatch stopped = new CountDownLatch(1);
AtomicReference<Throwable> readException = new AtomicReference<>();
AbstractHandler handler = new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
new Thread(() ->
{
try
{
int b = baseRequest.getHttpInput().read();
if (b == '1')
{
started.countDown();
completed.await(10, TimeUnit.SECONDS);
Thread.sleep(500);
if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
throw new IllegalStateException();
}
}
catch (Throwable t)
{
readException.set(t);
stopped.countDown();
}
}).start();
try
{
// wait for thread to start and read first byte
started.await(10, TimeUnit.SECONDS);
// give it time to block on second byte
Thread.sleep(1000);
}
catch (Throwable e)
{
throw new ServletException(e);
}
response.setStatus(200);
response.setContentType("text/plain");
response.getOutputStream().print("OK\r\n");
}
};
context.setHandler(handler);
server.start();
StringBuilder request = new StringBuilder();
request.append("POST /ctx/path/info HTTP/1.1\r\n")
.append("Host: localhost\r\n")
.append("Content-Type: test/data\r\n")
.append("Content-Length: 2\r\n")
.append("\r\n")
.append("1");
int port = connector.getLocalPort();
try (Socket socket = new Socket("localhost", port))
{
socket.setSoTimeout(10000);
OutputStream out = socket.getOutputStream();
out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
assertThat(response, notNullValue());
assertThat(response.getStatus(), is(200));
assertThat(response.getContent(), containsString("OK"));
completed.countDown();
Thread.sleep(1000);
// Async thread should have stopped
assertTrue(stopped.await(10, TimeUnit.SECONDS));
assertThat(readException.get(), instanceOf(IOException.class));
}
}
@Test
public void testStartAsyncThenBlockingReadThenTimeout() throws Exception
{
CountDownLatch started = new CountDownLatch(1);
CountDownLatch completed = new CountDownLatch(1);
CountDownLatch stopped = new CountDownLatch(1);
AtomicReference<Throwable> readException = new AtomicReference<>();
AbstractHandler handler = new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException
{
baseRequest.setHandled(true);
if (baseRequest.getDispatcherType() != DispatcherType.ERROR)
{
AsyncContext async = request.startAsync();
async.setTimeout(100);
new Thread(() ->
{
try
{
int b = baseRequest.getHttpInput().read();
if (b == '1')
{
started.countDown();
completed.await(10, TimeUnit.SECONDS);
Thread.sleep(500);
if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
throw new IllegalStateException();
}
}
catch (Throwable t)
{
readException.set(t);
stopped.countDown();
}
}).start();
try
{
// wait for thread to start and read first byte
started.await(10, TimeUnit.SECONDS);
// give it time to block on second byte
Thread.sleep(1000);
}
catch (Throwable e)
{
throw new ServletException(e);
}
}
}
};
context.setHandler(handler);
server.start();
StringBuilder request = new StringBuilder();
request.append("POST /ctx/path/info HTTP/1.1\r\n")
.append("Host: localhost\r\n")
.append("Content-Type: test/data\r\n")
.append("Content-Length: 2\r\n")
.append("\r\n")
.append("1");
int port = connector.getLocalPort();
try (Socket socket = new Socket("localhost", port))
{
socket.setSoTimeout(10000);
OutputStream out = socket.getOutputStream();
out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
assertThat(response, notNullValue());
assertThat(response.getStatus(), is(500));
assertThat(response.getContent(), containsString("AsyncContext timeout"));
completed.countDown();
Thread.sleep(1000);
// Async thread should have stopped
assertTrue(stopped.await(10, TimeUnit.SECONDS));
assertThat(readException.get(), instanceOf(IOException.class));
}
}
@Test
public void testBlockingReadThenSendError() throws Exception
{
CountDownLatch started = new CountDownLatch(1);
CountDownLatch stopped = new CountDownLatch(1);
AtomicReference<Throwable> readException = new AtomicReference<>();
AbstractHandler handler = new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
if (baseRequest.getDispatcherType() != DispatcherType.ERROR)
{
new Thread(() ->
{
try
{
int b = baseRequest.getHttpInput().read();
if (b == '1')
{
started.countDown();
if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
throw new IllegalStateException();
}
}
catch (Throwable t)
{
readException.set(t);
stopped.countDown();
}
}).start();
try
{
// wait for thread to start and read first byte
started.await(10, TimeUnit.SECONDS);
// give it time to block on second byte
Thread.sleep(1000);
}
catch (Throwable e)
{
throw new ServletException(e);
}
response.sendError(499);
}
}
};
context.setHandler(handler);
server.start();
StringBuilder request = new StringBuilder();
request.append("POST /ctx/path/info HTTP/1.1\r\n")
.append("Host: localhost\r\n")
.append("Content-Type: test/data\r\n")
.append("Content-Length: 2\r\n")
.append("\r\n")
.append("1");
int port = connector.getLocalPort();
try (Socket socket = new Socket("localhost", port))
{
socket.setSoTimeout(10000);
OutputStream out = socket.getOutputStream();
out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
assertThat(response, notNullValue());
assertThat(response.getStatus(), is(499));
// Async thread should have stopped
assertTrue(stopped.await(10, TimeUnit.SECONDS));
assertThat(readException.get(), instanceOf(IOException.class));
}
}
@Test
public void testBlockingWriteThenNormalComplete() throws Exception
{
CountDownLatch started = new CountDownLatch(1);
CountDownLatch stopped = new CountDownLatch(1);
AtomicReference<Throwable> readException = new AtomicReference<>();
AbstractHandler handler = new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException
{
baseRequest.setHandled(true);
response.setStatus(200);
response.setContentType("text/plain");
new Thread(() ->
{
try
{
byte[] data = new byte[16 * 1024];
Arrays.fill(data, (byte)'X');
data[data.length - 2] = '\r';
data[data.length - 1] = '\n';
OutputStream out = response.getOutputStream();
started.countDown();
while (true)
out.write(data);
}
catch (Throwable t)
{
readException.set(t);
stopped.countDown();
}
}).start();
try
{
// wait for thread to start and read first byte
started.await(10, TimeUnit.SECONDS);
// give it time to block on write
Thread.sleep(1000);
}
catch (Throwable e)
{
throw new ServletException(e);
}
}
};
context.setHandler(handler);
server.start();
StringBuilder request = new StringBuilder();
request.append("GET /ctx/path/info HTTP/1.1\r\n")
.append("Host: localhost\r\n")
.append("\r\n");
int port = connector.getLocalPort();
try (Socket socket = new Socket("localhost", port))
{
socket.setSoTimeout(10000);
OutputStream out = socket.getOutputStream();
out.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.ISO_8859_1));
// Read the header
List<String> header = new ArrayList<>();
while (true)
{
String line = in.readLine();
if (line.length() == 0)
break;
header.add(line);
}
assertThat(header.get(0), containsString("200 OK"));
// read one line of content
String content = in.readLine();
assertThat(content, is("4000"));
content = in.readLine();
assertThat(content, startsWith("XXXXXXXX"));
// check that writing thread is stopped by end of request handling
assertTrue(stopped.await(10, TimeUnit.SECONDS));
// read until last line
String last = null;
while (true)
{
String line = in.readLine();
if (line == null)
break;
last = line;
}
// last line is not empty chunk, ie abnormal completion
assertThat(last, startsWith("XXXXX"));
}
}
}

View File

@ -500,43 +500,28 @@ public class HttpConnectionTest
public void testBadPathDotDotPath() throws Exception
{
String response = connector.getResponse("GET /ooops/../../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad URI");
}
@Test
public void testOKPathEncodedDotDotPath() throws Exception
{
String response = connector.getResponse("GET /ooops/%2e%2e/path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 200 OK");
checkContains(response, 0, "pathInfo=/path");
}
@Test
public void testBadPathEncodedDotDotPath() throws Exception
{
String response = connector.getResponse("GET /ooops/%2e%2e/%2e%2e/path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad URI");
checkContains(response, 0, "HTTP/1.1 400 ");
}
@Test
public void testBadDotDotPath() throws Exception
{
String response = connector.getResponse("GET ../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad URI");
checkContains(response, 0, "HTTP/1.1 400 ");
}
@Test
public void testBadSlashDotDotPath() throws Exception
{
String response = connector.getResponse("GET /../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad URI");
checkContains(response, 0, "HTTP/1.1 400 ");
}
@Test
public void testEncodedBadDotDotPath() throws Exception
{
String response = connector.getResponse("GET %2e%2e/path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 Bad URI");
checkContains(response, 0, "HTTP/1.1 400 ");
}
@Test

View File

@ -1836,6 +1836,28 @@ public class RequestTest
assertEquals(0, request.getParameterMap().size());
}
@Test
public void testAmbiguousPaths() throws Exception
{
_handler._checker = (request, response) -> true;
String request = "GET /ambiguous/..;/path HTTP/1.0\r\n" +
"Host: whatever\r\n" +
"\r\n";
_connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
_connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230_LEGACY);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
_connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC2616);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
_connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC2616_LEGACY);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
}
private static long getFileCount(Path path)
{
try (Stream<Path> s = Files.list(path))

View File

@ -493,6 +493,7 @@ public class ResponseTest
assertEquals("foo2/bar2;charset=utf-8", response.getContentType());
response.recycle();
response.reopen();
response.setCharacterEncoding("utf16");
response.setContentType("text/html; charset=utf-8");
@ -505,6 +506,7 @@ public class ResponseTest
assertEquals("text/xml;charset=utf-8", response.getContentType());
response.recycle();
response.reopen();
response.setCharacterEncoding("utf-16");
response.setContentType("foo/bar");
assertEquals("foo/bar;charset=utf-16", response.getContentType());

View File

@ -283,7 +283,7 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc
_resourceService.setContentFactory(contentFactory);
_resourceService.setWelcomeFactory(this);
List<String> gzipEquivalentFileExtensions = new ArrayList<String>();
List<String> gzipEquivalentFileExtensions = new ArrayList<>();
String otherGzipExtensions = getInitParameter("otherGzipFileExtensions");
if (otherGzipExtensions != null)
{

View File

@ -229,7 +229,7 @@ public class AsyncContextTest
@Test
public void testDispatchAsyncContextEncodedUrl() throws Exception
{
String request = "GET /ctx/test/hello%2fthere?dispatch=true HTTP/1.1\r\n" +
String request = "GET /ctx/test/hello%20there?dispatch=true HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Connection: close\r\n" +
@ -253,16 +253,16 @@ public class AsyncContextTest
// async run attributes
assertThat("async run attr servlet path is original", responseBody, containsString("async:run:attr:servletPath:/test"));
assertThat("async run attr path info has correct encoding", responseBody, containsString("async:run:attr:pathInfo:/hello/there"));
assertThat("async run attr path info has correct encoding", responseBody, containsString("async:run:attr:pathInfo:/hello there"));
assertThat("async run attr query string", responseBody, containsString("async:run:attr:queryString:dispatch=true"));
assertThat("async run context path", responseBody, containsString("async:run:attr:contextPath:/ctx"));
assertThat("async run request uri has correct encoding", responseBody, containsString("async:run:attr:requestURI:/ctx/test/hello%2fthere"));
assertThat("async run request uri has correct encoding", responseBody, containsString("async:run:attr:requestURI:/ctx/test/hello%20there"));
}
@Test
public void testDispatchAsyncContextSelfEncodedUrl() throws Exception
{
String request = "GET /ctx/self/hello%2fthere?dispatch=true HTTP/1.1\r\n" +
String request = "GET /ctx/self/hello%20there?dispatch=true HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Connection: close\r\n" +
@ -272,8 +272,8 @@ public class AsyncContextTest
String responseBody = response.getContent();
assertThat("servlet request uri initial", responseBody, containsString("doGet.REQUEST.requestURI:/ctx/self/hello%2fthere"));
assertThat("servlet request uri async", responseBody, containsString("doGet.ASYNC.requestURI:/ctx/self/hello%2fthere"));
assertThat("servlet request uri initial", responseBody, containsString("doGet.REQUEST.requestURI:/ctx/self/hello%20there"));
assertThat("servlet request uri async", responseBody, containsString("doGet.ASYNC.requestURI:/ctx/self/hello%20there"));
}
@Test

View File

@ -0,0 +1,157 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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.servlet;
import java.io.IOException;
import java.nio.file.Path;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
public class CacheControlHeaderTest
{
private Server server;
private LocalConnector connector;
public static class ForceCacheControlFilter implements Filter
{
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
HttpServletResponse httpResponse = (HttpServletResponse)response;
httpResponse.setHeader(HttpHeader.CACHE_CONTROL.asString(), "max-age=0,private");
chain.doFilter(request, response);
}
@Override
public void destroy()
{
}
}
public void startServer(boolean forceFilter) throws Exception
{
server = new Server();
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(new HttpConfiguration());
connector = new LocalConnector(server, null, null, null, -1, httpConnectionFactory);
ServletContextHandler context = new ServletContextHandler();
ServletHolder servletHolder = new ServletHolder();
servletHolder.setServlet(new DefaultServlet());
servletHolder.setInitParameter("cacheControl", "max-age=3600,public");
Path resBase = MavenTestingUtils.getTestResourcePathDir("contextResources");
servletHolder.setInitParameter("resourceBase", resBase.toFile().toURI().toASCIIString());
context.addServlet(servletHolder, "/*");
if (forceFilter)
{
context.addFilter(ForceCacheControlFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
}
server.setHandler(context);
server.addConnector(connector);
server.start();
}
public void stopServer() throws Exception
{
if (server != null && server.isRunning())
{
server.stop();
}
}
@Test
public void testCacheControlFilterOverride() throws Exception
{
try
{
startServer(true);
StringBuffer req1 = new StringBuffer();
req1.append("GET /content.txt HTTP/1.1\r\n");
req1.append("Host: local\r\n");
req1.append("Accept: */*\r\n");
req1.append("Connection: close\r\n");
req1.append("\r\n");
String response = connector.getResponse(req1.toString());
assertThat("Response status",
response,
containsString("HTTP/1.1 200 OK"));
assertThat("Response headers",
response,
containsString(HttpHeader.CACHE_CONTROL.asString() + ": max-age=0,private"));
}
finally
{
stopServer();
}
}
@Test
public void testCacheControlDefaultServlet() throws Exception
{
try
{
startServer(false);
StringBuffer req1 = new StringBuffer();
req1.append("GET /content.txt HTTP/1.1\r\n");
req1.append("Host: local\r\n");
req1.append("Accept: */*\r\n");
req1.append("Connection: close\r\n");
req1.append("\r\n");
String response = connector.getResponse(req1.toString());
assertThat("Response status",
response,
containsString("HTTP/1.1 200 OK"));
assertThat("Response headers",
response,
containsString(HttpHeader.CACHE_CONTROL.asString() + ": max-age=3600,public"));
}
finally
{
stopServer();
}
}
}

View File

@ -47,12 +47,14 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.DateGenerator;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.ResourceContentFactory;
import org.eclipse.jetty.server.ResourceService;
@ -116,6 +118,7 @@ public class DefaultServletTest
connector = new LocalConnector(server);
connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setSendServerVersion(false);
connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230_LEGACY); // allow ambiguous path segments
File extraJarResources = MavenTestingUtils.getTestResourceFile(ODD_JAR);
URL[] urls = new URL[]{extraJarResources.toURI().toURL()};

View File

@ -44,6 +44,7 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.LocalConnector;
@ -87,7 +88,7 @@ public class GzipHandlerTest
private static final String __micro = __content.substring(0, 10);
private static final String __contentETag = String.format("W/\"%x\"", __content.hashCode());
private static final String __contentETagGzip = String.format("W/\"%x--gzip\"", __content.hashCode());
private static final String __contentETagGzip = String.format("W/\"%x" + CompressedContentFormat.GZIP.getEtagSuffix() + "\"", __content.hashCode());
private static final String __icontent = "BEFORE" + __content + "AFTER";
private Server _server;
@ -591,7 +592,7 @@ public class GzipHandlerTest
request.setURI("/ctx/content");
request.setVersion("HTTP/1.0");
request.setHeader("Host", "tester");
request.setHeader("If-Match", "WrongEtag--gzip");
request.setHeader("If-Match", "WrongEtag" + CompressedContentFormat.GZIP.getEtagSuffix());
request.setHeader("accept-encoding", "gzip");
response = HttpTester.parseResponse(_connector.getResponse(request.generate()));

View File

@ -34,6 +34,8 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.hamcrest.Matchers;
@ -112,6 +114,7 @@ public class RequestURITest
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
connector.getBean(HttpConnectionFactory.class).setHttpCompliance(HttpCompliance.RFC7230_LEGACY); // Allow ambiguous segments
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");

View File

@ -145,7 +145,7 @@ public class GzipDefaultTest
//A HEAD request should have similar headers, but no body
response = tester.executeRequest("HEAD", "/context/file.txt", 5, TimeUnit.SECONDS);
assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200));
assertThat("ETag", response.get("ETag"), containsString(CompressedContentFormat.GZIP._etag));
assertThat("ETag", response.get("ETag"), containsString(CompressedContentFormat.GZIP.getEtagSuffix()));
assertThat("Content encoding", response.get("Content-Encoding"), containsString("gzip"));
assertNull(response.get("Content-Length"), "Content length");

View File

@ -21,6 +21,7 @@ package org.eclipse.jetty.util;
import java.io.Closeable;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -50,10 +51,10 @@ public class SharedBlockingCallback
{
private static final Logger LOG = Log.getLogger(SharedBlockingCallback.class);
private static Throwable IDLE = new ConstantThrowable("IDLE");
private static Throwable SUCCEEDED = new ConstantThrowable("SUCCEEDED");
private static final Throwable IDLE = new ConstantThrowable("IDLE");
private static final Throwable SUCCEEDED = new ConstantThrowable("SUCCEEDED");
private static Throwable FAILED = new ConstantThrowable("FAILED");
private static final Throwable FAILED = new ConstantThrowable("FAILED");
private final ReentrantLock _lock = new ReentrantLock();
private final Condition _idle = _lock.newCondition();
@ -96,6 +97,26 @@ public class SharedBlockingCallback
}
}
public boolean fail(Throwable cause)
{
Objects.requireNonNull(cause);
_lock.lock();
try
{
if (_blocker._state == null)
{
_blocker._state = new BlockerFailedException(cause);
_complete.signalAll();
return true;
}
}
finally
{
_lock.unlock();
}
return false;
}
protected void notComplete(Blocker blocker)
{
LOG.warn("Blocker not complete {}", blocker);
@ -165,10 +186,12 @@ public class SharedBlockingCallback
_state = cause;
_complete.signalAll();
}
else if (_state instanceof BlockerTimeoutException)
else if (_state instanceof BlockerTimeoutException || _state instanceof BlockerFailedException)
{
// Failure arrived late, block() already
// modified the state, nothing more to do.
if (LOG.isDebugEnabled())
LOG.debug("Failed after {}", _state);
}
else
{
@ -297,4 +320,12 @@ public class SharedBlockingCallback
private static class BlockerTimeoutException extends TimeoutException
{
}
private static class BlockerFailedException extends Exception
{
public BlockerFailedException(Throwable cause)
{
super(cause);
}
}
}

View File

@ -782,11 +782,9 @@ public class URIUtil
}
/**
* Convert a decoded path to a canonical form.
* Convert an encoded path to a canonical form.
* <p>
* All instances of "." and ".." are factored out.
* </p>
* <p>
* Null is returned if the path tries to .. above its root.
* </p>
*
@ -795,31 +793,35 @@ public class URIUtil
*/
public static String canonicalPath(String path)
{
// See https://tools.ietf.org/html/rfc3986#section-5.2.4
if (path == null || path.isEmpty())
return path;
boolean slash = true;
int end = path.length();
int i = 0;
int dots = 0;
loop:
while (i < end)
loop: while (i < end)
{
char c = path.charAt(i);
switch (c)
{
case '/':
slash = true;
dots = 0;
break;
case '.':
if (slash)
if (dots == 0)
{
dots = 1;
break loop;
slash = false;
}
dots = -1;
break;
default:
slash = false;
dots = -1;
}
i++;
@ -831,7 +833,6 @@ public class URIUtil
StringBuilder canonical = new StringBuilder(path.length());
canonical.append(path, 0, i);
int dots = 1;
i++;
while (i <= end)
{
@ -839,14 +840,18 @@ public class URIUtil
switch (c)
{
case '\0':
if (dots == 2)
{
if (canonical.length() < 2)
return null;
canonical.setLength(canonical.length() - 1);
canonical.setLength(canonical.lastIndexOf("/") + 1);
}
break;
case '/':
switch (dots)
{
case 0:
if (c != '\0')
canonical.append(c);
break;
case 1:
break;
@ -858,36 +863,42 @@ public class URIUtil
break;
default:
while (dots-- > 0)
{
canonical.append('.');
}
if (c != '\0')
canonical.append(c);
canonical.append(c);
}
slash = true;
dots = 0;
break;
case '.':
if (dots > 0)
dots++;
else if (slash)
dots = 1;
else
canonical.append('.');
slash = false;
switch (dots)
{
case 0:
dots = 1;
break;
case 1:
dots = 2;
break;
case 2:
canonical.append("...");
dots = -1;
break;
default:
canonical.append('.');
}
break;
default:
while (dots-- > 0)
switch (dots)
{
canonical.append('.');
case 1:
canonical.append('.');
break;
case 2:
canonical.append("..");
break;
default:
}
canonical.append(c);
dots = 0;
slash = false;
dots = -1;
}
i++;

View File

@ -34,6 +34,10 @@ public class URIUtilCanonicalPathTest
{
String[][] canonical =
{
// Examples from RFC
{"/a/b/c/./../../g", "/a/g"},
{"mid/content=5/../6", "mid/6"},
// Basic examples (no changes expected)
{"/hello.html", "/hello.html"},
{"/css/main.css", "/css/main.css"},
@ -56,8 +60,12 @@ public class URIUtilCanonicalPathTest
{"/aaa/./bbb/", "/aaa/bbb/"},
{"/aaa/./bbb", "/aaa/bbb"},
{"./bbb/", "bbb/"},
{"./aaa", "aaa"},
{"./aaa/", "aaa/"},
{"/./aaa/", "/aaa/"},
{"./aaa/../bbb/", "bbb/"},
{"/foo/.", "/foo/"},
{"/foo/./", "/foo/"},
{"./", ""},
{".", ""},
{".//", "/"},
@ -121,6 +129,10 @@ public class URIUtilCanonicalPathTest
{"/foo/.;/bar", "/foo/.;/bar"},
{"/foo/..;/bar", "/foo/..;/bar"},
{"/foo/..;/..;/bar", "/foo/..;/..;/bar"},
// Trailing / is preserved
{"/foo/bar/..", "/foo/"},
{"/foo/bar/../", "/foo/"},
};
ArrayList<Arguments> ret = new ArrayList<>();
@ -135,6 +147,6 @@ public class URIUtilCanonicalPathTest
@MethodSource("data")
public void testCanonicalPath(String input, String expectedResult)
{
assertThat("Canonical", URIUtil.canonicalPath(input), is(expectedResult));
assertThat(URIUtil.canonicalPath(input), is(expectedResult));
}
}

View File

@ -0,0 +1,145 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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.http.client;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.util.DeferredContentProvider;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class BlockedIOTest extends AbstractTest<TransportScenario>
{
@Override
public void init(Transport transport) throws IOException
{
setScenario(new TransportScenario(transport));
}
@ParameterizedTest
@ArgumentsSource(TransportProvider.class)
public void testBlockingReadThenNormalComplete(Transport transport) throws Exception
{
CountDownLatch started = new CountDownLatch(1);
CountDownLatch stopped = new CountDownLatch(1);
AtomicReference<Throwable> readException = new AtomicReference<>();
AtomicReference<Throwable> rereadException = new AtomicReference<>();
init(transport);
scenario.start(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
new Thread(() ->
{
try
{
int b = baseRequest.getHttpInput().read();
if (b == '1')
{
started.countDown();
if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
throw new IllegalStateException();
}
}
catch (Throwable ex1)
{
readException.set(ex1);
try
{
if (baseRequest.getHttpInput().read() > Integer.MIN_VALUE)
throw new IllegalStateException();
}
catch (Throwable ex2)
{
rereadException.set(ex2);
}
finally
{
stopped.countDown();
}
}
}).start();
try
{
// wait for thread to start and read first byte
started.await(10, TimeUnit.SECONDS);
// give it time to block on second byte
Thread.sleep(1000);
}
catch (Throwable e)
{
throw new ServletException(e);
}
response.setStatus(200);
response.setContentType("text/plain");
response.getOutputStream().print("OK\r\n");
}
});
DeferredContentProvider contentProvider = new DeferredContentProvider();
CountDownLatch ok = new CountDownLatch(2);
scenario.client.newRequest(scenario.newURI())
.method("POST")
.content(contentProvider)
.onResponseContent((response, content) ->
{
assertThat(BufferUtil.toString(content), containsString("OK"));
ok.countDown();
})
.onResponseSuccess(response ->
{
try
{
assertThat(response.getStatus(), is(200));
stopped.await(10, TimeUnit.SECONDS);
ok.countDown();
}
catch (Throwable t)
{
t.printStackTrace();
}
})
.send(null);
contentProvider.offer(BufferUtil.toBuffer("1"));
assertTrue(ok.await(10, TimeUnit.SECONDS));
assertThat(readException.get(), instanceOf(IOException.class));
assertThat(rereadException.get(), instanceOf(IOException.class));
}
}