Skip to content

Commit

Permalink
Merge pull request jetty#5144 from eclipse/jetty-9.4.x-5104-incorrect…
Browse files Browse the repository at this point in the history
…_via_header

Jetty 9.4.x 5104 incorrect via header
  • Loading branch information
sbordet authored Aug 13, 2020
2 parents 9b5b43a + 7b3dccc commit 1b3cb2e
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ public boolean isSameName(HttpField field)
return _name.equalsIgnoreCase(field.getName());
}

public boolean is(String name)
{
return _name.equalsIgnoreCase(name);
}

private int nameHashCode()
{
int h = this.hash;
Expand Down
201 changes: 184 additions & 17 deletions jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.function.BiFunction;
import java.util.function.ToIntFunction;
import java.util.stream.Stream;

Expand Down Expand Up @@ -88,6 +89,152 @@ public HttpFields(HttpFields fields)
_size = fields._size;
}

/**
* <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, BiFunction<HttpField, T, Boolean> matcher)
{
// Look for first occurrence
int first = -1;
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (matcher.apply(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.apply(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;
}

public int size()
{
return _size;
Expand Down Expand Up @@ -167,7 +314,7 @@ public HttpField getField(String name)
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
return f;
}
return null;
Expand All @@ -189,6 +336,22 @@ public List<HttpField> getFields(HttpHeader header)
return fields == null ? Collections.emptyList() : fields;
}

public List<HttpField> getFields(String name)
{
List<HttpField> fields = null;
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f.is(name))
{
if (fields == null)
fields = new ArrayList<>();
fields.add(f);
}
}
return fields == null ? Collections.emptyList() : fields;
}

public boolean contains(HttpField field)
{
for (int i = _size; i-- > 0; )
Expand Down Expand Up @@ -216,7 +379,7 @@ public boolean contains(String name, String value)
for (int i = _size; i-- > 0; )
{
HttpField f = _fields[i];
if (f.getName().equalsIgnoreCase(name) && f.contains(value))
if (f.is(name) && f.contains(value))
return true;
}
return false;
Expand All @@ -238,7 +401,7 @@ public boolean containsKey(String name)
for (int i = _size; i-- > 0; )
{
HttpField f = _fields[i];
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
return true;
}
return false;
Expand Down Expand Up @@ -272,7 +435,7 @@ public String get(String header)
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f.getName().equalsIgnoreCase(header))
if (f.is(header))
return f.getValue();
}
return null;
Expand Down Expand Up @@ -308,7 +471,7 @@ public List<String> getValuesList(String name)
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
{
if (list == null)
list = new ArrayList<>(size() - i);
Expand Down Expand Up @@ -363,7 +526,7 @@ public boolean addCSV(String name, String... values)
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
{
if (existing == null)
existing = new QuotedCSV(false);
Expand Down Expand Up @@ -451,7 +614,7 @@ public List<String> getCSV(String name, boolean keepQuotes)
QuotedCSV values = null;
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
{
if (values == null)
values = new QuotedCSV(keepQuotes);
Expand Down Expand Up @@ -509,7 +672,7 @@ public List<String> getQualityCSV(String name)
QuotedQualityCSV values = null;
for (HttpField f : this)
{
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
{
if (values == null)
values = new QuotedQualityCSV();
Expand All @@ -531,7 +694,7 @@ public Enumeration<String> getValues(final String name)
{
final HttpField f = _fields[i];

if (f.getName().equalsIgnoreCase(name) && f.getValue() != null)
if (f.is(name) && f.getValue() != null)
{
final int first = i;
return new Enumeration<String>()
Expand All @@ -547,7 +710,7 @@ public boolean hasMoreElements()
while (i < _size)
{
field = _fields[i++];
if (field.getName().equalsIgnoreCase(name) && field.getValue() != null)
if (field.is(name) && field.getValue() != null)
return true;
}
field = null;
Expand Down Expand Up @@ -761,8 +924,7 @@ public HttpField remove(HttpHeader name)
if (f.getHeader() == name)
{
removed = f;
_size--;
System.arraycopy(_fields, i + 1, _fields, i, _size - i);
remove(i);
}
}
return removed;
Expand All @@ -780,16 +942,22 @@ public HttpField remove(String name)
for (int i = _size; i-- > 0; )
{
HttpField f = _fields[i];
if (f.getName().equalsIgnoreCase(name))
if (f.is(name))
{
removed = f;
_size--;
System.arraycopy(_fields, i + 1, _fields, i, _size - i);
remove(i);
}
}
return removed;
}

private void remove(int i)
{
_size--;
System.arraycopy(_fields, i + 1, _fields, i, _size - i);
_fields[_size] = null;
}

/**
* Get a header as an long value. Returns the value of an integer field or -1 if not found. The
* case of the field name is ignored.
Expand Down Expand Up @@ -1181,8 +1349,7 @@ public void remove()
{
if (_current < 0)
throw new IllegalStateException();
_size--;
System.arraycopy(_fields, _current + 1, _fields, _current, _size - _current);
HttpFields.this.remove(_current);
_fields[_size] = null;
_cursor = _current;
_current = -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -866,4 +866,32 @@ public void testStream()
assertThat(header.stream().count(), is(3L));
assertThat(header.stream().map(HttpField::getName).filter("name2"::equalsIgnoreCase).count(), is(1L));
}

@Test
public void testComputeField()
{
HttpFields header = new HttpFields();
assertThat(header.size(), is(0));

header.computeField("Test", (n, f) -> null);
assertThat(header.size(), is(0));

header.add(new HttpField("Before", "value"));
assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value"));

header.computeField("Test", (n, f) -> new HttpField(n, "one"));
assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "Test: one"));

header.add(new HttpField("After", "value"));
assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "Test: one", "After: value"));

header.add(new HttpField("Test", "extra"));
assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "Test: one", "After: value", "Test: extra"));

header.computeField("Test", (n, f) -> new HttpField("TEST", "count=" + f.size()));
assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "TEST: count=2", "After: value"));

header.computeField("TEST", (n, f) -> null);
assertThat(header.stream().map(HttpField::toString).collect(Collectors.toList()), contains("Before: value", "After: value"));
}
}
Loading

0 comments on commit 1b3cb2e

Please sign in to comment.