Adding initial gRPC support (#2388)

Passing TE trailers header through

Adding grpc-status as response trailer to ensure the right end of stream

Fixes gh-40
This commit is contained in:
Alberto C. Ríos
2021-10-13 17:30:12 +02:00
committed by GitHub
parent da10105147
commit af09c72a9a
16 changed files with 630 additions and 1 deletions

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>grpc</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Gateway gRPC Integration Test</name>
<description>Spring Cloud Gateway gRPC Integration Test</description>
<properties>
</properties>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
<version>3.1.0-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.41.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.41.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.41.0</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.17.3:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.41.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.cloud.gateway.tests.grpc;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import io.grpc.Grpc;
import io.grpc.Server;
import io.grpc.ServerCredentials;
import io.grpc.TlsServerCredentials;
import io.grpc.stub.StreamObserver;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.SocketUtils;
/**
* @author Alberto C. Ríos
*/
@SpringBootConfiguration
@EnableAutoConfiguration
public class GRPCApplication {
private static final int GRPC_SERVER_PORT = SocketUtils.findAvailableTcpPort();
public static void main(String[] args) {
SpringApplication.run(GRPCApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("grpc", r -> r.predicate(p -> true).uri("https://localhost:" + GRPC_SERVER_PORT))
.build();
}
@Component
static class GRPCServer implements ApplicationRunner {
private Server server;
@Override
public void run(ApplicationArguments args) throws Exception {
final GRPCServer server = new GRPCServer();
server.start();
}
private void start() throws Exception {
/* The port on which the server should run */
ServerCredentials creds = createServerCredentials();
server = Grpc.newServerBuilderForPort(GRPC_SERVER_PORT, creds).addService(new HelloService()).build()
.start();
System.out.println("Starting server in port " + GRPC_SERVER_PORT);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
GRPCServer.this.stop();
}
catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}));
}
private ServerCredentials createServerCredentials() throws IOException {
File privateKey = new ClassPathResource("private.key").getFile();
File certChain = new ClassPathResource("certificate.pem").getFile();
return TlsServerCredentials.create(certChain, privateKey);
}
private void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}
static class HelloService extends HelloServiceGrpc.HelloServiceImplBase {
@Override
public void hello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
String greeting = "Hello, " + request.getFirstName() + " " + request.getLastName();
System.out.println(greeting);
HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
}
}

View File

@@ -0,0 +1,16 @@
syntax = "proto3";
option java_multiple_files = true;
option java_package = "org.springframework.cloud.gateway.tests.grpc";
message HelloRequest {
string firstName = 1;
string lastName = 2;
}
message HelloResponse {
string greeting = 1;
}
service HelloService {
rpc hello(HelloRequest) returns (HelloResponse);
}

View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEIon96DANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV
bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD
VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du
MB4XDTIxMDkyNzE2NDQwNVoXDTMxMDkyNTE2NDQwNVowbDEQMA4GA1UEBhMHVW5r
bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE
ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIwevGHY30YdoUdCSB/5q7/F
c0KHetdjb71G2u6vFdeNvSwMpCV8Z1JznOJ/1zuY+0Z105QCPM7fi1ACi+tqxDR+
L7yjUHhUEMTiGgCHcYJIZZCYfWS3BQXVxgORXhDv7RduCUaCLnkaFY++iPMTUy0C
VxIkplIEhAmqcikIgWaa5ZjBkegKgahlPQLKlfD4Rz/kq2P+LLYFsHNNdKfWv6XQ
u4LMw7ZEAJtfdpaMTzmtQipbTt6Dh87vIa0CIVnCPdlQ3o/5WeaxEA1pnfOLas07
1VdHih2nC5vHhQcTPQDfa+uwzQvzHrchjuvMUUZaCYJzuT0G6nbGBba54EBT7yUC
AwEAAaMhMB8wHQYDVR0OBBYEFKOHmfytP5ab2C4iFHlSklu6tCcuMA0GCSqGSIb3
DQEBCwUAA4IBAQAdgWwdOtRbI796Z22weTBc0/tM8kLc6G0raNb08WyZMPZVki04
jPh73pPQCgYeI/pq5JqH46KgvehmygTzpWDAFIllW0kgABVw3Nu6duV+blt1JG8T
lWP7t5A+qDXgPDm3I5diii7O1YlLB3I37XiBdEV/+2WmF1VGQ7uBWAv+uotQeuW2
JvHOr4ICOiW45TzRYtAbzWukSYKg/A7lwBs7HE9KVomUxNrkD+7+ugRuy/31pyen
pHsEJQpx5juFRE222B6GXmX0w9xLIOapytl4EoPUx3K8Ecc+yI2q00UUC43x0v28
c05YqwRZ5vp+jUnRxkxaz85YdArfR7QFYWtO
-----END CERTIFICATE-----

