diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc
index 0b6d1247..5a7e7ca3 100644
--- a/docs/src/main/asciidoc/_configprops.adoc
+++ b/docs/src/main/asciidoc/_configprops.adoc
@@ -14,6 +14,7 @@
|spring.cloud.gateway.filter.add-request-header.enabled | `+++true+++` | Enables the add-request-header filter.
|spring.cloud.gateway.filter.add-request-parameter.enabled | `+++true+++` | Enables the add-request-parameter filter.
|spring.cloud.gateway.filter.add-response-header.enabled | `+++true+++` | Enables the add-response-header filter.
+|spring.cloud.gateway.filter.json-to-grpc.enabled | `+++true+++` | Enables the JSON to gRPC filter.
|spring.cloud.gateway.filter.circuit-breaker.enabled | `+++true+++` | Enables the circuit-breaker filter.
|spring.cloud.gateway.filter.dedupe-response-header.enabled | `+++true+++` | Enables the dedupe-response-header filter.
|spring.cloud.gateway.filter.fallback-headers.enabled | `+++true+++` | Enables the fallback-headers filter.
@@ -25,7 +26,7 @@
|spring.cloud.gateway.filter.preserve-host-header.enabled | `+++true+++` | Enables the preserve-host-header filter.
|spring.cloud.gateway.filter.redirect-to.enabled | `+++true+++` | Enables the redirect-to filter.
|spring.cloud.gateway.filter.remove-hop-by-hop.headers | |
-|spring.cloud.gateway.filter.remove-hop-by-hop.order | `+++0+++` |
+|spring.cloud.gateway.filter.remove-hop-by-hop.order | `+++0+++` |
|spring.cloud.gateway.filter.remove-request-header.enabled | `+++true+++` | Enables the remove-request-header filter.
|spring.cloud.gateway.filter.remove-request-parameter.enabled | `+++true+++` | Enables the remove-request-parameter filter.
|spring.cloud.gateway.filter.remove-response-header.enabled | `+++true+++` | Enables the remove-response-header filter.
@@ -41,16 +42,16 @@
|spring.cloud.gateway.filter.rewrite-path.enabled | `+++true+++` | Enables the rewrite-path filter.
|spring.cloud.gateway.filter.rewrite-response-header.enabled | `+++true+++` | Enables the rewrite-response-header filter.
|spring.cloud.gateway.filter.save-session.enabled | `+++true+++` | Enables the save-session filter.
-|spring.cloud.gateway.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
-|spring.cloud.gateway.filter.secure-headers.content-type-options | `+++nosniff+++` |
+|spring.cloud.gateway.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
+|spring.cloud.gateway.filter.secure-headers.content-type-options | `+++nosniff+++` |
|spring.cloud.gateway.filter.secure-headers.disable | |
-|spring.cloud.gateway.filter.secure-headers.download-options | `+++noopen+++` |
+|spring.cloud.gateway.filter.secure-headers.download-options | `+++noopen+++` |
|spring.cloud.gateway.filter.secure-headers.enabled | `+++true+++` | Enables the secure-headers filter.
-|spring.cloud.gateway.filter.secure-headers.frame-options | `+++DENY+++` |
-|spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
-|spring.cloud.gateway.filter.secure-headers.referrer-policy | `+++no-referrer+++` |
-|spring.cloud.gateway.filter.secure-headers.strict-transport-security | `+++max-age=631138519+++` |
-|spring.cloud.gateway.filter.secure-headers.xss-protection-header | `+++1 ; mode=block+++` |
+|spring.cloud.gateway.filter.secure-headers.frame-options | `+++DENY+++` |
+|spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
+|spring.cloud.gateway.filter.secure-headers.referrer-policy | `+++no-referrer+++` |
+|spring.cloud.gateway.filter.secure-headers.strict-transport-security | `+++max-age=631138519+++` |
+|spring.cloud.gateway.filter.secure-headers.xss-protection-header | `+++1 ; mode=block+++` |
|spring.cloud.gateway.filter.set-path.enabled | `+++true+++` | Enables the set-path filter.
|spring.cloud.gateway.filter.set-request-header.enabled | `+++true+++` | Enables the set-request-header filter.
|spring.cloud.gateway.filter.set-request-host-header.enabled | `+++true+++` | Enables the set-request-host-header filter.
@@ -105,7 +106,7 @@
|spring.cloud.gateway.httpclient.websocket.proxy-ping | `+++true+++` | Proxy ping frames to downstream services, defaults to true.
|spring.cloud.gateway.httpclient.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpClient.
|spring.cloud.gateway.httpserver.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpServer.
-|spring.cloud.gateway.loadbalancer.use404 | `+++false+++` |
+|spring.cloud.gateway.loadbalancer.use404 | `+++false+++` |
|spring.cloud.gateway.metrics.enabled | `+++false+++` | Enables the collection of metrics data.
|spring.cloud.gateway.metrics.prefix | `+++spring.cloud.gateway+++` | The prefix of all metrics emitted by gateway.
|spring.cloud.gateway.metrics.tags | | Tags map that added to metrics.
diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc
index 785de171..0285d51a 100644
--- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc
@@ -1830,6 +1830,86 @@ spring:
NOTE: This filter only works with http request (including https).
+=== The `JsonToGrpc` `GatewayFilter` Factory
+
+The JSONToGRPCFilter GatewayFilter Factory converts a JSON payload to a gRPC request.
+
+The filter takes the following arguments:
+
+* `protoDescriptor` Proto descriptor file.
+
+This file can be generated using `protoc` specifying the `--descriptor_set_out` flag:
+
+[source,bash]
+----
+protoc --proto_path=src/main/resources/proto/ \
+--descriptor_set_out=src/main/resources/proto/hello.pb \
+src/main/resources/proto/hello.proto
+----
+
+* `protoFile` Proto definition file.
+
+* `service` Fully qualified name of the service that will handle the request.
+
+* `method` Method name in the service that will handle the request.
+
+NOTE: `streaming` is not supported.
+
+
+*application.yml.*
+
+[source,java]
+----
+@Bean
+public RouteLocator routes(RouteLocatorBuilder builder) {
+ return builder.routes()
+ .route("json-grpc", r -> r.path("/json/hello").filters(f -> {
+ String protoDescriptor = "file:src/main/proto/hello.pb";
+ String protoFile = "file:src/main/proto/hello.proto";
+ String service = "HelloService";
+ String method = "hello";
+ return f.jsonToGRPC(protoDescriptor, protoFile, service, method);
+ }).uri(uri))
+----
+
+[source,yaml]
+----
+spring:
+ cloud:
+ gateway:
+ routes:
+ - id: json-grpc
+ uri: https://localhost:6565/testhello
+ predicates:
+ - Path=/json/**
+ filters:
+ - name: JsonToGrpc
+ args:
+ protoDescriptor: file:proto/hello.pb
+ protoFile: file:proto/hello.proto
+ service: com.example.grpcserver.hello.HelloService
+ method: hello
+
+----
+
+When a request is made through the gateway to `/json/hello` the request will be transformed using the definition provided in `hello.proto`, sent to `com.example.grpcserver.hello.HelloService/hello`, and transform the response back to JSON.
+
+By default, it will create a `NettyChannel` using the default `TrustManagerFactory`. However, this `TrustManager` can be customized by creating a bean of type `GRPCSSLContext`:
+
+[source,java]
+----
+
+@Configuration
+public class GRPCLocalConfiguration {
+ @Bean
+ public GRPCSSLContext sslContext() {
+ TrustManager trustManager = trustAllCerts();
+ return new GRPCSSLContext(trustManager);
+ }
+}
+----
+
+
=== Default Filters
To add a filter and apply it to all routes, you can use `spring.cloud.gateway.default-filters`.
diff --git a/spring-cloud-gateway-integration-tests/grpc/pom.xml b/spring-cloud-gateway-integration-tests/grpc/pom.xml
index 68f9e88c..3388c29e 100644
--- a/spring-cloud-gateway-integration-tests/grpc/pom.xml
+++ b/spring-cloud-gateway-integration-tests/grpc/pom.xml
@@ -11,6 +11,7 @@
Spring Cloud Gateway gRPC Integration Test
+ 1.47.0
@@ -29,26 +30,38 @@
org.springframework.cloud
spring-cloud-starter-gateway
-
io.grpc
grpc-netty-shaded
- 1.44.0
+ ${grpc.version}
io.grpc
grpc-protobuf
- 1.44.0
+ ${grpc.version}
io.grpc
grpc-stub
- 1.44.0
+ ${grpc.version}
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-protobuf
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
io.netty
netty-tcnative-boringssl-static
+
+ org.apache.httpcomponents
+ httpclient
+ test
+
org.apache.tomcat
annotations-api
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplication.java b/spring-cloud-gateway-integration-tests/grpc/src/main/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplication.java
index 95b1800f..62215ddc 100644
--- a/spring-cloud-gateway-integration-tests/grpc/src/main/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplication.java
+++ b/spring-cloud-gateway-integration-tests/grpc/src/main/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplication.java
@@ -18,8 +18,13 @@ package org.springframework.cloud.gateway.tests.grpc;
import java.io.File;
import java.io.IOException;
+import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
import io.grpc.Grpc;
import io.grpc.Server;
import io.grpc.ServerCredentials;
@@ -31,6 +36,7 @@ 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.config.GRPCSSLContext;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.test.TestSocketUtils;
@@ -53,8 +59,31 @@ public class GRPCApplication {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
- return builder.routes().route("grpc", r -> r.predicate(p -> true).uri("https://localhost:" + GRPC_SERVER_PORT))
- .build();
+ return builder.routes().route("json-grpc", r -> r.path("/json/hello").filters(f -> {
+ String protoDescriptor = "file:src/main/proto/hello.pb";
+ String protoFile = "file:src/main/proto/hello.proto";
+ String service = "HelloService";
+ String method = "hello";
+ return f.jsonToGRPC(protoDescriptor, protoFile, service, method);
+ }).uri("https://localhost:" + GRPC_SERVER_PORT))
+ .route("grpc", r -> r.predicate(p -> true).uri("https://localhost:" + GRPC_SERVER_PORT)).build();
+ }
+
+ @Bean
+ public GRPCSSLContext sslContext() throws SSLException {
+ TrustManager[] trustAllCerts = 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) {
+ }
+ } };
+
+ return new GRPCSSLContext(trustAllCerts[0]);
}
@Component
@@ -74,7 +103,7 @@ public class GRPCApplication {
server = Grpc.newServerBuilderForPort(GRPC_SERVER_PORT, creds).addService(new HelloService()).build()
.start();
- System.out.println("Starting server in port " + GRPC_SERVER_PORT);
+ System.out.println("Starting gRPC server in port " + GRPC_SERVER_PORT);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
@@ -104,7 +133,7 @@ public class GRPCApplication {
public void hello(HelloRequest request, StreamObserver responseObserver) {
String greeting = "Hello, " + request.getFirstName() + " " + request.getLastName();
- System.out.println(greeting);
+ System.out.println("Sending response: " + greeting);
HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting).build();
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.pb b/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.pb
new file mode 100644
index 00000000..584ded1a
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.pb
@@ -0,0 +1,11 @@
+
+ö
+hello.proto"H
+HelloRequest
+ firstName ( R firstName
+lastName ( RlastName"+
+
HelloResponse
+greeting ( Rgreeting26
+HelloService&
+hello
.HelloRequest.HelloResponseB0
+,org.springframework.cloud.gateway.tests.grpcPbproto3
\ No newline at end of file
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.proto b/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.proto
index 0a7769a7..24a34749 100644
--- a/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.proto
+++ b/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.proto
@@ -3,12 +3,12 @@ option java_multiple_files = true;
option java_package = "org.springframework.cloud.gateway.tests.grpc";
message HelloRequest {
- string firstName = 1;
- string lastName = 2;
+ optional string firstName = 1;
+ optional string lastName = 2;
}
message HelloResponse {
- string greeting = 1;
+ optional string greeting = 1;
}
service HelloService {
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplicationTests.java b/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplicationTests.java
index 9f9f6edc..a474c81b 100644
--- a/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplicationTests.java
+++ b/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplicationTests.java
@@ -27,10 +27,8 @@ 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.test.web.server.LocalServerPort;
import org.springframework.test.annotation.DirtiesContext;
@@ -40,7 +38,6 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
/**
* @author Alberto C. RÃos
*/
-@ExtendWith(OutputCaptureExtension.class)
@SpringBootTest(classes = GRPCApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class GRPCApplicationTests {
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/JsonToGrpcApplicationTests.java b/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/JsonToGrpcApplicationTests.java
new file mode 100644
index 00000000..f7ee4620
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/JsonToGrpcApplicationTests.java
@@ -0,0 +1,97 @@
+/*
+ * 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.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustStrategy;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
+import org.apache.http.ssl.SSLContexts;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+
+/**
+ * @author Alberto C. RÃos
+ */
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+public class JsonToGrpcApplicationTests {
+
+ @LocalServerPort
+ private int port;
+
+ private RestTemplate restTemplate;
+
+ @BeforeEach
+ void setUp() {
+ restTemplate = createUnsecureClient();
+ }
+
+ @Test
+ public void shouldConvertFromJSONToGRPC() {
+ String response = restTemplate.postForEntity("https://localhost:" + port + "/json/hello",
+ "{\"firstName\":\"Duff\", \"lastName\":\"McKagan\"}", String.class).getBody();
+
+ Assertions.assertThat(response).isNotNull();
+ Assertions.assertThat(response).contains("{\"greeting\":\"Hello, Duff McKagan\"}");
+ }
+
+ private RestTemplate createUnsecureClient() {
+ TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
+ SSLContext sslContext;
+ try {
+ sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
+ }
+ catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
+ throw new RuntimeException(e);
+ }
+ SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext,
+ NoopHostnameVerifier.INSTANCE);
+
+ Registry socketFactoryRegistry = RegistryBuilder.create()
+ .register("https", sslSocketFactory).register("http", new PlainConnectionSocketFactory()).build();
+
+ BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(
+ socketFactoryRegistry);
+ CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslSocketFactory)
+ .setConnectionManager(connectionManager).build();
+
+ HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
+
+ return new RestTemplate(requestFactory);
+ }
+
+}
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/test/resources/application.yml b/spring-cloud-gateway-integration-tests/grpc/src/test/resources/application.yml
index ba0449a3..7cc4ef2e 100644
--- a/spring-cloud-gateway-integration-tests/grpc/src/test/resources/application.yml
+++ b/spring-cloud-gateway-integration-tests/grpc/src/test/resources/application.yml
@@ -24,4 +24,4 @@ spring:
logging:
level:
reactor.netty: DEBUG
- org.springframework.cloud.gateway.filter: TRACE
+ org.springframework.cloud.gateway.filter: DEBUG
diff --git a/spring-cloud-gateway-server/pom.xml b/spring-cloud-gateway-server/pom.xml
index b6e5bfee..b0c74c9a 100644
--- a/spring-cloud-gateway-server/pom.xml
+++ b/spring-cloud-gateway-server/pom.xml
@@ -16,6 +16,7 @@
Spring Cloud Gateway Server
${basedir}/..
+ 1.47.0
@@ -81,6 +82,34 @@
io.projectreactor.addons
reactor-extra
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-protobuf
+ true
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ true
+
+
+ io.grpc
+ grpc-netty-shaded
+ true
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-protobuf
+ true
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-stub
+ true
+ ${grpc.version}
+
org.springframework.boot
spring-boot-autoconfigure-processor
diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GRPCSSLContext.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GRPCSSLContext.java
new file mode 100644
index 00000000..bbdb34a6
--- /dev/null
+++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GRPCSSLContext.java
@@ -0,0 +1,36 @@
+/*
+ * 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.config;
+
+import javax.net.ssl.TrustManager;
+
+/**
+ * @author Alberto C. RÃos
+ */
+public class GRPCSSLContext {
+
+ private final TrustManager trustManager;
+
+ public GRPCSSLContext(TrustManager trustManager) {
+ this.trustManager = trustManager;
+ }
+
+ public TrustManager getTrustManager() {
+ return trustManager;
+ }
+
+}
diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
index aebf9efb..1dea2a08 100644
--- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
+++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
@@ -16,10 +16,16 @@
package org.springframework.cloud.gateway.config;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
+import javax.net.ssl.TrustManagerFactory;
+
+import io.grpc.Channel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
@@ -68,6 +74,7 @@ import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGateway
import org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory;
@@ -147,6 +154,7 @@ import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.env.Environment;
+import org.springframework.core.io.ResourceLoader;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
@@ -292,6 +300,27 @@ public class GatewayAutoConfiguration {
return new GRPCResponseHeadersFilter();
}
+ @Bean
+ @ConditionalOnEnabledFilter
+ @ConditionalOnProperty(name = "server.http2.enabled", matchIfMissing = true)
+ @ConditionalOnClass(Channel.class)
+ public JsonToGrpcGatewayFilterFactory jsonToGRPCFilterFactory(GRPCSSLContext gRPCSSLContext,
+ ResourceLoader resourceLoader) {
+ return new JsonToGrpcGatewayFilterFactory(gRPCSSLContext, resourceLoader);
+ }
+
+ @Bean
+ @ConditionalOnEnabledFilter(JsonToGrpcGatewayFilterFactory.class)
+ @ConditionalOnMissingBean(GRPCSSLContext.class)
+ @ConditionalOnClass(Channel.class)
+ public GRPCSSLContext gRPCSSLContext() throws KeyStoreException, NoSuchAlgorithmException {
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory
+ .getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(KeyStore.getInstance(KeyStore.getDefaultType()));
+
+ return new GRPCSSLContext(trustManagerFactory.getTrustManagers()[0]);
+ }
+
@Bean
public TransferEncodingNormalizationHeadersFilter transferEncodingNormalizationHeadersFilter() {
return new TransferEncodingNormalizationHeadersFilter();
diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java
new file mode 100644
index 00000000..adec2f18
--- /dev/null
+++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java
@@ -0,0 +1,314 @@
+/*
+ * 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.factory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.Function;
+
+import javax.net.ssl.SSLException;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.dataformat.protobuf.ProtobufFactory;
+import com.fasterxml.jackson.dataformat.protobuf.schema.ProtobufSchema;
+import com.fasterxml.jackson.dataformat.protobuf.schema.ProtobufSchemaLoader;
+import com.google.protobuf.DescriptorProtos;
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.DynamicMessage;
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ManagedChannel;
+import io.grpc.MethodDescriptor;
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.grpc.protobuf.ProtoUtils;
+import io.grpc.stub.ClientCalls;
+import io.netty.buffer.PooledByteBufAllocator;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.cloud.gateway.config.GRPCSSLContext;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
+import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
+import org.springframework.cloud.gateway.route.Route;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.NettyDataBufferFactory;
+import org.springframework.http.codec.json.Jackson2JsonDecoder;
+import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
+import org.springframework.web.server.ServerWebExchange;
+
+import static io.grpc.netty.shaded.io.grpc.netty.NegotiationType.TLS;
+import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
+
+/**
+ * This filter takes a JSON payload, transform it into a protobuf object, send it to a
+ * given gRPC channel, and transform the response back to JSON.
+ *
+ * Making it transparent for the consumer that the service under the gateway is a gRPC
+ * one.
+ *
+ * @author Alberto C. RÃos
+ */
+public class JsonToGrpcGatewayFilterFactory
+ extends AbstractGatewayFilterFactory {
+
+ private final GRPCSSLContext sslContext;
+
+ private final ResourceLoader resourceLoader;
+
+ public JsonToGrpcGatewayFilterFactory(GRPCSSLContext sslContext, ResourceLoader resourceLoader) {
+ super(Config.class);
+ this.sslContext = sslContext;
+ this.resourceLoader = resourceLoader;
+ }
+
+ @Override
+ public List shortcutFieldOrder() {
+ return Arrays.asList("protoDescriptor", "protoFile", "service", "method");
+ }
+
+ @Override
+ public GatewayFilter apply(Config config) {
+ GatewayFilter filter = new GatewayFilter() {
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ GRPCResponseDecorator modifiedResponse = new GRPCResponseDecorator(exchange, config);
+
+ ServerWebExchangeUtils.setAlreadyRouted(exchange);
+ return modifiedResponse.writeWith(exchange.getRequest().getBody())
+ .then(chain.filter(exchange.mutate().response(modifiedResponse).build()));
+ }
+
+ @Override
+ public String toString() {
+ return filterToStringCreator(JsonToGrpcGatewayFilterFactory.this).toString();
+ }
+ };
+
+ int order = NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
+ return new OrderedGatewayFilter(filter, order);
+ }
+
+ public static class Config {
+
+ private String protoDescriptor;
+
+ private String protoFile;
+
+ private String service;
+
+ private String method;
+
+ public String getProtoDescriptor() {
+ return protoDescriptor;
+ }
+
+ public Config setProtoDescriptor(String protoDescriptor) {
+ this.protoDescriptor = protoDescriptor;
+ return this;
+ }
+
+ public String getProtoFile() {
+ return protoFile;
+ }
+
+ public Config setProtoFile(String protoFile) {
+ this.protoFile = protoFile;
+ return this;
+ }
+
+ public String getService() {
+ return service;
+ }
+
+ public Config setService(String service) {
+ this.service = service;
+ return this;
+ }
+
+ public String getMethod() {
+ return method;
+ }
+
+ public Config setMethod(String method) {
+ this.method = method;
+ return this;
+ }
+
+ }
+
+ class GRPCResponseDecorator extends ServerHttpResponseDecorator {
+
+ private final ServerWebExchange exchange;
+
+ private final Descriptors.Descriptor descriptor;
+
+ private final ObjectWriter objectWriter;
+
+ private final ObjectReader objectReader;
+
+ private final ClientCall clientCall;
+
+ private final ObjectNode objectNode;
+
+ GRPCResponseDecorator(ServerWebExchange exchange, Config config) {
+ super(exchange.getResponse());
+ this.exchange = exchange;
+ try {
+ Resource descriptorFile = resourceLoader.getResource(config.getProtoDescriptor());
+ Resource protoFile = resourceLoader.getResource(config.getProtoFile());
+
+ descriptor = DescriptorProtos.FileDescriptorProto.parseFrom(descriptorFile.getInputStream())
+ .getDescriptorForType();
+
+ Descriptors.Descriptor outputType = getOutputTypeDescriptor(config, descriptorFile.getInputStream());
+
+ clientCall = createClientCallForType(config, outputType);
+
+ ProtobufSchema schema = ProtobufSchemaLoader.std.load(protoFile.getInputStream());
+ ProtobufSchema responseType = schema.withRootType(outputType.getName());
+
+ ObjectMapper objectMapper = new ObjectMapper(new ProtobufFactory());
+ objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ objectWriter = objectMapper.writer(schema);
+ objectReader = objectMapper.readerFor(JsonNode.class).with(responseType);
+ objectNode = objectMapper.createObjectNode();
+
+ }
+ catch (IOException | Descriptors.DescriptorValidationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Mono writeWith(Publisher extends DataBuffer> body) {
+ exchange.getResponse().getHeaders().set("Content-Type", "application/json");
+
+ return getDelegate().writeWith(deserializeJSONRequest().map(callGRPCServer()).map(serialiseGRPCResponse())
+ .map(wrapGRPCResponse()).cast(DataBuffer.class).last());
+ }
+
+ private ClientCall createClientCallForType(Config config,
+ Descriptors.Descriptor outputType) {
+ MethodDescriptor.Marshaller marshaller = ProtoUtils
+ .marshaller(DynamicMessage.newBuilder(outputType).build());
+ MethodDescriptor methodDescriptor = MethodDescriptor
+ .newBuilder().setType(MethodDescriptor.MethodType.UNKNOWN)
+ .setFullMethodName(MethodDescriptor.generateFullMethodName(config.getService(), config.getMethod()))
+ .setRequestMarshaller(marshaller).setResponseMarshaller(marshaller).build();
+ Channel channel = createChannel();
+ return channel.newCall(methodDescriptor, CallOptions.DEFAULT);
+ }
+
+ private Descriptors.Descriptor getOutputTypeDescriptor(Config config, InputStream descriptorFile)
+ throws IOException, Descriptors.DescriptorValidationException {
+ DescriptorProtos.FileDescriptorSet fileDescriptorSet = DescriptorProtos.FileDescriptorSet
+ .parseFrom(descriptorFile);
+ DescriptorProtos.FileDescriptorProto fileProto = fileDescriptorSet.getFile(0);
+ Descriptors.FileDescriptor fileDescriptor = Descriptors.FileDescriptor.buildFrom(fileProto,
+ new Descriptors.FileDescriptor[0]);
+
+ List methods = fileDescriptor.findServiceByName(config.getService())
+ .getMethods();
+
+ return methods.stream().filter(method -> method.getName().equals(config.getMethod())).findFirst()
+ .orElseThrow(() -> new NoSuchElementException("No Method found")).getOutputType();
+ }
+
+ private ManagedChannel createChannel() {
+ URI requestURI = ((Route) exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR)).getUri();
+ return createChannelChannel(requestURI.getHost(), requestURI.getPort());
+ }
+
+ private Function callGRPCServer() {
+ return jsonRequest -> {
+ try {
+ byte[] request = objectWriter.writeValueAsBytes(jsonRequest);
+ return ClientCalls.blockingUnaryCall(clientCall, DynamicMessage.parseFrom(descriptor, request));
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ };
+ }
+
+ private Function serialiseGRPCResponse() {
+ return gRPCResponse -> {
+ try {
+ return objectReader.readValue(gRPCResponse.toByteArray());
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ };
+ }
+
+ private Flux deserializeJSONRequest() {
+ return exchange.getRequest().getBody().mapNotNull(dataBufferBody -> {
+ if (dataBufferBody.capacity() == 0) {
+ return objectNode;
+ }
+ ResolvableType targetType = ResolvableType.forType(JsonNode.class);
+ return new Jackson2JsonDecoder().decode(dataBufferBody, targetType, null, null);
+ }).cast(JsonNode.class);
+ }
+
+ private Function