refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- Move the Tauri app (React frontend + Rust backend) into desktop/ via git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts) - Root package.json becomes a private bun workspace container with orchestration scripts; desktop package renamed gridline-desktop - Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin, static output, ready for Dokploy) - Update release.yml for the new layout: projectPath: desktop, desktop/src-tauri resource paths, rust-cache workspace path - Update README + AGENTS.md structure trees and dev commands - Verified: vitest (1074), cargo test (354), desktop build, tauri dev (window launches), tauri build (dmg + app bundles), Astro build
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
//! TLS connector factory for tokio-postgres.
|
||||
//!
|
||||
//! Maps the user-facing SSL modes to rustls `ClientConfig` values:
|
||||
//! - `disable` -> no TLS (returns `None`)
|
||||
//! - `require` -> encrypt without verifying the server certificate (custom `NoVerifier`)
|
||||
//! - `verify-ca` / `verify-full` -> standard rustls webpki verification (chain AND
|
||||
//! hostname; `verify-ca` is intentionally identical to `verify-full` in v1)
|
||||
//!
|
||||
//! Client certificates are supported via optional `cert_path` / `key_path` pair.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
|
||||
use rustls::{ClientConfig, DigitallySignedStruct, RootCertStore};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TlsDecision {
|
||||
Disable,
|
||||
Require,
|
||||
Verify,
|
||||
}
|
||||
|
||||
pub fn tls_decision(ssl_mode: Option<&str>) -> TlsDecision {
|
||||
match ssl_mode {
|
||||
Some("require") => TlsDecision::Require,
|
||||
Some("verify-ca") | Some("verify-full") => TlsDecision::Verify,
|
||||
_ => TlsDecision::Disable,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a rustls `ClientConfig` for tokio-postgres, or `None` for disable.
|
||||
/// `ca_path` is required for Verify; `cert_path`/`key_path` are optional client auth.
|
||||
pub fn build_tls_config(
|
||||
decision: TlsDecision,
|
||||
ca_path: Option<&str>,
|
||||
cert_path: Option<&str>,
|
||||
key_path: Option<&str>,
|
||||
) -> Result<Option<Arc<ClientConfig>>, String> {
|
||||
if matches!(decision, TlsDecision::Disable) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let client_auth = match (cert_path, key_path) {
|
||||
(Some(c), Some(k)) => Some(load_client_identity(c, k)?),
|
||||
(Some(_), None) | (None, Some(_)) => {
|
||||
return Err("both ssl_cert_path and ssl_key_path must be set for client auth".into())
|
||||
}
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
let config = match decision {
|
||||
TlsDecision::Require => {
|
||||
// Encrypt without verifying the server certificate.
|
||||
let builder = ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerifier));
|
||||
match client_auth {
|
||||
Some((certs, key)) => builder
|
||||
.with_client_auth_cert(certs, key)
|
||||
.map_err(|e| format!("client cert: {e}"))?,
|
||||
None => builder.with_no_client_auth(),
|
||||
}
|
||||
}
|
||||
TlsDecision::Verify => {
|
||||
let mut roots = RootCertStore::empty();
|
||||
if let Some(ca) = ca_path {
|
||||
add_ca_file(&mut roots, ca)?;
|
||||
} else {
|
||||
return Err("ssl_ca_path is required for verify-ca / verify-full".into());
|
||||
}
|
||||
for ta in rustls_native_certs::load_native_certs()
|
||||
.map_err(|e| format!("native certs: {e}"))?
|
||||
{
|
||||
let _ = roots.add(ta);
|
||||
}
|
||||
let builder = ClientConfig::builder().with_root_certificates(roots);
|
||||
match client_auth {
|
||||
Some((certs, key)) => builder
|
||||
.with_client_auth_cert(certs, key)
|
||||
.map_err(|e| format!("client cert: {e}"))?,
|
||||
None => builder.with_no_client_auth(),
|
||||
}
|
||||
}
|
||||
TlsDecision::Disable => unreachable!(),
|
||||
};
|
||||
Ok(Some(Arc::new(config)))
|
||||
}
|
||||
|
||||
fn add_ca_file(roots: &mut RootCertStore, path: &str) -> Result<(), String> {
|
||||
let bytes = std::fs::read(path).map_err(|e| format!("failed to read CA file {path}: {e}"))?;
|
||||
let mut reader = std::io::BufReader::new(bytes.as_slice());
|
||||
let parsed = rustls_pemfile::certs(&mut reader)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("parse CA: {e}"))?;
|
||||
let added = parsed.into_iter().filter_map(|c| roots.add(c).ok()).count();
|
||||
if added == 0 {
|
||||
return Err("no usable CA certificates found".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_client_identity(
|
||||
cert_path: &str,
|
||||
key_path: &str,
|
||||
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), String> {
|
||||
// rustls-pemfile cannot decrypt PKCS#8-encrypted keys, so reject them up
|
||||
// front with a clear message before touching the certificate file.
|
||||
let kb = std::fs::read(key_path).map_err(|e| format!("read key: {e}"))?;
|
||||
if String::from_utf8_lossy(&kb).contains("ENCRYPTED PRIVATE KEY") {
|
||||
return Err(
|
||||
"encrypted client keys are not supported in v1; use an unencrypted PEM key".into(),
|
||||
);
|
||||
}
|
||||
let cb = std::fs::read(cert_path).map_err(|e| format!("read cert: {e}"))?;
|
||||
let certs: Vec<CertificateDer<'static>> =
|
||||
rustls_pemfile::certs(&mut std::io::BufReader::new(cb.as_slice()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("parse cert: {e}"))?
|
||||
.into_iter()
|
||||
.map(|c| c.into_owned())
|
||||
.collect();
|
||||
if certs.is_empty() {
|
||||
return Err("no client certificates parsed".into());
|
||||
}
|
||||
let key = rustls_pemfile::private_key(&mut std::io::BufReader::new(kb.as_slice()))
|
||||
.map_err(|e| format!("parse key: {e}"))?
|
||||
.ok_or_else(|| "no private key parsed".to_string())?
|
||||
.clone_key();
|
||||
Ok((certs, key))
|
||||
}
|
||||
|
||||
/// Accepts every certificate: TLS encryption without authentication (`require` mode).
|
||||
#[derive(Debug)]
|
||||
struct NoVerifier;
|
||||
|
||||
impl ServerCertVerifier for NoVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_ee: &CertificateDer<'_>,
|
||||
_ic: &[CertificateDer<'_>],
|
||||
_n: &ServerName<'_>,
|
||||
_ocsp: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tls_decision_maps_modes() {
|
||||
assert!(matches!(tls_decision(None), TlsDecision::Disable));
|
||||
assert!(matches!(
|
||||
tls_decision(Some("disable")),
|
||||
TlsDecision::Disable
|
||||
));
|
||||
assert!(matches!(
|
||||
tls_decision(Some("require")),
|
||||
TlsDecision::Require
|
||||
));
|
||||
assert!(matches!(
|
||||
tls_decision(Some("verify-ca")),
|
||||
TlsDecision::Verify
|
||||
));
|
||||
assert!(matches!(
|
||||
tls_decision(Some("verify-full")),
|
||||
TlsDecision::Verify
|
||||
));
|
||||
assert!(matches!(tls_decision(Some("bogus")), TlsDecision::Disable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tls_disable_returns_none() {
|
||||
assert!(build_tls_config(TlsDecision::Disable, None, None, None)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tls_require_returns_some_without_files() {
|
||||
assert!(build_tls_config(TlsDecision::Require, None, None, None)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tls_verify_missing_ca_errors() {
|
||||
let err = build_tls_config(TlsDecision::Verify, Some("/nonexistent/ca.pem"), None, None)
|
||||
.unwrap_err();
|
||||
assert!(err.to_lowercase().contains("ca"), "got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tls_client_cert_missing_key_errors() {
|
||||
// cert set without key
|
||||
let err = build_tls_config(
|
||||
TlsDecision::Require,
|
||||
None,
|
||||
Some("/nonexistent/cert.pem"),
|
||||
None,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.to_lowercase().contains("cert") || err.to_lowercase().contains("key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tls_rejects_encrypted_key_marker() {
|
||||
// rustls-pemfile cannot decrypt PKCS#8-encrypted keys, so an ENCRYPTED
|
||||
// PRIVATE KEY header must be rejected with a clear error. The cert file
|
||||
// is a dummy: the key check fires before the cert is read.
|
||||
let dir = std::env::temp_dir();
|
||||
let cert_path = dir.join("gl_tls_cert_dummy.pem");
|
||||
let key_path = dir.join("gl_tls_enc_key.pem");
|
||||
std::fs::write(
|
||||
&cert_path,
|
||||
"-----BEGIN CERTIFICATE-----\nZmFrZQ==\n-----END CERTIFICATE-----\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
&key_path,
|
||||
"-----BEGIN ENCRYPTED PRIVATE KEY-----\nabc\n-----END ENCRYPTED PRIVATE KEY-----\n",
|
||||
)
|
||||
.unwrap();
|
||||
let r = build_tls_config(
|
||||
TlsDecision::Require,
|
||||
None,
|
||||
Some(cert_path.to_str().unwrap()),
|
||||
Some(key_path.to_str().unwrap()),
|
||||
);
|
||||
assert!(r.is_err());
|
||||
assert!(
|
||||
r.unwrap_err().to_lowercase().contains("encrypt"),
|
||||
"must mention encryption"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user