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            dsn.tenant(),
257            dsn.role(),
258        )
259        .await?;
260
261        Ok(Self {
262            dsn: dsn.clone(),
263            session_id,
264        })
265    }
266
267    /// Perform authentication handshake.
268    async fn handshake(
269        client: &mut GeodeServiceClient<Channel>,
270        username: Option<&str>,
271        password: Option<&str>,
272        graph: Option<&str>,
273        tenant: Option<&str>,
274        role: Option<&str>,
275    ) -> Result<String> {
276        let request = proto::HelloRequest {
277            username: username.unwrap_or("").to_string(),
278            password: password.unwrap_or("").to_string(),
279            tenant_id: tenant.map(String::from),
280            client_name: "geode-rust".to_string(),
281            client_version: crate::VERSION.to_string(),
282            wanted_conformance: "minimum".to_string(),
283            graph: graph.map(String::from),
284            role: role.map(String::from),
285        };
286
287        let response = client
288            .handshake(Request::new(request))
289            .await
290            .map_err(|e| Error::connection(format!("Handshake failed: {}", e)))?;
291
292        let resp = response.into_inner();
293        if !resp.success {
294            return Err(Error::auth(resp.error_message));
295        }
296
297        Ok(resp.session_id)
298    }
299
300    /// Execute a GQL query.
301    pub async fn query(&mut self, gql: &str) -> Result<(Page, Option<String>)> {
302        self.query_with_params(gql, &HashMap::new()).await
303    }
304
305    /// Execute a GQL query with parameters.
306    pub async fn query_with_params(
307        &mut self,
308        gql: &str,
309        params: &HashMap<String, Value>,
310    ) -> Result<(Page, Option<String>)> {
311        let proto_params: Vec<proto::Param> = params
312            .iter()
313            .map(|(k, v)| proto::Param {
314                name: k.clone(),
315                value: Some(v.to_proto_value()),
316            })
317            .collect();
318
319        let request = proto::ExecuteRequest {
320            session_id: self.session_id.clone(),
321            query: gql.to_string(),
322            params: proto_params,
323        };
324
325        // The current server can leave tonic channels in a bad state after
326        // previous RPCs. Use a fresh channel per RPC and keep only the
327        // server-side session stable via session_id.
328        let mut client = Self::connect_service(&self.dsn).await?;
329        let response = client
330            .execute(Request::new(request))
331            .await
332            .map_err(|e| Error::query(format!("Query execution failed: {}", e)))?;
333
334        // Process streaming response
335        let mut stream = response.into_inner();
336        let mut columns = Vec::new();
337        let mut rows = Vec::new();
338        let mut final_page = true;
339        let mut ordered = false;
340        let mut order_keys = Vec::new();
341
342        while let Some(exec_resp) = stream
343            .message()
344            .await
345            .map_err(|e| Error::query(format!("Failed to read response: {}", e)))?
346        {
347            if let Some(payload) = exec_resp.payload {
348                match payload {
349                    Payload::Schema(schema) => {
350                        columns = schema
351                            .columns
352                            .into_iter()
353                            .map(|c| Column {
354                                name: c.name,
355                                col_type: c.r#type,
356                            })
357                            .collect();
358                    }
359                    Payload::Page(page) => {
360                        for row in page.rows {
361                            let mut row_map = HashMap::new();
362                            for (i, col) in columns.iter().enumerate() {
363                                let value = if i < row.values.len() {
364                                    crate::convert::proto_to_value(&row.values[i])
365                                } else {
366                                    Value::null()
367                                };
368                                row_map.insert(col.name.clone(), value);
369                            }
370                            rows.push(row_map);
371                        }
372                        final_page = page.r#final;
373                        ordered = page.ordered;
374                        order_keys = page.order_keys;
375                    }
376                    Payload::Error(err) => {
377                        return Err(Error::Query {
378                            code: err.code,
379                            message: err.message,
380                        });
381                    }
382                    Payload::Metrics(_) | Payload::Heartbeat(_) => {
383                        // Informational payloads, continue
384                    }
385                    Payload::Explain(_) | Payload::Profile(_) => {
386                        // Plan/profile payloads, continue
387                    }
388                }
389            }
390        }
391
392        Ok((
393            Page {
394                columns,
395                rows,
396                ordered,
397                order_keys,
398                final_page,
399            },
400            None,
401        ))
402    }
403
404    /// Begin a transaction.
405    pub async fn begin(&mut self) -> Result<()> {
406        let request = proto::BeginRequest {
407            read_only: false,
408            session_id: self.session_id.clone(),
409        };
410
411        let mut client = Self::connect_service(&self.dsn).await?;
412        client
413            .begin(Request::new(request))
414            .await
415            .map_err(|e| Error::connection(format!("Begin transaction failed: {}", e)))?;
416
417        Ok(())
418    }
419
420    /// Commit a transaction.
421    pub async fn commit(&mut self) -> Result<()> {
422        let request = proto::CommitRequest {
423            session_id: self.session_id.clone(),
424        };
425
426        let mut client = Self::connect_service(&self.dsn).await?;
427        client
428            .commit(Request::new(request))
429            .await
430            .map_err(|e| Error::connection(format!("Commit failed: {}", e)))?;
431
432        Ok(())
433    }
434
435    /// Rollback a transaction.
436    pub async fn rollback(&mut self) -> Result<()> {
437        let request = proto::RollbackRequest {
438            session_id: self.session_id.clone(),
439        };
440
441        let mut client = Self::connect_service(&self.dsn).await?;
442        client
443            .rollback(Request::new(request))
444            .await
445            .map_err(|e| Error::connection(format!("Rollback failed: {}", e)))?;
446
447        Ok(())
448    }
449
450    /// Create a savepoint within the current transaction.
451    ///
452    /// Note: Savepoints are not yet supported via the gRPC service definition.
453    /// This method will return an error until the gRPC service is updated.
454    pub async fn savepoint(&mut self, _name: &str) -> Result<()> {
455        Err(Error::connection(
456            "savepoint is not yet supported via gRPC transport",
457        ))
458    }
459
460    /// Roll back to a previously created savepoint.
461    ///
462    /// Note: Rollback-to-savepoint is not yet supported via the gRPC service definition.
463    /// This method will return an error until the gRPC service is updated.
464    pub async fn rollback_to(&mut self, _name: &str) -> Result<()> {
465        Err(Error::connection(
466            "rollback_to is not yet supported via gRPC transport",
467        ))
468    }
469
470    /// Send a ping request.
471    pub async fn ping(&mut self) -> Result<bool> {
472        let mut client = Self::connect_service(&self.dsn).await?;
473        let response = client
474            .ping(Request::new(proto::PingRequest {}))
475            .await
476            .map_err(|e| Error::connection(format!("Ping failed: {}", e)))?;
477
478        Ok(response.into_inner().ok)
479    }
480
481    /// Close the connection.
482    pub fn close(&mut self) -> Result<()> {
483        // gRPC channels are automatically closed when dropped
484        Ok(())
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use crate::proto;
491
492    #[test]
493    fn test_convert_proto_value_string() {
494        let proto_val = proto::Value {
495            kind: Some(proto::value::Kind::StringVal(proto::StringValue {
496                value: "hello".to_string(),
497                kind: 0,
498            })),
499        };
500        let val = crate::convert::proto_to_value(&proto_val);
501        assert_eq!(val.as_string().unwrap(), "hello");
502    }
503
504    #[test]
505    fn test_convert_proto_value_int() {
506        let proto_val = proto::Value {
507            kind: Some(proto::value::Kind::IntVal(proto::IntValue {
508                value: 42,
509                kind: 0,
510            })),
511        };
512        let val = crate::convert::proto_to_value(&proto_val);
513        assert_eq!(val.as_int().unwrap(), 42);
514    }
515
516    #[test]
517    fn test_convert_proto_value_bool() {
518        let proto_val = proto::Value {
519            kind: Some(proto::value::Kind::BoolVal(true)),
520        };
521        let val = crate::convert::proto_to_value(&proto_val);
522        assert!(val.as_bool().unwrap());
523    }
524
525    #[test]
526    fn test_convert_proto_value_null() {
527        let proto_val = proto::Value {
528            kind: Some(proto::value::Kind::NullVal(proto::NullValue {})),
529        };
530        let val = crate::convert::proto_to_value(&proto_val);
531        assert!(val.is_null());
532    }
533
534    #[test]
535    fn test_convert_proto_value_none() {
536        let proto_val = proto::Value { kind: None };
537        let val = crate::convert::proto_to_value(&proto_val);
538        assert!(val.is_null());
539    }
540
541    #[test]
542    fn test_convert_proto_value_double() {
543        let proto_val = proto::Value {
544            kind: Some(proto::value::Kind::DoubleVal(proto::DoubleValue {
545                value: 3.15,
546                kind: 0,
547            })),
548        };
549        let val = crate::convert::proto_to_value(&proto_val);
550        assert!(val.as_decimal().is_ok());
551    }
552
553    #[test]
554    fn test_convert_proto_value_list() {
555        let proto_val = proto::Value {
556            kind: Some(proto::value::Kind::ListVal(proto::ListValue {
557                values: vec![
558                    proto::Value {
559                        kind: Some(proto::value::Kind::IntVal(proto::IntValue {
560                            value: 1,
561                            kind: 0,
562                        })),
563                    },
564                    proto::Value {
565                        kind: Some(proto::value::Kind::IntVal(proto::IntValue {
566                            value: 2,
567                            kind: 0,
568                        })),
569                    },
570                ],
571            })),
572        };
573        let val = crate::convert::proto_to_value(&proto_val);
574        let arr = val.as_array().unwrap();
575        assert_eq!(arr.len(), 2);
576        assert_eq!(arr[0].as_int().unwrap(), 1);
577        assert_eq!(arr[1].as_int().unwrap(), 2);
578    }
579
580    #[test]
581    fn test_convert_proto_value_map() {
582        let proto_val = proto::Value {
583            kind: Some(proto::value::Kind::MapVal(proto::MapValue {
584                entries: vec![proto::MapEntry {
585                    key: "name".to_string(),
586                    value: Some(proto::Value {
587                        kind: Some(proto::value::Kind::StringVal(proto::StringValue {
588                            value: "Alice".to_string(),
589                            kind: 0,
590                        })),
591                    }),
592                }],
593            })),
594        };
595        let val = crate::convert::proto_to_value(&proto_val);
596        let obj = val.as_object().unwrap();
597        assert_eq!(obj.get("name").unwrap().as_string().unwrap(), "Alice");
598    }
599}