Common trait for tonic client/channel

222 views Asked by At

Disclaimer: I'm starting to learn Rust and trying to do something real I'm working on some gRPC clients using tonic.

The main question is that I'd like to create a client based on a channel which might change depending on the configuration, e.g. connecting over HTTP or HTTPS, using per-request authentication or not or even connecting through a HTTP Proxy of a Socks proxy.

It seems that each of those connections will generate a client of a different type (qualified by different type parameters).

I'd like to store this client (or to store the inner channel for it). I'm relunctant to parameterize where I store it, because I guess I would just be shifting the problem to some other place.

I'd tried using GrpcService as the type for the underlying channel, but I couldn't figure out all the parameters that need to be provided, which, in some cases seems to refer to crate-private types.

I'd tried using an enum type with the different channel types for the different branch. I understand that the next thing would be to implement the appropriate trait (GrpcService?) for the enum type.... but again, it is difficult to figure out all the parameters required.

Perhaps, I'm overcomplicating things in creating the clients. I've placed in https://replit.com/@j-santander/TonicConnections the full code for the examples below:

HTTP

    let channel = Client::builder().http2_only(true).build_http();
    let mut client = HelloServiceClient::with_origin(
        channel,
        Uri::from_maybe_shared(format!("http://{addr}", addr = args.addr))?,
    );

This probably can be simplified further, but for this exercise I wanted to separate the client and the channel.

Channel type is Client<HttpConnector, BoxBody>

HTTPS

Here, since I wanted to support connecting to self-signed certificates, things start to be complicated, since the default channel doesn't provide access to the dangerous part of the TLS configuration

    let roots = RootCertStore::empty();

    let mut tls = ClientConfig::builder()
        .with_safe_defaults()
        .with_root_certificates(roots)
        .with_no_client_auth();

    tls.dangerous()
        .set_certificate_verifier(Arc::new(NoCertificateVerification {}));

    let connector = hyper_rustls::HttpsConnectorBuilder::new()
        .with_tls_config(tls)
        .https_or_http()
        .enable_http2()
        .build();

    let channel = Client::builder().build(connector.clone());
    let mut client = HelloServiceClient::with_origin(
        channel,
        Uri::from_maybe_shared(format!("https://{addr}", addr = args.addr))?,
    );

Channel type is Client<HttpsConnector<HttpConnector>, BoxBody>

Request Authentication

It seems that request authentication requires an interceptor. Well, this is a different type:

    let roots = RootCertStore::empty();

    let mut tls = ClientConfig::builder()
        .with_safe_defaults()
        .with_root_certificates(roots)
        .with_no_client_auth();

    tls.dangerous()
        .set_certificate_verifier(Arc::new(NoCertificateVerification {}));

    let connector = hyper_rustls::HttpsConnectorBuilder::new()
        .with_tls_config(tls)
        .https_or_http()
        .enable_http2()
        .build();

    let inner = Client::builder().build(connector.clone());

    let channel = InterceptedService::new(inner, move |mut req: Request<()>| {
        req.metadata_mut()
            .insert("username", args.username.parse().unwrap());
        req.metadata_mut()
            .insert("password", args.password.parse().unwrap());
        req.metadata_mut()
            .insert("secure", "false".parse().unwrap());
        Ok(req)
    });

    let mut client = HelloServiceClient::with_origin(
        channel,
        Uri::from_maybe_shared(format!("https://{addr}", addr = args.addr))?,
    );

Channel type is InterceptedService<Client<HttpsConnector<HttpConnector>, BoxBody>, fn(Request<()>) -> Result<Request<()>, Status>>

Socks Proxy

I tried to create a plain HTTP Proxy example, but I couldn't make it work (perhaps the proxies I'm using do not properly support HTTP2 or I'm misconfiguring them). I had more luck with socks-proxy:

    let mut lower_connector = HttpConnector::new();
    lower_connector.enforce_http(false);
    let proxy_connector = SocksConnector {
        proxy_addr: args.proxy.parse::<Uri>().unwrap(),
        auth: None,
        connector: lower_connector,
    };

    let roots = RootCertStore::empty();

    let mut tls = ClientConfig::builder()
        .with_safe_defaults()
        .with_root_certificates(roots)
        .with_no_client_auth();

    tls.dangerous()
        .set_certificate_verifier(Arc::new(NoCertificateVerification {}));

    let connector = hyper_rustls::HttpsConnectorBuilder::new()
        .with_tls_config(tls)
        .https_or_http()
        .enable_http2()
        .wrap_connector(proxy_connector);

    let inner = Client::builder().build(connector.clone());

    let channel = InterceptedService::new(inner, move |mut req: Request<()>| {
        req.metadata_mut()
            .insert("username", args.username.parse().unwrap());
        req.metadata_mut()
            .insert("password", args.password.parse().unwrap());
        req.metadata_mut()
            .insert("secure", "false".parse().unwrap());
        Ok(req)
    });

    let mut client = HelloServiceClient::with_origin(
        channel,
        Uri::from_maybe_shared(format!("https://{addr}", addr = args.addr))?,
    );

Channel type is InterceptedService<Client<HttpsConnector<SocksConnector<HttpConnector>>, BoxBody>, fn(Request<()>) -> Result<Request<()>, Status>>

So, as a summary:

  • How do I define a variable to hold client or channel in the above examples? What is the recommended approach for this sort of task?
  • Any advice in simplifying the different clients I've shown above?
0

There are 0 answers