View File

@@ -0,0 +1,32 @@
Bag Attributes
friendlyName: mykey
localKeyID: 54 69 6D 65 20 31 36 33 32 38 32 34 38 37 31 33 33 35
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCMHrxh2N9GHaFH
Qkgf+au/xXNCh3rXY2+9RtrurxXXjb0sDKQlfGdSc5zif9c7mPtGddOUAjzO34tQ
AovrasQ0fi+8o1B4VBDE4hoAh3GCSGWQmH1ktwUF1cYDkV4Q7+0XbglGgi55GhWP
vojzE1MtAlcSJKZSBIQJqnIpCIFmmuWYwZHoCoGoZT0CypXw+Ec/5Ktj/iy2BbBz
TXSn1r+l0LuCzMO2RACbX3aWjE85rUIqW07eg4fO7yGtAiFZwj3ZUN6P+VnmsRAN
aZ3zi2rNO9VXR4odpwubx4UHEz0A32vrsM0L8x63IY7rzFFGWgmCc7k9Bup2xgW2
ueBAU+8lAgMBAAECggEAGu2xQJDAYCZDn4FCgTqnYkSdIRUOa6SFjfe3DZYCeZmY
2IVZaobdCICFjxYIlECTUfhFADXp38wgZvEGWOj86iWyIOu2BFoLmvrlCmL9Uo99
TWuw9ZEi2vs5gegHDvQ9OXqBN9a+/bEgoa55fVWib4z6lNcMS8joYz8pj28+ByzE
LW0/3T3p6beM2fUcCJWn3d2M3wUgSuXmcdjVXJSkhEwKTVcc4vTTcOeF6xb2VZ/g
Pozv/39G1qZ+QtM58yBiqnJ1Z2gtAk71l/1ztQa3uY22gzw8Kj5dmqcHgdiN5DWI
bNE+k5Q93FzUmZNYPzmY6YVkdEzaNMtmi96sdtEu5QKBgQD36YdGXUyoRXeNDRuc
yMXr6j/9/ewii+byHhFoUvjLuXWIQ06V+nOtYqohg/zgGIiC5LQ8EB0uFZDMhbDf
kSwtoXbpUDLYD2OPgIyLaPqHiYQ9BampbUz5vHlfYLr0vB+Xp5r22Eb6nDQRRtb5
EXHoYgAokgpYdeTIcdKRcB/2DwKBgQCQsPePeu7PagM9vYEo4zfbFxfY3Qkr4lOQ
BCZ/tgsS0b0jAAxpfUH0/3O5oXizmB/5K7vgKqGnTuBWJJ/hdCWq27FkKxJ+8ejU
8V9TFd5VQ89VeB5OekZwPks8vftwzW4L82ZRW5hvyQB0jPR+lwqjrNqds+xxH7i+
c+RFx15biwKBgQCgkGGK0zao7YUGl+zAWNDHgQo9KM5deZr0SUEg/kwhNlbHEEC/
plxxeauS1XdcdMdFb3bER/N+O31y2Uu7IL0qOJ9ZcRXdFep3sNxWFoHcctZw51AB
accnIEjD21R62bTkditJoL4n5i9a2TS2T/QkfAR6QkvtCz5IDGBCzgoFRQKBgC51
VBfy3gklPgMt/PHW+1FSuep9FnvLwQ8F9iKdnjKdu8AoPNQGTw5Ok6bwDOSFnQaR
n1Kb/anN7sRaICfw9kNFJVFHbznpjNwK4JO5+tif3EvSNND3+/QAXIIVck3G+GXH
8nt/EJQcExRZSgv3jYf+cXeflPTBvb0RUyOAn3B/AoGAM9aUi2dg3XjlDkZahQlY
P5QNIr9BY25Ordga3GLwfR6rE+jiWTeTmreXBSJ7nvcaEQUNyFMX9n3v/QI84etk
lMJkZ4o2TgzRnCsFJoBV36Ihsa6B5uXVWAvMRLKvwKpptKXfO0TnhUg7oKtN4t3a
/FzAOS2Eu1PFP27z74gAMKY=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.cloud.gateway.tests.grpc;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import io.grpc.ManagedChannel;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.annotation.DirtiesContext;
import static io.grpc.netty.shaded.io.grpc.netty.NegotiationType.TLS;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
/**
* @author Alberto C. Ríos
*/
@ExtendWith(OutputCaptureExtension.class)
@SpringBootTest(classes = GRPCApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class GRPCApplicationTests {
@LocalServerPort
private int port;
@Test
public void gRPCUnaryCalShouldReturnResponse() throws SSLException {
ManagedChannel channel = createSecuredChannel(port);
final HelloResponse response = HelloServiceGrpc.newBlockingStub(channel)
.hello(HelloRequest.newBuilder().setFirstName("Sir").setLastName("FromClient").build());
Assertions.assertThat(response.getGreeting()).isEqualTo("Hello, Sir FromClient");
}
private ManagedChannel createSecuredChannel(int port) throws SSLException {
TrustManager[] trustAllCerts = createTrustAllTrustManager();
return NettyChannelBuilder.forAddress("localhost", port).useTransportSecurity()
.sslContext(GrpcSslContexts.forClient().trustManager(trustAllCerts[0]).build()).negotiationType(TLS)
.build();
}
private TrustManager[] createTrustAllTrustManager() {
return new TrustManager[] { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
} };
}
}

