Skip to content

Commit 8ee0949

Browse files
authored
Let header validator find host header field when :authority pseudo-header field is missing (#324)
Motivation: Some proxy server sends target host name and port number in host header field instead of using :authority pseudo-header field. According to the HTTP/3 spec, this is a valid way to send the target endpoint, but current header validator checks only :authority header. It causes false-positive request validation errors. Modifications: Class Http3HeadersSink checks if host header field exists when :authority pseudo-header is missing in the request. Result: No false-positive request validation errors.
1 parent a5c2df7 commit 8ee0949

3 files changed

Lines changed: 79 additions & 35 deletions

File tree

src/main/java/io/netty/incubator/codec/http3/Http3Headers.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,35 @@ enum PseudoHeaderName {
3131
/**
3232
* {@code :method}.
3333
*/
34-
METHOD(":method", true),
34+
METHOD(":method", true, 0x1),
3535

3636
/**
3737
* {@code :scheme}.
3838
*/
39-
SCHEME(":scheme", true),
39+
SCHEME(":scheme", true, 0x2),
4040

4141
/**
4242
* {@code :authority}.
4343
*/
44-
AUTHORITY(":authority", true),
44+
AUTHORITY(":authority", true, 0x4),
4545

4646
/**
4747
* {@code :path}.
4848
*/
49-
PATH(":path", true),
49+
PATH(":path", true, 0x8),
5050

5151
/**
5252
* {@code :status}.
5353
*/
54-
STATUS(":status", false);
54+
STATUS(":status", false, 0x10);
5555

5656
private static final char PSEUDO_HEADER_PREFIX = ':';
5757
private static final byte PSEUDO_HEADER_PREFIX_BYTE = (byte) PSEUDO_HEADER_PREFIX;
5858

5959
private final AsciiString value;
6060
private final boolean requestOnly;
61+
// The position of the bit in the flag indicates the type of the header field
62+
private final int flag;
6163
private static final CharSequenceMap<PseudoHeaderName> PSEUDO_HEADERS = new CharSequenceMap<PseudoHeaderName>();
6264

6365
static {
@@ -66,9 +68,10 @@ enum PseudoHeaderName {
6668
}
6769
}
6870

69-
PseudoHeaderName(String value, boolean requestOnly) {
71+
PseudoHeaderName(String value, boolean requestOnly, int flag) {
7072
this.value = AsciiString.cached(value);
7173
this.requestOnly = requestOnly;
74+
this.flag = flag;
7275
}
7376

7477
public AsciiString value() {
@@ -120,6 +123,10 @@ public static PseudoHeaderName getPseudoHeader(CharSequence name) {
120123
public boolean isRequestOnly() {
121124
return requestOnly;
122125
}
126+
127+
public int getFlag() {
128+
return flag;
129+
}
123130
}
124131

125132
/**

src/main/java/io/netty/incubator/codec/http3/Http3HeadersSink.java

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@
1515
*/
1616
package io.netty.incubator.codec.http3;
1717

18+
import io.netty.handler.codec.http.HttpHeaderNames;
1819
import io.netty.handler.codec.http.HttpMethod;
1920

2021
import java.util.function.BiConsumer;
2122

23+
import static io.netty.incubator.codec.http3.Http3Headers.PseudoHeaderName.AUTHORITY;
24+
import static io.netty.incubator.codec.http3.Http3Headers.PseudoHeaderName.METHOD;
25+
import static io.netty.incubator.codec.http3.Http3Headers.PseudoHeaderName.PATH;
26+
import static io.netty.incubator.codec.http3.Http3Headers.PseudoHeaderName.SCHEME;
27+
import static io.netty.incubator.codec.http3.Http3Headers.PseudoHeaderName.STATUS;
2228
import static io.netty.incubator.codec.http3.Http3Headers.PseudoHeaderName.getPseudoHeader;
2329
import static io.netty.incubator.codec.http3.Http3Headers.PseudoHeaderName.hasPseudoHeaderFormat;
2430

@@ -36,7 +42,7 @@ final class Http3HeadersSink implements BiConsumer<CharSequence, CharSequence> {
3642
private Http3HeadersValidationException validationException;
3743
private HeaderType previousType;
3844
private boolean request;
39-
private int pseudoHeadersCount;
45+
private int receivedPseudoHeaders;
4046

4147
Http3HeadersSink(Http3Headers headers, long maxHeaderListSize, boolean validate, boolean trailer) {
4248
this.headers = headers;
@@ -58,7 +64,7 @@ void finish() throws Http3HeadersValidationException, Http3Exception {
5864
}
5965
if (validate) {
6066
if (trailer) {
61-
if (pseudoHeadersCount != 0) {
67+
if (receivedPseudoHeaders != 0) {
6268
// Trailers must not have pseudo headers.
6369
throw new Http3HeadersValidationException("Pseudo-header(s) included in trailers.");
6470
}
@@ -69,16 +75,12 @@ void finish() throws Http3HeadersValidationException, Http3Exception {
6975
if (request) {
7076
CharSequence method = headers.method();
7177
// fast-path
72-
if (pseudoHeadersCount < 2) {
73-
// There can't be any duplicates for pseudy header names.
74-
throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
75-
}
7678
if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) {
7779
// For CONNECT we must only include:
7880
// - :method
7981
// - :authority
80-
if (pseudoHeadersCount != 2 || headers.authority() == null) {
81-
// There can't be any duplicates for pseudy header names.
82+
final int requiredPseudoHeaders = METHOD.getFlag() | AUTHORITY.getFlag();
83+
if (receivedPseudoHeaders != requiredPseudoHeaders) {
8284
throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
8385
}
8486
} else if (HttpMethod.OPTIONS.asciiName().contentEqualsIgnoreCase(method)) {
@@ -90,36 +92,43 @@ void finish() throws Http3HeadersValidationException, Http3Exception {
9092
// - :scheme
9193
// - :authority
9294
// - :path
93-
if (pseudoHeadersCount != 4 &&
94-
// - :method
95-
// - :scheme
96-
// - :path
97-
!(pseudoHeadersCount == 3 && headers.authority() == null &&
98-
"*".contentEquals(headers.path()))) {
95+
final int requiredPseudoHeaders = METHOD.getFlag() | SCHEME.getFlag() | PATH.getFlag();
96+
if ((receivedPseudoHeaders & requiredPseudoHeaders) != requiredPseudoHeaders ||
97+
(!authorityOrHostHeaderReceived() && !"*".contentEquals(headers.path()))) {
9998
throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
10099
}
101100
} else {
102-
// For requests we must include:
101+
// For other requests we must include:
103102
// - :method
104103
// - :scheme
105104
// - :authority
106105
// - :path
107-
if (pseudoHeadersCount != 4) {
108-
// There can't be any duplicates for pseudy header names.
106+
final int requiredPseudoHeaders = METHOD.getFlag() | SCHEME.getFlag() | PATH.getFlag();
107+
if ((receivedPseudoHeaders & requiredPseudoHeaders) != requiredPseudoHeaders ||
108+
!authorityOrHostHeaderReceived()) {
109109
throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
110110
}
111111
}
112112
} else {
113113
// For responses we must include:
114114
// - :status
115-
if (pseudoHeadersCount != 1) {
116-
// There can't be any duplicates for pseudy header names.
115+
if (receivedPseudoHeaders != STATUS.getFlag()) {
117116
throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
118117
}
119118
}
120119
}
121120
}
122121

122+
/**
123+
* Find host header field in case the :authority pseudo header is not specified.
124+
* See:
125+
* https://www.rfc-editor.org/rfc/rfc9110#section-7.2
126+
*/
127+
private boolean authorityOrHostHeaderReceived() {
128+
return (receivedPseudoHeaders & AUTHORITY.getFlag()) == AUTHORITY.getFlag() ||
129+
headers.contains(HttpHeaderNames.HOST);
130+
}
131+
123132
@Override
124133
public void accept(CharSequence name, CharSequence value) {
125134
headersLength += QpackHeaderField.sizeOf(name, value);
@@ -154,19 +163,15 @@ private void validate(Http3Headers headers, CharSequence name) {
154163
throw new Http3HeadersValidationException(
155164
String.format("Invalid HTTP/3 pseudo-header '%s' encountered.", name));
156165
}
157-
158-
final HeaderType currentHeaderType = pseudoHeader.isRequestOnly() ?
159-
HeaderType.REQUEST_PSEUDO_HEADER : HeaderType.RESPONSE_PSEUDO_HEADER;
160-
if (previousType != null && currentHeaderType != previousType) {
161-
throw new Http3HeadersValidationException("Mix of request and response pseudo-headers.");
162-
}
163-
164-
if (headers.contains(name)) {
166+
if ((receivedPseudoHeaders & pseudoHeader.getFlag()) != 0) {
165167
// There can't be any duplicates for pseudy header names.
166168
throw new Http3HeadersValidationException(
167169
String.format("Pseudo-header field '%s' exists already.", name));
168170
}
169-
pseudoHeadersCount++;
171+
receivedPseudoHeaders |= pseudoHeader.getFlag();
172+
173+
final HeaderType currentHeaderType = pseudoHeader.isRequestOnly() ?
174+
HeaderType.REQUEST_PSEUDO_HEADER : HeaderType.RESPONSE_PSEUDO_HEADER;
170175
request = pseudoHeader.isRequestOnly();
171176
previousType = currentHeaderType;
172177
} else {

src/test/java/io/netty/incubator/codec/http3/Http3HeadersSinkTest.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package io.netty.incubator.codec.http3;
1717

1818

19+
import io.netty.handler.codec.http.HttpHeaderNames;
20+
import io.netty.util.AsciiString;
1921
import org.junit.jupiter.api.Test;
2022

2123
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -143,14 +145,44 @@ public void testAuthorityNotRequiredForOptionsWildcard() throws Http3Exception {
143145
}
144146

145147
@Test
146-
public void testAuthorityRequiredForOptionsNonWildcard() throws Http3Exception {
148+
public void testOptionsNonWildcardWithAuthority() throws Http3Exception {
149+
Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false);
150+
sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "OPTIONS");
151+
sink.accept(Http3Headers.PseudoHeaderName.PATH.value(), "/something");
152+
sink.accept(Http3Headers.PseudoHeaderName.SCHEME.value(), "https");
153+
sink.accept(Http3Headers.PseudoHeaderName.AUTHORITY.value(), "example.com:4433");
154+
sink.finish();
155+
}
156+
157+
@Test
158+
public void testOptionsNonWildcardWithHost() throws Http3Exception {
159+
Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false);
160+
sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "OPTIONS");
161+
sink.accept(Http3Headers.PseudoHeaderName.PATH.value(), "/something");
162+
sink.accept(Http3Headers.PseudoHeaderName.SCHEME.value(), "https");
163+
sink.accept(new AsciiString(HttpHeaderNames.HOST), "example.com:4433");
164+
sink.finish();
165+
}
166+
167+
@Test
168+
public void testAuthorityOrHostRequiredForOptionsNonWildcard() throws Http3Exception {
147169
Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false);
148170
sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "OPTIONS");
149171
sink.accept(Http3Headers.PseudoHeaderName.PATH.value(), "/something");
150172
sink.accept(Http3Headers.PseudoHeaderName.SCHEME.value(), "https");
151173
assertThrows(Http3HeadersValidationException.class, () -> sink.finish());
152174
}
153175

176+
@Test
177+
public void testHostExistsInsteadOfAuthority() throws Http3Exception {
178+
Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false);
179+
sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "GET");
180+
sink.accept(Http3Headers.PseudoHeaderName.PATH.value(), "/");
181+
sink.accept(Http3Headers.PseudoHeaderName.SCHEME.value(), "https");
182+
sink.accept(new AsciiString(HttpHeaderNames.HOST), "example.com:4433");
183+
sink.finish();
184+
}
185+
154186
private static void addMandatoryPseudoHeaders(Http3HeadersSink sink, boolean req) {
155187
if (req) {
156188
sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "GET");

0 commit comments

Comments
 (0)