Skip to content

Commit

Permalink
Fix handling of optional whitespace at the beginning of headers. (hel…
Browse files Browse the repository at this point in the history
  • Loading branch information
tomas-langer authored Nov 17, 2022
1 parent 4e78028 commit f2d018d
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public final class Bytes {
* {@code %} byte.
*/
public static final byte PERCENT_BYTE = (byte) '%';
/**
* Horizontal tabulator byte.
*/
public static final byte TAB_BYTE = (byte) '\t';

private Bytes() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class LazyString {
private final Charset charset;

private String stringValue;
private String owsLessValue;

/**
* New instance.
Expand Down Expand Up @@ -57,11 +58,52 @@ public LazyString(byte[] buffer, Charset charset) {
this.charset = charset;
}

/**
* Strip optional whitespace(s) from beginning and end of the String.
* Defined by the HTTP specification, OWS is a sequence of zero to n space and/or horizontal tab characters.
*
* @return string without optional leading and trailing whitespaces
*/
public String stripOws() {
if (owsLessValue == null) {
int newOffset = offset;
int newLength = length;
for (int i = offset; i < offset + length; i++) {
if (isOws(buffer[i])) {
newOffset = i + 1;
newLength = length - (newOffset - offset);
} else {
// no more white space, go from the end now
break;
}
}
// now we need to go from the end of the string
for (int i = offset + length - 1; i > newOffset; i--) {
if (isOws(buffer[i])) {
newLength--;
} else {
break;
}
}
newLength = Math.max(newLength, 0);
owsLessValue = new String(buffer, newOffset, newLength, charset);
}

return owsLessValue;
}

@Override
public String toString() {
if (stringValue == null) {
stringValue = new String(buffer, offset, length, charset);
}
return stringValue;
}

private boolean isOws(byte aByte) {
return switch (aByte) {
case Bytes.SPACE_BYTE, Bytes.TAB_BYTE -> true;
default -> false;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.common.buffers;

import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

class LazyStringTest {
@ParameterizedTest
@MethodSource("owsData")
void testOwsHandling(OwsTestData data) {
assertThat(data.string().stripOws(), is(data.expected()));
}

private static Stream<OwsTestData> owsData() {
return Stream.of(
new OwsTestData(new LazyString("some-value".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString(" some-value".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString("\tsome-value".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString(" some-value".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString("\t\tsome-value".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString(" \tsome-value".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString("some-value ".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString("some-value\t".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString("some-value ".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString("some-value\t\t".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString("some-value \t".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString(" some-value ".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString("\tsome-value\t".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString(" some-value ".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString("\t\tsome-value\t\t".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString(" \tsome-value\t ".getBytes(US_ASCII), US_ASCII), "some-value"),
new OwsTestData(new LazyString(" \t\t ".getBytes(US_ASCII), US_ASCII), ""),
new OwsTestData(new LazyString(" \t\r\t ".getBytes(US_ASCII), US_ASCII), "\r")
);
}

record OwsTestData(LazyString string, String expected) {
@Override
public String toString() {
StringBuilder result = new StringBuilder();

for (char c : string().toString().toCharArray()) {
switch (c) {
case '\t' -> result.append("\\t");
case '\r' -> result.append("\\r");
default -> result.append(c);
}
}

return "\"" + result + "\"";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,22 @@ class HeaderValueLazy extends HeaderValueBase {
public Http.HeaderValueWriteable addValue(String value) {
if (values == null) {
values = new ArrayList<>(2);
values.add(this.value.toString());
values.add(this.value.stripOws());
}
values.add(value);
return this;
}

@Override
public String value() {
return value.toString();
return value.stripOws();
}

@Override
public List<String> allValues() {
if (values == null) {
values = new ArrayList<>(2);
values.add(value.toString());
values.add(value.stripOws());
}
return values;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@
*/
public final class Http1HeadersParser {
// TODO expand set of fastpath headers
private static final byte[] HD_HOST = (HeaderEnum.HOST.defaultCase() + ": ").getBytes(StandardCharsets.UTF_8);
private static final byte[] HD_ACCEPT = (HeaderEnum.ACCEPT.defaultCase() + ": ").getBytes(StandardCharsets.UTF_8);
private static final byte[] HD_HOST = (HeaderEnum.HOST.defaultCase() + ":").getBytes(StandardCharsets.UTF_8);
private static final byte[] HD_ACCEPT = (HeaderEnum.ACCEPT.defaultCase() + ":").getBytes(StandardCharsets.UTF_8);
private static final byte[] HD_CONNECTION =
(HeaderEnum.CONNECTION.defaultCase() + ": ").getBytes(StandardCharsets.UTF_8);
(HeaderEnum.CONNECTION.defaultCase() + ":").getBytes(StandardCharsets.UTF_8);

private static final byte[] HD_USER_AGENT =
(HeaderEnum.USER_AGENT.defaultCase() + ":").getBytes(StandardCharsets.UTF_8);

private Http1HeadersParser() {
}
Expand All @@ -53,7 +56,7 @@ public static WritableHeaders<?> readHeaders(DataReader reader, int maxHeadersSi
return headers;
}

Http.HeaderName header = readHeaderName(reader, headers, maxLength, validate);
Http.HeaderName header = readHeaderName(reader, maxLength, validate);
maxLength -= header.defaultCase().length() + 2;
int eol = reader.findNewLine(maxLength);
if (eol == maxLength) {
Expand All @@ -72,7 +75,6 @@ public static WritableHeaders<?> readHeaders(DataReader reader, int maxHeadersSi
}

private static Http.HeaderName readHeaderName(DataReader reader,
WritableHeaders<?> headers,
int maxLength,
boolean validate) {
switch (reader.lookup()) {
Expand All @@ -94,6 +96,12 @@ private static Http.HeaderName readHeaderName(DataReader reader,
return HeaderEnum.CONNECTION;
}
}
case (byte) 'U' -> {
if (reader.startsWith(HD_USER_AGENT)) {
reader.skip(HD_USER_AGENT.length);
return HeaderEnum.USER_AGENT;
}
}
default -> {
}
}
Expand All @@ -109,10 +117,8 @@ private static Http.HeaderName readHeaderName(DataReader reader,
HttpToken.validate(headerName);
}
Http.HeaderName header = Http.Header.create(headerName);
reader.skip(1);
if (Bytes.SPACE_BYTE != reader.read()) {
throw new IllegalArgumentException("Invalid header, space not after colon: " + reader.debugDataHex());
}
reader.skip(1); // skip the colon character

return header;
}
}

0 comments on commit f2d018d

Please sign in to comment.