View File

@@ -0,0 +1,27 @@
server:
http2:
enabled: true
ssl:
key-store-type: PKCS12
key-store: classpath:keystore.p12
key-store-password: password
key-password: password
enabled: true
spring:
cloud:
gateway:
httpserver:
wiretap: true
httpclient:
wiretap: true
ssl:
key-store-type: PKCS12
key-store: classpath:keystore.p12
key-store-password: password
key-password: password
use-insecure-trust-manager: true
logging:
level:
reactor.netty: DEBUG
org.springframework.cloud.gateway.filter: TRACE

View File

@@ -21,6 +21,7 @@
</parent>
<modules>
<module>grpc</module>
<module>http2</module>
<module>mvc-failure-analyzer</module>
</modules>

View File

@@ -109,6 +109,8 @@ import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyEncod
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.GRPCRequestHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.GRPCResponseHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter;
@@ -176,6 +178,7 @@ import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool
* @author Spencer Gibb
* @author Ziemowit Stolarczyk
* @author Mete Alpaslan Katırcıoğlu
* @author Alberto C. Ríos
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@@ -290,6 +293,18 @@ public class GatewayAutoConfiguration {
return new XForwardedHeadersFilter();
}
@Bean
@ConditionalOnProperty(name = "server.http2.enabled", matchIfMissing = true)
public GRPCRequestHeadersFilter gRPCRequestHeadersFilter() {
return new GRPCRequestHeadersFilter();
}
@Bean
@ConditionalOnProperty(name = "server.http2.enabled", matchIfMissing = true)
public GRPCResponseHeadersFilter gRPCResponseHeadersFilter() {
return new GRPCResponseHeadersFilter();
}
// GlobalFilter beans
@Bean

View File

