netty: Support pseudo headers in all GrpcHttp2RequestHeaders methods

The previous code assumed that only gRPC would be using these methods.
But twice now Netty has made a change (generally relating to security)
that used a method for pseudo headers that previously wasn't supported.
Let's stop the whack-a-mole and just implement them all.

This restores compatibility with Netty 4.1.75.Final. Fixes #8981
This commit is contained in:
Eric Anderson 2022-03-22 07:39:48 -07:00 committed by GitHub
parent 2d7302d4fd
commit 4a0fe99f8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 162 additions and 26 deletions

View File

@ -340,7 +340,12 @@ class GrpcHttp2HeadersUtils {
AsciiString name = validateName(requireAsciiString(csName)); AsciiString name = validateName(requireAsciiString(csName));
AsciiString value = requireAsciiString(csValue); AsciiString value = requireAsciiString(csValue);
if (isPseudoHeader(name)) { if (isPseudoHeader(name)) {
addPseudoHeader(name, value); AsciiString previous = getPseudoHeader(name);
if (previous != null) {
PlatformDependent.throwException(
connectionError(PROTOCOL_ERROR, "Duplicate %s header", name));
}
setPseudoHeader(name, value);
return this; return this;
} }
if (equals(TE_HEADER, name)) { if (equals(TE_HEADER, name)) {
@ -353,44 +358,42 @@ class GrpcHttp2HeadersUtils {
@Override @Override
public CharSequence get(CharSequence csName) { public CharSequence get(CharSequence csName) {
AsciiString name = requireAsciiString(csName); AsciiString name = requireAsciiString(csName);
checkArgument(!isPseudoHeader(name), "Use direct accessor methods for pseudo headers."); if (isPseudoHeader(name)) {
return getPseudoHeader(name);
}
if (equals(TE_HEADER, name)) { if (equals(TE_HEADER, name)) {
return te; return te;
} }
return get(name); return get(name);
} }
private void addPseudoHeader(CharSequence csName, CharSequence csValue) { private AsciiString getPseudoHeader(AsciiString name) {
AsciiString name = requireAsciiString(csName);
AsciiString value = requireAsciiString(csValue);
if (equals(PATH_HEADER, name)) { if (equals(PATH_HEADER, name)) {
if (path != null) { return path;
PlatformDependent.throwException( } else if (equals(AUTHORITY_HEADER, name)) {
connectionError(PROTOCOL_ERROR, "Duplicate :path header")); return authority;
} else if (equals(METHOD_HEADER, name)) {
return method;
} else if (equals(SCHEME_HEADER, name)) {
return scheme;
} else {
return null;
} }
}
private void setPseudoHeader(AsciiString name, AsciiString value) {
if (equals(PATH_HEADER, name)) {
path = value; path = value;
} else if (equals(AUTHORITY_HEADER, name)) { } else if (equals(AUTHORITY_HEADER, name)) {
if (authority != null) {
PlatformDependent.throwException(
connectionError(PROTOCOL_ERROR, "Duplicate :authority header"));
}
authority = value; authority = value;
} else if (equals(METHOD_HEADER, name)) { } else if (equals(METHOD_HEADER, name)) {
if (method != null) {
PlatformDependent.throwException(
connectionError(PROTOCOL_ERROR, "Duplicate :method header"));
}
method = value; method = value;
} else if (equals(SCHEME_HEADER, name)) { } else if (equals(SCHEME_HEADER, name)) {
if (scheme != null) {
PlatformDependent.throwException(
connectionError(PROTOCOL_ERROR, "Duplicate :scheme header"));
}
scheme = value; scheme = value;
} else { } else {
PlatformDependent.throwException( PlatformDependent.throwException(
connectionError(PROTOCOL_ERROR, "Illegal pseudo-header '%s' in request.", name)); connectionError(PROTOCOL_ERROR, "Illegal pseudo-header '%s' in request.", name));
throw new AssertionError(); // Make flow control obvious to javac
} }
} }
@ -418,8 +421,12 @@ class GrpcHttp2HeadersUtils {
public List<CharSequence> getAll(CharSequence csName) { public List<CharSequence> getAll(CharSequence csName) {
AsciiString name = requireAsciiString(csName); AsciiString name = requireAsciiString(csName);
if (isPseudoHeader(name)) { if (isPseudoHeader(name)) {
// This code should never be reached. AsciiString value = getPseudoHeader(name);
throw new IllegalArgumentException("Use direct accessor methods for pseudo headers."); if (value == null) {
return Collections.emptyList();
} else {
return Collections.singletonList(value);
}
} }
if (equals(TE_HEADER, name)) { if (equals(TE_HEADER, name)) {
return Collections.singletonList((CharSequence) te); return Collections.singletonList((CharSequence) te);
@ -431,8 +438,12 @@ class GrpcHttp2HeadersUtils {
public boolean remove(CharSequence csName) { public boolean remove(CharSequence csName) {
AsciiString name = requireAsciiString(csName); AsciiString name = requireAsciiString(csName);
if (isPseudoHeader(name)) { if (isPseudoHeader(name)) {
// This code should never be reached. if (getPseudoHeader(name) == null) {
throw new IllegalArgumentException("Use direct accessor methods for pseudo headers."); return false;
} else {
setPseudoHeader(name, null);
return true;
}
} }
if (equals(TE_HEADER, name)) { if (equals(TE_HEADER, name)) {
boolean wasPresent = te != null; boolean wasPresent = te != null;

View File

@ -22,6 +22,7 @@ import static io.grpc.internal.GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
import static io.netty.util.AsciiString.of; import static io.netty.util.AsciiString.of;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.io.BaseEncoding; import com.google.common.io.BaseEncoding;
@ -133,6 +134,130 @@ public class GrpcHttp2HeadersUtilsTest {
assertThat(decodedHeaders.toString()).contains("[]"); assertThat(decodedHeaders.toString()).contains("[]");
} }
// contains() is used by Netty 4.1.75+. https://github.com/grpc/grpc-java/issues/8981
// Just implement everything pseudo headers for all methods; too many recent breakages.
@Test
public void grpcHttp2RequestHeaders_pseudoHeaders_notPresent() {
Http2Headers http2Headers = new GrpcHttp2RequestHeaders(2);
assertThat(http2Headers.get(AsciiString.of(":path"))).isNull();
assertThat(http2Headers.get(AsciiString.of(":authority"))).isNull();
assertThat(http2Headers.get(AsciiString.of(":method"))).isNull();
assertThat(http2Headers.get(AsciiString.of(":scheme"))).isNull();
assertThat(http2Headers.get(AsciiString.of(":status"))).isNull();
assertThat(http2Headers.getAll(AsciiString.of(":path"))).isEmpty();
assertThat(http2Headers.getAll(AsciiString.of(":authority"))).isEmpty();
assertThat(http2Headers.getAll(AsciiString.of(":method"))).isEmpty();
assertThat(http2Headers.getAll(AsciiString.of(":scheme"))).isEmpty();
assertThat(http2Headers.getAll(AsciiString.of(":status"))).isEmpty();
assertThat(http2Headers.contains(AsciiString.of(":path"))).isFalse();
assertThat(http2Headers.contains(AsciiString.of(":authority"))).isFalse();
assertThat(http2Headers.contains(AsciiString.of(":method"))).isFalse();
assertThat(http2Headers.contains(AsciiString.of(":scheme"))).isFalse();
assertThat(http2Headers.contains(AsciiString.of(":status"))).isFalse();
assertThat(http2Headers.remove(AsciiString.of(":path"))).isFalse();
assertThat(http2Headers.remove(AsciiString.of(":authority"))).isFalse();
assertThat(http2Headers.remove(AsciiString.of(":method"))).isFalse();
assertThat(http2Headers.remove(AsciiString.of(":scheme"))).isFalse();
assertThat(http2Headers.remove(AsciiString.of(":status"))).isFalse();
}
@Test
public void grpcHttp2RequestHeaders_pseudoHeaders_present() {
Http2Headers http2Headers = new GrpcHttp2RequestHeaders(2);
http2Headers.add(AsciiString.of(":path"), AsciiString.of("mypath"));
http2Headers.add(AsciiString.of(":authority"), AsciiString.of("myauthority"));
http2Headers.add(AsciiString.of(":method"), AsciiString.of("mymethod"));
http2Headers.add(AsciiString.of(":scheme"), AsciiString.of("myscheme"));
assertThat(http2Headers.get(AsciiString.of(":path"))).isEqualTo(AsciiString.of("mypath"));
assertThat(http2Headers.get(AsciiString.of(":authority")))
.isEqualTo(AsciiString.of("myauthority"));
assertThat(http2Headers.get(AsciiString.of(":method"))).isEqualTo(AsciiString.of("mymethod"));
assertThat(http2Headers.get(AsciiString.of(":scheme"))).isEqualTo(AsciiString.of("myscheme"));
assertThat(http2Headers.getAll(AsciiString.of(":path")))
.containsExactly(AsciiString.of("mypath"));
assertThat(http2Headers.getAll(AsciiString.of(":authority")))
.containsExactly(AsciiString.of("myauthority"));
assertThat(http2Headers.getAll(AsciiString.of(":method")))
.containsExactly(AsciiString.of("mymethod"));
assertThat(http2Headers.getAll(AsciiString.of(":scheme")))
.containsExactly(AsciiString.of("myscheme"));
assertThat(http2Headers.contains(AsciiString.of(":path"))).isTrue();
assertThat(http2Headers.contains(AsciiString.of(":authority"))).isTrue();
assertThat(http2Headers.contains(AsciiString.of(":method"))).isTrue();
assertThat(http2Headers.contains(AsciiString.of(":scheme"))).isTrue();
assertThat(http2Headers.remove(AsciiString.of(":path"))).isTrue();
assertThat(http2Headers.remove(AsciiString.of(":authority"))).isTrue();
assertThat(http2Headers.remove(AsciiString.of(":method"))).isTrue();
assertThat(http2Headers.remove(AsciiString.of(":scheme"))).isTrue();
assertThat(http2Headers.contains(AsciiString.of(":path"))).isFalse();
assertThat(http2Headers.contains(AsciiString.of(":authority"))).isFalse();
assertThat(http2Headers.contains(AsciiString.of(":method"))).isFalse();
assertThat(http2Headers.contains(AsciiString.of(":scheme"))).isFalse();
}
@Test
public void grpcHttp2RequestHeaders_pseudoHeaders_set() {
Http2Headers http2Headers = new GrpcHttp2RequestHeaders(2);
http2Headers.set(AsciiString.of(":path"), AsciiString.of("mypath"));
http2Headers.set(AsciiString.of(":authority"), AsciiString.of("myauthority"));
http2Headers.set(AsciiString.of(":method"), AsciiString.of("mymethod"));
http2Headers.set(AsciiString.of(":scheme"), AsciiString.of("myscheme"));
assertThat(http2Headers.getAll(AsciiString.of(":path")))
.containsExactly(AsciiString.of("mypath"));
assertThat(http2Headers.getAll(AsciiString.of(":authority")))
.containsExactly(AsciiString.of("myauthority"));
assertThat(http2Headers.getAll(AsciiString.of(":method")))
.containsExactly(AsciiString.of("mymethod"));
assertThat(http2Headers.getAll(AsciiString.of(":scheme")))
.containsExactly(AsciiString.of("myscheme"));
http2Headers.set(AsciiString.of(":path"), AsciiString.of("mypath2"));
http2Headers.set(AsciiString.of(":authority"), AsciiString.of("myauthority2"));
http2Headers.set(AsciiString.of(":method"), AsciiString.of("mymethod2"));
http2Headers.set(AsciiString.of(":scheme"), AsciiString.of("myscheme2"));
assertThat(http2Headers.getAll(AsciiString.of(":path")))
.containsExactly(AsciiString.of("mypath2"));
assertThat(http2Headers.getAll(AsciiString.of(":authority")))
.containsExactly(AsciiString.of("myauthority2"));
assertThat(http2Headers.getAll(AsciiString.of(":method")))
.containsExactly(AsciiString.of("mymethod2"));
assertThat(http2Headers.getAll(AsciiString.of(":scheme")))
.containsExactly(AsciiString.of("myscheme2"));
}
@Test
public void grpcHttp2RequestHeaders_pseudoHeaders_addWhenPresent_throws() {
Http2Headers http2Headers = new GrpcHttp2RequestHeaders(2);
http2Headers.add(AsciiString.of(":path"), AsciiString.of("mypath"));
try {
http2Headers.add(AsciiString.of(":path"), AsciiString.of("mypath2"));
fail("Expected exception");
} catch (Exception ex) {
// expected
}
}
@Test
public void grpcHttp2RequestHeaders_pseudoHeaders_addInvalid_throws() {
Http2Headers http2Headers = new GrpcHttp2RequestHeaders(2);
try {
http2Headers.add(AsciiString.of(":status"), AsciiString.of("mystatus"));
fail("Expected exception");
} catch (Exception ex) {
// expected
}
}
@Test @Test
public void dupBinHeadersWithComma() { public void dupBinHeadersWithComma() {
Key<byte[]> key = Key.of("bytes-bin", BINARY_BYTE_MARSHALLER); Key<byte[]> key = Key.of("bytes-bin", BINARY_BYTE_MARSHALLER);