spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.49k stars 3.3k forks source link

spring-cloud-gateway grpc supports H2C #2474

Open jianmo1997 opened 2 years ago

jianmo1997 commented 2 years ago

Our business scenarios need to support non-SSL mode transmission,That means we need to support H2C

Could you give us some suggestions and tell us how to modify so that spring-cloud-gateway can support H2C?

Many thanks

nkonev commented 2 years ago

I was able to configure gateway to do both HTTP1.1 & H2C:

@Configuration
public class H2CConfiguration {

    // https://projectreactor.io/docs/netty/release/reference/index.html#_http2
    @Bean
    public NettyWebServerFactoryCustomizer h2cServerCustomizer(Environment environment, ServerProperties serverProperties) {
        return new NettyWebServerFactoryCustomizer(environment, serverProperties) {
            @Override
            public void customize(NettyReactiveWebServerFactory factory) {
                factory.addServerCustomizers(httpServer -> httpServer.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C));
                super.customize(factory);
            }
        };
    }
}
@Component
public class H2CAwareNettyRoutingFilter extends NettyRoutingFilter {

    public H2CAwareNettyRoutingFilter(HttpClient httpClient, ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider, HttpClientProperties properties) {
        super(httpClient, headersFiltersProvider, properties);
    }

    @Override
    protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) {
        HttpClient httpClient = super.getHttpClient(route, exchange);

        boolean h2cRoute = ofNullable(route.getMetadata().get("h2c"))
                .map(o -> (Boolean)o)
                .orElse(false);
        if (h2cRoute) {
            // https://projectreactor.io/docs/netty/release/reference/index.html#_http2_2
            return httpClient.protocol(HttpProtocol.H2C);
        } else {
            return httpClient;
        }
    }

    @Override
    public int getOrder() {
        return super.getOrder() - 1;
    }
}
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - predicates:
            - Path=/h2c
          uri: http://127.0.0.1:8088
          metadata:
            h2c: true
        - predicates:
            - Path=/http
          uri: http://127.0.0.1:8089

See here for full project.

I also tried to set both HTTP1.1 & H2C in client

@Bean
    public HttpClientCustomizer h2ClientCustomizer() {
        return httpClient -> httpClient.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C);
    }

but it isn't working, seems it requires to dig deeper in reactor-netty

sodaRyCN commented 2 years ago

we also encountered the same problem (we both set h2c and h1 in spring cloud gateway , and use grpc server) and got answers from relevant communities: grpc-java reactor-netty

so we first make it clear that there are four protocl:

  1. h2 with tls/ssl;
  2. h2 without tls/ssl;
  3. Negotiable H2,no tls/ssl
  4. http1.1

for protocol No.3 it is a negotiation protocol based on http1.1 and just carries keywords such as upgrade in some positions such as the header.

And it's clear from https://projectreactor.io/docs/netty/release/reference/index.html#_protocol_selection_2, for reactor-netty

in some doc and our test case about protocol No3 :

we reslove this problem like @nkonev's code, but we define two independent clients instead of modifying the protocol when using:one client set h1 protocol ,and other one set h2c protocol. decide which client to use according to the header (H2 has a unique header) or business-related routing, etc.

