From 22a9aec8bf6991bb72cd36116be19895173f5f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20C=2E=20R=C3=ADos?= Date: Fri, 24 Jun 2022 14:43:58 +0200 Subject: [PATCH 1/3] * Bump testcontainers * Update _configprops.adoc * Add grpc integration test --- docs/src/main/asciidoc/_configprops.adoc | 21 +- pom.xml | 2 +- .../gateway/tests/grpc/GRPCApplication.java | 107 +++++--- .../grpc/src/main/resources/application.yml | 0 .../grpc/src/main/resources/certificate.pem | 21 -- .../grpc/src/main/resources/keystore.p12 | Bin 2581 -> 2746 bytes .../grpc/src/main/resources/private.key | 56 ++-- .../grpc/src/main/resources/public.cert | 21 ++ .../grpc/JsonToGrpcApplicationTests.java | 1 + .../grpc/src/test/resources/application.yml | 25 ++ .../GatewayControllerEndpointTests.java | 44 +++ .../gateway/route/builder/RouteDslTests.kt | 254 +++++++++--------- 12 files changed, 322 insertions(+), 230 deletions(-) mode change 100644 => 100755 spring-cloud-gateway-integration-tests/grpc/src/main/resources/application.yml delete mode 100644 spring-cloud-gateway-integration-tests/grpc/src/main/resources/certificate.pem mode change 100644 => 100755 spring-cloud-gateway-integration-tests/grpc/src/main/resources/private.key create mode 100755 spring-cloud-gateway-integration-tests/grpc/src/main/resources/public.cert diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 90d82129..50923ed8 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. @@ -26,7 +27,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. @@ -42,16 +43,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. @@ -106,7 +107,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/pom.xml b/pom.xml index 98a25c8d..c1ee9ac8 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ 1.0.0 2.1.4-SNAPSHOT 3.1.4-SNAPSHOT - 1.16.3 + 1.17.2 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 b2dc170c..a4204b68 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,17 +18,18 @@ 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; import io.grpc.TlsServerCredentials; +import io.grpc.netty.shaded.io.grpc.netty.NettySslContextServerCredentials; +import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig; +import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolNames; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; import io.grpc.stub.StreamObserver; import org.springframework.boot.ApplicationArguments; @@ -36,13 +37,8 @@ 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.context.annotation.Bean; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; -import org.springframework.util.SocketUtils; /** * @author Alberto C. Ríos @@ -51,40 +47,42 @@ import org.springframework.util.SocketUtils; @EnableAutoConfiguration public class GRPCApplication { - private static final int GRPC_SERVER_PORT = SocketUtils.findAvailableTcpPort(); + private static final int GRPC_SERVER_PORT = 8095; public static void main(String[] args) { SpringApplication.run(GRPCApplication.class, args); } - @Bean - public RouteLocator customRouteLocator(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("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]); - } + // @Bean + // public RouteLocator customRouteLocator(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("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 static class GRPCServer implements ApplicationRunner { @@ -100,8 +98,8 @@ public class GRPCApplication { 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(); + server = Grpc.newServerBuilderForPort(GRPC_SERVER_PORT, creds) + .addService(new HelloService()).build().start(); System.out.println("Starting gRPC server in port " + GRPC_SERVER_PORT); @@ -116,11 +114,31 @@ public class GRPCApplication { } private ServerCredentials createServerCredentials() throws IOException { + File certChain = new ClassPathResource("public.cert").getFile(); File privateKey = new ClassPathResource("private.key").getFile(); - File certChain = new ClassPathResource("certificate.pem").getFile(); + File keystore = new ClassPathResource("keystore.p12").getFile(); + // return nettyCredentials(certChain, privateKey); return TlsServerCredentials.create(certChain, privateKey); } + private ServerCredentials nettyCredentials(File certChain, File privateKey) + throws SSLException { + ApplicationProtocolConfig apn = new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + // NO_ADVERTISE is currently the only mode supported by both OpenSsl + // and JDK providers. + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + // ACCEPT is currently the only mode supported by both OpenSsl and JDK + // providers. + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, ApplicationProtocolNames.HTTP_1_1); + + ServerCredentials serverCredentials = NettySslContextServerCredentials + .create(SslContextBuilder.forServer(certChain, privateKey) + .applicationProtocolConfig(apn).build()); + return serverCredentials; + } + private void stop() throws InterruptedException { if (server != null) { server.shutdown().awaitTermination(30, TimeUnit.SECONDS); @@ -130,12 +148,15 @@ public class GRPCApplication { static class HelloService extends HelloServiceGrpc.HelloServiceImplBase { @Override - public void hello(HelloRequest request, StreamObserver responseObserver) { + public void hello(HelloRequest request, + StreamObserver responseObserver) { - String greeting = "Hello, " + request.getFirstName() + " " + request.getLastName(); + String greeting = "Hello, " + request.getFirstName() + " " + + request.getLastName(); System.out.println("Sending response: " + greeting); - HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting).build(); + HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting) + .build(); responseObserver.onNext(response); responseObserver.onCompleted(); diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/application.yml b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/application.yml old mode 100644 new mode 100755 diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/certificate.pem b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/certificate.pem deleted file mode 100644 index 808fb06c..00000000 --- a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/certificate.pem +++ /dev/null @@ -1,21 +0,0 @@ ------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----- diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/keystore.p12 b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/keystore.p12 index b22b3349cbbc5dd0cab17271ce3403608c94d198..45662014a66a558693c3e49db49b47c0ea8366cc 100644 GIT binary patch literal 2746 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 cxH0#^_;x?^B%$x{kbuF -----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= +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCr+JyQOvGQcuSm +t1BN615YtrI0IoRFxfWHcbvU26A3TiGQtlnvXUIUTZApm1uSLR0o2VeS6ph+i3Ku +WFZTjF9wTem4IY8U22f4SymD7tFU/X4//YL0GgcyJCT/gGaYY58EVEH8+K3Pz1Hy +z/UnNuRrttxo+u94tB3OgTAYBB1KqY7Bqe7iZGleNz5baatOzYfEXmW4owHmqnfE +LZAmXdFj55Rbndkr0LBiKkBHM8aDC1l0OSJBzKt6RF07+we8Nte1Oynd2F3lZRHa +D8Pk7leDuV+KjC4W0X6kkwvZftmT/Y9C18+t2+J1BAz0kMsDjH1P87BconqVEhcM +PehXGgXFAgMBAAECggEAIzrxSBLrPf5rnUPcrbnUQDRdWZTgqDKf1DmWk0rTDcFx +2uWgkwr16JbjO8LaBZ48ZQvxhuWMjBAhVFpAhSkyvB0aDmDBoTI5oII1ZRPdyp2L +6awT0dIrOzhwY+94FSwDfa2NPzfq07HTRf0YagoyzWZOzSrrOD0eBhotMh5Vqd+w +UJ3VJCAWC0/Nz/4N5Ia2Hkyn2fz4fXdzux+/s5UKqaEHCXQWYmHj0XFLYSUb9rrO +qMYKM8sJjma4R3XthNalZEd6TD+07jNl6BwFmMBMj6D0VBWxV02woKkoWltmydUD +XhkdikCAB33CvHoj56PqCAFHrI6tnSRs9UqF/HPGQQKBgQDlfTJ3l2Orq1hy/D3d +8Z3ndOthwjm7QLOX8dJi4CHtymX8Lg4qfHcrs5ltoazXMYgdFHLX1rNs+gJg9I5p +vQS2jpRJ3BMzddrHaXjqhHv8WY1VYkPGML2tPol51LU7fx+2g+YnMzWNB7AIUz9K +OZkvlPWRO6jOcjvGgLjKARQUzQKBgQC/1mgGpKKBnAfEoDGZ1SWpI21BFKL3DWMx +YaOeFx+DETfAoUeIu1Ge2u8AIhrNE4dT8xOcG0vwGzHhKV63pabbMZ4+2pE92SEw +U6VsGvd/lcKPJvxgLS3xFQEzEmgL4v1mSBsxqaGZf4XHpdb32bQeybAQwUm25MCw +sEq/ZkX02QKBgAJOvvoq4IqyX2JQnQKlUlQofdFu1YvHe8bUXKw32r98YIgnombU +95HN7YYHsSg8zESWlw0KkKVQ4kM9Uk1H5Es9pcoUV3EWB0woCFo7WM/RyrUIFuxg +QxgB3/oEpInjnlsEeoT8Y9Z2NFPxGlrRt3OeCNYcBneE+IrncnQ9jIklAoGACUF0 +/20OEibPESORgRSRnpmA+fTe8ACLWqSVWllPQemgwQCHDQfMpld11JHQrThV/Szr +M0r13P3S6EQwt7ecV1MLiYjOHSfOvCAtCQw8CMHXA0UkRBep8cnLpwUqU/h9tWOh +PEIs89T9RWaw+oBcemfMwOIyhkp/KYc39AghM/ECgYAdTIT7J613mg8Ck/eUOBwx +udT1wbpcrCqHJX8TKWmBOLh9teZvmq7OIuXGudf1tDRjw85p/7bYBcG+gvaiH0Yf +qz5Npr4/+knxI2CBF2m81ln4Otq3X5siDL1wPCswawpnfdH+0qyfH6nxeGy0XSj2 +IQMa/LX1XqbUCry4sFl0yQ== -----END PRIVATE KEY----- diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/public.cert b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/public.cert new file mode 100755 index 00000000..e48355c8 --- /dev/null +++ b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/public.cert @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIIAZ+HRnyzONYwDQYJKoZIhvcNAQELBQAwbDEQMA4GA1UE +BhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQ +MA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5r +bm93bjAeFw0yMjA4MDUxNTEzMzBaFw0zMjA4MDIxNTEzMzBaMGwxEDAOBgNVBAYT +B1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vua25vd24xEDAO +BgNVBAoTB1Vua25vd24xEDAOBgNVBAsTB1Vua25vd24xEDAOBgNVBAMTB1Vua25v +d24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCr+JyQOvGQcuSmt1BN +615YtrI0IoRFxfWHcbvU26A3TiGQtlnvXUIUTZApm1uSLR0o2VeS6ph+i3KuWFZT +jF9wTem4IY8U22f4SymD7tFU/X4//YL0GgcyJCT/gGaYY58EVEH8+K3Pz1Hyz/Un +NuRrttxo+u94tB3OgTAYBB1KqY7Bqe7iZGleNz5baatOzYfEXmW4owHmqnfELZAm +XdFj55Rbndkr0LBiKkBHM8aDC1l0OSJBzKt6RF07+we8Nte1Oynd2F3lZRHaD8Pk +7leDuV+KjC4W0X6kkwvZftmT/Y9C18+t2+J1BAz0kMsDjH1P87BconqVEhcMPehX +GgXFAgMBAAGjITAfMB0GA1UdDgQWBBTC2DG76TuGW8VAEJawy96C2VPBpDANBgkq +hkiG9w0BAQsFAAOCAQEAduMMKQvGMMbmN6OdnrV9oBzpphKs1BWvbDBPsSrFm1zX +d1yRLSn2GyICQIA7SNX6Z59rWHe2HSXtzW6gbXm0yvAcPBI1DcRbamO12VF75Xjl +yg9oGuwckIcK2YtqYw7iTjFHyDFShTdWpYePxmyZ4HyZKWX3gKn1hGHA0SWYUJY+ +HkfPat0iwuwXDfAoyMchZrd7mOsXAhlD6+39gw9Vr+WhRFhv29hU8uuzcY+ahZfQ +9H8OF2nW0ePcc41909uorWfa3hWAVA2xfvzoBunFVMJt6mzT5M2Es5I/0wSXflbU +e4NjO4usVx7aU3jPkHEPGlDrwFJ8olDpKPutr2dGVA== +-----END CERTIFICATE----- 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 index f7ee4620..48af3d0b 100644 --- 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 @@ -58,6 +58,7 @@ public class JsonToGrpcApplicationTests { @BeforeEach void setUp() { restTemplate = createUnsecureClient(); +// restTemplate = new RestTemplate(); } @Test 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 7cc4ef2e..510c30ea 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 @@ -7,6 +7,15 @@ server: key-store-password: password key-password: password enabled: true + key-alias: bootapp +# key-store-provider: SUN +# trust-certificate: classpath:certificate.pem +# trust-certificate-private-key: classpath:private.key +# trust-store: classpath:keystore.jks +# trust-store-password: password +# trust-certificate-private-key: classpath:private.key +# trust-store-provider: classpath:keystore.jks + spring: cloud: gateway: @@ -20,6 +29,22 @@ spring: key-store-password: password key-password: password use-insecure-trust-manager: true +# key-store-type: JKS +# key-store: classpath:keystore.jks +# key-store-password: password +# trusted-x509-certificates: classpath:certificate.pem +# trust-store: classpath:keystore.jks +# trust-store-password: password +# trust-certificate-private-key: classpath:private.key +# key-store-provider: classpath:keystore.jks +# key-password: password + + routes: + - uri: https://localhost:8095 + predicates: + - Path=/json/hello + filters: + - JsonToGrpc=file:src/main/proto/hello.pb,file:src/main/proto/hello.proto,HelloService,hello logging: level: diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java index 27f21421..4a3f39a4 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java @@ -163,6 +163,50 @@ public class GatewayControllerEndpointTests { .expectStatus().isCreated(); } + @Test + public void testPostValidShortcutRouteDefinition() { +// 04-08-2022 08:41:31.866 [reactor-http-epoll-1] TRACE org.springframework.web.HttpLogging.trace - [0a3d20a5-43] Decoded [RouteDefinition{id='gatewaywithgrpcfiltertest-0-104014-8916263311295787431172436062-test-gateway-tls-client-mapping-0', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/json/hello}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=1}}, FilterDefinition{name='JsonToGrpcGatewayFilterFactory', args={_genkey_0=file:src/main/proto/hello.pb, _genkey_1=file:src/main/proto/hello.proto, _genkey_2=HelloService, _genkey_3=hello}}], uri=https://localhost:8095, order=0, metadata={}}] +// 04-08-2022 08:41:31.870 [reactor-http-epoll-1] WARN o.s.c.g.a.GatewayControllerEndpoint.handleUnavailableDefinition - Invalid FilterDefinition: [JsonToGrpcGatewayFilterFactory] +// 04-08-2022 08:41:31.882 [reactor-http-epoll-1] DEBUG o.s.w.s.h.ResponseStatusExceptionHandler.handle - [0a3d20a5-43] Resolved [ResponseStatusException: "400 BAD_REQUEST "Invalid FilterDefinition: [JsonToGrpcGatewayFilterFactory]""] for HTTP POST /actuator/gateway/routes/gatewaywithgrpcfiltertest-0-104014-8916263311295787431172436062-test-gateway-tls-client-mapping-0 +// 04-08-2022 08:41:31.883 [reactor-http-epoll-1] TRACE o.s.w.s.a.HttpWebHandlerAdapter.traceDebug - [0a3d20a5-43] Completed 400 BAD_REQUEST, headers={masked} +// +//[RouteDefinition{id='gatewaywithgrpcfiltertest-0-104014-8916263311295787431172436062-test-gateway-tls-client-mapping-0', + // predicates=[PredicateDefinition{name='Path', args={_genkey_0=/json/hello}}], + // filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=1}}, + // FilterDefinition{name='JsonToGrpcGatewayFilterFactory', + // args={_genkey_0=file:src/main/proto/hello.pb, _genkey_1=file:src/main/proto/hello.proto, _genkey_2=HelloService, _genkey_3=hello}}], + + // uri=https://localhost:8095, order=0, metadata={}}] + + RouteDefinition testRouteDefinition = new RouteDefinition(); + testRouteDefinition.setId("gatewaywithgrpcfiltertest-0-104014-8916263311295787431172436062-test-gateway-tls-client-mapping-0"); + testRouteDefinition.setUri(URI.create("https://localhost:8095")); + testRouteDefinition.setOrder(0); + testRouteDefinition.setMetadata(Collections.emptyMap()); + + FilterDefinition longFilterDefinition = new FilterDefinition(); + FilterDefinition stripPrefix = new FilterDefinition(); + stripPrefix.setName("StripPrefix"); + stripPrefix.addArg("_genkey_0","1"); + + longFilterDefinition.setName("JsonToGrpc"); + longFilterDefinition.addArg("_genkey_0","file:src/main/proto/hello.pb"); + longFilterDefinition.addArg("_genkey_1","file:src/main/proto/hello.proto"); + longFilterDefinition.addArg("_genkey_2","HelloService"); + longFilterDefinition.addArg("_genkey_3","hello"); + testRouteDefinition.setFilters(Collections.singletonList(longFilterDefinition)); + + PredicateDefinition hostRoutePredicateDefinition = new PredicateDefinition(); + hostRoutePredicateDefinition.setName("Path"); + hostRoutePredicateDefinition.addArg("_genkey_0","/json/hello"); + testRouteDefinition.setPredicates( + Arrays.asList(hostRoutePredicateDefinition)); + + testClient.post().uri("http://localhost:" + port + "/actuator/gateway/routes/test-route") + .accept(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(testRouteDefinition)).exchange() + .expectStatus().isCreated(); + } + @Test public void testPostRouteWithNotExistingFilter() { diff --git a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt index 283c6322..3bdf16ff 100644 --- a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt +++ b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt @@ -1,127 +1,127 @@ -/* - * 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.route.builder - -import org.junit.Test -import org.junit.runner.RunWith -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.EnableAutoConfiguration -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.cloud.gateway.support.ServerWebExchangeUtils -import org.springframework.context.annotation.Configuration -import org.springframework.mock.http.server.reactive.MockServerHttpRequest -import org.springframework.mock.web.server.MockServerWebExchange -import org.springframework.test.context.junit4.SpringRunner -import org.springframework.web.server.ServerWebExchange -import reactor.core.publisher.toMono -import reactor.test.StepVerifier -import java.net.URI - -@RunWith(SpringRunner::class) -@SpringBootTest(classes = arrayOf(Config::class)) -class RouteDslTests { - - @Autowired - lateinit var builder: RouteLocatorBuilder - - @Test - fun sampleRouteDsl() { - val routeLocator = builder.routes { - route(id = "test") { - host("**.abc.org") and path("/image/png") - filters { - addResponseHeader("X-TestHeader", "foobar") - } - uri("http://httpbin.org:80") - } - - route(id = "test2") { - path("/image/webp") or path("/image/anotherone") - filters { - addResponseHeader("X-AnotherHeader", "baz") - addResponseHeader("X-AnotherHeader-2", "baz-2") - } - uri("https://httpbin.org:443") - } - } - - StepVerifier - .create(routeLocator.routes) - .expectNextMatches({ - it.id == "test" && it.filters.size == 1 && it.uri == URI.create("http://httpbin.org:80") - }) - .expectNextMatches({ - it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") - }) - .expectComplete() - .verify() - - val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp") - .header("Host", "test.abc.org").build()) - - val filteredRoutes = routeLocator.routes.filter({ - sampleExchange.attributes.put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, it.id) - it.predicate.apply(sampleExchange).toMono().block() - }) - - StepVerifier.create(filteredRoutes) - .expectNextMatches({ - it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") - }) - .expectComplete() - .verify() - } - - @Test - fun dslWithFunctionParameters() { - val routerLocator = builder.routes { - route(id = "test1", order = 10, uri = "http://httpbin.org") { - host("**.abc.org") - } - route(id = "test2", order = 10, uri = "http://someurl") { - host("**.abc.org") - uri("http://override-url") - } - } - - StepVerifier.create(routerLocator.routes) - .expectNextMatches({ - it.id == "test1" && - it.uri == URI.create("http://httpbin.org:80") && - it.order == 10 && - it.predicate.apply(MockServerWebExchange - .from(MockServerHttpRequest - .get("/someuri").header("Host", "test.abc.org"))) - .toMono().block() - }) - .expectNextMatches({ - it.id == "test2" && - it.uri == URI.create("http://override-url:80") && - it.order == 10 && - it.predicate.apply(MockServerWebExchange - .from(MockServerHttpRequest - .get("/someuri").header("Host", "test.abc.org"))) - .toMono().block() - }) - .expectComplete() - .verify() - } -} - -@Configuration(proxyBeanMethods = false) -@EnableAutoConfiguration -open class Config {} \ No newline at end of file +///* +// * 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.route.builder +// +//import org.junit.Test +//import org.junit.runner.RunWith +//import org.springframework.beans.factory.annotation.Autowired +//import org.springframework.boot.autoconfigure.EnableAutoConfiguration +//import org.springframework.boot.test.context.SpringBootTest +//import org.springframework.cloud.gateway.support.ServerWebExchangeUtils +//import org.springframework.context.annotation.Configuration +//import org.springframework.mock.http.server.reactive.MockServerHttpRequest +//import org.springframework.mock.web.server.MockServerWebExchange +//import org.springframework.test.context.junit4.SpringRunner +//import org.springframework.web.server.ServerWebExchange +//import reactor.core.publisher.toMono +//import reactor.test.StepVerifier +//import java.net.URI +// +//@RunWith(SpringRunner::class) +//@SpringBootTest(classes = arrayOf(Config::class)) +//class RouteDslTests { +// +// @Autowired +// lateinit var builder: RouteLocatorBuilder +// +// @Test +// fun sampleRouteDsl() { +// val routeLocator = builder.routes { +// route(id = "test") { +// host("**.abc.org") and path("/image/png") +// filters { +// addResponseHeader("X-TestHeader", "foobar") +// } +// uri("http://httpbin.org:80") +// } +// +// route(id = "test2") { +// path("/image/webp") or path("/image/anotherone") +// filters { +// addResponseHeader("X-AnotherHeader", "baz") +// addResponseHeader("X-AnotherHeader-2", "baz-2") +// } +// uri("https://httpbin.org:443") +// } +// } +// +// StepVerifier +// .create(routeLocator.routes) +// .expectNextMatches({ +// it.id == "test" && it.filters.size == 1 && it.uri == URI.create("http://httpbin.org:80") +// }) +// .expectNextMatches({ +// it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") +// }) +// .expectComplete() +// .verify() +// +// val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp") +// .header("Host", "test.abc.org").build()) +// +// val filteredRoutes = routeLocator.routes.filter({ +// sampleExchange.attributes.put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, it.id) +// it.predicate.apply(sampleExchange).toMono().block() +// }) +// +// StepVerifier.create(filteredRoutes) +// .expectNextMatches({ +// it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") +// }) +// .expectComplete() +// .verify() +// } +// +// @Test +// fun dslWithFunctionParameters() { +// val routerLocator = builder.routes { +// route(id = "test1", order = 10, uri = "http://httpbin.org") { +// host("**.abc.org") +// } +// route(id = "test2", order = 10, uri = "http://someurl") { +// host("**.abc.org") +// uri("http://override-url") +// } +// } +// +// StepVerifier.create(routerLocator.routes) +// .expectNextMatches({ +// it.id == "test1" && +// it.uri == URI.create("http://httpbin.org:80") && +// it.order == 10 && +// it.predicate.apply(MockServerWebExchange +// .from(MockServerHttpRequest +// .get("/someuri").header("Host", "test.abc.org"))) +// .toMono().block() +// }) +// .expectNextMatches({ +// it.id == "test2" && +// it.uri == URI.create("http://override-url:80") && +// it.order == 10 && +// it.predicate.apply(MockServerWebExchange +// .from(MockServerHttpRequest +// .get("/someuri").header("Host", "test.abc.org"))) +// .toMono().block() +// }) +// .expectComplete() +// .verify() +// } +//} +// +//@Configuration(proxyBeanMethods = false) +//@EnableAutoConfiguration +//open class Config {} \ No newline at end of file From 774975a0feaadaa41fc4da915ef9086be60c6c54 Mon Sep 17 00:00:00 2001 From: Abel Salgado Romero Date: Mon, 8 Aug 2022 14:35:30 +0200 Subject: [PATCH 2/3] Adds support for 'use-insecure-trust-manager' & 'trustedX509Certificates' for GRPC Filter * Generated certificate with domain name (required for 'trustedX509Certificates') * Injected SslProperties in GRPC components to configure SslContext (ideally we want to reuse, not replicate, SCG context) --- .../main/asciidoc/spring-cloud-gateway.adoc | 2 +- .../grpc/pom.xml | 6 +- .../gateway/tests/grpc/GRPCApplication.java | 97 ++----- .../grpc/src/main/resources/application.yml | 0 .../grpc/src/main/resources/keystore.p12 | Bin 2746 -> 2586 bytes .../grpc/src/main/resources/private.key | 54 ++-- .../grpc/src/main/resources/public.cert | 35 ++- .../tests/grpc/GRPCApplicationTests.java | 8 +- .../grpc/JsonToGrpcApplicationTests.java | 63 ++++- .../grpc/src/test/resources/application.yml | 45 ++-- spring-cloud-gateway-server/pom.xml | 2 +- .../gateway/config/AbstractSslConfigurer.java | 139 ++++++++++ .../cloud/gateway/config/GRPCSSLContext.java | 36 --- .../config/GatewayAutoConfiguration.java | 24 +- .../gateway/config/GrpcSslConfigurer.java | 59 ++++ .../gateway/config/HttpClientFactory.java | 166 +++--------- .../config/HttpClientSslConfigurer.java | 76 ++++++ .../JsonToGrpcGatewayFilterFactory.java | 18 +- .../GatewayControllerEndpointTests.java | 31 +-- .../config/GatewayAutoConfigurationTests.java | 69 +++-- .../gateway/route/builder/RouteDslTests.kt | 254 +++++++++--------- 21 files changed, 678 insertions(+), 506 deletions(-) mode change 100755 => 100644 spring-cloud-gateway-integration-tests/grpc/src/main/resources/application.yml mode change 100755 => 100644 spring-cloud-gateway-integration-tests/grpc/src/main/resources/private.key mode change 100755 => 100644 spring-cloud-gateway-integration-tests/grpc/src/main/resources/public.cert create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/AbstractSslConfigurer.java delete mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GRPCSSLContext.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GrpcSslConfigurer.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientSslConfigurer.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 0285d51a..df165283 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -1894,7 +1894,7 @@ spring: 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`: +By default, it will create a `NettyChannel` using the default `TrustManagerFactory`. However, this `TrustManager` can be customized by creating a bean of type `GrpcSslConfigurer`: [source,java] ---- diff --git a/spring-cloud-gateway-integration-tests/grpc/pom.xml b/spring-cloud-gateway-integration-tests/grpc/pom.xml index 29bc5339..3c89d222 100644 --- a/spring-cloud-gateway-integration-tests/grpc/pom.xml +++ b/spring-cloud-gateway-integration-tests/grpc/pom.xml @@ -30,9 +30,13 @@ org.springframework.cloud spring-cloud-starter-gateway + + org.springframework.boot + spring-boot-starter-actuator + io.grpc - grpc-netty-shaded + grpc-netty ${grpc.version} 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 a4204b68..a417fa3e 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 @@ -20,23 +20,20 @@ import java.io.File; import java.io.IOException; import java.util.concurrent.TimeUnit; -import javax.net.ssl.SSLException; - import io.grpc.Grpc; import io.grpc.Server; import io.grpc.ServerCredentials; import io.grpc.TlsServerCredentials; -import io.grpc.netty.shaded.io.grpc.netty.NettySslContextServerCredentials; -import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig; -import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolNames; -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; import io.grpc.stub.StreamObserver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; @@ -47,61 +44,39 @@ import org.springframework.stereotype.Component; @EnableAutoConfiguration public class GRPCApplication { - private static final int GRPC_SERVER_PORT = 8095; - public static void main(String[] args) { SpringApplication.run(GRPCApplication.class, args); } - // @Bean - // public RouteLocator customRouteLocator(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("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 static class GRPCServer implements ApplicationRunner { + private static final Logger log = LoggerFactory.getLogger(GRPCServer.class); + + private final Environment environment; + private Server server; + GRPCServer(Environment environment) { + this.environment = environment; + } + @Override public void run(ApplicationArguments args) throws Exception { - final GRPCServer server = new GRPCServer(); + final GRPCServer server = new GRPCServer(environment); server.start(); } - private void start() throws Exception { - /* The port on which the server should run */ + private void start() throws IOException { + Integer serverPort = environment.getProperty("local.server.port", Integer.class); + int grpcPort = serverPort + 1; + /* + * The port on which the server should run. We run + */ ServerCredentials creds = createServerCredentials(); - server = Grpc.newServerBuilderForPort(GRPC_SERVER_PORT, creds) - .addService(new HelloService()).build().start(); + server = Grpc.newServerBuilderForPort(grpcPort, creds).addService(new HelloService()).build().start(); - System.out.println("Starting gRPC server in port " + GRPC_SERVER_PORT); + log.info("Starting gRPC server in port " + grpcPort); Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { @@ -116,29 +91,10 @@ public class GRPCApplication { private ServerCredentials createServerCredentials() throws IOException { File certChain = new ClassPathResource("public.cert").getFile(); File privateKey = new ClassPathResource("private.key").getFile(); - File keystore = new ClassPathResource("keystore.p12").getFile(); - // return nettyCredentials(certChain, privateKey); + return TlsServerCredentials.create(certChain, privateKey); } - private ServerCredentials nettyCredentials(File certChain, File privateKey) - throws SSLException { - ApplicationProtocolConfig apn = new ApplicationProtocolConfig( - ApplicationProtocolConfig.Protocol.ALPN, - // NO_ADVERTISE is currently the only mode supported by both OpenSsl - // and JDK providers. - ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, - // ACCEPT is currently the only mode supported by both OpenSsl and JDK - // providers. - ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, - ApplicationProtocolNames.HTTP_2, ApplicationProtocolNames.HTTP_1_1); - - ServerCredentials serverCredentials = NettySslContextServerCredentials - .create(SslContextBuilder.forServer(certChain, privateKey) - .applicationProtocolConfig(apn).build()); - return serverCredentials; - } - private void stop() throws InterruptedException { if (server != null) { server.shutdown().awaitTermination(30, TimeUnit.SECONDS); @@ -148,15 +104,12 @@ public class GRPCApplication { static class HelloService extends HelloServiceGrpc.HelloServiceImplBase { @Override - public void hello(HelloRequest request, - StreamObserver responseObserver) { + public void hello(HelloRequest request, StreamObserver responseObserver) { - String greeting = "Hello, " + request.getFirstName() + " " - + request.getLastName(); - System.out.println("Sending response: " + greeting); + String greeting = String.format("Hello, %s %s", request.getFirstName(), request.getLastName()); + log.info("Sending response: " + greeting); - HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting) - .build(); + HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting).build(); responseObserver.onNext(response); responseObserver.onCompleted(); diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/application.yml b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/application.yml old mode 100755 new mode 100644 diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/keystore.p12 b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/keystore.p12 index 45662014a66a558693c3e49db49b47c0ea8366cc..991ecbcf7a1628d62f5ef50135b44550e111331a 100644 GIT binary patch delta 2389 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 -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCr+JyQOvGQcuSm -t1BN615YtrI0IoRFxfWHcbvU26A3TiGQtlnvXUIUTZApm1uSLR0o2VeS6ph+i3Ku -WFZTjF9wTem4IY8U22f4SymD7tFU/X4//YL0GgcyJCT/gGaYY58EVEH8+K3Pz1Hy -z/UnNuRrttxo+u94tB3OgTAYBB1KqY7Bqe7iZGleNz5baatOzYfEXmW4owHmqnfE -LZAmXdFj55Rbndkr0LBiKkBHM8aDC1l0OSJBzKt6RF07+we8Nte1Oynd2F3lZRHa -D8Pk7leDuV+KjC4W0X6kkwvZftmT/Y9C18+t2+J1BAz0kMsDjH1P87BconqVEhcM -PehXGgXFAgMBAAECggEAIzrxSBLrPf5rnUPcrbnUQDRdWZTgqDKf1DmWk0rTDcFx -2uWgkwr16JbjO8LaBZ48ZQvxhuWMjBAhVFpAhSkyvB0aDmDBoTI5oII1ZRPdyp2L -6awT0dIrOzhwY+94FSwDfa2NPzfq07HTRf0YagoyzWZOzSrrOD0eBhotMh5Vqd+w -UJ3VJCAWC0/Nz/4N5Ia2Hkyn2fz4fXdzux+/s5UKqaEHCXQWYmHj0XFLYSUb9rrO -qMYKM8sJjma4R3XthNalZEd6TD+07jNl6BwFmMBMj6D0VBWxV02woKkoWltmydUD -XhkdikCAB33CvHoj56PqCAFHrI6tnSRs9UqF/HPGQQKBgQDlfTJ3l2Orq1hy/D3d -8Z3ndOthwjm7QLOX8dJi4CHtymX8Lg4qfHcrs5ltoazXMYgdFHLX1rNs+gJg9I5p -vQS2jpRJ3BMzddrHaXjqhHv8WY1VYkPGML2tPol51LU7fx+2g+YnMzWNB7AIUz9K -OZkvlPWRO6jOcjvGgLjKARQUzQKBgQC/1mgGpKKBnAfEoDGZ1SWpI21BFKL3DWMx -YaOeFx+DETfAoUeIu1Ge2u8AIhrNE4dT8xOcG0vwGzHhKV63pabbMZ4+2pE92SEw -U6VsGvd/lcKPJvxgLS3xFQEzEmgL4v1mSBsxqaGZf4XHpdb32bQeybAQwUm25MCw -sEq/ZkX02QKBgAJOvvoq4IqyX2JQnQKlUlQofdFu1YvHe8bUXKw32r98YIgnombU -95HN7YYHsSg8zESWlw0KkKVQ4kM9Uk1H5Es9pcoUV3EWB0woCFo7WM/RyrUIFuxg -QxgB3/oEpInjnlsEeoT8Y9Z2NFPxGlrRt3OeCNYcBneE+IrncnQ9jIklAoGACUF0 -/20OEibPESORgRSRnpmA+fTe8ACLWqSVWllPQemgwQCHDQfMpld11JHQrThV/Szr -M0r13P3S6EQwt7ecV1MLiYjOHSfOvCAtCQw8CMHXA0UkRBep8cnLpwUqU/h9tWOh -PEIs89T9RWaw+oBcemfMwOIyhkp/KYc39AghM/ECgYAdTIT7J613mg8Ck/eUOBwx -udT1wbpcrCqHJX8TKWmBOLh9teZvmq7OIuXGudf1tDRjw85p/7bYBcG+gvaiH0Yf -qz5Npr4/+knxI2CBF2m81ln4Otq3X5siDL1wPCswawpnfdH+0qyfH6nxeGy0XSj2 -IQMa/LX1XqbUCry4sFl0yQ== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClCZtSgRjM2uYM +O70+acD4rhwcc8ND23SwoKTNLPH6s1K2Pi5MZqF4Rnx+REJwerQrkgfMnsQ0QHcb +NjI9dhSwHw6aPigAGJQEzR8whDu2PkR57iBmySr3GLtpR+WkVGo/xAK92ozKt9aW +Saxm0miygYI8mgzrNpbaQAu6sZcS3RHkPYWw/izYVZLBB6IEpAJJrUqjzlu53TyN +X4VC4FWddItjOCiyrHe2+A+tFLNK3IZnFZBqoMcYR6/Rq+zsXx8ynCggkUwTFaPd +IVyzSYtooE2oq+aSjkJzKvoPA7BObLnrW+5Dklwj1iPtLhfuCflz2oa54/Hpl60R +Ke6y6/t5AgMBAAECggEAQvJ/sFswDUGq2kGNhfjuT7KJMr1+81Ldphy0XYqi6li9 +77GPpGxpidnF/I6CCRCtb5NAWK/61VtlNYOpo6b3w24FxWn7XfaabMwsn8i0VDw0 +GOYQ/MEUDcJZm96PeDbKFu3TUuKKBF3IzZQ7PEaUM/03MJApN20gio46c9RAfWTK +hN9jX9YkBpkSWPfx9RA9BOIXRrRMTXhidsVPnNxhhM7OAGZ1LhggDPaeiCIbVx6l +yMsyCkNvqdQbKu0hFcVyeQsGjovCC1Ye71F3em3ACguqDu+VAy3flpTbcDVf2D7T +zP2T5e5OEJ2hRTGdnEVSsXxWOWnq6Gt6ydNEe2AWVQKBgQC5HcQAUTnvIMaTez44 +7SBfELNY1AzF24dha24JFIGY78QNRZi+4Agsvxjkx4fKSd4DfmPsCqentmEkEcDr +w6fH0CAFm/rrkzLHmesBCC8byl4WtfMW5i8h1yzOeQbYpx3MGIu5xhkE92gbNj+o +ioM7nkoBvloU0odiYTVnoNYnFwKBgQDkO5ldA8Vl3+BWhmhOtPu+1EKssWry5rdO +m6IiZ1uB6uG4bhAj5dSVVgDRX6pcuppMVZqIbPpdkS7/i0LVwohT7EmpMMJ5bezz +EhG1z6AvZDcGMDC/PPMp7SLjJviYZDaTWLekKwY6yV1T9gtOY70jjWkUcfN1IHpA +renHNNyL7wKBgQCEYvIqW+y3xFPfY1MzePoeop3wl+3ujjo6hI7z9XNdgZNO/ofn +cebGwX+3Fa9aDwu0qe4h/9i4y2ibWAsFUS6ran+MI2oGkYXOU5hKa6TtFgPF8CfC +J6prZCxKGSm5RYK81I0Qtchs0dblJx3NlgmWWHSK3Kwlmg5yYBzGWLLuzwKBgDzg +rNrDq76txcAun9oGqnPPWG2J8XYTFmgQWWIF4cG4rjasnP+GSXr/8r3mX6HWYFvm +JY8oSmv00u107wHnnseL6mYHzIfpS1/WvQSa+iZJ++dZqVcJYe8YAstGVN8JNAl/ +i5RtqX66wXso0QE613OJP7MlZgQjApkICqiJMB7fAoGAVFdv/Nphcju7MCkXGuQe +amk7Z6Q2PjMWvLwiyB39PrIzvTOdaQyQk7oYdnAGFJbkmi7mmGiet5uvcE9OjNRb ++XeXo2LEZ2QWJEnHi9uCXSQF3eeBlJ2wGUsELPTHglWIUTpi5iDB2hHzczPzO8GY +qPb+ckCl8mTsHRKQQse4Bm0= -----END PRIVATE KEY----- diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/public.cert b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/public.cert old mode 100755 new mode 100644 index e48355c8..ecb750c6 --- a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/public.cert +++ b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/public.cert @@ -1,21 +1,18 @@ -----BEGIN CERTIFICATE----- -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 +Je5gI59jCbxHs5g= -----END CERTIFICATE----- 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 0c6289ed..f540862a 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 @@ -23,8 +23,8 @@ 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 io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -32,7 +32,7 @@ import org.springframework.boot.test.context.SpringBootTest; 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 io.grpc.netty.NegotiationType.TLS; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; /** @@ -47,7 +47,7 @@ public class GRPCApplicationTests { @Test public void gRPCUnaryCalShouldReturnResponse() throws SSLException { - ManagedChannel channel = createSecuredChannel(port); + ManagedChannel channel = createSecuredChannel(port + 1); final HelloResponse response = HelloServiceGrpc.newBlockingStub(channel) .hello(HelloRequest.newBuilder().setFirstName("Sir").setLastName("FromClient").build()); 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 index 48af3d0b..b0ec1e62 100644 --- 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 @@ -19,6 +19,11 @@ package org.springframework.cloud.gateway.tests.grpc; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import javax.net.ssl.SSLContext; @@ -39,13 +44,19 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.client.RestTemplate; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; /** * @author Alberto C. Ríos + * @author Abel Salgado Romero */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class JsonToGrpcApplicationTests { @@ -58,12 +69,18 @@ public class JsonToGrpcApplicationTests { @BeforeEach void setUp() { restTemplate = createUnsecureClient(); -// restTemplate = new RestTemplate(); } @Test public void shouldConvertFromJSONToGRPC() { - String response = restTemplate.postForEntity("https://localhost:" + port + "/json/hello", + // Since GRPC server and GW run in same instance and don't know server port until + // test starts, + // we need to configure route dynamically using the actuator endpoint. + final RouteConfigurer configurer = new RouteConfigurer(port); + configurer.addRoute(port + 1, "/json/hello", + "JsonToGrpc=file:src/main/proto/hello.pb,file:src/main/proto/hello.proto,HelloService,hello"); + + String response = restTemplate.postForEntity("https://localhost:" + this.port + "/json/hello", "{\"firstName\":\"Duff\", \"lastName\":\"McKagan\"}", String.class).getBody(); Assertions.assertThat(response).isNotNull(); @@ -95,4 +112,46 @@ public class JsonToGrpcApplicationTests { return new RestTemplate(requestFactory); } + class RouteConfigurer { + + private final WebTestClient actuatorWebClient; + + private final int actuatorPort; + + RouteConfigurer(int actuatorPort) { + this.actuatorPort = actuatorPort; + this.actuatorWebClient = WebTestClient.bindToServer().baseUrl("http://localhost:" + actuatorPort).build(); + + } + + public void addRoute(int uriPort, String path, String filter) { + final String routeId = "test-route-" + UUID.randomUUID(); + + Map route = new HashMap<>(); + route.put("id", routeId); + route.put("uri", "http://localhost:" + uriPort); + route.put("predicates", Collections.singletonList("Path=" + path)); + route.put("filters", Arrays.asList(filter)); + + ResponseEntity exchange = restTemplate.exchange(url("/actuator/gateway/routes/" + routeId), + HttpMethod.POST, new HttpEntity<>(route), String.class); + + assert exchange.getStatusCode() == HttpStatus.CREATED; + + refreshRoutes(); + } + + private void refreshRoutes() { + ResponseEntity exchange = restTemplate.exchange(url("/actuator/gateway/refresh"), HttpMethod.POST, + new HttpEntity<>(""), String.class); + + assert exchange.getStatusCode() == HttpStatus.OK; + } + + private String url(String context) { + return String.format("https://localhost:%s%s", this.actuatorPort, context); + } + + } + } 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 510c30ea..97c1b950 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 @@ -2,19 +2,21 @@ server: http2: enabled: true ssl: - key-store-type: PKCS12 key-store: classpath:keystore.p12 + key-store-type: pkcs12 key-store-password: password - key-password: password - enabled: true key-alias: bootapp -# key-store-provider: SUN -# trust-certificate: classpath:certificate.pem -# trust-certificate-private-key: classpath:private.key -# trust-store: classpath:keystore.jks -# trust-store-password: password -# trust-certificate-private-key: classpath:private.key -# trust-store-provider: classpath:keystore.jks + +management: + endpoint: + health: + show-details: when_authorized + gateway: + enabled: true + endpoints: + web: + exposure: + include: "*" spring: cloud: @@ -24,27 +26,10 @@ spring: 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 -# key-store-type: JKS -# key-store: classpath:keystore.jks -# key-store-password: password -# trusted-x509-certificates: classpath:certificate.pem -# trust-store: classpath:keystore.jks -# trust-store-password: password -# trust-certificate-private-key: classpath:private.key -# key-store-provider: classpath:keystore.jks -# key-password: password - - routes: - - uri: https://localhost:8095 - predicates: - - Path=/json/hello - filters: - - JsonToGrpc=file:src/main/proto/hello.pb,file:src/main/proto/hello.proto,HelloService,hello +# Requires faking domain name (modifying /etc/hosts) +# trustedX509Certificates: +# - classpath:public.cert logging: level: diff --git a/spring-cloud-gateway-server/pom.xml b/spring-cloud-gateway-server/pom.xml index d3756764..1c422fbe 100644 --- a/spring-cloud-gateway-server/pom.xml +++ b/spring-cloud-gateway-server/pom.xml @@ -94,7 +94,7 @@ io.grpc - grpc-netty-shaded + grpc-netty true ${grpc.version} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/AbstractSslConfigurer.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/AbstractSslConfigurer.java new file mode 100644 index 00000000..5f310d3f --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/AbstractSslConfigurer.java @@ -0,0 +1,139 @@ +/* + * 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 java.io.IOException; +import java.net.URL; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchProviderException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManagerFactory; + +import io.netty.handler.ssl.SslContextBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.ResourceUtils; + +/** + * Base class to configure SSL for component T. Returns an instance S with the resulting + * configuration (can be the same as T). + * + * @author Abel Salgado Romero + */ +public abstract class AbstractSslConfigurer { + + protected final Log logger = LogFactory.getLog(this.getClass()); + + private final HttpClientProperties.Ssl ssl; + + protected AbstractSslConfigurer(HttpClientProperties.Ssl sslProperties) { + this.ssl = sslProperties; + } + + abstract public S configureSsl(T client) throws SSLException; + + protected HttpClientProperties.Ssl getSslProperties() { + return ssl; + } + + protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { + + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + ArrayList allCerts = new ArrayList<>(); + for (String trustedCert : ssl.getTrustedX509Certificates()) { + try { + URL url = ResourceUtils.getURL(trustedCert); + Collection certs = certificateFactory.generateCertificates(url.openStream()); + allCerts.addAll(certs); + } + catch (IOException e) { + throw new RuntimeException("Could not load certificate '" + trustedCert + "'", e); + } + } + return allCerts.toArray(new X509Certificate[allCerts.size()]); + } + catch (CertificateException e1) { + throw new RuntimeException("Could not load CertificateFactory X.509", e1); + } + } + + protected KeyManagerFactory getKeyManagerFactory() { + + try { + if (ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) { + KeyManagerFactory keyManagerFactory = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + char[] keyPassword = ssl.getKeyPassword() != null ? ssl.getKeyPassword().toCharArray() : null; + + if (keyPassword == null && ssl.getKeyStorePassword() != null) { + keyPassword = ssl.getKeyStorePassword().toCharArray(); + } + + keyManagerFactory.init(this.createKeyStore(), keyPassword); + + return keyManagerFactory; + } + + return null; + } + catch (Exception e) { + throw new IllegalStateException(e); + } + } + + protected KeyStore createKeyStore() { + + try { + KeyStore store = ssl.getKeyStoreProvider() != null + ? KeyStore.getInstance(ssl.getKeyStoreType(), ssl.getKeyStoreProvider()) + : KeyStore.getInstance(ssl.getKeyStoreType()); + try { + URL url = ResourceUtils.getURL(ssl.getKeyStore()); + store.load(url.openStream(), + ssl.getKeyStorePassword() != null ? ssl.getKeyStorePassword().toCharArray() : null); + } + catch (Exception e) { + throw new RuntimeException("Could not load key store ' " + ssl.getKeyStore() + "'", e); + } + + return store; + } + catch (KeyStoreException | NoSuchProviderException e) { + throw new RuntimeException("Could not load KeyStore for given type and provider", e); + } + } + + protected void setTrustManager(SslContextBuilder sslContextBuilder, X509Certificate... trustedX509Certificates) { + sslContextBuilder.trustManager(trustedX509Certificates); + } + + protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { + sslContextBuilder.trustManager(factory); + } + +} 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 deleted file mode 100644 index bbdb34a6..00000000 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GRPCSSLContext.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 99a5f54d..80717349 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 @@ -25,6 +25,7 @@ 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; @@ -302,22 +303,23 @@ public class GatewayAutoConfiguration { @Bean @ConditionalOnEnabledFilter @ConditionalOnProperty(name = "server.http2.enabled", matchIfMissing = true) - @ConditionalOnClass(name = "io.grpc.Channel") - public JsonToGrpcGatewayFilterFactory jsonToGRPCFilterFactory(GRPCSSLContext gRPCSSLContext, + @ConditionalOnClass(Channel.class) + public JsonToGrpcGatewayFilterFactory jsonToGRPCFilterFactory(GrpcSslConfigurer gRPCSSLContext, ResourceLoader resourceLoader) { return new JsonToGrpcGatewayFilterFactory(gRPCSSLContext, resourceLoader); } @Bean @ConditionalOnEnabledFilter(JsonToGrpcGatewayFilterFactory.class) - @ConditionalOnMissingBean(GRPCSSLContext.class) + @ConditionalOnMissingBean(GrpcSslConfigurer.class) @ConditionalOnClass(name = "io.grpc.Channel") - public GRPCSSLContext gRPCSSLContext() throws KeyStoreException, NoSuchAlgorithmException { + public GrpcSslConfigurer grpcSslConfigurer(HttpClientProperties properties) + throws KeyStoreException, NoSuchAlgorithmException { TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(KeyStore.getInstance(KeyStore.getDefaultType())); - return new GRPCSSLContext(trustManagerFactory.getTrustManagers()[0]); + return new GrpcSslConfigurer(properties.getSsl()); } @Bean @@ -683,11 +685,19 @@ public class GatewayAutoConfiguration { }; } + @Bean + public HttpClientSslConfigurer httpClientSslConfigurer(ServerProperties serverProperties, + HttpClientProperties httpClientProperties) { + return new HttpClientSslConfigurer(httpClientProperties.getSsl(), serverProperties) { + }; + } + @Bean @ConditionalOnMissingBean({ HttpClient.class, HttpClientFactory.class }) public HttpClientFactory gatewayHttpClientFactory(HttpClientProperties properties, - ServerProperties serverProperties, List customizers) { - return new HttpClientFactory(properties, serverProperties, customizers); + ServerProperties serverProperties, List customizers, + HttpClientSslConfigurer sslConfigurer) { + return new HttpClientFactory(properties, serverProperties, sslConfigurer, customizers); } @Bean diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GrpcSslConfigurer.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GrpcSslConfigurer.java new file mode 100644 index 00000000..f3b6907e --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GrpcSslConfigurer.java @@ -0,0 +1,59 @@ +/* + * 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.SSLException; + +import io.grpc.ManagedChannel; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +/** + * @author Alberto C. Ríos + */ +public class GrpcSslConfigurer extends AbstractSslConfigurer { + + public GrpcSslConfigurer(HttpClientProperties.Ssl sslProperties) { + super(sslProperties); + } + + @Override + public ManagedChannel configureSsl(NettyChannelBuilder NettyChannelBuilder) throws SSLException { + return NettyChannelBuilder.useTransportSecurity().sslContext(getSslContext()).build(); + } + + private SslContext getSslContext() throws SSLException { + + final SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient(); + + final HttpClientProperties.Ssl ssl = getSslProperties(); + boolean useInsecureTrustManager = ssl.isUseInsecureTrustManager(); + if (useInsecureTrustManager) { + sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE.getTrustManagers()[0]); + } + + if (!useInsecureTrustManager && ssl.getTrustedX509Certificates().size() > 0) { + sslContextBuilder.trustManager(getTrustedX509CertificatesForTrustManager()); + } + + return sslContextBuilder.keyManager(getKeyManagerFactory()).build(); + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java index 4d033750..f2b66576 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java @@ -16,18 +16,9 @@ package org.springframework.cloud.gateway.config; -import java.io.IOException; -import java.net.URL; import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchProviderException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import javax.net.ssl.KeyManagerFactory; @@ -35,9 +26,6 @@ import javax.net.ssl.TrustManagerFactory; import io.netty.channel.ChannelOption; import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import reactor.netty.http.Http11SslContextSpec; -import reactor.netty.http.Http2SslContextSpec; import reactor.netty.http.HttpProtocol; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpResponseDecoderSpec; @@ -50,7 +38,6 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.util.CollectionUtils; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.DISABLED; @@ -69,12 +56,23 @@ public class HttpClientFactory extends AbstractFactoryBean { protected final ServerProperties serverProperties; + protected final HttpClientSslConfigurer sslConfigurer; + protected final List customizers; public HttpClientFactory(HttpClientProperties properties, ServerProperties serverProperties, List customizers) { this.properties = properties; this.serverProperties = serverProperties; + this.sslConfigurer = new HttpClientSslConfigurer(properties.getSsl(), serverProperties); + this.customizers = customizers; + } + + public HttpClientFactory(HttpClientProperties properties, ServerProperties serverProperties, + HttpClientSslConfigurer sslConfigurer, List customizers) { + this.properties = properties; + this.serverProperties = serverProperties; + this.sslConfigurer = sslConfigurer; this.customizers = customizers; } @@ -117,6 +115,34 @@ public class HttpClientFactory extends AbstractFactoryBean { return httpClient; } + protected HttpClient configureSsl(HttpClient httpClient) { + return sslConfigurer.configureSsl(httpClient); + } + + protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { + sslConfigurer.configureSslContext(ssl, sslContextSpec); + } + + protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { + return sslConfigurer.getTrustedX509CertificatesForTrustManager(); + } + + protected KeyManagerFactory getKeyManagerFactory() { + return sslConfigurer.getKeyManagerFactory(); + } + + protected KeyStore createKeyStore() { + return sslConfigurer.createKeyStore(); + } + + protected void setTrustManager(SslContextBuilder sslContextBuilder, X509Certificate... trustedX509Certificates) { + sslConfigurer.setTrustManager(sslContextBuilder, trustedX509Certificates); + } + + protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { + sslConfigurer.setTrustManager(sslContextBuilder, factory); + } + private HttpClient applyCustomizers(HttpClient httpClient) { if (!CollectionUtils.isEmpty(customizers)) { customizers.sort(AnnotationAwareOrderComparator.INSTANCE); @@ -127,43 +153,6 @@ public class HttpClientFactory extends AbstractFactoryBean { return httpClient; } - protected HttpClient configureSsl(HttpClient httpClient) { - HttpClientProperties.Ssl ssl = properties.getSsl(); - if ((ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) - || getTrustedX509CertificatesForTrustManager().length > 0 || ssl.isUseInsecureTrustManager()) { - httpClient = httpClient.secure(sslContextSpec -> { - // configure ssl - configureSslContext(ssl, sslContextSpec); - }); - } - return httpClient; - } - - protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { - SslProvider.ProtocolSslContextSpec clientSslContext = (serverProperties.getHttp2().isEnabled()) - ? Http2SslContextSpec.forClient() : Http11SslContextSpec.forClient(); - clientSslContext.configure(sslContextBuilder -> { - X509Certificate[] trustedX509Certificates = getTrustedX509CertificatesForTrustManager(); - if (trustedX509Certificates.length > 0) { - setTrustManager(sslContextBuilder, trustedX509Certificates); - } - else if (ssl.isUseInsecureTrustManager()) { - setTrustManager(sslContextBuilder, InsecureTrustManagerFactory.INSTANCE); - } - - try { - sslContextBuilder.keyManager(getKeyManagerFactory()); - } - catch (Exception e) { - logger.error(e); - } - }); - - sslContextSpec.sslContext(clientSslContext).handshakeTimeout(ssl.getHandshakeTimeout()) - .closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout()) - .closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout()); - } - protected HttpClient configureProxy(HttpClient httpClient) { // configure proxy if proxy host is set. if (StringUtils.hasText(properties.getProxy().getHost())) { @@ -201,83 +190,6 @@ public class HttpClientFactory extends AbstractFactoryBean { return spec; } - protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { - HttpClientProperties.Ssl ssl = properties.getSsl(); - - try { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - ArrayList allCerts = new ArrayList<>(); - for (String trustedCert : ssl.getTrustedX509Certificates()) { - try { - URL url = ResourceUtils.getURL(trustedCert); - Collection certs = certificateFactory.generateCertificates(url.openStream()); - allCerts.addAll(certs); - } - catch (IOException e) { - throw new RuntimeException("Could not load certificate '" + trustedCert + "'", e); - } - } - return allCerts.toArray(new X509Certificate[allCerts.size()]); - } - catch (CertificateException e1) { - throw new RuntimeException("Could not load CertificateFactory X.509", e1); - } - } - - protected KeyManagerFactory getKeyManagerFactory() { - HttpClientProperties.Ssl ssl = properties.getSsl(); - try { - if (ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) { - KeyManagerFactory keyManagerFactory = KeyManagerFactory - .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - char[] keyPassword = ssl.getKeyPassword() != null ? ssl.getKeyPassword().toCharArray() : null; - - if (keyPassword == null && ssl.getKeyStorePassword() != null) { - keyPassword = ssl.getKeyStorePassword().toCharArray(); - } - - keyManagerFactory.init(this.createKeyStore(), keyPassword); - - return keyManagerFactory; - } - - return null; - } - catch (Exception e) { - throw new IllegalStateException(e); - } - } - - protected KeyStore createKeyStore() { - HttpClientProperties.Ssl ssl = properties.getSsl(); - try { - KeyStore store = ssl.getKeyStoreProvider() != null - ? KeyStore.getInstance(ssl.getKeyStoreType(), ssl.getKeyStoreProvider()) - : KeyStore.getInstance(ssl.getKeyStoreType()); - try { - URL url = ResourceUtils.getURL(ssl.getKeyStore()); - store.load(url.openStream(), - ssl.getKeyStorePassword() != null ? ssl.getKeyStorePassword().toCharArray() : null); - } - catch (Exception e) { - throw new RuntimeException("Could not load key store ' " + ssl.getKeyStore() + "'", e); - } - - return store; - } - catch (KeyStoreException | NoSuchProviderException e) { - throw new RuntimeException("Could not load KeyStore for given type and provider", e); - } - } - - protected void setTrustManager(SslContextBuilder sslContextBuilder, X509Certificate... trustedX509Certificates) { - sslContextBuilder.trustManager(trustedX509Certificates); - } - - protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { - sslContextBuilder.trustManager(factory); - } - protected ConnectionProvider buildConnectionProvider(HttpClientProperties properties) { HttpClientProperties.Pool pool = properties.getPool(); diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientSslConfigurer.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientSslConfigurer.java new file mode 100644 index 00000000..0938a891 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientSslConfigurer.java @@ -0,0 +1,76 @@ +/* + * 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 java.security.cert.X509Certificate; + +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import reactor.netty.http.Http11SslContextSpec; +import reactor.netty.http.Http2SslContextSpec; +import reactor.netty.http.client.HttpClient; +import reactor.netty.tcp.SslProvider; + +import org.springframework.boot.autoconfigure.web.ServerProperties; + +public class HttpClientSslConfigurer extends AbstractSslConfigurer { + + private final ServerProperties serverProperties; + + public HttpClientSslConfigurer(HttpClientProperties.Ssl sslProperties, ServerProperties serverProperties) { + super(sslProperties); + this.serverProperties = serverProperties; + } + + public HttpClient configureSsl(HttpClient client) { + final HttpClientProperties.Ssl ssl = getSslProperties(); + + if ((ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) + || getTrustedX509CertificatesForTrustManager().length > 0 || ssl.isUseInsecureTrustManager()) { + client = client.secure(sslContextSpec -> { + // configure ssl + configureSslContext(ssl, sslContextSpec); + }); + } + return client; + } + + protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { + SslProvider.ProtocolSslContextSpec clientSslContext = (serverProperties.getHttp2().isEnabled()) + ? Http2SslContextSpec.forClient() : Http11SslContextSpec.forClient(); + clientSslContext.configure(sslContextBuilder -> { + X509Certificate[] trustedX509Certificates = getTrustedX509CertificatesForTrustManager(); + if (trustedX509Certificates.length > 0) { + setTrustManager(sslContextBuilder, trustedX509Certificates); + } + else if (ssl.isUseInsecureTrustManager()) { + setTrustManager(sslContextBuilder, InsecureTrustManagerFactory.INSTANCE); + } + + try { + sslContextBuilder.keyManager(getKeyManagerFactory()); + } + catch (Exception e) { + logger.error(e); + } + }); + + sslContextSpec.sslContext(clientSslContext).handshakeTimeout(ssl.getHandshakeTimeout()) + .closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout()) + .closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout()); + } + +} 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 index adec2f18..aed1911e 100644 --- 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 @@ -45,8 +45,7 @@ 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.netty.NettyChannelBuilder; import io.grpc.protobuf.ProtoUtils; import io.grpc.stub.ClientCalls; import io.netty.buffer.PooledByteBufAllocator; @@ -54,7 +53,7 @@ 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.config.GrpcSslConfigurer; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter; @@ -70,7 +69,6 @@ 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; /** @@ -85,13 +83,13 @@ import static org.springframework.cloud.gateway.support.GatewayToStringStyler.fi public class JsonToGrpcGatewayFilterFactory extends AbstractGatewayFilterFactory { - private final GRPCSSLContext sslContext; + private final GrpcSslConfigurer grpcSslConfigurer; private final ResourceLoader resourceLoader; - public JsonToGrpcGatewayFilterFactory(GRPCSSLContext sslContext, ResourceLoader resourceLoader) { + public JsonToGrpcGatewayFilterFactory(GrpcSslConfigurer grpcSslConfigurer, ResourceLoader resourceLoader) { super(Config.class); - this.sslContext = sslContext; + this.grpcSslConfigurer = grpcSslConfigurer; this.resourceLoader = resourceLoader; } @@ -298,11 +296,11 @@ public class JsonToGrpcGatewayFilterFactory }; } + // We are creating this on every call, should optimize? private ManagedChannel createChannelChannel(String host, int port) { + NettyChannelBuilder nettyChannelBuilder = NettyChannelBuilder.forAddress(host, port); try { - return NettyChannelBuilder.forAddress(host, port).useTransportSecurity() - .sslContext(GrpcSslContexts.forClient().trustManager(sslContext.getTrustManager()).build()) - .negotiationType(TLS).build(); + return grpcSslConfigurer.configureSsl(nettyChannelBuilder); } catch (SSLException e) { throw new RuntimeException(e); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java index 4a3f39a4..0a1094b9 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java @@ -165,21 +165,9 @@ public class GatewayControllerEndpointTests { @Test public void testPostValidShortcutRouteDefinition() { -// 04-08-2022 08:41:31.866 [reactor-http-epoll-1] TRACE org.springframework.web.HttpLogging.trace - [0a3d20a5-43] Decoded [RouteDefinition{id='gatewaywithgrpcfiltertest-0-104014-8916263311295787431172436062-test-gateway-tls-client-mapping-0', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/json/hello}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=1}}, FilterDefinition{name='JsonToGrpcGatewayFilterFactory', args={_genkey_0=file:src/main/proto/hello.pb, _genkey_1=file:src/main/proto/hello.proto, _genkey_2=HelloService, _genkey_3=hello}}], uri=https://localhost:8095, order=0, metadata={}}] -// 04-08-2022 08:41:31.870 [reactor-http-epoll-1] WARN o.s.c.g.a.GatewayControllerEndpoint.handleUnavailableDefinition - Invalid FilterDefinition: [JsonToGrpcGatewayFilterFactory] -// 04-08-2022 08:41:31.882 [reactor-http-epoll-1] DEBUG o.s.w.s.h.ResponseStatusExceptionHandler.handle - [0a3d20a5-43] Resolved [ResponseStatusException: "400 BAD_REQUEST "Invalid FilterDefinition: [JsonToGrpcGatewayFilterFactory]""] for HTTP POST /actuator/gateway/routes/gatewaywithgrpcfiltertest-0-104014-8916263311295787431172436062-test-gateway-tls-client-mapping-0 -// 04-08-2022 08:41:31.883 [reactor-http-epoll-1] TRACE o.s.w.s.a.HttpWebHandlerAdapter.traceDebug - [0a3d20a5-43] Completed 400 BAD_REQUEST, headers={masked} -// -//[RouteDefinition{id='gatewaywithgrpcfiltertest-0-104014-8916263311295787431172436062-test-gateway-tls-client-mapping-0', - // predicates=[PredicateDefinition{name='Path', args={_genkey_0=/json/hello}}], - // filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=1}}, - // FilterDefinition{name='JsonToGrpcGatewayFilterFactory', - // args={_genkey_0=file:src/main/proto/hello.pb, _genkey_1=file:src/main/proto/hello.proto, _genkey_2=HelloService, _genkey_3=hello}}], - - // uri=https://localhost:8095, order=0, metadata={}}] - RouteDefinition testRouteDefinition = new RouteDefinition(); - testRouteDefinition.setId("gatewaywithgrpcfiltertest-0-104014-8916263311295787431172436062-test-gateway-tls-client-mapping-0"); + testRouteDefinition.setId( + "gatewaywithgrpcfiltertest-0-104014-8916263311295787431172436062-test-gateway-tls-client-mapping-0"); testRouteDefinition.setUri(URI.create("https://localhost:8095")); testRouteDefinition.setOrder(0); testRouteDefinition.setMetadata(Collections.emptyMap()); @@ -187,20 +175,19 @@ public class GatewayControllerEndpointTests { FilterDefinition longFilterDefinition = new FilterDefinition(); FilterDefinition stripPrefix = new FilterDefinition(); stripPrefix.setName("StripPrefix"); - stripPrefix.addArg("_genkey_0","1"); + stripPrefix.addArg("_genkey_0", "1"); longFilterDefinition.setName("JsonToGrpc"); - longFilterDefinition.addArg("_genkey_0","file:src/main/proto/hello.pb"); - longFilterDefinition.addArg("_genkey_1","file:src/main/proto/hello.proto"); - longFilterDefinition.addArg("_genkey_2","HelloService"); - longFilterDefinition.addArg("_genkey_3","hello"); + longFilterDefinition.addArg("_genkey_0", "file:src/main/proto/hello.pb"); + longFilterDefinition.addArg("_genkey_1", "file:src/main/proto/hello.proto"); + longFilterDefinition.addArg("_genkey_2", "HelloService"); + longFilterDefinition.addArg("_genkey_3", "hello"); testRouteDefinition.setFilters(Collections.singletonList(longFilterDefinition)); PredicateDefinition hostRoutePredicateDefinition = new PredicateDefinition(); hostRoutePredicateDefinition.setName("Path"); - hostRoutePredicateDefinition.addArg("_genkey_0","/json/hello"); - testRouteDefinition.setPredicates( - Arrays.asList(hostRoutePredicateDefinition)); + hostRoutePredicateDefinition.addArg("_genkey_0", "/json/hello"); + testRouteDefinition.setPredicates(Arrays.asList(hostRoutePredicateDefinition)); testClient.post().uri("http://localhost:" + port + "/actuator/gateway/routes/test-route") .accept(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(testRouteDefinition)).exchange() diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index 6c46143c..dbb4bd14 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -51,6 +51,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint; import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint; +import org.springframework.cloud.gateway.config.GatewayAutoConfigurationTests.CustomHttpClientFactory.CustomSslConfigurer; import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory; import org.springframework.cloud.gateway.filter.headers.GRPCRequestHeadersFilter; import org.springframework.cloud.gateway.filter.headers.GRPCResponseHeadersFilter; @@ -96,7 +97,7 @@ public class GatewayAutoConfigurationTests { assertThat(factory.connectionProvider.maxConnections()).isEqualTo(Integer.MAX_VALUE); // elastic assertThat(factory.proxyProvider).isNull(); - assertThat(factory.sslConfigured).isFalse(); + assertThat(factory.isSslConfigured()).isFalse(); assertThat(httpClient.configuration().isAcceptGzip()).isFalse(); assertThat(httpClient.configuration().loggingHandler()).isNull(); @@ -147,8 +148,8 @@ public class GatewayAutoConfigurationTests { assertThat(factory.proxyProvider).isNotNull(); assertThat(factory.proxyProvider.build().getAddress().get().getHostName()).isEqualTo("myhost"); - assertThat(factory.sslConfigured).isTrue(); - assertThat(factory.insecureTrustManagerSet).isTrue(); + assertThat(factory.isSslConfigured()).isTrue(); + assertThat(factory.isInsecureTrustManagerSet()).isTrue(); assertThat(context).hasSingleBean(ReactorNettyRequestUpgradeStrategy.class); ReactorNettyRequestUpgradeStrategy upgradeStrategy = context @@ -285,7 +286,7 @@ public class GatewayAutoConfigurationTests { .withPropertyValues("server.http2.enabled=true").run(context -> { assertThat(context).hasSingleBean(HttpClient.class); CustomHttpClientFactory factory = context.getBean(CustomHttpClientFactory.class); - assertThat(factory.insecureTrustManagerSet).isFalse(); + assertThat(factory.isInsecureTrustManagerSet()).isFalse(); }); } @@ -310,25 +311,32 @@ public class GatewayAutoConfigurationTests { @Bean @Primary CustomHttpClientFactory customHttpClientFactory(HttpClientProperties properties, - ServerProperties serverProperties, List customizers) { - return new CustomHttpClientFactory(properties, serverProperties, customizers); + ServerProperties serverProperties, List customizers, + HttpClientSslConfigurer sslConfigurer) { + return new CustomHttpClientFactory(properties, serverProperties, sslConfigurer, customizers); + } + + @Bean + @Primary + CustomSslConfigurer customSslContextFactory(ServerProperties serverProperties, + HttpClientProperties httpClientProperties) { + return new CustomSslConfigurer(httpClientProperties.getSsl(), serverProperties); } } protected static class CustomHttpClientFactory extends HttpClientFactory { - boolean insecureTrustManagerSet; - - boolean sslConfigured; - private ConnectionProvider connectionProvider; private ProxyProvider.Builder proxyProvider; + private CustomSslConfigurer customSslContextFactory; + public CustomHttpClientFactory(HttpClientProperties properties, ServerProperties serverProperties, - List customizers) { - super(properties, serverProperties, customizers); + HttpClientSslConfigurer sslConfigurer, List customizers) { + super(properties, serverProperties, sslConfigurer, customizers); + this.customSslContextFactory = (CustomSslConfigurer) sslConfigurer; } @Override @@ -344,16 +352,37 @@ public class GatewayAutoConfigurationTests { return proxyProvider; } - @Override - protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { - sslConfigured = true; - super.configureSslContext(ssl, sslContextSpec); + public boolean isSslConfigured() { + return customSslContextFactory.sslConfigured; } - @Override - protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { - insecureTrustManagerSet = factory == InsecureTrustManagerFactory.INSTANCE; - super.setTrustManager(sslContextBuilder, factory); + public boolean isInsecureTrustManagerSet() { + return customSslContextFactory.insecureTrustManagerSet; + } + + protected static class CustomSslConfigurer extends HttpClientSslConfigurer { + + boolean sslConfigured; + + boolean insecureTrustManagerSet; + + protected CustomSslConfigurer(HttpClientProperties.Ssl sslProperties, ServerProperties serverProperties) { + super(sslProperties, serverProperties); + } + + @Override + protected void configureSslContext(HttpClientProperties.Ssl ssl, + SslProvider.SslContextSpec sslContextSpec) { + sslConfigured = true; + super.configureSslContext(getSslProperties(), sslContextSpec); + } + + @Override + protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { + insecureTrustManagerSet = factory == InsecureTrustManagerFactory.INSTANCE; + super.setTrustManager(sslContextBuilder, factory); + } + } } diff --git a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt index 3bdf16ff..283c6322 100644 --- a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt +++ b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt @@ -1,127 +1,127 @@ -///* -// * 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.route.builder -// -//import org.junit.Test -//import org.junit.runner.RunWith -//import org.springframework.beans.factory.annotation.Autowired -//import org.springframework.boot.autoconfigure.EnableAutoConfiguration -//import org.springframework.boot.test.context.SpringBootTest -//import org.springframework.cloud.gateway.support.ServerWebExchangeUtils -//import org.springframework.context.annotation.Configuration -//import org.springframework.mock.http.server.reactive.MockServerHttpRequest -//import org.springframework.mock.web.server.MockServerWebExchange -//import org.springframework.test.context.junit4.SpringRunner -//import org.springframework.web.server.ServerWebExchange -//import reactor.core.publisher.toMono -//import reactor.test.StepVerifier -//import java.net.URI -// -//@RunWith(SpringRunner::class) -//@SpringBootTest(classes = arrayOf(Config::class)) -//class RouteDslTests { -// -// @Autowired -// lateinit var builder: RouteLocatorBuilder -// -// @Test -// fun sampleRouteDsl() { -// val routeLocator = builder.routes { -// route(id = "test") { -// host("**.abc.org") and path("/image/png") -// filters { -// addResponseHeader("X-TestHeader", "foobar") -// } -// uri("http://httpbin.org:80") -// } -// -// route(id = "test2") { -// path("/image/webp") or path("/image/anotherone") -// filters { -// addResponseHeader("X-AnotherHeader", "baz") -// addResponseHeader("X-AnotherHeader-2", "baz-2") -// } -// uri("https://httpbin.org:443") -// } -// } -// -// StepVerifier -// .create(routeLocator.routes) -// .expectNextMatches({ -// it.id == "test" && it.filters.size == 1 && it.uri == URI.create("http://httpbin.org:80") -// }) -// .expectNextMatches({ -// it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") -// }) -// .expectComplete() -// .verify() -// -// val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp") -// .header("Host", "test.abc.org").build()) -// -// val filteredRoutes = routeLocator.routes.filter({ -// sampleExchange.attributes.put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, it.id) -// it.predicate.apply(sampleExchange).toMono().block() -// }) -// -// StepVerifier.create(filteredRoutes) -// .expectNextMatches({ -// it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") -// }) -// .expectComplete() -// .verify() -// } -// -// @Test -// fun dslWithFunctionParameters() { -// val routerLocator = builder.routes { -// route(id = "test1", order = 10, uri = "http://httpbin.org") { -// host("**.abc.org") -// } -// route(id = "test2", order = 10, uri = "http://someurl") { -// host("**.abc.org") -// uri("http://override-url") -// } -// } -// -// StepVerifier.create(routerLocator.routes) -// .expectNextMatches({ -// it.id == "test1" && -// it.uri == URI.create("http://httpbin.org:80") && -// it.order == 10 && -// it.predicate.apply(MockServerWebExchange -// .from(MockServerHttpRequest -// .get("/someuri").header("Host", "test.abc.org"))) -// .toMono().block() -// }) -// .expectNextMatches({ -// it.id == "test2" && -// it.uri == URI.create("http://override-url:80") && -// it.order == 10 && -// it.predicate.apply(MockServerWebExchange -// .from(MockServerHttpRequest -// .get("/someuri").header("Host", "test.abc.org"))) -// .toMono().block() -// }) -// .expectComplete() -// .verify() -// } -//} -// -//@Configuration(proxyBeanMethods = false) -//@EnableAutoConfiguration -//open class Config {} \ No newline at end of file +/* + * 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.route.builder + +import org.junit.Test +import org.junit.runner.RunWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils +import org.springframework.context.annotation.Configuration +import org.springframework.mock.http.server.reactive.MockServerHttpRequest +import org.springframework.mock.web.server.MockServerWebExchange +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.web.server.ServerWebExchange +import reactor.core.publisher.toMono +import reactor.test.StepVerifier +import java.net.URI + +@RunWith(SpringRunner::class) +@SpringBootTest(classes = arrayOf(Config::class)) +class RouteDslTests { + + @Autowired + lateinit var builder: RouteLocatorBuilder + + @Test + fun sampleRouteDsl() { + val routeLocator = builder.routes { + route(id = "test") { + host("**.abc.org") and path("/image/png") + filters { + addResponseHeader("X-TestHeader", "foobar") + } + uri("http://httpbin.org:80") + } + + route(id = "test2") { + path("/image/webp") or path("/image/anotherone") + filters { + addResponseHeader("X-AnotherHeader", "baz") + addResponseHeader("X-AnotherHeader-2", "baz-2") + } + uri("https://httpbin.org:443") + } + } + + StepVerifier + .create(routeLocator.routes) + .expectNextMatches({ + it.id == "test" && it.filters.size == 1 && it.uri == URI.create("http://httpbin.org:80") + }) + .expectNextMatches({ + it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") + }) + .expectComplete() + .verify() + + val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp") + .header("Host", "test.abc.org").build()) + + val filteredRoutes = routeLocator.routes.filter({ + sampleExchange.attributes.put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, it.id) + it.predicate.apply(sampleExchange).toMono().block() + }) + + StepVerifier.create(filteredRoutes) + .expectNextMatches({ + it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") + }) + .expectComplete() + .verify() + } + + @Test + fun dslWithFunctionParameters() { + val routerLocator = builder.routes { + route(id = "test1", order = 10, uri = "http://httpbin.org") { + host("**.abc.org") + } + route(id = "test2", order = 10, uri = "http://someurl") { + host("**.abc.org") + uri("http://override-url") + } + } + + StepVerifier.create(routerLocator.routes) + .expectNextMatches({ + it.id == "test1" && + it.uri == URI.create("http://httpbin.org:80") && + it.order == 10 && + it.predicate.apply(MockServerWebExchange + .from(MockServerHttpRequest + .get("/someuri").header("Host", "test.abc.org"))) + .toMono().block() + }) + .expectNextMatches({ + it.id == "test2" && + it.uri == URI.create("http://override-url:80") && + it.order == 10 && + it.predicate.apply(MockServerWebExchange + .from(MockServerHttpRequest + .get("/someuri").header("Host", "test.abc.org"))) + .toMono().block() + }) + .expectComplete() + .verify() + } +} + +@Configuration(proxyBeanMethods = false) +@EnableAutoConfiguration +open class Config {} \ No newline at end of file From 0b58d771024f5d8d56464679d806f1bd30a0f299 Mon Sep 17 00:00:00 2001 From: spencergibb Date: Wed, 31 Aug 2022 11:48:16 -0400 Subject: [PATCH 3/3] Polish gh-2699 Updates HttpClientFactory for backwards compatibility If a user has extended HttpClientFactory and doesn't use the new SslConfigurer, the original protected methods need to be there. Those methods are marked as deprecated. --- .../gateway/config/HttpClientFactory.java | 151 +++++++++++++++--- .../config/GatewayAutoConfigurationTests.java | 110 +++++++++++++ 2 files changed, 240 insertions(+), 21 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java index f2b66576..7f28268d 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java @@ -16,9 +16,18 @@ package org.springframework.cloud.gateway.config; +import java.io.IOException; +import java.net.URL; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchProviderException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import javax.net.ssl.KeyManagerFactory; @@ -26,6 +35,9 @@ import javax.net.ssl.TrustManagerFactory; import io.netty.channel.ChannelOption; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import reactor.netty.http.Http11SslContextSpec; +import reactor.netty.http.Http2SslContextSpec; import reactor.netty.http.HttpProtocol; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpResponseDecoderSpec; @@ -38,6 +50,7 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.util.CollectionUtils; +import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.DISABLED; @@ -64,7 +77,7 @@ public class HttpClientFactory extends AbstractFactoryBean { List customizers) { this.properties = properties; this.serverProperties = serverProperties; - this.sslConfigurer = new HttpClientSslConfigurer(properties.getSsl(), serverProperties); + this.sslConfigurer = null; this.customizers = customizers; } @@ -116,31 +129,45 @@ public class HttpClientFactory extends AbstractFactoryBean { } protected HttpClient configureSsl(HttpClient httpClient) { - return sslConfigurer.configureSsl(httpClient); + if (sslConfigurer != null) { + return sslConfigurer.configureSsl(httpClient); + } + + HttpClientProperties.Ssl ssl = properties.getSsl(); + if ((ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) + || getTrustedX509CertificatesForTrustManager().length > 0 || ssl.isUseInsecureTrustManager()) { + httpClient = httpClient.secure(sslContextSpec -> { + // configure ssl + configureSslContext(ssl, sslContextSpec); + }); + } + return httpClient; } + @Deprecated protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { - sslConfigurer.configureSslContext(ssl, sslContextSpec); - } + SslProvider.ProtocolSslContextSpec clientSslContext = (serverProperties.getHttp2().isEnabled()) + ? Http2SslContextSpec.forClient() : Http11SslContextSpec.forClient(); + clientSslContext.configure(sslContextBuilder -> { + X509Certificate[] trustedX509Certificates = getTrustedX509CertificatesForTrustManager(); + if (trustedX509Certificates.length > 0) { + setTrustManager(sslContextBuilder, trustedX509Certificates); + } + else if (ssl.isUseInsecureTrustManager()) { + setTrustManager(sslContextBuilder, InsecureTrustManagerFactory.INSTANCE); + } - protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { - return sslConfigurer.getTrustedX509CertificatesForTrustManager(); - } + try { + sslContextBuilder.keyManager(getKeyManagerFactory()); + } + catch (Exception e) { + logger.error(e); + } + }); - protected KeyManagerFactory getKeyManagerFactory() { - return sslConfigurer.getKeyManagerFactory(); - } - - protected KeyStore createKeyStore() { - return sslConfigurer.createKeyStore(); - } - - protected void setTrustManager(SslContextBuilder sslContextBuilder, X509Certificate... trustedX509Certificates) { - sslConfigurer.setTrustManager(sslContextBuilder, trustedX509Certificates); - } - - protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { - sslConfigurer.setTrustManager(sslContextBuilder, factory); + sslContextSpec.sslContext(clientSslContext).handshakeTimeout(ssl.getHandshakeTimeout()) + .closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout()) + .closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout()); } private HttpClient applyCustomizers(HttpClient httpClient) { @@ -165,6 +192,88 @@ public class HttpClientFactory extends AbstractFactoryBean { return httpClient; } + @Deprecated + protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { + HttpClientProperties.Ssl ssl = properties.getSsl(); + + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + ArrayList allCerts = new ArrayList<>(); + for (String trustedCert : ssl.getTrustedX509Certificates()) { + try { + URL url = ResourceUtils.getURL(trustedCert); + Collection certs = certificateFactory.generateCertificates(url.openStream()); + allCerts.addAll(certs); + } + catch (IOException e) { + throw new RuntimeException("Could not load certificate '" + trustedCert + "'", e); + } + } + return allCerts.toArray(new X509Certificate[allCerts.size()]); + } + catch (CertificateException e1) { + throw new RuntimeException("Could not load CertificateFactory X.509", e1); + } + } + + @Deprecated + protected KeyManagerFactory getKeyManagerFactory() { + HttpClientProperties.Ssl ssl = properties.getSsl(); + try { + if (ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) { + KeyManagerFactory keyManagerFactory = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + char[] keyPassword = ssl.getKeyPassword() != null ? ssl.getKeyPassword().toCharArray() : null; + + if (keyPassword == null && ssl.getKeyStorePassword() != null) { + keyPassword = ssl.getKeyStorePassword().toCharArray(); + } + + keyManagerFactory.init(this.createKeyStore(), keyPassword); + + return keyManagerFactory; + } + + return null; + } + catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Deprecated + protected KeyStore createKeyStore() { + HttpClientProperties.Ssl ssl = properties.getSsl(); + try { + KeyStore store = ssl.getKeyStoreProvider() != null + ? KeyStore.getInstance(ssl.getKeyStoreType(), ssl.getKeyStoreProvider()) + : KeyStore.getInstance(ssl.getKeyStoreType()); + try { + URL url = ResourceUtils.getURL(ssl.getKeyStore()); + store.load(url.openStream(), + ssl.getKeyStorePassword() != null ? ssl.getKeyStorePassword().toCharArray() : null); + } + catch (Exception e) { + throw new RuntimeException("Could not load key store ' " + ssl.getKeyStore() + "'", e); + } + + return store; + } + catch (KeyStoreException | NoSuchProviderException e) { + throw new RuntimeException("Could not load KeyStore for given type and provider", e); + } + } + + @Deprecated + protected void setTrustManager(SslContextBuilder sslContextBuilder, X509Certificate... trustedX509Certificates) { + sslContextBuilder.trustManager(trustedX509Certificates); + } + + @Deprecated + protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { + sslContextBuilder.trustManager(factory); + } + protected ProxyProvider.Builder configureProxyProvider(HttpClientProperties.Proxy proxy, ProxyProvider.TypeSpec proxySpec) { ProxyProvider.Builder builder = proxySpec.type(proxy.getType()).host(proxy.getHost()); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index dbb4bd14..32a3a5df 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -18,9 +18,12 @@ package org.springframework.cloud.gateway.config; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.security.KeyStore; +import java.security.cert.X509Certificate; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; import io.netty.channel.ChannelOption; @@ -164,6 +167,28 @@ public class GatewayAutoConfigurationTests { }); } + @Test + @Deprecated + public void nettyHttpClientNoSslConfigurerIsBackwardsCompatible() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class, + NoSslConfigurerCustomHttpClientFactoryConfig.class)) + .withPropertyValues("spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager=true") + .run(context -> { + assertThat(context).hasSingleBean(HttpClient.class); + NoSslConfigurerHttpClientFactory factory = context.getBean(NoSslConfigurerHttpClientFactory.class); + + assertThat(factory.configureSslCalled).isTrue(); + assertThat(factory.configureSslContextCalled).isTrue(); + assertThat(factory.getTrustedX509CertificatesForTrustManagerCalled).isTrue(); + assertThat(factory.getKeyManagerFactoryCalled).isTrue(); + assertThat(factory.createKeyStoreCalled).isFalse(); + assertThat(factory.setTrustManagerCertCalled).isFalse(); + assertThat(factory.setTrustManagerFactoryCalled).isTrue(); + }); + } + @Test public void verboseActuatorEnabledByDefault() { try (ConfigurableApplicationContext ctx = SpringApplication.run(Config.class, "--spring.jmx.enabled=false", @@ -325,6 +350,21 @@ public class GatewayAutoConfigurationTests { } + @Configuration + @EnableConfigurationProperties(ServerProperties.class) + @AutoConfigureBefore(GatewayAutoConfiguration.class) + @Deprecated + protected static class NoSslConfigurerCustomHttpClientFactoryConfig { + + @Bean + @Primary + NoSslConfigurerHttpClientFactory noSslConfigurerHttpClientFactory(HttpClientProperties properties, + ServerProperties serverProperties, List customizers) { + return new NoSslConfigurerHttpClientFactory(properties, serverProperties, customizers); + } + + } + protected static class CustomHttpClientFactory extends HttpClientFactory { private ConnectionProvider connectionProvider; @@ -387,6 +427,76 @@ public class GatewayAutoConfigurationTests { } + /* + * Class to test backwards compatibility if no `SslConfigurer` used. + */ + @Deprecated + protected static class NoSslConfigurerHttpClientFactory extends HttpClientFactory { + + boolean configureSslCalled; + + boolean configureSslContextCalled; + + boolean getTrustedX509CertificatesForTrustManagerCalled; + + boolean getKeyManagerFactoryCalled; + + boolean createKeyStoreCalled; + + boolean setTrustManagerCertCalled; + + boolean setTrustManagerFactoryCalled; + + public NoSslConfigurerHttpClientFactory(HttpClientProperties properties, ServerProperties serverProperties, + List customizers) { + super(properties, serverProperties, customizers); + } + + @Override + protected HttpClient configureSsl(HttpClient httpClient) { + configureSslCalled = true; + return super.configureSsl(httpClient); + } + + @Override + protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { + configureSslContextCalled = true; + super.configureSslContext(ssl, sslContextSpec); + } + + @Override + protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { + getTrustedX509CertificatesForTrustManagerCalled = true; + return super.getTrustedX509CertificatesForTrustManager(); + } + + @Override + protected KeyManagerFactory getKeyManagerFactory() { + getKeyManagerFactoryCalled = true; + return super.getKeyManagerFactory(); + } + + @Override + protected KeyStore createKeyStore() { + createKeyStoreCalled = true; + return super.createKeyStore(); + } + + @Override + protected void setTrustManager(SslContextBuilder sslContextBuilder, + X509Certificate... trustedX509Certificates) { + setTrustManagerCertCalled = true; + super.setTrustManager(sslContextBuilder, trustedX509Certificates); + } + + @Override + protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { + setTrustManagerFactoryCalled = true; + super.setTrustManager(sslContextBuilder, factory); + } + + } + @EnableAutoConfiguration @SpringBootConfiguration protected static class Config {