Skip to content

Commit af09c72

Browse files
authored
Adding initial gRPC support (spring-cloud#2388)
Passing TE trailers header through Adding grpc-status as response trailer to ensure the right end of stream Fixes gh-40
1 parent da10105 commit af09c72

File tree

16 files changed

+630
-1
lines changed

16 files changed

+630
-1
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<artifactId>grpc</artifactId>
8+
<packaging>jar</packaging>
9+
10+
<name>Spring Cloud Gateway gRPC Integration Test</name>
11+
<description>Spring Cloud Gateway gRPC Integration Test</description>
12+
13+
<properties>
14+
</properties>
15+
16+
<parent>
17+
<groupId>org.springframework.cloud</groupId>
18+
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
19+
<version>3.1.0-SNAPSHOT</version>
20+
<relativePath>..</relativePath> <!-- lookup parent from repository -->
21+
</parent>
22+
23+
<dependencies>
24+
<dependency>
25+
<groupId>org.springframework.boot</groupId>
26+
<artifactId>spring-boot-starter-webflux</artifactId>
27+
</dependency>
28+
<dependency>
29+
<groupId>org.springframework.cloud</groupId>
30+
<artifactId>spring-cloud-starter-gateway</artifactId>
31+
</dependency>
32+
33+
<dependency>
34+
<groupId>io.grpc</groupId>
35+
<artifactId>grpc-netty-shaded</artifactId>
36+
<version>1.41.0</version>
37+
</dependency>
38+
<dependency>
39+
<groupId>io.grpc</groupId>
40+
<artifactId>grpc-protobuf</artifactId>
41+
<version>1.41.0</version>
42+
</dependency>
43+
<dependency>
44+
<groupId>io.grpc</groupId>
45+
<artifactId>grpc-stub</artifactId>
46+
<version>1.41.0</version>
47+
</dependency>
48+
<dependency>
49+
<groupId>io.netty</groupId>
50+
<artifactId>netty-tcnative-boringssl-static</artifactId>
51+
</dependency>
52+
<dependency>
53+
<groupId>org.springframework.boot</groupId>
54+
<artifactId>spring-boot-starter-test</artifactId>
55+
<scope>test</scope>
56+
</dependency>
57+
<dependency>
58+
<groupId>io.projectreactor</groupId>
59+
<artifactId>reactor-test</artifactId>
60+
<scope>test</scope>
61+
</dependency>
62+
<dependency>
63+
<groupId>org.assertj</groupId>
64+
<artifactId>assertj-core</artifactId>
65+
<scope>test</scope>
66+
</dependency>
67+
</dependencies>
68+
<build>
69+
<extensions>
70+
<extension>
71+
<groupId>kr.motd.maven</groupId>
72+
<artifactId>os-maven-plugin</artifactId>
73+
<version>1.6.2</version>
74+
</extension>
75+
</extensions>
76+
<plugins>
77+
<plugin>
78+
<artifactId>maven-deploy-plugin</artifactId>
79+
<configuration>
80+
<skip>true</skip>
81+
</configuration>
82+
</plugin>
83+
<plugin>
84+
<groupId>org.xolstice.maven.plugins</groupId>
85+
<artifactId>protobuf-maven-plugin</artifactId>
86+
<version>0.6.1</version>
87+
<configuration>
88+
<protocArtifact>com.google.protobuf:protoc:3.17.3:exe:${os.detected.classifier}</protocArtifact>
89+
<pluginId>grpc-java</pluginId>
90+
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.41.0:exe:${os.detected.classifier}</pluginArtifact>
91+
</configuration>
92+
<executions>
93+
<execution>
94+
<goals>
95+
<goal>compile</goal>
96+
<goal>compile-custom</goal>
97+
</goals>
98+
</execution>
99+
</executions>
100+
</plugin>
101+
</plugins>
102+
</build>
103+
</project>
104+
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2013-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.tests.grpc;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.util.concurrent.TimeUnit;
22+
23+
import io.grpc.Grpc;
24+
import io.grpc.Server;
25+
import io.grpc.ServerCredentials;
26+
import io.grpc.TlsServerCredentials;
27+
import io.grpc.stub.StreamObserver;
28+
29+
import org.springframework.boot.ApplicationArguments;
30+
import org.springframework.boot.ApplicationRunner;
31+
import org.springframework.boot.SpringApplication;
32+
import org.springframework.boot.SpringBootConfiguration;
33+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
34+
import org.springframework.cloud.gateway.route.RouteLocator;
35+
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
36+
import org.springframework.context.annotation.Bean;
37+
import org.springframework.core.io.ClassPathResource;
38+
import org.springframework.stereotype.Component;
39+
import org.springframework.util.SocketUtils;
40+
41+
/**
42+
* @author Alberto C. Ríos
43+
*/
44+
@SpringBootConfiguration
45+
@EnableAutoConfiguration
46+
public class GRPCApplication {
47+
48+
private static final int GRPC_SERVER_PORT = SocketUtils.findAvailableTcpPort();
49+
50+
public static void main(String[] args) {
51+
SpringApplication.run(GRPCApplication.class, args);
52+
}
53+
54+
@Bean
55+
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
56+
return builder.routes().route("grpc", r -> r.predicate(p -> true).uri("https://localhost:" + GRPC_SERVER_PORT))
57+
.build();
58+
}
59+
60+
@Component
61+
static class GRPCServer implements ApplicationRunner {
62+
63+
private Server server;
64+
65+
@Override
66+
public void run(ApplicationArguments args) throws Exception {
67+
final GRPCServer server = new GRPCServer();
68+
server.start();
69+
}
70+
71+
private void start() throws Exception {
72+
/* The port on which the server should run */
73+
ServerCredentials creds = createServerCredentials();
74+
server = Grpc.newServerBuilderForPort(GRPC_SERVER_PORT, creds).addService(new HelloService()).build()
75+
.start();
76+
77+
System.out.println("Starting server in port " + GRPC_SERVER_PORT);
78+
79+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
80+
try {
81+
GRPCServer.this.stop();
82+
}
83+
catch (InterruptedException e) {
84+
e.printStackTrace(System.err);
85+
}
86+
}));
87+
}
88+
89+
private ServerCredentials createServerCredentials() throws IOException {
90+
File privateKey = new ClassPathResource("private.key").getFile();
91+
File certChain = new ClassPathResource("certificate.pem").getFile();
92+
return TlsServerCredentials.create(certChain, privateKey);
93+
}
94+
95+
private void stop() throws InterruptedException {
96+
if (server != null) {
97+
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
98+
}
99+
}
100+
101+
static class HelloService extends HelloServiceGrpc.HelloServiceImplBase {
102+
103+
@Override
104+
public void hello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
105+
106+
String greeting = "Hello, " + request.getFirstName() + " " + request.getLastName();
107+
System.out.println(greeting);
108+
109+
HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting).build();
110+
111+
responseObserver.onNext(response);
112+
responseObserver.onCompleted();
113+
}
114+
115+
}
116+
117+
}
118+
119+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
syntax = "proto3";
2+
option java_multiple_files = true;
3+
option java_package = "org.springframework.cloud.gateway.tests.grpc";
4+
5+
message HelloRequest {
6+
string firstName = 1;
7+
string lastName = 2;
8+
}
9+
10+
message HelloResponse {
11+
string greeting = 1;
12+
}
13+
14+
service HelloService {
15+
rpc hello(HelloRequest) returns (HelloResponse);
16+
}

