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:
104
spring-cloud-gateway-integration-tests/grpc/pom.xml
Normal file
104
spring-cloud-gateway-integration-tests/grpc/pom.xml
Normal 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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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-----
|
||||
Binary file not shown.
@@ -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-----
|
||||
@@ -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) {
|
||||
}
|
||||
} };
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -21,6 +21,7 @@
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
<module>grpc</module>
|
||||
<module>http2</module>
|
||||
<module>mvc-failure-analyzer</module>
|
||||
</modules>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user