@@ -60,7 +60,8 @@ public class GatewayProperties {
private List<FilterDefinition> defaultFilters = new ArrayList<>();
private List<MediaType> streamingMediaTypes = Arrays.asList(MediaType.TEXT_EVENT_STREAM,
MediaType.APPLICATION_STREAM_JSON);
MediaType.APPLICATION_STREAM_JSON, new MediaType("application", "grpc"),
new MediaType("application", "grpc+protobuf"), new MediaType("application", "grpc+json"));
/**
* Option to fail on route definition errors, defaults to true. Otherwise, a warning

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* 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
*
* https://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 org.springframework.cloud.gateway.filter.headers;
import java.util.List;
import java.util.Map;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Alberto C. Ríos
*/
public class GRPCRequestHeadersFilter implements HttpHeadersFilter, Ordered {
@Override
public HttpHeaders filter(HttpHeaders headers, ServerWebExchange exchange) {
HttpHeaders updated = new HttpHeaders();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
updated.addAll(entry.getKey(), entry.getValue());
}
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2
if (isGRPC(headers.getFirst(HttpHeaders.CONTENT_TYPE))) {
updated.add("te", "trailers");
}
return updated;
}
private boolean isGRPC(String contentTypeValue) {
return StringUtils.startsWithIgnoreCase(contentTypeValue, "application/grpc");
}
@Override
public boolean supports(Type type) {
return Type.REQUEST.equals(type);
}
@Override
public int getOrder() {
return 0;
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* 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
*
* https://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 org.springframework.cloud.gateway.filter.headers;
import reactor.netty.http.server.HttpServerResponse;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.AbstractServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Alberto C. Ríos
*/
public class GRPCResponseHeadersFilter implements HttpHeadersFilter, Ordered {
@Override
public HttpHeaders filter(HttpHeaders headers, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
HttpHeaders responseHeaders = response.getHeaders();
if (isGRPC(exchange)) {
String trailerHeaderValue = "grpc-status";
String originalTrailerHeaderValue = responseHeaders.getFirst(HttpHeaders.TRAILER);
if (originalTrailerHeaderValue != null) {
trailerHeaderValue += "," + originalTrailerHeaderValue;
}
responseHeaders.set(HttpHeaders.TRAILER, trailerHeaderValue);
((HttpServerResponse) ((AbstractServerHttpResponse) response).getNativeResponse())
.trailerHeaders(h -> h.set("grpc-status", "0"));
}
return headers;
}
private boolean isGRPC(ServerWebExchange exchange) {
String contentTypeValue = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
return StringUtils.startsWithIgnoreCase(contentTypeValue, "application/grpc");
}
@Override
public boolean supports(Type type) {
return Type.RESPONSE.equals(type);
}
@Override
public int getOrder() {
return 0;
}
}

View File

@@ -40,6 +40,8 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex
import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint;
import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint;
import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.headers.GRPCRequestHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.GRPCResponseHeadersFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
@@ -231,6 +233,30 @@ public class GatewayAutoConfigurationTests {
assertThat(spec2.protocols()).isNull();
}
@Test
public void gRPCFiltersConfiguredWhenHTTP2Enabled() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class,
HttpClientCustomizedConfig.class, ServerPropertiesConfig.class))
.withPropertyValues("server.http2.enabled=true").run(context -> {
assertThat(context).hasSingleBean(GRPCRequestHeadersFilter.class);
assertThat(context).hasSingleBean(GRPCResponseHeadersFilter.class);
});
}
@Test
public void gRPCFiltersNotConfiguredWhenHTTP2Disabled() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class,
HttpClientCustomizedConfig.class, ServerPropertiesConfig.class))
.withPropertyValues("server.http2.enabled=false").run(context -> {
assertThat(context).doesNotHaveBean(GRPCRequestHeadersFilter.class);
assertThat(context).doesNotHaveBean(GRPCResponseHeadersFilter.class);
});
}
@Configuration
@EnableConfigurationProperties(ServerProperties.class)
protected static class ServerPropertiesConfig {

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* 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
*
* https://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 org.springframework.cloud.gateway.filter.headers;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Alberto C. Ríos
*/
public class GRPCRequestHeadersFilterTest {
@Test
public void shouldIncludeTrailersHeaderIfGRPC() {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.header(HttpHeaders.CONTENT_TYPE, "application/grpc").build();
GRPCRequestHeadersFilter filter = new GRPCRequestHeadersFilter();
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys("te");
assertThat(headers.getFirst("te")).isEqualTo("trailers");
}
@Test
public void shouldNotIncludeTrailersHeaderIfNotGRPC() {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.header(HttpHeaders.CONTENT_TYPE, "application/json").build();
GRPCRequestHeadersFilter filter = new GRPCRequestHeadersFilter();
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).doesNotContainKeys("te");
}
}