by the way, I suspectused like this, it may cause the code return httpClient; use of H2 protocol if httpclient base on pool if (h2cRoute) { // https://projectreactor.io/docs/netty/release/reference/index.html#_http2_2 return httpClient.protocol(HttpProtocol.H2C); } else { return httpClient; }

jasonhao518 commented 1 year ago

@sodaRyCN I'm facing the same issue, can you provide some code sample for that ?

abelsromero commented 1 year ago

Analogous configurations for what is supported on the server side could be added for client site. Right now, only H2 is supported because ssl is assumed https://github.com/spring-cloud/spring-cloud-gateway/blob/be2abff70ea68b2bc7a1bd75bc6e0af74f45e9b2/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java#L86.

sodaRyCN commented 1 year ago

@jasonhao518 u cloud refer to the following code, if you have a better implementation, please share. Also strongly recommend reading my previous review carefully.

@Configuration(proxyBeanMethods = false)
//Indicates that the scg server supports H2 protocol without tls
@ConditionalOnExpression("${server.http2.enabled:false} && !${server.ssl.enabled:true}")
@ConditionalOnClass({DispatcherHandler.class})
@AutoConfigureAfter({GatewayNoLoadBalancerClientAutoConfiguration.class)
public class GrpcAutoConfiguration {
        @Bean("grpcHttpClientCustomizer")
    public GrpcHttpClientCustomizer grpcHttpClientCustomizer(
            KeeperClientEventLoop clientEventLoopForGrpc) {
        return httpClient -> httpClient.protocol(HttpProtocol.H2C);
    }

    @Bean("grpcHttpClient")
    public GrpcHttpClient grpcHttpClient(HttpClientProperties properties, GrpcHttpClientCustomizer grpcHttpClientCustomizer) {
        return GrpcHttpClient.GrpcHttpClientBuilder.config(properties, grpcHttpClientCustomizer).create();
    }

        @Bean
    @ConditionalOnMissingClass
    public GrpcRoutingFilter grpcRoutingFilter(GrpcHttpClient grpcHttpClient,
                                               ObjectProvider<List<HttpHeadersFilter>> headersFilters,
                                               HttpClientProperties properties) {
        return new GrpcRoutingFilter(grpcHttpClient, headersFilters, properties);
    }

    @Bean("grpcRouteBuilder")
    public RouteLocatorBuilder.Builder grpcRouteBuilder(RouteLocatorBuilder builder, GrpcRoutingFilter grpcRoutingFilter) {
                return builder.routes()
              .route("grpc", r -> r.header("Content-Type", "^(application/grpc)( *;.*)*?$")
                    .filters(f -> f.filters(grpcRoutingFilter))
                    .uri("xxxx"));
    }

    @Bean("grpcRouteLocator")
    public RouteLocator routeLocator(RouteLocatorBuilder.Builder grpcRouteBuilder) {
        return grpcRouteBuilder.build();
    }
}

public class GrpcRoutingFilter implements GatewayFilter, Ordered {
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ....
        }
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({DispatcherHandler.class})
@AutoConfigureAfter({GatewayNoLoadBalancerClientAutoConfiguration.class)
@EnableWebFluxSecurity
public class HttpAutoConfiguration {
        @Bean
    public HttpClientCustomizer httpClientCustomizer(KeeperClientEventLoop clientEventLoop) {
        return httpClient -> httpClient.protocol(HttpProtocol.HTTP11);
    }

        @Bean("baseRouteBuilder")
    public RouteLocatorBuilder.Builder baseRouteBuilder(RouteLocatorBuilder builder, NettyRoutingFilter nettyRoutingFilter) {
        return builder.routes()
              .route("http", r -> r.path("/xx/xx").filters(f -> f.filters(nettyRoutingFilter))
                    .uri("xxx"));

    }

        @Bean("baseRouteLocator")
    public RouteLocator routeLocator(RouteLocatorBuilder.Builder baseRouteBuilder) {
        return baseRouteBuilder.build();
    }
}

public class NettyRoutingFilter implements GatewayFilter, Ordered {
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ....
        }
}
hengyunabc commented 11 months ago

There is a simple solution, although it is not very reasonable.

  1. Copy the source code of org.springframework.cloud.gateway.config.HttpClientFactory to your project, making sure it is in the same package.
  2. Refer to the modifications below, which only support H2C.
  3. Since the target/classes of the application take precedence over the jar, modifying the version of HttpClientFactory is more prioritized.
    @Override
    protected HttpClient createInstance() {
        // configure pool resources
        ConnectionProvider connectionProvider = buildConnectionProvider(properties);

        HttpClient httpClient = HttpClient.create(connectionProvider)
                // TODO: move customizations to HttpClientCustomizers
                .httpResponseDecoder(this::httpResponseDecoder);

        if (serverProperties.getHttp2().isEnabled()) {
            httpClient = httpClient.protocol(HttpProtocol.HTTP11, HttpProtocol.H2);
        }

        if (properties.getConnectTimeout() != null) {
            httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getConnectTimeout());
        }

        httpClient = configureProxy(httpClient);

        httpClient = configureSsl(httpClient);

change into:

    @Override
    protected HttpClient createInstance() {
        // configure pool resources
        ConnectionProvider connectionProvider = buildConnectionProvider(properties);

        HttpClient httpClient = HttpClient.create(connectionProvider)
                // TODO: move customizations to HttpClientCustomizers
                .httpResponseDecoder(this::httpResponseDecoder);

        if (serverProperties.getHttp2().isEnabled()) {
            httpClient = httpClient.protocol(HttpProtocol.H2C);
        }

        if (properties.getConnectTimeout() != null) {
            httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getConnectTimeout());
        }

        httpClient = configureProxy(httpClient);

//      httpClient = configureSsl(httpClient);
liorderei commented 1 month ago

I was able to make it work as @nkonev suggested, is there still no OOTB solution for this?