Skip to content

Commit 35444ab

Browse files
franz1981vietj
authored andcommitted
Improve HTTP validation for inlining and specialized types
1 parent 221fb10 commit 35444ab

File tree

3 files changed

+275
-43
lines changed

3 files changed

+275
-43
lines changed

vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java

Lines changed: 169 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -604,67 +604,181 @@ public static void validateHeader(CharSequence name, Iterable<? extends CharSequ
604604
});
605605
}
606606

607-
public static void validateHeaderValue(CharSequence seq) {
607+
public static void validateHeaderValue(CharSequence value) {
608+
if (value instanceof AsciiString) {
609+
validateAsciiHeaderValue((AsciiString) value);
610+
} else if (value instanceof String) {
611+
validateStringHeaderValue((String) value);
612+
} else {
613+
validateSequenceHeaderValue(value);
614+
}
615+
}
616+
617+
private static void validateAsciiHeaderValue(AsciiString value) {
618+
final int length = value.length();
619+
if (length == 0) {
620+
return;
621+
}
622+
byte[] asciiChars = value.array();
623+
int off = value.arrayOffset();
624+
if (off == 0 && length == asciiChars.length) {
625+
for (int index = 0; index < asciiChars.length; index++) {
626+
int latinChar = asciiChars[index] & 0xFF;
627+
if (latinChar == 0x7F) {
628+
throw new IllegalArgumentException("a header value contains a prohibited character '127': " + value);
629+
}
630+
// non-printable chars are rare so let's make it a fall-back method, whilst still accepting HTAB
631+
if (latinChar < 32 && latinChar != 0x09) {
632+
validateSequenceHeaderValue(value, index - off);
633+
break;
634+
}
635+
}
636+
} else {
637+
validateAsciiRangeHeaderValue(value, off, length, asciiChars);
638+
}
639+
}
640+
641+
/**
642+
* This method is the slow-path generic version of {@link #validateAsciiHeaderValue(AsciiString)} which
643+
* is optimized for {@link AsciiString} instances which are backed by a 0-offset full-blown byte array.
644+
*/
645+
private static void validateAsciiRangeHeaderValue(AsciiString value, int off, int length, byte[] asciiChars) {
646+
int end = off + length;
647+
for (int index = off; index < end; index++) {
648+
int latinChar = asciiChars[index] & 0xFF;
649+
if (latinChar == 0x7F) {
650+
throw new IllegalArgumentException("a header value contains a prohibited character '127': " + value);
651+
}
652+
// non-printable chars are rare so let's make it a fall-back method, whilst still accepting HTAB
653+
if (latinChar < 32 && latinChar != 0x09) {
654+
validateSequenceHeaderValue(value, index - off);
655+
break;
656+
}
657+
}
658+
}
608659

609-
int state = 0;
610-
// Start looping through each of the character
611-
for (int index = 0; index < seq.length(); index++) {
612-
state = validateValueChar(seq, state, seq.charAt(index));
660+
private static void validateStringHeaderValue(String value) {
661+
final int length = value.length();
662+
if (length == 0) {
663+
return;
613664
}
614665

615-
if (state != 0) {
616-
throw new IllegalArgumentException("a header value must not end with '\\r' or '\\n':" + seq);
666+
for (int index = 0; index < length; index++) {
667+
char latinChar = value.charAt(index);
668+
if (latinChar == 0x7F) {
669+
throw new IllegalArgumentException("a header value contains a prohibited character '127': " + value);
670+
}
671+
// non-printable chars are rare so let's make it a fall-back method, whilst still accepting HTAB
672+
if (latinChar < 32 && latinChar != 0x09) {
673+
validateSequenceHeaderValue(value, index);
674+
break;
675+
}
676+
}
677+
}
678+
679+
private static void validateSequenceHeaderValue(CharSequence value) {
680+
final int length = value.length();
681+
if (length == 0) {
682+
return;
683+
}
684+
685+
for (int index = 0; index < length; index++) {
686+
char latinChar = value.charAt(index);
687+
if (latinChar == 0x7F) {
688+
throw new IllegalArgumentException("a header value contains a prohibited character '127': " + value);
689+
}
690+
// non-printable chars are rare so let's make it a fall-back method, whilst still accepting HTAB
691+
if (latinChar < 32 && latinChar != 0x09) {
692+
validateSequenceHeaderValue(value, index);
693+
break;
694+
}
617695
}
618696
}
619697

620698
private static final int HIGHEST_INVALID_VALUE_CHAR_MASK = ~0x1F;
699+
private static final int NO_CR_LF_STATE = 0;
700+
private static final int CR_STATE = 1;
701+
private static final int LF_STATE = 2;
702+
703+
/**
704+
* This method is taken as we need to validate the header value for the non-printable characters.
705+
*/
706+
private static void validateSequenceHeaderValue(CharSequence seq, int index) {
707+
// we already expect the very-first character to be non-printable
708+
int state = validateValueChar(seq, NO_CR_LF_STATE, seq.charAt(index));
709+
for (int i = index + 1; i < seq.length(); i++) {
710+
state = validateValueChar(seq, state, seq.charAt(i));
711+
}
712+
if (state != NO_CR_LF_STATE) {
713+
throw new IllegalArgumentException("a header value must not end with '\\r' or '\\n':" + seq);
714+
}
715+
}
621716

622-
private static int validateValueChar(CharSequence seq, int state, char character) {
717+
private static int validateValueChar(CharSequence seq, int state, char ch) {
623718
/*
624719
* State:
625720
* 0: Previous character was neither CR nor LF
626721
* 1: The previous character was CR
627722
* 2: The previous character was LF
628723
*/
629-
if ((character & HIGHEST_INVALID_VALUE_CHAR_MASK) == 0 || character == 0x7F) { // 0x7F is "DEL".
630-
// The only characters allowed in the range 0x00-0x1F are : HTAB, LF and CR
631-
switch (character) {
632-
case 0x09: // Horizontal tab - HTAB
633-
case 0x0a: // Line feed - LF
634-
case 0x0d: // Carriage return - CR
635-
break;
636-
default:
637-
throw new IllegalArgumentException("a header value contains a prohibited character '" + (int) character + "': " + seq);
724+
if (ch == 0x7F) {
725+
throw new IllegalArgumentException("a header value contains a prohibited character '127': " + seq);
726+
}
727+
if ((ch & HIGHEST_INVALID_VALUE_CHAR_MASK) == 0) {
728+
// this is a rare scenario
729+
validateNonPrintableCtrlChar(seq, ch);
730+
// this can include LF and CR as they are non-printable characters
731+
if (state == NO_CR_LF_STATE) {
732+
// Check the CRLF (HT | SP) pattern
733+
switch (ch) {
734+
case '\r':
735+
return CR_STATE;
736+
case '\n':
737+
return LF_STATE;
738+
}
739+
return NO_CR_LF_STATE;
638740
}
639741
}
742+
if (state != NO_CR_LF_STATE) {
743+
// this is a rare scenario
744+
return validateCrLfChar(seq, state, ch);
745+
} else {
746+
return NO_CR_LF_STATE;
747+
}
748+
}
640749

641-
// Check the CRLF (HT | SP) pattern
750+
private static int validateCrLfChar(CharSequence seq, int state, char ch) {
642751
switch (state) {
643-
case 0:
644-
switch (character) {
645-
case '\r':
646-
return 1;
647-
case '\n':
648-
return 2;
649-
}
650-
break;
651-
case 1:
652-
switch (character) {
653-
case '\n':
654-
return 2;
655-
default:
656-
throw new IllegalArgumentException("only '\\n' is allowed after '\\r': " + seq);
752+
case CR_STATE:
753+
if (ch == '\n') {
754+
return LF_STATE;
657755
}
658-
case 2:
659-
switch (character) {
756+
throw new IllegalArgumentException("only '\\n' is allowed after '\\r': " + seq);
757+
case LF_STATE:
758+
switch (ch) {
660759
case '\t':
661760
case ' ':
662-
return 0;
761+
// return to the normal state
762+
return NO_CR_LF_STATE;
663763
default:
664764
throw new IllegalArgumentException("only ' ' and '\\t' are allowed after '\\n': " + seq);
665765
}
766+
default:
767+
// this should never happen
768+
throw new AssertionError();
769+
}
770+
}
771+
772+
private static void validateNonPrintableCtrlChar(CharSequence seq, int ch) {
773+
// The only characters allowed in the range 0x00-0x1F are : HTAB, LF and CR
774+
switch (ch) {
775+
case 0x09: // Horizontal tab - HTAB
776+
case 0x0a: // Line feed - LF
777+
case 0x0d: // Carriage return - CR
778+
break;
779+
default:
780+
throw new IllegalArgumentException("a header value contains a prohibited character '" + (int) ch + "': " + seq);
666781
}
667-
return state;
668782
}
669783

670784
private static final boolean[] VALID_H_NAME_ASCII_CHARS;
@@ -700,13 +814,15 @@ private static int validateValueChar(CharSequence seq, int state, char character
700814
public static void validateHeaderName(CharSequence value) {
701815
if (value instanceof AsciiString) {
702816
// no need to check for ASCII-ness anymore
703-
validateHeaderName((AsciiString) value);
817+
validateAsciiHeaderName((AsciiString) value);
818+
} else if(value instanceof String) {
819+
validateStringHeaderName((String) value);
704820
} else {
705-
validateHeaderName0(value);
821+
validateSequenceHeaderName(value);
706822
}
707823
}
708824

709-
private static void validateHeaderName(AsciiString value) {
825+
private static void validateAsciiHeaderName(AsciiString value) {
710826
final int len = value.length();
711827
final int off = value.arrayOffset();
712828
final byte[] asciiChars = value.array();
@@ -722,7 +838,20 @@ private static void validateHeaderName(AsciiString value) {
722838
}
723839
}
724840

725-
private static void validateHeaderName0(CharSequence value) {
841+
private static void validateStringHeaderName(String value) {
842+
for (int i = 0; i < value.length(); i++) {
843+
final char c = value.charAt(i);
844+
// Check to see if the character is not an ASCII character, or invalid
845+
if (c > 0x7f) {
846+
throw new IllegalArgumentException("a header name cannot contain non-ASCII character: " + value);
847+
}
848+
if (!VALID_H_NAME_ASCII_CHARS[c & 0x7F]) {
849+
throw new IllegalArgumentException("a header name cannot contain some prohibited characters, such as : " + value);
850+
}
851+
}
852+
}
853+
854+
private static void validateSequenceHeaderName(CharSequence value) {
726855
for (int i = 0; i < value.length(); i++) {
727856
final char c = value.charAt(i);
728857
// Check to see if the character is not an ASCII character, or invalid

vertx-core/src/test/java/io/vertx/benchmarks/BenchmarkBase.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@
2929
@Threads(1)
3030
@BenchmarkMode(Mode.Throughput)
3131
@Fork(value = 1, jvmArgs = {
32-
"-XX:+UseBiasedLocking",
33-
"-XX:BiasedLockingStartupDelay=0",
34-
"-XX:+AggressiveOpts",
3532
"-Djmh.executor=CUSTOM",
3633
"-Djmh.executor.class=io.vertx.benchmarks.VertxExecutorService"
3734
})
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package io.vertx.benchmarks;
2+
3+
import java.util.concurrent.TimeUnit;
4+
import java.util.function.Consumer;
5+
6+
import org.openjdk.jmh.annotations.Benchmark;
7+
import org.openjdk.jmh.annotations.CompilerControl;
8+
import org.openjdk.jmh.annotations.Measurement;
9+
import org.openjdk.jmh.annotations.Param;
10+
import org.openjdk.jmh.annotations.Scope;
11+
import org.openjdk.jmh.annotations.Setup;
12+
import org.openjdk.jmh.annotations.State;
13+
import org.openjdk.jmh.annotations.Warmup;
14+
15+
import io.netty.handler.codec.DefaultHeaders;
16+
import io.netty.handler.codec.http.DefaultHttpHeadersFactory;
17+
import io.vertx.core.http.impl.HttpUtils;
18+
19+
@State(Scope.Thread)
20+
@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
21+
@Measurement(iterations = 5, time = 400, timeUnit = TimeUnit.MILLISECONDS)
22+
public class HeadersValidationBenchmark extends BenchmarkBase {
23+
24+
@Param({ "true", "false" })
25+
public boolean ascii;
26+
27+
private CharSequence[] headerNames;
28+
private CharSequence[] headerValues;
29+
private static final DefaultHeaders.NameValidator<CharSequence> nettyNameValidator = DefaultHttpHeadersFactory.headersFactory().getNameValidator();
30+
private static final DefaultHeaders.ValueValidator<CharSequence> nettyValueValidator = DefaultHttpHeadersFactory.headersFactory().getValueValidator();
31+
private static final Consumer<CharSequence> vertxNameValidator = HttpUtils::validateHeaderName;
32+
private static final Consumer<CharSequence> vertxValueValidator = HttpUtils::validateHeaderValue;
33+
34+
@Setup
35+
public void setup() {
36+
headerNames = new CharSequence[4];
37+
headerValues = new CharSequence[4];
38+
headerNames[0] = io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
39+
headerValues[0] = io.vertx.core.http.HttpHeaders.createOptimized("text/plain");
40+
headerNames[1] = io.vertx.core.http.HttpHeaders.CONTENT_LENGTH;
41+
headerValues[1] = io.vertx.core.http.HttpHeaders.createOptimized("20");
42+
headerNames[2] = io.vertx.core.http.HttpHeaders.SERVER;
43+
headerValues[2] = io.vertx.core.http.HttpHeaders.createOptimized("vert.x");
44+
headerNames[3] = io.vertx.core.http.HttpHeaders.DATE;
45+
headerValues[3] = io.vertx.core.http.HttpHeaders.createOptimized(HeadersUtils.DATE_FORMAT.format(new java.util.Date(0)));
46+
if (!ascii) {
47+
for (int i = 0; i < headerNames.length; i++) {
48+
headerNames[i] = headerNames[i].toString();
49+
}
50+
}
51+
if (!ascii) {
52+
for (int i = 0; i < headerValues.length; i++) {
53+
headerValues[i] = headerValues[i].toString();
54+
}
55+
}
56+
}
57+
58+
@Benchmark
59+
public void validateNameNetty() {
60+
for (CharSequence headerName : headerNames) {
61+
nettyNameValidation(headerName);
62+
}
63+
}
64+
65+
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
66+
private void nettyNameValidation(CharSequence headerName) {
67+
nettyNameValidator.validateName(headerName);
68+
}
69+
70+
@Benchmark
71+
public void validateNameVertx() {
72+
for (CharSequence headerName : headerNames) {
73+
vertxNameValidation(headerName);
74+
}
75+
}
76+
77+
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
78+
private void vertxNameValidation(CharSequence headerName) {
79+
vertxNameValidator.accept(headerName);
80+
}
81+
82+
83+
@Benchmark
84+
public void validateValueNetty() {
85+
for (CharSequence headerValue : headerValues) {
86+
nettyValueValidation(headerValue);
87+
}
88+
}
89+
90+
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
91+
private void nettyValueValidation(CharSequence headerValue) {
92+
nettyValueValidator.validate(headerValue);
93+
}
94+
95+
@Benchmark
96+
public void validateValueVertx() {
97+
for (CharSequence headerValue : headerValues) {
98+
vertxValueValidation(headerValue);
99+
}
100+
}
101+
102+
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
103+
private void vertxValueValidation(CharSequence headerValue) {
104+
vertxValueValidator.accept(headerValue);
105+
}
106+
}

0 commit comments

Comments
 (0)