spring-cloud-gateway-integration-tests/grpc/src/main/resources/application.yml

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDdzCCAl+gAwIBAgIEIon96DANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV
3+
bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD
4+
VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du
5+
MB4XDTIxMDkyNzE2NDQwNVoXDTMxMDkyNTE2NDQwNVowbDEQMA4GA1UEBhMHVW5r
6+
bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE
7+
ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC
8+
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIwevGHY30YdoUdCSB/5q7/F
9+
c0KHetdjb71G2u6vFdeNvSwMpCV8Z1JznOJ/1zuY+0Z105QCPM7fi1ACi+tqxDR+
10+
L7yjUHhUEMTiGgCHcYJIZZCYfWS3BQXVxgORXhDv7RduCUaCLnkaFY++iPMTUy0C
11+
VxIkplIEhAmqcikIgWaa5ZjBkegKgahlPQLKlfD4Rz/kq2P+LLYFsHNNdKfWv6XQ
12+
u4LMw7ZEAJtfdpaMTzmtQipbTt6Dh87vIa0CIVnCPdlQ3o/5WeaxEA1pnfOLas07
13+
1VdHih2nC5vHhQcTPQDfa+uwzQvzHrchjuvMUUZaCYJzuT0G6nbGBba54EBT7yUC
14+
AwEAAaMhMB8wHQYDVR0OBBYEFKOHmfytP5ab2C4iFHlSklu6tCcuMA0GCSqGSIb3
15+
DQEBCwUAA4IBAQAdgWwdOtRbI796Z22weTBc0/tM8kLc6G0raNb08WyZMPZVki04
16+
jPh73pPQCgYeI/pq5JqH46KgvehmygTzpWDAFIllW0kgABVw3Nu6duV+blt1JG8T
17+
lWP7t5A+qDXgPDm3I5diii7O1YlLB3I37XiBdEV/+2WmF1VGQ7uBWAv+uotQeuW2
18+
JvHOr4ICOiW45TzRYtAbzWukSYKg/A7lwBs7HE9KVomUxNrkD+7+ugRuy/31pyen
19+
pHsEJQpx5juFRE222B6GXmX0w9xLIOapytl4EoPUx3K8Ecc+yI2q00UUC43x0v28
20+
c05YqwRZ5vp+jUnRxkxaz85YdArfR7QFYWtO
21+
-----END CERTIFICATE-----
Binary file not shown.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Bag Attributes
2+
friendlyName: mykey
3+
localKeyID: 54 69 6D 65 20 31 36 33 32 38 32 34 38 37 31 33 33 35
4+
Key Attributes: <No Attributes>
5+
-----BEGIN PRIVATE KEY-----
6+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCMHrxh2N9GHaFH
7+
Qkgf+au/xXNCh3rXY2+9RtrurxXXjb0sDKQlfGdSc5zif9c7mPtGddOUAjzO34tQ
8+
AovrasQ0fi+8o1B4VBDE4hoAh3GCSGWQmH1ktwUF1cYDkV4Q7+0XbglGgi55GhWP
9+
vojzE1MtAlcSJKZSBIQJqnIpCIFmmuWYwZHoCoGoZT0CypXw+Ec/5Ktj/iy2BbBz
10+
TXSn1r+l0LuCzMO2RACbX3aWjE85rUIqW07eg4fO7yGtAiFZwj3ZUN6P+VnmsRAN
11+
aZ3zi2rNO9VXR4odpwubx4UHEz0A32vrsM0L8x63IY7rzFFGWgmCc7k9Bup2xgW2
12+
ueBAU+8lAgMBAAECggEAGu2xQJDAYCZDn4FCgTqnYkSdIRUOa6SFjfe3DZYCeZmY
13+
2IVZaobdCICFjxYIlECTUfhFADXp38wgZvEGWOj86iWyIOu2BFoLmvrlCmL9Uo99
14+
TWuw9ZEi2vs5gegHDvQ9OXqBN9a+/bEgoa55fVWib4z6lNcMS8joYz8pj28+ByzE
15+
LW0/3T3p6beM2fUcCJWn3d2M3wUgSuXmcdjVXJSkhEwKTVcc4vTTcOeF6xb2VZ/g
16+
Pozv/39G1qZ+QtM58yBiqnJ1Z2gtAk71l/1ztQa3uY22gzw8Kj5dmqcHgdiN5DWI
17+
bNE+k5Q93FzUmZNYPzmY6YVkdEzaNMtmi96sdtEu5QKBgQD36YdGXUyoRXeNDRuc
18+
yMXr6j/9/ewii+byHhFoUvjLuXWIQ06V+nOtYqohg/zgGIiC5LQ8EB0uFZDMhbDf
19+
kSwtoXbpUDLYD2OPgIyLaPqHiYQ9BampbUz5vHlfYLr0vB+Xp5r22Eb6nDQRRtb5
20+
EXHoYgAokgpYdeTIcdKRcB/2DwKBgQCQsPePeu7PagM9vYEo4zfbFxfY3Qkr4lOQ
21+
BCZ/tgsS0b0jAAxpfUH0/3O5oXizmB/5K7vgKqGnTuBWJJ/hdCWq27FkKxJ+8ejU
22+
8V9TFd5VQ89VeB5OekZwPks8vftwzW4L82ZRW5hvyQB0jPR+lwqjrNqds+xxH7i+
23+
c+RFx15biwKBgQCgkGGK0zao7YUGl+zAWNDHgQo9KM5deZr0SUEg/kwhNlbHEEC/
24+
plxxeauS1XdcdMdFb3bER/N+O31y2Uu7IL0qOJ9ZcRXdFep3sNxWFoHcctZw51AB
25+
accnIEjD21R62bTkditJoL4n5i9a2TS2T/QkfAR6QkvtCz5IDGBCzgoFRQKBgC51
26+
VBfy3gklPgMt/PHW+1FSuep9FnvLwQ8F9iKdnjKdu8AoPNQGTw5Ok6bwDOSFnQaR
27+
n1Kb/anN7sRaICfw9kNFJVFHbznpjNwK4JO5+tif3EvSNND3+/QAXIIVck3G+GXH
28+
8nt/EJQcExRZSgv3jYf+cXeflPTBvb0RUyOAn3B/AoGAM9aUi2dg3XjlDkZahQlY
29+
P5QNIr9BY25Ordga3GLwfR6rE+jiWTeTmreXBSJ7nvcaEQUNyFMX9n3v/QI84etk
30+
lMJkZ4o2TgzRnCsFJoBV36Ihsa6B5uXVWAvMRLKvwKpptKXfO0TnhUg7oKtN4t3a
31+
/FzAOS2Eu1PFP27z74gAMKY=
32+
-----END PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2013-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.tests.grpc;
18+
19+
import java.security.cert.X509Certificate;
20+
21+
import javax.net.ssl.SSLException;
22+
import javax.net.ssl.TrustManager;
23+
import javax.net.ssl.X509TrustManager;
24+
25+
import io.grpc.ManagedChannel;
26+
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
27+
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
28+
import org.assertj.core.api.Assertions;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.extension.ExtendWith;
31+
32+
import org.springframework.boot.test.context.SpringBootTest;
33+
import org.springframework.boot.test.system.OutputCaptureExtension;
34+
import org.springframework.boot.web.server.LocalServerPort;
35+
import org.springframework.test.annotation.DirtiesContext;
36+
37+
import static io.grpc.netty.shaded.io.grpc.netty.NegotiationType.TLS;
38+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
39+
40+
/**
41+
* @author Alberto C. Ríos
42+
*/
43+
@ExtendWith(OutputCaptureExtension.class)
44+
@SpringBootTest(classes = GRPCApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)
45+
@DirtiesContext
46+
public class GRPCApplicationTests {
47+
48+
@LocalServerPort
49+
private int port;
50+
51+
@Test
52+
public void gRPCUnaryCalShouldReturnResponse() throws SSLException {
53+
ManagedChannel channel = createSecuredChannel(port);
54+
55+
final HelloResponse response = HelloServiceGrpc.newBlockingStub(channel)
56+
.hello(HelloRequest.newBuilder().setFirstName("Sir").setLastName("FromClient").build());
57+
58+
Assertions.assertThat(response.getGreeting()).isEqualTo("Hello, Sir FromClient");
59+
}
60+
61+
private ManagedChannel createSecuredChannel(int port) throws SSLException {
62+
TrustManager[] trustAllCerts = createTrustAllTrustManager();
63+
64+
return NettyChannelBuilder.forAddress("localhost", port).useTransportSecurity()
65+
.sslContext(GrpcSslContexts.forClient().trustManager(trustAllCerts[0]).build()).negotiationType(TLS)
66+
.build();
67+
}
68+
69+
private TrustManager[] createTrustAllTrustManager() {
70+
return new TrustManager[] { new X509TrustManager() {
71+
public X509Certificate[] getAcceptedIssuers() {
72+
return new X509Certificate[0];
73+
}
74+
75+
public void checkClientTrusted(X509Certificate[] certs, String authType) {
76+
}
77+
78+
public void checkServerTrusted(X509Certificate[] certs, String authType) {
79+
}
80+
} };
81+
}
82+
83+
}

0 commit comments

Comments
 (0)