Merged branch 'jetty-9.4.x' into 'jetty-10.0.x'.
This commit is contained in:
commit
46220a0f58
|
@ -384,11 +384,9 @@ public class HttpField
|
||||||
return _name.equalsIgnoreCase(field.getName());
|
return _name.equalsIgnoreCase(field.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public boolean is(String name)
|
||||||
public String toString()
|
|
||||||
{
|
{
|
||||||
String v = getValue();
|
return _name.equalsIgnoreCase(name);
|
||||||
return getName() + ": " + (v == null ? "" : v);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int nameHashCode()
|
private int nameHashCode()
|
||||||
|
@ -411,6 +409,13 @@ public class HttpField
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
String v = getValue();
|
||||||
|
return getName() + ": " + (v == null ? "" : v);
|
||||||
|
}
|
||||||
|
|
||||||
public static class IntValueHttpField extends HttpField
|
public static class IntValueHttpField extends HttpField
|
||||||
{
|
{
|
||||||
private final int _int;
|
private final int _int;
|
||||||
|
|
|
@ -26,9 +26,12 @@ import java.util.Enumeration;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
import java.util.function.ToIntFunction;
|
import java.util.function.ToIntFunction;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
@ -119,7 +122,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
for (HttpField f : this)
|
for (HttpField f : this)
|
||||||
{
|
{
|
||||||
if (f.getName().equalsIgnoreCase(name) && f.contains(value))
|
if (f.is(name) && f.contains(value))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -149,7 +152,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
for (HttpField f : this)
|
for (HttpField f : this)
|
||||||
{
|
{
|
||||||
if (f.getName().equalsIgnoreCase(name))
|
if (f.is(name))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -169,7 +172,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
for (HttpField f : this)
|
for (HttpField f : this)
|
||||||
{
|
{
|
||||||
if (f.getName().equalsIgnoreCase(header))
|
if (f.is(header))
|
||||||
return f.getValue();
|
return f.getValue();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -211,7 +214,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
QuotedCSV values = null;
|
QuotedCSV values = null;
|
||||||
for (HttpField f : this)
|
for (HttpField f : this)
|
||||||
{
|
{
|
||||||
if (f.getName().equalsIgnoreCase(name))
|
if (f.is(name))
|
||||||
{
|
{
|
||||||
if (values == null)
|
if (values == null)
|
||||||
values = new QuotedCSV(keepQuotes);
|
values = new QuotedCSV(keepQuotes);
|
||||||
|
@ -266,7 +269,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
for (HttpField f : this)
|
for (HttpField f : this)
|
||||||
{
|
{
|
||||||
if (f.getName().equalsIgnoreCase(name))
|
if (f.is(name))
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -301,7 +304,19 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
*/
|
*/
|
||||||
default List<HttpField> getFields(HttpHeader header)
|
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;
|
QuotedQualityCSV values = null;
|
||||||
for (HttpField f : this)
|
for (HttpField f : this)
|
||||||
{
|
{
|
||||||
if (f.getName().equalsIgnoreCase(name))
|
if (f.is(name))
|
||||||
{
|
{
|
||||||
if (values == null)
|
if (values == null)
|
||||||
values = new QuotedQualityCSV();
|
values = new QuotedQualityCSV();
|
||||||
|
@ -411,7 +426,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
while (i.hasNext())
|
while (i.hasNext())
|
||||||
{
|
{
|
||||||
HttpField f = i.next();
|
HttpField f = i.next();
|
||||||
if (f.getName().equalsIgnoreCase(name) && f.getValue() != null)
|
if (f.is(name) && f.getValue() != null)
|
||||||
{
|
{
|
||||||
_field = f;
|
_field = f;
|
||||||
return true;
|
return true;
|
||||||
|
@ -462,7 +477,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
final List<String> list = new ArrayList<>();
|
final List<String> list = new ArrayList<>();
|
||||||
for (HttpField f : this)
|
for (HttpField f : this)
|
||||||
{
|
{
|
||||||
if (f.getName().equalsIgnoreCase(name))
|
if (f.is(name))
|
||||||
list.add(f.getValue());
|
list.add(f.getValue());
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
|
@ -685,7 +700,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
QuotedCSV existing = null;
|
QuotedCSV existing = null;
|
||||||
for (HttpField f : this)
|
for (HttpField f : this)
|
||||||
{
|
{
|
||||||
if (f.getName().equalsIgnoreCase(name))
|
if (f.is(name))
|
||||||
{
|
{
|
||||||
if (existing == null)
|
if (existing == null)
|
||||||
existing = new QuotedCSV(false);
|
existing = new QuotedCSV(false);
|
||||||
|
@ -781,7 +796,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
if (_size == 0)
|
if (_size == 0)
|
||||||
throw new IllegalStateException();
|
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));
|
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) -> 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) ->
|
||||||
|
* {
|
||||||
|
* // No cookies, nothing to do.
|
||||||
|
* if (fields == null)
|
||||||
|
* return null;
|
||||||
|
*
|
||||||
|
* // Coalesces all cookies.
|
||||||
|
* String coalesced = fields.stream()
|
||||||
|
* .flatMap(field -> 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) ->
|
||||||
|
* {
|
||||||
|
* 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) -> 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.
|
* Remove a field.
|
||||||
*
|
*
|
||||||
|
@ -927,7 +1088,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
HttpField f = _fields[i];
|
HttpField f = _fields[i];
|
||||||
if (f.getHeader() == name)
|
if (f.getHeader() == name)
|
||||||
System.arraycopy(_fields, i + 1, _fields, i, _size-- - i-- - 1);
|
remove(i);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -938,7 +1099,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
HttpField f = _fields[i];
|
HttpField f = _fields[i];
|
||||||
if (fields.contains(f.getHeader()))
|
if (fields.contains(f.getHeader()))
|
||||||
System.arraycopy(_fields, i + 1, _fields, i, _size-- - i-- - 1);
|
remove(i);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -954,12 +1115,19 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
for (int i = 0; i < _size; i++)
|
for (int i = 0; i < _size; i++)
|
||||||
{
|
{
|
||||||
HttpField f = _fields[i];
|
HttpField f = _fields[i];
|
||||||
if (f.getName().equalsIgnoreCase(name))
|
if (f.is(name))
|
||||||
System.arraycopy(_fields, i + 1, _fields, i, _size-- - i-- - 1);
|
remove(i);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void remove(int i)
|
||||||
|
{
|
||||||
|
_size--;
|
||||||
|
System.arraycopy(_fields, i + 1, _fields, i, _size - i);
|
||||||
|
_fields[_size] = null;
|
||||||
|
}
|
||||||
|
|
||||||
public int size()
|
public int size()
|
||||||
{
|
{
|
||||||
return _size;
|
return _size;
|
||||||
|
@ -1077,9 +1245,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
if (_current < 0)
|
if (_current < 0)
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
_size--;
|
Mutable.this.remove(_current);
|
||||||
System.arraycopy(_fields, _current + 1, _fields, _current, _size - _current);
|
|
||||||
_fields[_size] = null;
|
|
||||||
_cursor = _current;
|
_cursor = _current;
|
||||||
_current = -1;
|
_current = -1;
|
||||||
}
|
}
|
||||||
|
@ -1141,7 +1307,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
// default impl overridden for efficiency
|
// default impl overridden for efficiency
|
||||||
for (HttpField f : _fields)
|
for (HttpField f : _fields)
|
||||||
if (f.getName().equalsIgnoreCase(header))
|
if (f.is(header))
|
||||||
return f.getValue();
|
return f.getValue();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1171,7 +1337,7 @@ public interface HttpFields extends Iterable<HttpField>
|
||||||
{
|
{
|
||||||
// default impl overridden for efficiency
|
// default impl overridden for efficiency
|
||||||
for (HttpField f : _fields)
|
for (HttpField f : _fields)
|
||||||
if (f.getName().equalsIgnoreCase(name))
|
if (f.is(name))
|
||||||
return f;
|
return f;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import java.util.ListIterator;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.eclipse.jetty.util.BufferUtil;
|
import org.eclipse.jetty.util.BufferUtil;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
|
@ -38,6 +39,7 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.contains;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
@ -879,4 +881,44 @@ public class HttpFieldsTest
|
||||||
assertThat(i.next().getName(), is("name4"));
|
assertThat(i.next().getName(), is("name4"));
|
||||||
assertThat(i.hasNext(), is(false));
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@ import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import javax.servlet.AsyncContext;
|
import javax.servlet.AsyncContext;
|
||||||
import javax.servlet.ServletConfig;
|
import javax.servlet.ServletConfig;
|
||||||
import javax.servlet.ServletContext;
|
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.HttpHeaderValue;
|
||||||
import org.eclipse.jetty.http.HttpScheme;
|
import org.eclipse.jetty.http.HttpScheme;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.eclipse.jetty.http.HttpVersion;
|
||||||
import org.eclipse.jetty.io.ClientConnector;
|
import org.eclipse.jetty.io.ClientConnector;
|
||||||
import org.eclipse.jetty.util.HttpCookieStore;
|
import org.eclipse.jetty.util.HttpCookieStore;
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
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()));
|
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)
|
protected void copyRequestHeaders(HttpServletRequest clientRequest, Request proxyRequest)
|
||||||
{
|
{
|
||||||
// First clear possibly existing headers, as we are going to copy those from the client request.
|
// 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);
|
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)
|
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)
|
protected void addXForwardedHeaders(HttpServletRequest clientRequest, Request proxyRequest)
|
||||||
|
|
|
@ -48,7 +48,6 @@ import org.eclipse.jetty.client.api.Response;
|
||||||
import org.eclipse.jetty.client.api.Result;
|
import org.eclipse.jetty.client.api.Result;
|
||||||
import org.eclipse.jetty.client.util.AsyncRequestContent;
|
import org.eclipse.jetty.client.util.AsyncRequestContent;
|
||||||
import org.eclipse.jetty.http.HttpHeader;
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
import org.eclipse.jetty.http.HttpVersion;
|
|
||||||
import org.eclipse.jetty.io.ByteBufferPool;
|
import org.eclipse.jetty.io.ByteBufferPool;
|
||||||
import org.eclipse.jetty.io.RuntimeIOException;
|
import org.eclipse.jetty.io.RuntimeIOException;
|
||||||
import org.eclipse.jetty.util.BufferUtil;
|
import org.eclipse.jetty.util.BufferUtil;
|
||||||
|
@ -93,9 +92,7 @@ public class AsyncMiddleManServlet extends AbstractProxyServlet
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Request proxyRequest = getHttpClient().newRequest(rewrittenTarget)
|
Request proxyRequest = newProxyRequest(clientRequest, rewrittenTarget);
|
||||||
.method(clientRequest.getMethod())
|
|
||||||
.version(HttpVersion.fromString(clientRequest.getProtocol()));
|
|
||||||
|
|
||||||
copyRequestHeaders(clientRequest, proxyRequest);
|
copyRequestHeaders(clientRequest, proxyRequest);
|
||||||
|
|
||||||
|
@ -118,7 +115,6 @@ public class AsyncMiddleManServlet extends AbstractProxyServlet
|
||||||
{
|
{
|
||||||
// Must delay the call to request.getInputStream()
|
// Must delay the call to request.getInputStream()
|
||||||
// that sends the 100 Continue to the client.
|
// that sends the 100 Continue to the client.
|
||||||
proxyRequest.attribute(CLIENT_REQUEST_ATTRIBUTE, clientRequest);
|
|
||||||
proxyRequest.attribute(CONTINUE_ACTION_ATTRIBUTE, (Runnable)() ->
|
proxyRequest.attribute(CONTINUE_ACTION_ATTRIBUTE, (Runnable)() ->
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -71,9 +71,7 @@ public class ProxyServlet extends AbstractProxyServlet
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Request proxyRequest = getHttpClient().newRequest(rewrittenTarget)
|
Request proxyRequest = newProxyRequest(request, rewrittenTarget);
|
||||||
.method(request.getMethod())
|
|
||||||
.version(HttpVersion.fromString(request.getProtocol()));
|
|
||||||
|
|
||||||
copyRequestHeaders(request, proxyRequest);
|
copyRequestHeaders(request, proxyRequest);
|
||||||
|
|
||||||
|
@ -92,7 +90,6 @@ public class ProxyServlet extends AbstractProxyServlet
|
||||||
// that sends the 100 Continue to the client.
|
// that sends the 100 Continue to the client.
|
||||||
AsyncRequestContent delegate = new AsyncRequestContent();
|
AsyncRequestContent delegate = new AsyncRequestContent();
|
||||||
proxyRequest.body(delegate);
|
proxyRequest.body(delegate);
|
||||||
proxyRequest.attribute(CLIENT_REQUEST_ATTRIBUTE, request);
|
|
||||||
proxyRequest.attribute(CONTINUE_ACTION_ATTRIBUTE, (Runnable)() ->
|
proxyRequest.attribute(CONTINUE_ACTION_ATTRIBUTE, (Runnable)() ->
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -60,6 +60,7 @@ import javax.servlet.ServletResponse;
|
||||||
import javax.servlet.http.Cookie;
|
import javax.servlet.http.Cookie;
|
||||||
import javax.servlet.http.HttpServlet;
|
import javax.servlet.http.HttpServlet;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletRequestWrapper;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.eclipse.jetty.client.ConnectionPool;
|
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.IO;
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
import org.eclipse.jetty.util.StringUtil;
|
||||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||||
import org.hamcrest.Matchers;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
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.eclipse.jetty.http.tools.matchers.HttpFieldsMatchers.containsHeader;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
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.hamcrest.Matchers.instanceOf;
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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
|
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();
|
QueuedThreadPool proxyPool = new QueuedThreadPool();
|
||||||
proxyPool.setName("proxy");
|
proxyPool.setName("proxy");
|
||||||
|
@ -161,9 +170,8 @@ public class ProxyServletTest
|
||||||
proxyConnector = new ServerConnector(proxy, new HttpConnectionFactory(configuration));
|
proxyConnector = new ServerConnector(proxy, new HttpConnectionFactory(configuration));
|
||||||
proxy.addConnector(proxyConnector);
|
proxy.addConnector(proxyConnector);
|
||||||
|
|
||||||
proxyServlet = proxyServletClass.getDeclaredConstructor().newInstance();
|
|
||||||
|
|
||||||
proxyContext = new ServletContextHandler(proxy, "/", true, false);
|
proxyContext = new ServletContextHandler(proxy, "/", true, false);
|
||||||
|
this.proxyServlet = proxyServlet;
|
||||||
ServletHolder proxyServletHolder = new ServletHolder(proxyServlet);
|
ServletHolder proxyServletHolder = new ServletHolder(proxyServlet);
|
||||||
proxyServletHolder.setInitParameters(initParams);
|
proxyServletHolder.setInitParameters(initParams);
|
||||||
proxyContext.addServlet(proxyServletHolder, "/*");
|
proxyContext.addServlet(proxyServletHolder, "/*");
|
||||||
|
@ -554,7 +562,98 @@ public class ProxyServletTest
|
||||||
ContentResponse response = client.GET("http://localhost:" + serverConnector.getLocalPort());
|
ContentResponse response = client.GET("http://localhost:" + serverConnector.getLocalPort());
|
||||||
assertThat("Response expected to contain content of X-Forwarded-Host Header from the request",
|
assertThat("Response expected to contain content of X-Forwarded-Host Header from the request",
|
||||||
response.getContentAsString(),
|
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
|
@ParameterizedTest
|
||||||
|
@ -948,7 +1047,7 @@ public class ProxyServletTest
|
||||||
ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort())
|
ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort())
|
||||||
.timeout(5, TimeUnit.SECONDS)
|
.timeout(5, TimeUnit.SECONDS)
|
||||||
.send();
|
.send();
|
||||||
assertThat(response.getStatus(), Matchers.greaterThanOrEqualTo(500));
|
assertThat(response.getStatus(), greaterThanOrEqualTo(500));
|
||||||
}
|
}
|
||||||
catch (ExecutionException e)
|
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.
|
// Make sure there is error page content, as the proxy-to-client response has been reset.
|
||||||
InputStream input = listener.getInputStream();
|
InputStream input = listener.getInputStream();
|
||||||
String body = IO.toString(input);
|
String body = IO.toString(input);
|
||||||
assertThat(body, Matchers.containsString("HTTP ERROR 504"));
|
assertThat(body, containsString("HTTP ERROR 504"));
|
||||||
chunk1Latch.countDown();
|
chunk1Latch.countDown();
|
||||||
|
|
||||||
// Result succeeds because a 504 is a valid HTTP response.
|
// Result succeeds because a 504 is a valid HTTP response.
|
||||||
|
|
Loading…
Reference in New Issue