Skip to main content

geode_client/
grpc.rs

1//! gRPC transport implementation for Geode.
2//!
3//! This module provides gRPC client functionality using the tonic-generated
4//! `GeodeServiceClient` from `crate::proto::geode_service_client`.
5
6use std::collections::HashMap;
7use std::fs;
8use std::future::Future;
9use std::net::IpAddr;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::task::{Context, Poll};
13
14use http::Uri;
15use hyper_util::rt::TokioIo;
16use tokio::net::TcpStream;
17use tokio_rustls::TlsConnector as RustlsConnector;
18use tonic::Request;
19use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint};
20use tower_service::Service;
21
22use crate::client::{Column, Page};
23use crate::dsn::Dsn;
24use crate::error::{Error, Result};
25use crate::proto;
26use crate::proto::execution_response::Payload;
27use crate::proto::geode_service_client::GeodeServiceClient;
28use crate::types::Value;
29
30#[derive(Debug)]
31struct SkipServerVerification;
32
33impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
34    fn verify_server_cert(
35        &self,
36        _end_entity: &rustls::pki_types::CertificateDer<'_>,
37        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
38        _server_name: &rustls::pki_types::ServerName<'_>,
39        _ocsp_response: &[u8],
40        _now: rustls::pki_types::UnixTime,
41    ) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
42        Ok(rustls::client::danger::ServerCertVerified::assertion())
43    }
44
45    fn verify_tls12_signature(
46        &self,
47        _message: &[u8],
48        _cert: &rustls::pki_types::CertificateDer<'_>,
49        _dss: &rustls::DigitallySignedStruct,
50    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
51        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
52    }
53
54    fn verify_tls13_signature(
55        &self,
56        _message: &[u8],
57        _cert: &rustls::pki_types::CertificateDer<'_>,
58        _dss: &rustls::DigitallySignedStruct,
59    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
60        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
61    }
62
63    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
64        vec![
65            rustls::SignatureScheme::RSA_PKCS1_SHA256,
66            rustls::SignatureScheme::RSA_PKCS1_SHA384,
67            rustls::SignatureScheme::RSA_PKCS1_SHA512,
68            rustls::SignatureScheme::RSA_PSS_SHA256,
69            rustls::SignatureScheme::RSA_PSS_SHA384,
70            rustls::SignatureScheme::RSA_PSS_SHA512,
71            rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
72            rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
73            rustls::SignatureScheme::ED25519,
74        ]
75    }
76}
77
78#[derive(Clone)]
79struct InsecureTlsConnector {
80    config: Arc<rustls::ClientConfig>,
81    server_name: rustls::pki_types::ServerName<'static>,
82}
83
84impl Service<Uri> for InsecureTlsConnector {
85    type Response = TokioIo<tokio_rustls::client::TlsStream<TcpStream>>;
86    type Error = std::io::Error;
87    type Future =
88        Pin<Box<dyn Future<Output = std::result::Result<Self::Response, Self::Error>> + Send>>;
89
90    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
91        Poll::Ready(Ok(()))
92    }
93
94    fn call(&mut self, uri: Uri) -> Self::Future {
95        let addr = match (uri.host(), uri.port_u16()) {
96            (Some(host), Some(port)) => format!("{host}:{port}"),
97            (Some(host), None) => format!("{host}:443"),
98            _ => {
99                return Box::pin(async {
100                    Err(std::io::Error::new(
101                        std::io::ErrorKind::InvalidInput,
102                        "missing host in URI",
103                    ))
104                });
105            }
106        };
107        let connector = RustlsConnector::from(self.config.clone());
108        let server_name = self.server_name.clone();
109
110        Box::pin(async move {
111            let stream = TcpStream::connect(addr).await?;
112            stream.set_nodelay(true)?;
113            let tls = connector
114                .connect(server_name, stream)
115                .await
116                .map_err(std::io::Error::other)?;
117            Ok(TokioIo::new(tls))
118        })
119    }
120}
121
122/// gRPC client for Geode.
123///
124/// Provides gRPC-based connection to the Geode database server using the
125/// tonic-generated service client.
126pub struct GrpcClient {
127    dsn: Dsn,
128    session_id: String,
129}
130
131impl GrpcClient {
132    async fn connect_channel(dsn: &Dsn) -> Result<Channel> {
133        // tonic/rustls TLS setup requires a process-wide crypto provider.
134        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
135
136        let addr = if dsn.tls_enabled() && !dsn.skip_verify() {
137            format!("https://{}", dsn.address())
138        } else {
139            format!("http://{}", dsn.address())
140        };
141
142        let mut endpoint = Endpoint::from_shared(addr.clone())
143            .map_err(|e| Error::connection(format!("Invalid endpoint: {}", e)))?;
144
145        let tls_server_name =
146            dsn.server_name()
147                .map(str::to_string)
148                .or_else(|| match dsn.host().parse::<IpAddr>() {
149                    Ok(_) => Some("localhost".to_string()),
150                    Err(_) => None,
151                });
152
153        if dsn.tls_enabled() {
154            if let Some(server_name) = &tls_server_name {
155                let origin = format!("https://{}:{}", server_name, dsn.port())
156                    .parse()
157                    .map_err(|e| Error::tls(format!("Invalid TLS origin: {}", e)))?;
158                endpoint = endpoint.origin(origin);
159            }
160        }
161
162        if dsn.tls_enabled() && dsn.skip_verify() {
163            let mut client_config = rustls::ClientConfig::builder()
164                .dangerous()
165                .with_custom_certificate_verifier(Arc::new(SkipServerVerification))
166                .with_no_client_auth();
167            client_config.alpn_protocols.push(b"h2".to_vec());
168
169            let server_name = tls_server_name
170                .unwrap_or_else(|| dsn.host().to_string())
171                .try_into()
172                .map_err(|e| Error::tls(format!("Invalid TLS server name: {}", e)))?;
173
174            endpoint
175                .connect_with_connector(InsecureTlsConnector {
176                    config: Arc::new(client_config),
177                    server_name,
178                })
179                .await
180                .map_err(|e| {
181                    Error::connection(format!(
182                        "gRPC connection failed to {}: {} ({:?})",
183                        addr, e, e
184                    ))
185                })
186        } else if dsn.tls_enabled() {
187            let mut tls = ClientTlsConfig::new()
188                .with_enabled_roots()
189                .assume_http2(true);
190
191            if let Some(server_name) = tls_server_name {
192                tls = tls.domain_name(server_name);
193            }
194
195            if let Some(ca_cert_path) = dsn.ca_cert() {
196                let ca_pem = fs::read(ca_cert_path).map_err(|e| {
197                    Error::tls(format!(
198                        "Failed to read CA certificate {}: {}",
199                        ca_cert_path, e
200                    ))
201                })?;
202                tls = tls.ca_certificate(Certificate::from_pem(ca_pem));
203            }
204
205            endpoint
206                .tls_config(tls)
207                .map_err(|e| Error::tls(format!("TLS config error: {}", e)))?
208                .connect()
209                .await
210                .map_err(|e| {
211                    Error::connection(format!(
212                        "gRPC connection failed to {}: {} ({:?})",
213                        addr, e, e
214                    ))
215                })
216        } else {
217            endpoint.connect().await.map_err(|e| {
218                Error::connection(format!(
219                    "gRPC connection failed to {}: {} ({:?})",
220                    addr, e, e
221                ))
222            })
223        }
224    }
225
226    async fn connect_service(dsn: &Dsn) -> Result<GeodeServiceClient<Channel>> {
227        let channel = Self::connect_channel(dsn).await?;
228        Ok(GeodeServiceClient::new(channel))
229    }
230
231    /// Connect to a Geode server using gRPC.
232    ///
233    /// # Arguments
234    ///
235    /// * `dsn` - Parsed DSN with gRPC transport
236    ///
237    /// # Example
238    ///
239    /// ```no_run
240    /// use geode_client::dsn::Dsn;
241    /// use geode_client::grpc::GrpcClient;
242    ///
243    /// # async fn example() -> geode_client::Result<()> {
244    /// let dsn = Dsn::parse("grpc://localhost:50051")?;
245    /// let client = GrpcClient::connect(&dsn).await?;
246    /// # Ok(())
247    /// # }
248    /// ```
249    pub async fn connect(dsn: &Dsn) -> Result<Self> {
250        let mut grpc_client = Self::connect_service(dsn).await?;
251        let session_id = Self::handshake(
252            &mut grpc_client,
253            dsn.username(),
254            dsn.password(),
255            dsn.graph(),
256        )
257        .await?;
258
259        Ok(Self {
260            dsn: dsn.clone(),
261            session_id,
262        })
263    }
264
265    /// Perform authentication handshake.
266    async fn handshake(
267        client: &mut GeodeServiceClient<Channel>,
268        username: Option<&str>,
269        password: Option<&str>,
270        graph: Option<&str>,
271    ) -> Result<String> {
272        let request = proto::HelloRequest {
273            username: username.unwrap_or("").to_string(),
274            password: password.unwrap_or("").to_string(),
275            tenant_id: None,
276            client_name: "geode-rust".to_string(),
277            client_version: crate::VERSION.to_string(),
278            wanted_conformance: "minimum".to_string(),
279            graph: graph.map(String::from),
280        };
281
282        let response = client
283            .handshake(Request::new(request))
284            .await
285            .map_err(|e| Error::connection(format!("Handshake failed: {}", e)))?;
286
287        let resp = response.into_inner();
288        if !resp.success {
289            return Err(Error::auth(resp.error_message));
290        }
291
292        Ok(resp.session_id)
293    }
294
295    /// Execute a GQL query.
296    pub async fn query(&mut self, gql: &str) -> Result<(Page, Option<String>)> {
297        self.query_with_params(gql, &HashMap::new()).await
298    }
299
300    /// Execute a GQL query with parameters.
301    pub async fn query_with_params(
302        &mut self,
303        gql: &str,
304        params: &HashMap<String, Value>,
305    ) -> Result<(Page, Option<String>)> {
306        let proto_params: Vec<proto::Param> = params
307            .iter()
308            .map(|(k, v)| proto::Param {
309                name: k.clone(),
310                value: Some(v.to_proto_value()),
311            })
312            .collect();
313
314        let request = proto::ExecuteRequest {
315            session_id: self.session_id.clone(),
316            query: gql.to_string(),
317            params: proto_params,
318        };
319
320        // The current server can leave tonic channels in a bad state after
321        // previous RPCs. Use a fresh channel per RPC and keep only the
322        // server-side session stable via session_id.
323        let mut client = Self::connect_service(&self.dsn).await?;
324        let response = client
325            .execute(Request::new(request))
326            .await
327            .map_err(|e| Error::query(format!("Query execution failed: {}", e)))?;
328
329        // Process streaming response
330        let mut stream = response.into_inner();
331        let mut columns = Vec::new();
332        let mut rows = Vec::new();
333        let mut final_page = true;
334        let mut ordered = false;
335        let mut order_keys = Vec::new();
336
337        while let Some(exec_resp) = stream
338            .message()
339            .await
340            .map_err(|e| Error::query(format!("Failed to read response: {}", e)))?
341        {
342            if let Some(payload) = exec_resp.payload {
343                match payload {
344                    Payload::Schema(schema) => {
345                        columns = schema
346                            .columns
347                            .into_iter()
348                            .map(|c| Column {
349                                name: c.name,
350                                col_type: c.r#type,
351                            })
352                            .collect();
353                    }
354                    Payload::Page(page) => {
355                        for row in page.rows {
356                            let mut row_map = HashMap::new();
357                            for (i, col) in columns.iter().enumerate() {
358                                let value = if i < row.values.len() {
359                                    crate::convert::proto_to_value(&row.values[i])
360                                } else {
361                                    Value::null()
362                                };
363                                row_map.insert(col.name.clone(), value);
364                            }
365                            rows.push(row_map);
366                        }
367                        final_page = page.r#final;
368                        ordered = page.ordered;
369                        order_keys = page.order_keys;
370                    }
371                    Payload::Error(err) => {
372                        return Err(Error::Query {
373                            code: err.code,
374                            message: err.message,
375                        });
376                    }
377                    Payload::Metrics(_) | Payload::Heartbeat(_) => {
378                        // Informational payloads, continue
379                    }
380                    Payload::Explain(_) | Payload::Profile(_) => {
381                        // Plan/profile payloads, continue
382                    }
383                }
384            }
385        }
386
387        Ok((
388            Page {
389                columns,
390                rows,
391                ordered,
392                order_keys,
393                final_page,
394            },
395            None,
396        ))
397    }
398
399    /// Begin a transaction.
400    pub async fn begin(&mut self) -> Result<()> {
401        let request = proto::BeginRequest {
402            read_only: false,
403            session_id: self.session_id.clone(),
404        };
405
406        let mut client = Self::connect_service(&self.dsn).await?;
407        client
408            .begin(Request::new(request))
409            .await
410            .map_err(|e| Error::connection(format!("Begin transaction failed: {}", e)))?;
411
412        Ok(())
413    }
414
415    /// Commit a transaction.
416    pub async fn commit(&mut self) -> Result<()> {
417        let request = proto::CommitRequest {
418            session_id: self.session_id.clone(),
419        };
420
421        let mut client = Self::connect_service(&self.dsn).await?;
422        client
423            .commit(Request::new(request))
424            .await
425            .map_err(|e| Error::connection(format!("Commit failed: {}", e)))?;
426
427        Ok(())
428    }
429
430    /// Rollback a transaction.
431    pub async fn rollback(&mut self) -> Result<()> {
432        let request = proto::RollbackRequest {
433            session_id: self.session_id.clone(),
434        };
435
436        let mut client = Self::connect_service(&self.dsn).await?;
437        client
438            .rollback(Request::new(request))
439            .await
440            .map_err(|e| Error::connection(format!("Rollback failed: {}", e)))?;
441
442        Ok(())
443    }
444
445    /// Create a savepoint within the current transaction.
446    ///
447    /// Note: Savepoints are not yet supported via the gRPC service definition.
448    /// This method will return an error until the gRPC service is updated.
449    pub async fn savepoint(&mut self, _name: &str) -> Result<()> {
450        Err(Error::connection(
451            "savepoint is not yet supported via gRPC transport",
452        ))
453    }
454
455    /// Roll back to a previously created savepoint.
456    ///
457    /// Note: Rollback-to-savepoint is not yet supported via the gRPC service definition.
458    /// This method will return an error until the gRPC service is updated.
459    pub async fn rollback_to(&mut self, _name: &str) -> Result<()> {
460        Err(Error::connection(
461            "rollback_to is not yet supported via gRPC transport",
462        ))
463    }
464
465    /// Send a ping request.
466    pub async fn ping(&mut self) -> Result<bool> {
467        let mut client = Self::connect_service(&self.dsn).await?;
468        let response = client
469            .ping(Request::new(proto::PingRequest {}))
470            .await
471            .map_err(|e| Error::connection(format!("Ping failed: {}", e)))?;
472
473        Ok(response.into_inner().ok)
474    }
475
476    /// Close the connection.
477    pub fn close(&mut self) -> Result<()> {
478        // gRPC channels are automatically closed when dropped
479        Ok(())
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use crate::proto;
486
487    #[test]
488    fn test_convert_proto_value_string() {
489        let proto_val = proto::Value {
490            kind: Some(proto::value::Kind::StringVal(proto::StringValue {
491                value: "hello".to_string(),
492                kind: 0,
493            })),
494        };
495        let val = crate::convert::proto_to_value(&proto_val);
496        assert_eq!(val.as_string().unwrap(), "hello");
497    }
498
499    #[test]
500    fn test_convert_proto_value_int() {
501        let proto_val = proto::Value {
502            kind: Some(proto::value::Kind::IntVal(proto::IntValue {
503                value: 42,
504                kind: 0,
505            })),
506        };
507        let val = crate::convert::proto_to_value(&proto_val);
508        assert_eq!(val.as_int().unwrap(), 42);
509    }
510
511    #[test]
512    fn test_convert_proto_value_bool() {
513        let proto_val = proto::Value {
514            kind: Some(proto::value::Kind::BoolVal(true)),
515        };
516        let val = crate::convert::proto_to_value(&proto_val);
517        assert!(val.as_bool().unwrap());
518    }
519
520    #[test]
521    fn test_convert_proto_value_null() {
522        let proto_val = proto::Value {
523            kind: Some(proto::value::Kind::NullVal(proto::NullValue {})),
524        };
525        let val = crate::convert::proto_to_value(&proto_val);
526        assert!(val.is_null());
527    }
528
529    #[test]
530    fn test_convert_proto_value_none() {
531        let proto_val = proto::Value { kind: None };
532        let val = crate::convert::proto_to_value(&proto_val);
533        assert!(val.is_null());
534    }
535
536    #[test]
537    fn test_convert_proto_value_double() {
538        let proto_val = proto::Value {
539            kind: Some(proto::value::Kind::DoubleVal(proto::DoubleValue {
540                value: 3.15,
541                kind: 0,
542            })),
543        };
544        let val = crate::convert::proto_to_value(&proto_val);
545        assert!(val.as_decimal().is_ok());
546    }
547
548    #[test]
549    fn test_convert_proto_value_list() {
550        let proto_val = proto::Value {
551            kind: Some(proto::value::Kind::ListVal(proto::ListValue {
552                values: vec![
553                    proto::Value {
554                        kind: Some(proto::value::Kind::IntVal(proto::IntValue {
555                            value: 1,
556                            kind: 0,
557                        })),
558                    },
559                    proto::Value {
560                        kind: Some(proto::value::Kind::IntVal(proto::IntValue {
561                            value: 2,
562                            kind: 0,
563                        })),
564                    },
565                ],
566            })),
567        };
568        let val = crate::convert::proto_to_value(&proto_val);
569        let arr = val.as_array().unwrap();
570        assert_eq!(arr.len(), 2);
571        assert_eq!(arr[0].as_int().unwrap(), 1);
572        assert_eq!(arr[1].as_int().unwrap(), 2);
573    }
574
575    #[test]
576    fn test_convert_proto_value_map() {
577        let proto_val = proto::Value {
578            kind: Some(proto::value::Kind::MapVal(proto::MapValue {
579                entries: vec![proto::MapEntry {
580                    key: "name".to_string(),
581                    value: Some(proto::Value {
582                        kind: Some(proto::value::Kind::StringVal(proto::StringValue {
583                            value: "Alice".to_string(),
584                            kind: 0,
585                        })),
586                    }),
587                }],
588            })),
589        };
590        let val = crate::convert::proto_to_value(&proto_val);
591        let obj = val.as_object().unwrap();
592        assert_eq!(obj.get("name").unwrap().as_string().unwrap(), "Alice");
593    }
594}