Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-5133-webappcontext-extraclasspath-cleanup

This commit is contained in:
Joakim Erdfelt 2020-08-13 11:12:20 -05:00
commit 5d3711c3c0
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
13 changed files with 446 additions and 65 deletions

View File

@ -384,11 +384,9 @@ public class HttpField
return _name.equalsIgnoreCase(field.getName());
}
@Override
public String toString()
public boolean is(String name)
{
String v = getValue();
return getName() + ": " + (v == null ? "" : v);
return _name.equalsIgnoreCase(name);
}
private int nameHashCode()
@ -411,6 +409,13 @@ public class HttpField
return h;
}
@Override
public String toString()
{
String v = getValue();
return getName() + ": " + (v == null ? "" : v);
}
public static class IntValueHttpField extends HttpField
{
private final int _int;

View File

@ -26,9 +26,12 @@ import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -119,7 +122,7 @@ public interface HttpFields extends Iterable<HttpField>
{
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(name) && f.contains(value))
if (f.is(name) && f.contains(value))
return true;
}
return false;
@ -149,7 +152,7 @@ public interface HttpFields extends Iterable<HttpField>
{
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
return true;
}
return false;
@ -169,7 +172,7 @@ public interface HttpFields extends Iterable<HttpField>
{
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(header))
if (f.is(header))
return f.getValue();
}
return null;
@ -211,7 +214,7 @@ public interface HttpFields extends Iterable<HttpField>
QuotedCSV values = null;
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
{
if (values == null)
values = new QuotedCSV(keepQuotes);
@ -266,7 +269,7 @@ public interface HttpFields extends Iterable<HttpField>
{
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
return f;
}
return null;
@ -301,7 +304,19 @@ public interface HttpFields extends Iterable<HttpField>
*/
default List<HttpField> getFields(HttpHeader header)
{
return stream().filter(f -> f.getHeader().equals(header)).collect(Collectors.toList());
return getFields(header, (f, h) -> f.getHeader() == h);
}
default List<HttpField> getFields(String name)
{
return getFields(name, (f, n) -> f.is(name));
}
private <T> List<HttpField> getFields(T header, BiPredicate<HttpField, T> predicate)
{
return stream()
.filter(f -> predicate.test(f, header))
.collect(Collectors.toList());
}
/**
@ -380,7 +395,7 @@ public interface HttpFields extends Iterable<HttpField>
QuotedQualityCSV values = null;
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
{
if (values == null)
values = new QuotedQualityCSV();
@ -411,7 +426,7 @@ public interface HttpFields extends Iterable<HttpField>
while (i.hasNext())
{
HttpField f = i.next();
if (f.getName().equalsIgnoreCase(name) && f.getValue() != null)
if (f.is(name) && f.getValue() != null)
{
_field = f;
return true;
@ -462,7 +477,7 @@ public interface HttpFields extends Iterable<HttpField>
final List<String> list = new ArrayList<>();
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
list.add(f.getValue());
}
return list;
@ -685,7 +700,7 @@ public interface HttpFields extends Iterable<HttpField>
QuotedCSV existing = null;
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
{
if (existing == null)
existing = new QuotedCSV(false);
@ -781,7 +796,7 @@ public interface HttpFields extends Iterable<HttpField>
{
if (_size == 0)
throw new IllegalStateException();
System.arraycopy(_fields, _index, _fields, _index - 1, _size-- - _index--);
Mutable.this.remove(_index - 1);
}
};
}
@ -915,6 +930,152 @@ public interface HttpFields extends Iterable<HttpField>
return put(name, Long.toString(value));
}
/**
* <p>Computes a single field for the given HttpHeader and for existing fields with the same header.</p>
*
* <p>The compute function receives the field name and a list of fields with the same name
* so that their values can be used to compute the value of the field that is returned
* by the compute function.
* If the compute function returns {@code null}, the fields with the given name are removed.</p>
* <p>This method comes handy when you want to add an HTTP header if it does not exist,
* or add a value if the HTTP header already exists, similarly to
* {@link Map#compute(Object, BiFunction)}.</p>
*
* <p>This method can be used to {@link #put(HttpField) put} a new field (or blindly replace its value):</p>
* <pre>
* httpFields.computeField("X-New-Header",
* (name, fields) -&gt; new HttpField(name, "NewValue"));
* </pre>
*
* <p>This method can be used to coalesce many fields into one:</p>
* <pre>
* // Input:
* GET / HTTP/1.1
* Host: localhost
* Cookie: foo=1
* Cookie: bar=2,baz=3
* User-Agent: Jetty
*
* // Computation:
* httpFields.computeField("Cookie", (name, fields) -&gt;
* {
* // No cookies, nothing to do.
* if (fields == null)
* return null;
*
* // Coalesces all cookies.
* String coalesced = fields.stream()
* .flatMap(field -&gt; Stream.of(field.getValues()))
* .collect(Collectors.joining(", "));
*
* // Returns a single Cookie header with all cookies.
* return new HttpField(name, coalesced);
* }
*
* // Output:
* GET / HTTP/1.1
* Host: localhost
* Cookie: foo=1, bar=2, baz=3
* User-Agent: Jetty
* </pre>
*
* <p>This method can be used to replace a field:</p>
* <pre>
* httpFields.computeField("X-Length", (name, fields) -&gt;
* {
* if (fields == null)
* return null;
*
* // Get any value among the X-Length headers.
* String length = fields.stream()
* .map(HttpField::getValue)
* .findAny()
* .orElse("0");
*
* // Replace X-Length headers with X-Capacity header.
* return new HttpField("X-Capacity", length);
* });
* </pre>
*
* <p>This method can be used to remove a field:</p>
* <pre>
* httpFields.computeField("Connection", (name, fields) -&gt; null);
* </pre>
*
* @param header the HTTP header
* @param computeFn the compute function
*/
public void computeField(HttpHeader header, BiFunction<HttpHeader, List<HttpField>, HttpField> computeFn)
{
computeField(header, computeFn, (f, h) -> f.getHeader() == h);
}
/**
* <p>Computes a single field for the given HTTP header name and for existing fields with the same name.</p>
*
* @param name the HTTP header name
* @param computeFn the compute function
* @see #computeField(HttpHeader, BiFunction)
*/
public void computeField(String name, BiFunction<String, List<HttpField>, HttpField> computeFn)
{
computeField(name, computeFn, HttpField::is);
}
private <T> void computeField(T header, BiFunction<T, List<HttpField>, HttpField> computeFn, BiPredicate<HttpField, T> matcher)
{
// Look for first occurrence
int first = -1;
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (matcher.test(f, header))
{
first = i;
break;
}
}
// If the header is not found, add a new one;
if (first < 0)
{
HttpField newField = computeFn.apply(header, null);
if (newField != null)
add(newField);
return;
}
// Are there any more occurrences?
List<HttpField> found = null;
for (int i = first + 1; i < _size; i++)
{
HttpField f = _fields[i];
if (matcher.test(f, header))
{
if (found == null)
{
found = new ArrayList<>();
found.add(_fields[first]);
}
// Remember and remove additional fields
found.add(f);
remove(i);
}
}
// If no additional fields were found, handle singleton case
if (found == null)
found = Collections.singletonList(_fields[first]);
else
found = Collections.unmodifiableList(found);
HttpField newField = computeFn.apply(header, found);
if (newField == null)
remove(first);
else
_fields[first] = newField;
}
/**
* Remove a field.
*
@ -927,7 +1088,7 @@ public interface HttpFields extends Iterable<HttpField>
{
HttpField f = _fields[i];
if (f.getHeader() == name)
System.arraycopy(_fields, i + 1, _fields, i, _size-- - i-- - 1);
remove(i);
}
return this;
}
@ -938,7 +1099,7 @@ public interface HttpFields extends Iterable<HttpField>
{
HttpField f = _fields[i];
if (fields.contains(f.getHeader()))
System.arraycopy(_fields, i + 1, _fields, i, _size-- - i-- - 1);
remove(i);
}
return this;
}
@ -954,12 +1115,19 @@ public interface HttpFields extends Iterable<HttpField>
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f.getName().equalsIgnoreCase(name))
System.arraycopy(_fields, i + 1, _fields, i, _size-- - i-- - 1);
if (f.is(name))
remove(i);
}
return this;
}
private void remove(int i)
{
_size--;
System.arraycopy(_fields, i + 1, _fields, i, _size - i);
_fields[_size] = null;
}
public int size()
{
return _size;
@ -1077,9 +1245,7 @@ public interface HttpFields extends Iterable<HttpField>
{
if (_current < 0)
throw new IllegalStateException();
_size--;
System.arraycopy(_fields, _current + 1, _fields, _current, _size - _current);
_fields[_size] = null;
Mutable.this.remove(_current);
_cursor = _current;
_current = -1;
}
@ -1141,7 +1307,7 @@ public interface HttpFields extends Iterable<HttpField>
{
// default impl overridden for efficiency
for (HttpField f : _fields)
if (f.getName().equalsIgnoreCase(header))
if (f.is(header))
return f.getValue();
return null;
}
@ -1171,7 +1337,7 @@ public interface HttpFields extends Iterable<HttpField>
{
// default impl overridden for efficiency
for (HttpField f : _fields)
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
return f;
return null;
}

View File

@ -30,6 +30,7 @@ import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.BufferUtil;
import org.hamcrest.Matchers;
@ -38,6 +39,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
@ -879,4 +881,44 @@ public class HttpFieldsTest
assertThat(i.next().getName(), is("name4"));
assertThat(i.hasNext(), is(false));
}
@Test
public void testStream()
{
HttpFields.Mutable fields = HttpFields.build();
assertThat(fields.stream().count(), is(0L));
fields.put("name1", "valueA");
fields.put("name2", "valueB");
fields.add("name3", "valueC");
assertThat(fields.stream().count(), is(3L));
assertThat(fields.stream().map(HttpField::getName).filter("name2"::equalsIgnoreCase).count(), is(1L));
}
@Test
public void testComputeField()
{
HttpFields.Mutable fields = HttpFields.build();
assertThat(fields.size(), is(0));
fields.computeField("Test", (n, f) -> null);
assertThat(fields.size(), is(0));
fields.add(new HttpField("Before", "value"));
assertThat(fields.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value"));
fields.computeField("Test", (n, f) -> new HttpField(n, "one"));
assertThat(fields.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "Test: one"));
fields.add(new HttpField("After", "value"));
assertThat(fields.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "Test: one", "After: value"));
fields.add(new HttpField("Test", "extra"));
assertThat(fields.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "Test: one", "After: value", "Test: extra"));
fields.computeField("Test", (n, f) -> new HttpField("TEST", "count=" + f.size()));
assertThat(fields.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "TEST: count=2", "After: value"));
fields.computeField("TEST", (n, f) -> null);
assertThat(fields.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "After: value"));
}
}

View File

@ -28,6 +28,8 @@ import java.util.Locale;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.AsyncContext;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
@ -49,6 +51,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.HttpCookieStore;
import org.eclipse.jetty.util.StringUtil;
@ -466,6 +469,14 @@ public abstract class AbstractProxyServlet extends HttpServlet
return HttpHeaderValue.CONTINUE.is(request.getHeader(HttpHeader.EXPECT.asString()));
}
protected Request newProxyRequest(HttpServletRequest request, String rewrittenTarget)
{
return getHttpClient().newRequest(rewrittenTarget)
.method(request.getMethod())
.version(HttpVersion.fromString(request.getProtocol()))
.attribute(CLIENT_REQUEST_ATTRIBUTE, request);
}
protected void copyRequestHeaders(HttpServletRequest clientRequest, Request proxyRequest)
{
// First clear possibly existing headers, as we are going to copy those from the client request.
@ -529,9 +540,50 @@ public abstract class AbstractProxyServlet extends HttpServlet
addXForwardedHeaders(clientRequest, proxyRequest);
}
/**
* Adds the HTTP {@code Via} header to the proxied request.
*
* @param proxyRequest the request being proxied
* @see #addViaHeader(HttpServletRequest, Request)
*/
protected void addViaHeader(Request proxyRequest)
{
proxyRequest.headers(headers -> headers.add(HttpHeader.VIA, "http/1.1 " + getViaHost()));
HttpServletRequest clientRequest = (HttpServletRequest)proxyRequest.getAttributes().get(CLIENT_REQUEST_ATTRIBUTE);
addViaHeader(clientRequest, proxyRequest);
}
/**
* <p>Adds the HTTP {@code Via} header to the proxied request, taking into account data present in the client request.</p>
* <p>This method considers the protocol of the client request when forming the proxied request. If it
* is HTTP, then the protocol name will not be included in the {@code Via} header that is sent by the proxy, and only
* the protocol version will be sent. If it is not, the entire protocol (name and version) will be included.
* If the client request includes a {@code Via} header, the result will be appended to that to form a chain.</p>
*
* @param clientRequest the client request
* @param proxyRequest the request being proxied
* @see <a href="https://tools.ietf.org/html/rfc7230#section-5.7.1">RFC 7230 section 5.7.1</a>
*/
protected void addViaHeader(HttpServletRequest clientRequest, Request proxyRequest)
{
String protocol = clientRequest.getProtocol();
String[] parts = protocol.split("/", 2);
// Retain only the version if the protocol is HTTP.
String protocolPart = parts.length == 2 && "HTTP".equalsIgnoreCase(parts[0]) ? parts[1] : protocol;
String viaHeaderValue = protocolPart + " " + getViaHost();
proxyRequest.headers(headers -> headers.computeField(HttpHeader.VIA, (header, viaFields) ->
{
if (viaFields == null || viaFields.isEmpty())
return new HttpField(header, viaHeaderValue);
String separator = ", ";
String newValue = viaFields.stream()
.flatMap(field -> Stream.of(field.getValues()))
.filter(value -> !StringUtil.isBlank(value))
.collect(Collectors.joining(separator));
if (newValue.length() > 0)
newValue += separator;
newValue += viaHeaderValue;
return new HttpField(HttpHeader.VIA, newValue);
}));
}
protected void addXForwardedHeaders(HttpServletRequest clientRequest, Request proxyRequest)

View File

@ -48,7 +48,6 @@ import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.AsyncRequestContent;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.util.BufferUtil;
@ -93,9 +92,7 @@ public class AsyncMiddleManServlet extends AbstractProxyServlet
return;
}
final Request proxyRequest = getHttpClient().newRequest(rewrittenTarget)
.method(clientRequest.getMethod())
.version(HttpVersion.fromString(clientRequest.getProtocol()));
Request proxyRequest = newProxyRequest(clientRequest, rewrittenTarget);
copyRequestHeaders(clientRequest, proxyRequest);
@ -118,7 +115,6 @@ public class AsyncMiddleManServlet extends AbstractProxyServlet
{
// Must delay the call to request.getInputStream()
// that sends the 100 Continue to the client.
proxyRequest.attribute(CLIENT_REQUEST_ATTRIBUTE, clientRequest);
proxyRequest.attribute(CONTINUE_ACTION_ATTRIBUTE, (Runnable)() ->
{
try

View File

@ -71,9 +71,7 @@ public class ProxyServlet extends AbstractProxyServlet
return;
}
Request proxyRequest = getHttpClient().newRequest(rewrittenTarget)
.method(request.getMethod())
.version(HttpVersion.fromString(request.getProtocol()));
Request proxyRequest = newProxyRequest(request, rewrittenTarget);
copyRequestHeaders(request, proxyRequest);
@ -92,7 +90,6 @@ public class ProxyServlet extends AbstractProxyServlet
// that sends the 100 Continue to the client.
AsyncRequestContent delegate = new AsyncRequestContent();
proxyRequest.body(delegate);
proxyRequest.attribute(CLIENT_REQUEST_ATTRIBUTE, request);
proxyRequest.attribute(CONTINUE_ACTION_ATTRIBUTE, (Runnable)() ->
{
try

View File

@ -60,6 +60,7 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.ConnectionPool;
@ -89,15 +90,18 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
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 org.junit.jupiter.params.provider.ValueSource;
import static org.eclipse.jetty.http.tools.matchers.HttpFieldsMatchers.containsHeader;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -147,6 +151,11 @@ public class ProxyServletTest
}
private void startProxy(Class<? extends ProxyServlet> proxyServletClass, Map<String, String> initParams) throws Exception
{
startProxy(proxyServletClass.getConstructor().newInstance(), initParams);
}
private void startProxy(AbstractProxyServlet proxyServlet, Map<String, String> initParams) throws Exception
{
QueuedThreadPool proxyPool = new QueuedThreadPool();
proxyPool.setName("proxy");
@ -161,9 +170,8 @@ public class ProxyServletTest
proxyConnector = new ServerConnector(proxy, new HttpConnectionFactory(configuration));
proxy.addConnector(proxyConnector);
proxyServlet = proxyServletClass.getDeclaredConstructor().newInstance();
proxyContext = new ServletContextHandler(proxy, "/", true, false);
this.proxyServlet = proxyServlet;
ServletHolder proxyServletHolder = new ServletHolder(proxyServlet);
proxyServletHolder.setInitParameters(initParams);
proxyContext.addServlet(proxyServletHolder, "/*");
@ -554,7 +562,98 @@ public class ProxyServletTest
ContentResponse response = client.GET("http://localhost:" + serverConnector.getLocalPort());
assertThat("Response expected to contain content of X-Forwarded-Host Header from the request",
response.getContentAsString(),
Matchers.equalTo("localhost:" + serverConnector.getLocalPort()));
equalTo("localhost:" + serverConnector.getLocalPort()));
}
@ParameterizedTest
@MethodSource("impls")
public void testProxyViaHeaderIsAdded(Class<? extends ProxyServlet> proxyServletClass) throws Exception
{
startServer(new EmptyHttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
PrintWriter writer = response.getWriter();
List<String> viaValues = Collections.list(request.getHeaders("Via"));
writer.write(String.join(", ", viaValues));
}
});
String viaHost = "my-good-via-host.example.org";
startProxy(proxyServletClass, Collections.singletonMap("viaHost", viaHost));
startClient();
ContentResponse response = client.GET("http://localhost:" + serverConnector.getLocalPort());
assertThat(response.getContentAsString(), equalTo("1.1 " + viaHost));
}
@ParameterizedTest
@MethodSource("impls")
public void testProxyViaHeaderValueIsAppended(Class<? extends ProxyServlet> proxyServletClass) throws Exception
{
startServer(new EmptyHttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
// Make sure the proxy coalesced the Via headers into just one.
org.eclipse.jetty.server.Request jettyRequest = (org.eclipse.jetty.server.Request)request;
assertEquals(1, jettyRequest.getHttpFields().getFields(HttpHeader.VIA).size());
PrintWriter writer = response.getWriter();
List<String> viaValues = Collections.list(request.getHeaders("Via"));
writer.write(String.join(", ", viaValues));
}
});
String viaHost = "beatrix";
startProxy(proxyServletClass, Collections.singletonMap("viaHost", viaHost));
startClient();
String existingViaHeader = "1.0 charon";
ContentResponse response = client.newRequest("http://localhost:" + serverConnector.getLocalPort())
.header(HttpHeader.VIA, existingViaHeader)
.send();
String expected = String.join(", ", existingViaHeader, "1.1 " + viaHost);
assertThat(response.getContentAsString(), equalTo(expected));
}
@ParameterizedTest
@ValueSource(strings = {"HTTP/2.0", "FCGI/1.0"})
public void testViaHeaderProtocols(String protocol) throws Exception
{
startServer(new EmptyHttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
PrintWriter writer = response.getWriter();
List<String> viaValues = Collections.list(request.getHeaders("Via"));
writer.write(String.join(", ", viaValues));
}
});
String viaHost = "proxy";
startProxy(new ProxyServlet()
{
@Override
protected void addViaHeader(HttpServletRequest clientRequest, Request proxyRequest)
{
HttpServletRequest wrapped = new HttpServletRequestWrapper(clientRequest)
{
@Override
public String getProtocol()
{
return protocol;
}
};
super.addViaHeader(wrapped, proxyRequest);
}
}, Collections.singletonMap("viaHost", viaHost));
startClient();
ContentResponse response = client.GET("http://localhost:" + serverConnector.getLocalPort());
String expectedProtocol = protocol.startsWith("HTTP/") ? protocol.substring("HTTP/".length()) : protocol;
String expected = expectedProtocol + " " + viaHost;
assertThat(response.getContentAsString(), equalTo(expected));
}
@ParameterizedTest
@ -948,7 +1047,7 @@ public class ProxyServletTest
ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort())
.timeout(5, TimeUnit.SECONDS)
.send();
assertThat(response.getStatus(), Matchers.greaterThanOrEqualTo(500));
assertThat(response.getStatus(), greaterThanOrEqualTo(500));
}
catch (ExecutionException e)
{
@ -1082,7 +1181,7 @@ public class ProxyServletTest
// Make sure there is error page content, as the proxy-to-client response has been reset.
InputStream input = listener.getInputStream();
String body = IO.toString(input);
assertThat(body, Matchers.containsString("HTTP ERROR 504"));
assertThat(body, containsString("HTTP ERROR 504"));
chunk1Latch.countDown();
// Result succeeds because a 504 is a valid HTTP response.

View File

@ -140,13 +140,13 @@ public class Pool<T> implements AutoCloseable, Dumpable
* method called or be removed via {@link Pool.Entry#remove()} or
* {@link Pool#remove(Pool.Entry)}.
*
* @param maxReservations the max desired number of reserved entries,
* @param allotment the desired allotment, where each entry handles an allotment of maxMultiplex,
* or a negative number to always trigger the reservation of a new entry.
* @return a disabled entry that is contained in the pool,
* or null if the pool is closed or if the pool already contains
* {@link #getMaxEntries()} entries.
* {@link #getMaxEntries()} entries, or the allotment has already been reserved
*/
public Entry reserve(int maxReservations)
public Entry reserve(int allotment)
{
try (AutoLock l = lock.lock())
{
@ -159,9 +159,8 @@ public class Pool<T> implements AutoCloseable, Dumpable
// The pending count is an AtomicInteger that is only ever incremented here with
// the lock held. Thus the pending count can be reduced immediately after the
// test below, but never incremented. Thus the maxReservations limit can be
// enforced.
if (maxReservations >= 0 && pending.get() >= maxReservations)
// test below, but never incremented. Thus the allotment limit can be enforced.
if (allotment >= 0 && (pending.get() * getMaxMultiplex()) >= allotment)
return null;
pending.incrementAndGet();

View File

@ -227,15 +227,15 @@ public class PathResource extends Resource
Path absPath = path;
try
{
absPath = path.toAbsolutePath();
absPath = path.toRealPath(NO_FOLLOW_LINKS);
}
catch (IOError ioError)
catch (IOError | IOException e)
{
// Not able to resolve absolute path from provided path
// Not able to resolve real/canonical path from provided path
// This could be due to a glob reference, or a reference
// to a path that doesn't exist (yet)
if (LOG.isDebugEnabled())
LOG.debug("Unable to get absolute path for {}", path, ioError);
LOG.debug("Unable to get real/canonical path for {}", path, e);
}
// cleanup any lingering relative path nonsense (like "/./" and "/../")

View File

@ -27,6 +27,7 @@ import org.eclipse.jetty.util.Scanner;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -49,11 +50,22 @@ public class KeyStoreScanner extends ContainerLifeCycle implements Scanner.Discr
this.sslContextFactory = sslContextFactory;
try
{
keystoreFile = sslContextFactory.getKeyStoreResource().getFile();
if (keystoreFile == null || !keystoreFile.exists())
Resource keystoreResource = sslContextFactory.getKeyStoreResource();
File monitoredFile = keystoreResource.getFile();
if (monitoredFile == null || !monitoredFile.exists())
throw new IllegalArgumentException("keystore file does not exist");
if (keystoreFile.isDirectory())
if (monitoredFile.isDirectory())
throw new IllegalArgumentException("expected keystore file not directory");
if (keystoreResource.getAlias() != null)
{
// this resource has an alias, use the alias, as that's what's returned in the Scanner
monitoredFile = new File(keystoreResource.getAlias());
}
keystoreFile = monitoredFile;
if (LOG.isDebugEnabled())
LOG.debug("Monitored Keystore File: {}", monitoredFile);
}
catch (IOException e)
{
@ -122,7 +134,8 @@ public class KeyStoreScanner extends ContainerLifeCycle implements Scanner.Discr
try
{
sslContextFactory.reload(scf -> {});
sslContextFactory.reload(scf ->
{});
}
catch (Throwable t)
{

View File

@ -44,7 +44,7 @@
<jetty.surefire.argLine>-Dfile.encoding=UTF-8 -Duser.language=en -Duser.region=US -showversion -Xmx2g -Xms2g -Xlog:gc:stderr:time,level,tags</jetty.surefire.argLine>
<!-- some maven plugins versions -->
<maven.surefire.version>3.0.0-M5</maven.surefire.version>
<maven.surefire.version>3.0.0-M4</maven.surefire.version>
<maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version>
<maven.dependency.plugin.version>3.1.1</maven.dependency.plugin.version>
<maven.resources.plugin.version>3.1.0</maven.resources.plugin.version>

View File

@ -26,6 +26,7 @@ import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.FormRequestContent;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.Fields;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
@ -70,6 +71,7 @@ public class DemoBaseTests extends AbstractDistributionTest
}
@Test
@Tag("external")
public void testAsyncRest() throws Exception
{
String jettyVersion = System.getProperty("jettyVersion");

View File

@ -21,6 +21,7 @@ package org.eclipse.jetty.test;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
@ -47,11 +48,14 @@ import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.condition.OS.WINDOWS;
@ExtendWith(WorkDirExtension.class)
public class KeyStoreScannerTest
@ -161,7 +165,8 @@ public class KeyStoreScannerTest
// Delete the keystore.
try (StacklessLogging ignored = new StacklessLogging(KeyStoreScanner.class))
{
useKeystore(null);
Path keystorePath = keystoreDir.resolve("keystore");
assertTrue(Files.deleteIfExists(keystorePath));
keystoreScanner.scan();
}
@ -176,6 +181,7 @@ public class KeyStoreScannerTest
}
@Test
@DisabledOnOs(WINDOWS) // does not support symbolic link
public void testReloadChangingSymbolicLink() throws Exception
{
Path keystorePath = keystoreDir.resolve("symlinkKeystore");
@ -202,13 +208,19 @@ public class KeyStoreScannerTest
}
@Test
@DisabledOnOs(WINDOWS) // does not support symbolic link
public void testReloadChangingTargetOfSymbolicLink() throws Exception
{
Path keystoreLink = keystoreDir.resolve("symlinkKeystore");
Path oldKeystoreSrc = MavenTestingUtils.getTestResourcePathFile("oldKeystore");
Path newKeystoreSrc = MavenTestingUtils.getTestResourcePathFile("newKeystore");
Path target = keystoreDir.resolve("keystore");
start(sslContextFactory ->
{
Path keystorePath = keystoreDir.resolve("symlinkKeystore");
Files.createSymbolicLink(keystorePath, useKeystore("oldKeystore"));
sslContextFactory.setKeyStorePath(keystorePath.toString());
Files.copy(oldKeystoreSrc, target);
Files.createSymbolicLink(keystoreLink, target);
sslContextFactory.setKeyStorePath(keystoreLink.toString());
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setKeyManagerPassword("keypwd");
});
@ -218,7 +230,8 @@ public class KeyStoreScannerTest
assertThat(getExpiryYear(cert1), is(2015));
// Change the target file of the symlink to the newKeystore which has a later expiry date.
useKeystore("newKeystore");
Files.copy(newKeystoreSrc, target, StandardCopyOption.REPLACE_EXISTING);
System.err.println("### Triggering scan");
keystoreScanner.scan();
// The scanner should have detected the updated keystore, expiry should be renewed.
@ -232,11 +245,7 @@ public class KeyStoreScannerTest
if (Files.exists(keystorePath))
Files.delete(keystorePath);
if (keystore == null)
return null;
Files.copy(MavenTestingUtils.getTestResourceFile(keystore).toPath(), keystorePath);
keystorePath.toFile().deleteOnExit();
Files.copy(MavenTestingUtils.getTestResourcePath(keystore), keystorePath);
if (!Files.exists(keystorePath))
throw new IllegalStateException("keystore file was not created");
@ -260,6 +269,7 @@ public class KeyStoreScannerTest
HttpsURLConnection connection = (HttpsURLConnection)serverUrl.openConnection();
connection.setHostnameVerifier((a, b) -> true);
connection.setRequestProperty("Connection", "close");
connection.connect();
Certificate[] certs = connection.getServerCertificates();
connection.disconnect();