diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ddf4022c9..a3967a570 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -56,6 +56,12 @@ jobs:
run: |
mise run --output prefix test
+ - name: Show proxy logs on failure
+ if: failure()
+ run: |
+ docker logs --timestamps proxy 2>&1 || true
+ docker logs --timestamps proxy-tls 2>&1 || true
+
- uses: ./.github/actions/send-slack-notification
with:
channel: engineering
diff --git a/Cargo.lock b/Cargo.lock
index d247b5539..3d1e7f58d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -450,6 +450,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
[[package]]
name = "base64ct"
version = "1.7.3"
@@ -622,9 +628,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.11.1"
+version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
dependencies = [
"serde",
]
@@ -738,7 +744,7 @@ dependencies = [
"async-mutex",
"async-trait",
"base16ct",
- "base64",
+ "base64 0.22.1",
"base64ct",
"base85",
"blake3",
@@ -751,7 +757,7 @@ dependencies = [
"dirs",
"futures",
"hex",
- "hmac",
+ "hmac 0.12.1",
"itertools 0.12.1",
"lazy_static",
"log",
@@ -771,7 +777,7 @@ dependencies = [
"serde_cbor",
"serde_json",
"serdect",
- "sha2",
+ "sha2 0.10.8",
"stack-auth",
"stack-profile",
"static_assertions",
@@ -807,12 +813,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1946988e0b7f9de259d85b10c9c1fd7e1327751103b808c2c6e930b9a87c25c9"
dependencies = [
"getrandom 0.2.15",
- "hmac",
+ "hmac 0.12.1",
"lazy_static",
"num-bigint",
"rand 0.8.6",
"regex",
- "sha2",
+ "sha2 0.10.8",
"thiserror 1.0.69",
]
@@ -822,7 +828,6 @@ version = "3.0.1"
dependencies = [
"arc-swap",
"async-trait",
- "aws-lc-rs",
"bigdecimal",
"blake3",
"bytes",
@@ -836,12 +841,11 @@ dependencies = [
"eql-mapper",
"exitcode",
"hex",
- "md-5",
+ "md-5 0.10.6",
"metrics",
"metrics-exporter-prometheus",
"moka",
- "oid-registry",
- "pg_escape",
+ "pg-proto",
"postgres-protocol",
"postgres-types",
"rand 0.9.2",
@@ -849,24 +853,23 @@ dependencies = [
"regex",
"rust_decimal",
"rustls",
+ "rustls-native-certs",
"rustls-pki-types",
"rustls-platform-verifier 0.5.1",
"serde",
"serde_json",
- "socket2 0.5.8",
+ "socket2 0.6.5",
"sqltk",
"temp-env",
"thiserror 2.0.18",
"tokio",
"tokio-postgres",
"tokio-postgres-rustls",
- "tokio-rustls",
"tokio-util",
"tracing",
"tracing-subscriber",
"uuid",
"vitaminc-protected 0.1.0-pre4.2",
- "x509-parser",
]
[[package]]
@@ -972,6 +975,12 @@ dependencies = [
"cc",
]
+[[package]]
+name = "cmov"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
+
[[package]]
name = "colorchoice"
version = "1.0.3"
@@ -1031,6 +1040,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
[[package]]
name = "constant_time_eq"
version = "0.3.1"
@@ -1192,6 +1207,15 @@ dependencies = [
"vitaminc",
]
+[[package]]
+name = "ctutils"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
+dependencies = [
+ "cmov",
+]
+
[[package]]
name = "darling"
version = "0.20.10"
@@ -1248,7 +1272,7 @@ version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
- "const-oid",
+ "const-oid 0.9.6",
"der_derive",
"flagset",
"zeroize",
@@ -1393,7 +1417,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer 0.12.1",
+ "const-oid 0.10.2",
"crypto-common 0.2.2",
+ "ctutils",
]
[[package]]
@@ -1569,7 +1595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -2006,6 +2032,15 @@ dependencies = [
"digest 0.10.7",
]
+[[package]]
+name = "hmac"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+dependencies = [
+ "digest 0.11.3",
+]
+
[[package]]
name = "http"
version = "1.3.1"
@@ -2108,7 +2143,7 @@ version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
@@ -2120,7 +2155,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2 0.6.1",
+ "socket2 0.6.5",
"system-configuration",
"tokio",
"tower-service",
@@ -2477,7 +2512,7 @@ version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
- "base64",
+ "base64 0.22.1",
"js-sys",
"pem",
"ring",
@@ -2500,9 +2535,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
-version = "0.2.177"
+version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libm"
@@ -2592,6 +2627,16 @@ dependencies = [
"digest 0.10.7",
]
+[[package]]
+name = "md-5"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
+dependencies = [
+ "cfg-if",
+ "digest 0.11.3",
+]
+
[[package]]
name = "memchr"
version = "2.7.4"
@@ -2614,7 +2659,7 @@ version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df88858cd28baaaf2cfc894e37789ed4184be0e1351157aec7bf3c2266c793fd"
dependencies = [
- "base64",
+ "base64 0.22.1",
"http-body-util",
"hyper",
"hyper-rustls",
@@ -2654,7 +2699,7 @@ dependencies = [
"cfg-if",
"miette-derive",
"thiserror 1.0.69",
- "unicode-width",
+ "unicode-width 0.1.14",
]
[[package]]
@@ -2691,13 +2736,13 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.0.3"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2972,7 +3017,7 @@ version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
- "base64",
+ "base64 0.22.1",
"serde_core",
]
@@ -2983,45 +3028,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
-name = "pg_escape"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c7bc82ccbe2c7ef7ceed38dcac90d7ff46681e061e9d7310cbcd409113e303"
-dependencies = [
- "phf",
-]
-
-[[package]]
-name = "phf"
-version = "0.11.3"
+name = "pg-proto"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+checksum = "72f5b88d40736045b71bb72d114a6f212c98095e1f43cfc998c948e429b55012"
dependencies = [
- "phf_macros",
- "phf_shared",
+ "base64 0.23.1",
+ "bytes",
+ "hmac 0.13.0",
+ "pg-proto-fsm",
+ "postgres-protocol",
+ "rand 0.10.2",
+ "rustls",
+ "sha2 0.11.0",
+ "socket2 0.6.5",
+ "stringprep",
+ "subtle",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "x509-parser",
]
[[package]]
-name = "phf_generator"
-version = "0.11.3"
+name = "pg-proto-fsm"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+checksum = "dc37a28331c337145f6500402d7857565ff807891712d79833e0fbc00567c48d"
dependencies = [
- "phf_shared",
- "rand 0.8.6",
+ "proc-macro2",
+ "quote",
+ "railroad",
+ "syn 3.0.3",
]
[[package]]
-name = "phf_macros"
+name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
- "phf_generator",
"phf_shared",
- "proc-macro2",
- "quote",
- "syn 2.0.117",
]
[[package]]
@@ -3083,19 +3130,19 @@ dependencies = [
[[package]]
name = "postgres-protocol"
-version = "0.6.8"
+version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54"
+checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514"
dependencies = [
- "base64",
+ "base64 0.22.1",
"byteorder",
"bytes",
"fallible-iterator",
- "hmac",
- "md-5",
+ "hmac 0.13.0",
+ "md-5 0.11.0",
"memchr",
- "rand 0.9.2",
- "sha2",
+ "rand 0.10.2",
+ "sha2 0.11.0",
"stringprep",
]
@@ -3205,9 +3252,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.95"
+version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
@@ -3325,14 +3372,14 @@ dependencies = [
"once_cell",
"socket2 0.5.8",
"tracing",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
name = "quote"
-version = "1.0.40"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
@@ -3355,6 +3402,15 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+[[package]]
+name = "railroad"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bf842ad92d09c4dd1e68be1507189b01898c46fe01916c0467fb405b1f9ee0a"
+dependencies = [
+ "unicode-width 0.2.2",
+]
+
[[package]]
name = "rand"
version = "0.8.6"
@@ -3474,7 +3530,7 @@ dependencies = [
"rand_chacha 0.3.1",
"serde",
"serde_cbor",
- "sha2",
+ "sha2 0.10.8",
"thiserror 1.0.69",
"zeroize",
]
@@ -3495,7 +3551,7 @@ dependencies = [
"rand_chacha 0.3.1",
"serde",
"serde_cbor",
- "sha2",
+ "sha2 0.10.8",
"thiserror 1.0.69",
"zeroize",
]
@@ -3557,7 +3613,7 @@ checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.2",
+ "syn 3.0.3",
]
[[package]]
@@ -3604,7 +3660,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
@@ -3775,14 +3831,14 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
name = "rustls"
-version = "0.23.28"
+version = "0.23.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
dependencies = [
"aws-lc-rs",
"log",
@@ -3807,11 +3863,12 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.11.0"
+version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"web-time",
+ "zeroize",
]
[[package]]
@@ -3832,7 +3889,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs 0.26.8",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -3864,9 +3921,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
-version = "0.103.3"
+version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
@@ -4161,6 +4218,17 @@ dependencies = [
"digest 0.10.7",
]
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
+]
+
[[package]]
name = "sharded-slab"
version = "0.1.7"
@@ -4258,12 +4326,12 @@ dependencies = [
[[package]]
name = "socket2"
-version = "0.6.1"
+version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4310,7 +4378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1c9c640571eba8fa5a705ccebe5c5e50aa27d4d51f4ec1614ffd1841b8a13a"
dependencies = [
"aquamarine",
- "base64",
+ "base64 0.22.1",
"cts-common",
"jsonwebtoken",
"miette",
@@ -4355,7 +4423,7 @@ dependencies = [
"cfg-if",
"libc",
"psm",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -4417,9 +4485,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "3.0.2"
+version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
@@ -4626,9 +4694,9 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.48.0"
+version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"bytes",
"libc",
@@ -4636,20 +4704,20 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2 0.6.1",
+ "socket2 0.6.5",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
-version = "2.6.0"
+version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 3.0.3",
]
[[package]]
@@ -4684,7 +4752,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27d684bad428a0f2481f42241f821db42c54e2dc81d8c00db8536c506b0a0144"
dependencies = [
- "const-oid",
+ "const-oid 0.9.6",
"ring",
"rustls",
"tokio",
@@ -4695,9 +4763,9 @@ dependencies = [
[[package]]
name = "tokio-rustls"
-version = "0.26.2"
+version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
@@ -4705,15 +4773,15 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.14"
+version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"futures-util",
- "hashbrown 0.14.5",
+ "libc",
"pin-project-lite",
"tokio",
]
@@ -4964,6 +5032,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
[[package]]
name = "unicode-xid"
version = "0.2.6"
@@ -5056,7 +5130,7 @@ dependencies = [
"atomic",
"getrandom 0.3.2",
"js-sys",
- "md-5",
+ "md-5 0.10.6",
"serde",
"sha1_smol",
"wasm-bindgen",
@@ -5496,7 +5570,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -5678,15 +5752,6 @@ dependencies = [
"windows-targets 0.52.6",
]
-[[package]]
-name = "windows-sys"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
-dependencies = [
- "windows-targets 0.53.5",
-]
-
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -6100,7 +6165,7 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
dependencies = [
- "const-oid",
+ "const-oid 0.9.6",
"der",
"spki",
"tls_codec",
@@ -6108,9 +6173,9 @@ dependencies = [
[[package]]
name = "x509-parser"
-version = "0.17.0"
+version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460"
+checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
dependencies = [
"asn1-rs",
"data-encoding",
@@ -6240,7 +6305,7 @@ version = "0.12.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f731f2de99e66396928faef44b02b39288dc9a93f77a4a3e1dcdc33c1adad0"
dependencies = [
- "base64",
+ "base64 0.22.1",
"cipherstash-config",
"const-hex",
"cts-common",
diff --git a/PG_PROTO_FOLLOWUPS.md b/PG_PROTO_FOLLOWUPS.md
new file mode 100644
index 000000000..b8633cb02
--- /dev/null
+++ b/PG_PROTO_FOLLOWUPS.md
@@ -0,0 +1,43 @@
+# pg-proto follow-ups
+
+The proxy migration delegates framing, typed messages, startup/authentication
+state, typed startup middleware, async runtime middleware, demultiplexing, and
+compile-time checked bounded pipeline dispatch to `pg-proto` 0.5.0. The
+remaining transport adapters below would be better eliminated in `pg-proto`
+itself.
+
+The 0.5.0 version above is the historical baseline when these follow-ups were
+written; the completed migration now uses pg-proto 0.10.6.
+
+## Preserve buffered transport state across a split
+
+`Buffered::into_inner()` cannot return retained inbound bytes, pending outbound
+bytes, or demultiplexer state. The proxy must finish startup through
+`ReadyForQuery` before splitting the bidirectional stream and then construct new
+`Buffered` values for concurrent frontend/backend processing.
+
+An `into_parts`/`from_parts` API, or a buffer-preserving split API, would let a
+proxy change transport ownership without risking loss of bytes already read past
+a message boundary. It should preserve both codec buffers and the demultiplexer.
+
+## Transfer a demultiplexer between transport owners
+
+The proxy routes backend messages through pg-proto's demultiplexer, but startup
+and concurrent runtime currently use separate `Buffered` owners. A supported way
+to extract and restore `Demux` state would retain startup parameter status,
+cancellation-key, and readiness state without application bookkeeping.
+
+This may naturally be solved by the buffer-preserving transport-parts API above.
+
+## Publish the example proxy driver as a library API
+
+`pg-proto` demonstrates a clean `Buffered` + `Middleware` forwarding loop in
+`examples/proxy_support`, but does not expose a configurable proxy driver from
+the crate. CipherStash therefore still owns connection orchestration, concurrent
+forwarding, and the small amount of glue that invokes middleware.
+
+A library-level proxy builder should accept downstream/upstream transports,
+startup and authentication policy, typed frontend/backend middleware, timeout
+policy, and an output strategy. It should own framing, phase transitions,
+bounded pipeline dispatch, demultiplexing, and shutdown. Applications would then
+only supply policy and message transformations.
diff --git a/PG_PROTO_MIGRATION_PLAN.md b/PG_PROTO_MIGRATION_PLAN.md
new file mode 100644
index 000000000..5660a6125
--- /dev/null
+++ b/PG_PROTO_MIGRATION_PLAN.md
@@ -0,0 +1,41 @@
+# pg-proto Migration
+
+> Historical plan: this records the migration's initial 0.2.1 baseline. The
+> completed implementation now uses pg-proto 0.10.6.
+
+## Summary
+
+- Create `/Users/jamessadler/cipherstash/proxy-pg-proto` from current `main` (`15b7f996`) on branch `refactor/pg-proto`.
+- Save this migration plan as `PG_PROTO_MIGRATION_PLAN.md` in that worktree’s repository root.
+- Limit the initial deliverable to the worktree, branch, and plan document; implementation follows separately.
+- Target a full migration to published [`pg-proto` 0.2.1](https://crates.io/crates/pg-proto/0.2.1), covering codecs, startup/authentication, runtime protocol-state validation, and bounded pipeline orchestration.
+
+## Implementation Changes
+
+- Replace handwritten framing, startup packet parsing, message codes, and message serialization with direction-specific `pg-proto` frontend/backend codecs.
+- Convert CipherStash-specific behavior into adapters over `pg-proto` messages:
+ - Preserve Parse/Query SQL rewriting and parameter OID mapping.
+ - Preserve Bind format-code semantics, nulls, parameter reshaping, and encryption.
+ - Preserve ParameterDescription, RowDescription, and DataRow rewriting and batched decryption.
+ - Retain diagnostic-response factories while emitting `pg-proto` response types.
+- Use `pg-proto` pre-startup and authentication APIs for SSL negotiation, startup, cancellation, client-facing MD5 authentication, and upstream cleartext/MD5/SCRAM authentication. Continue using existing TLS configuration and certificate policy.
+- Pair downstream server-role and upstream client-role runtime FSMs through `Intermediary`. Advance both sides for forwarded messages and only the affected side for locally intercepted or synthesised messages.
+- Preserve concurrent client-to-server and server-to-client processing, connection timeouts, response ordering, metrics, logging, schema reloads, and row buffering.
+- Track protocol state even when encryption mapping is disabled. Preserve one-to-one cancellation forwarding; do not introduce pooling or cancellation-key translation.
+- Reject unknown message tags as protocol errors, matching `pg-proto`’s fail-closed behavior.
+- Remove obsolete handwritten protocol modules and direct low-level dependencies once unused. Preserve the public configuration and CLI surfaces; retain existing `ProtocolError` variants for source compatibility even where `pg-proto` supersedes them.
+
+## Test Plan
+
+- Port existing message round-trip and rewrite tests to `pg-proto` message fixtures.
+- Add coverage for partial/oversized frames, malformed messages, unknown-tag rejection, SSL/TLS startup, cancellation, and all supported authentication modes.
+- Exercise simple queries and extended Parse/Bind/Describe/Execute/Close/Sync pipelines, including pipelining and error draining through Sync.
+- Verify text/binary formats, nulls, reshaped parameters, prepared statements, portals, COPY messages, asynchronous backend messages, and buffered DataRow decryption.
+- Run formatting, clippy, proxy unit tests, and TCP/TLS integration suites. Unset `CS_PROMETHEUS__ENABLED` for the baseline unit suite; its current environment value causes the otherwise unrelated Prometheus test to fail.
+
+## Assumptions
+
+- The existing untracked `.claude/worktrees/` directory remains untouched.
+- The worktree path and branch are currently available.
+- No compatibility feature flag or dual protocol implementation is required.
+- The plan document is left as an uncommitted worktree change unless a commit is requested separately.
diff --git a/docs/errors.md b/docs/errors.md
index 8f9ae535c..53b844447 100644
--- a/docs/errors.md
+++ b/docs/errors.md
@@ -42,6 +42,20 @@
+## Internal PostgreSQL protocol error
+
+CipherStash Proxy encountered an unexpected internal protocol state while processing a PostgreSQL response.
+
+### Error message
+
+```
+CipherStash Proxy encountered an internal PostgreSQL protocol error.
+```
+
+### How to fix
+
+Retry the operation. If the error persists, collect the proxy logs and report the issue to CipherStash support. The logs retain the detailed internal error needed for diagnosis.
+
# Authentication errors
diff --git a/mise.toml b/mise.toml
index 95770d8d4..cca6550f2 100644
--- a/mise.toml
+++ b/mise.toml
@@ -67,7 +67,8 @@ run = """
# Exit immediately if any command returns non-zero. Stops proxy from starting when the build fails
set -e
-{% set target = arch() ~ "-unknown-linux-gnu" | replace(from="arm64", to="aarch64") | replace(from="x64", to="x86_64") %}
+{% set target_arch = arch() | replace(from="arm64", to="aarch64") | replace(from="x64", to="x86_64") %}
+{% set target = target_arch ~ "-unknown-linux-gnu" %}
{% set docker_platform = "linux/" ~ arch() | replace(from="x64", to="amd64") %}
# build a binary
@@ -232,7 +233,7 @@ run = "mise run test:nextest {{arg(name='test',default='')}}"
dir = "{{config_root}}"
run = """
{% set default_host = get_env(name="CS_DATABASE__HOST",default="localhost") %}
-{% set default_port = get_env(name="CS_DATABASE__PORT",default=5432) %}
+{% set default_port = get_env(name="CS_DATABASE__PORT",default="5432") %}
host={{option(name="host",default=default_host)}}
port={{option(name="port",default=default_port)}}
tls={{flag(name="tls")}}
@@ -244,12 +245,11 @@ echo "Testing presence of Postgres at ${host}:${port} with a maximum of ${max_re
postgres_ready () {
# tls test
if [ "$1" = "true" ]; then
- response="$(echo | openssl s_client -starttls postgres -connect $host:$port -state -no_ign_eof 2>&1)"
- success=$?
- if [ -n "${DEBUG}" ]; then
- echo "response: ${response}"
- fi
- return $success
+ # Use psql so TLS negotiation and a real PostgreSQL connection both
+ # complete before the service is considered ready on Linux and macOS.
+ PGPASSWORD="$CS_DATABASE__PASSWORD" PGCONNECT_TIMEOUT=1 PGSSLMODE=require \
+ psql -h "$host" -p "$port" -d "$CS_DATABASE__NAME" \
+ -U "$CS_DATABASE__USERNAME" -c 'SELECT 1' >/dev/null 2>&1
else
# plaintext test
# We blackhole stderr (2>/dev/null) below because nc is echoing to stderr,
@@ -653,7 +653,7 @@ fi
export CIPHERSTASH_CLIENT_SECONDARY_USER_AGENT="cipherstash-proxy/${BUILD_VERSION}"
# cross-compile
-rustup update
+rustup toolchain install stable --profile minimal --no-self-update
rustup target add --toolchain stable {{ target }}
cargo build --locked --target {{ target }} --release --package cipherstash-proxy
diff --git a/packages/cipherstash-proxy-integration/src/common.rs b/packages/cipherstash-proxy-integration/src/common.rs
index d9422c526..9b13eabad 100644
--- a/packages/cipherstash-proxy-integration/src/common.rs
+++ b/packages/cipherstash-proxy-integration/src/common.rs
@@ -208,6 +208,11 @@ pub fn connection_config(port: u16) -> tokio_postgres::Config {
}
pub async fn connect_with_tls(port: u16) -> Client {
+ let (client, _connection_task) = connect_with_tls_and_task(port).await;
+ client
+}
+
+pub async fn connect_with_tls_and_task(port: u16) -> (Client, tokio::task::JoinHandle<()>) {
let tls_config = configure_test_client();
let tls = tokio_postgres_rustls::MakeRustlsConnect::new(tls_config);
@@ -217,12 +222,12 @@ pub async fn connect_with_tls(port: u16) -> Client {
.await
.expect("connection to database to succeed");
- tokio::spawn(async move {
+ let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {e}");
}
});
- client
+ (client, connection_task)
}
pub async fn connect(port: u16) -> Client {
diff --git a/packages/cipherstash-proxy-integration/src/diagnostics.rs b/packages/cipherstash-proxy-integration/src/diagnostics.rs
index 25edc4926..8415d2f2f 100644
--- a/packages/cipherstash-proxy-integration/src/diagnostics.rs
+++ b/packages/cipherstash-proxy-integration/src/diagnostics.rs
@@ -1,11 +1,10 @@
#[cfg(test)]
mod tests {
- use crate::common::{clear, connect_with_tls, PROXY, PROXY_METRICS_PORT};
+ use crate::common::{clear, connect_with_tls_and_task, PROXY, PROXY_METRICS_PORT};
/// Maximum number of retry attempts for fetching metrics.
- /// 5 retries with 200ms delay gives ~1 second total wait time,
- /// sufficient for Prometheus scrape interval in CI environments.
- const METRICS_FETCH_MAX_RETRIES: u32 = 5;
+ /// 25 retries with 200ms delay gives ~5 seconds total wait time.
+ const METRICS_FETCH_MAX_RETRIES: u32 = 25;
/// Delay between retry attempts in milliseconds.
/// 200ms provides a reasonable balance between responsiveness and allowing
@@ -13,9 +12,14 @@ mod tests {
const METRICS_FETCH_RETRY_DELAY_MS: u64 = 200;
/// Fetch metrics with retry logic to handle CI timing variability.
- async fn fetch_metrics_with_retry(max_retries: u32, delay_ms: u64) -> String {
- let url = format!("http://localhost:{}/metrics", *PROXY_METRICS_PORT);
+ async fn fetch_metrics_with_retry(
+ max_retries: u32,
+ delay_ms: u64,
+ expected: &[&str],
+ ) -> String {
+ let url = format!("http://127.0.0.1:{}/metrics", *PROXY_METRICS_PORT);
let mut last_error = None;
+ let mut last_body = None;
for attempt in 0..max_retries {
if attempt > 0 {
@@ -24,23 +28,26 @@ mod tests {
match reqwest::get(&url).await {
Ok(response) => match response.text().await {
- Ok(body) => return body,
+ Ok(body) if expected.iter().all(|value| body.contains(value)) => return body,
+ Ok(body) => last_body = Some(body),
Err(e) => last_error = Some(format!("Failed to read response: {}", e)),
},
Err(e) => last_error = Some(format!("Failed to fetch metrics: {}", e)),
}
}
- panic!(
- "Failed to fetch metrics after {} retries: {}",
- max_retries,
- last_error.unwrap_or_else(|| "unknown error".to_string())
- );
+ last_body.unwrap_or_else(|| {
+ panic!(
+ "Failed to fetch metrics after {} retries: {}",
+ max_retries,
+ last_error.unwrap_or_else(|| "unknown error".to_string())
+ )
+ })
}
#[tokio::test]
async fn metrics_include_statement_labels() {
- let client = connect_with_tls(*PROXY).await;
+ let (client, connection_task) = connect_with_tls_and_task(*PROXY).await;
clear().await;
@@ -59,14 +66,30 @@ mod tests {
.await
.unwrap();
+ // Closing the client gives the background connection driver a
+ // deterministic completion boundary before the metrics scrape.
+ drop(client);
+ connection_task
+ .await
+ .expect("PostgreSQL connection task should finish");
+
// Fetch metrics with retry logic for CI robustness
- let body =
- fetch_metrics_with_retry(METRICS_FETCH_MAX_RETRIES, METRICS_FETCH_RETRY_DELAY_MS).await;
+ let body = fetch_metrics_with_retry(
+ METRICS_FETCH_MAX_RETRIES,
+ METRICS_FETCH_RETRY_DELAY_MS,
+ &[
+ "statement_type=\"insert\"",
+ "statement_type=\"select\"",
+ "multi_statement=\"false\"",
+ ],
+ )
+ .await;
// Assert that the metrics include the expected labels
assert!(
body.contains("statement_type=\"insert\""),
- "Metrics should include insert statement_type label"
+ "Metrics should include insert statement_type label. Found: {}",
+ body
);
assert!(
body.contains("statement_type=\"select\""),
@@ -80,7 +103,7 @@ mod tests {
#[tokio::test]
async fn slow_statement_metrics_and_logs() {
- let client = connect_with_tls(*PROXY).await;
+ let (client, connection_task) = connect_with_tls_and_task(*PROXY).await;
clear().await;
@@ -88,9 +111,23 @@ mod tests {
// We use pg_sleep(2.1) to ensure it's considered slow
client.query("SELECT pg_sleep(2.1)", &[]).await.unwrap();
+ // Ensure the completed statement has been observed by the proxy's
+ // connection driver before polling its metrics endpoint.
+ drop(client);
+ connection_task
+ .await
+ .expect("PostgreSQL connection task should finish");
+
// Fetch metrics with retry logic
- let body =
- fetch_metrics_with_retry(METRICS_FETCH_MAX_RETRIES, METRICS_FETCH_RETRY_DELAY_MS).await;
+ let body = fetch_metrics_with_retry(
+ METRICS_FETCH_MAX_RETRIES,
+ METRICS_FETCH_RETRY_DELAY_MS,
+ &[
+ "cipherstash_proxy_slow_statements_total",
+ "cipherstash_proxy_statements_session_duration_seconds",
+ ],
+ )
+ .await;
// Assert that the slow statements counter is present and non-zero
assert!(
diff --git a/packages/cipherstash-proxy-integration/src/eql_regression.rs b/packages/cipherstash-proxy-integration/src/eql_regression.rs
index e8e3580de..3099578ab 100644
--- a/packages/cipherstash-proxy-integration/src/eql_regression.rs
+++ b/packages/cipherstash-proxy-integration/src/eql_regression.rs
@@ -7,13 +7,13 @@
//! ## How to use these tests:
//!
//! 1. **Generate fixtures from main branch:**
-//! ```
+//! ```text
//! git checkout main
//! CS_GENERATE_EQL_FIXTURES=1 cargo nextest run -p cipherstash-proxy-integration eql_regression::generate
//! ```
//!
//! 2. **Run regression tests on new branch:**
-//! ```
+//! ```text
//! git checkout
//! cargo nextest run -p cipherstash-proxy-integration eql_regression::regression
//! ```
diff --git a/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_id.rs b/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_id.rs
index f76f221ce..57e4f8de6 100644
--- a/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_id.rs
+++ b/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_id.rs
@@ -69,8 +69,15 @@ mod tests {
let result = client.query(&sql, &[]).await;
assert!(result.is_ok());
+ // A decryption failure is connection-fatal, so verify tenant isolation on
+ // a disposable connection while preserving this connection for the
+ // TENANT_1 -> TENANT_2 -> TENANT_1 switch under test.
+ let tenant_2_client = connect_with_tls(*PROXY).await;
+ let sql = format!("SET CIPHERSTASH.KEYSET_ID = '{tenant_keyset_id_2}'");
+ tenant_2_client.query(&sql, &[]).await.unwrap();
+
// SELECT data created by TENANT_1 AS TENANT_2
- let result = client.query(select_sql, &[&tenant_1_id]).await;
+ let result = tenant_2_client.query(select_sql, &[&tenant_1_id]).await;
assert!(result.is_err());
// --------
@@ -150,9 +157,16 @@ mod tests {
let result = client.simple_query(&sql).await;
assert!(result.is_ok());
+ // A decryption failure is connection-fatal, so verify tenant isolation on
+ // a disposable connection while preserving this connection for the
+ // TENANT_1 -> TENANT_2 -> TENANT_1 switch under test.
+ let tenant_2_client = connect_with_tls(*PROXY).await;
+ let sql = format!("SET CIPHERSTASH.KEYSET_ID = '{tenant_keyset_id_2}'");
+ tenant_2_client.simple_query(&sql).await.unwrap();
+
// SELECT data created by TENANT_1 AS TENANT_2
let sql = format!("SELECT id, encrypted_text FROM encrypted WHERE id = {tenant_1_id}");
- let result = client.simple_query(&sql).await;
+ let result = tenant_2_client.simple_query(&sql).await;
assert!(result.is_err());
// --------
diff --git a/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_name.rs b/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_name.rs
index 0d9af1757..0448adeca 100644
--- a/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_name.rs
+++ b/packages/cipherstash-proxy-integration/src/multitenant/set_keyset_name.rs
@@ -64,8 +64,15 @@ mod tests {
let result = client.query(&sql, &[]).await;
assert!(result.is_ok());
+ // A decryption failure is connection-fatal, so verify tenant isolation on
+ // a disposable connection while preserving this connection for the
+ // TENANT_1 -> TENANT_2 -> TENANT_1 switch under test.
+ let tenant_2_client = connect_with_tls(*PROXY).await;
+ let sql = format!("SET CIPHERSTASH.KEYSET_NAME = '{tenant_keyset_name_2}'");
+ tenant_2_client.query(&sql, &[]).await.unwrap();
+
// SELECT data created by TENANT_1 AS TENANT_2
- let result = client.query(select_sql, &[&tenant_1_id]).await;
+ let result = tenant_2_client.query(select_sql, &[&tenant_1_id]).await;
assert!(result.is_err());
// --------
@@ -146,9 +153,16 @@ mod tests {
let result = client.simple_query(&sql).await;
assert!(result.is_ok());
+ // A decryption failure is connection-fatal, so verify tenant isolation on
+ // a disposable connection while preserving this connection for the
+ // TENANT_1 -> TENANT_2 -> TENANT_1 switch under test.
+ let tenant_2_client = connect_with_tls(*PROXY).await;
+ let sql = format!("SET CIPHERSTASH.KEYSET_NAME = '{tenant_keyset_name_2}'");
+ tenant_2_client.simple_query(&sql).await.unwrap();
+
// SELECT data created by TENANT_1 AS TENANT_2
let sql = format!("SELECT id, encrypted_text FROM encrypted WHERE id = {tenant_1_id}");
- let result = client.simple_query(&sql).await;
+ let result = tenant_2_client.simple_query(&sql).await;
assert!(result.is_err());
// --------
diff --git a/packages/cipherstash-proxy/Cargo.toml b/packages/cipherstash-proxy/Cargo.toml
index 73dce1b9b..f694b0f08 100644
--- a/packages/cipherstash-proxy/Cargo.toml
+++ b/packages/cipherstash-proxy/Cargo.toml
@@ -5,7 +5,6 @@ edition = "2021"
[dependencies]
async-trait = "0.1"
-aws-lc-rs = "1.13.3"
bigdecimal = { version = "0.4.6", features = ["serde-json"] }
blake3 = "1"
arc-swap = "1.7.1"
@@ -29,8 +28,7 @@ md-5 = "0.10.6"
metrics = "0.24.3"
metrics-exporter-prometheus = "0.17"
moka = { version = "0.12", features = ["future"] }
-oid-registry = "0.8"
-pg_escape = "0.1.1"
+pg-proto = "0.11.1"
postgres-protocol = "0.6.7"
postgres-types = { version = "0.2.8", features = ["with-serde_json-1"] }
rand = "0.9"
@@ -40,10 +38,11 @@ rust_decimal = { version = "1.36.0", default-features = false, features = [
] }
rustls = { version = "0.23.20", default-features = false, features = ["std"] }
rustls-platform-verifier = "0.5.0"
+rustls-native-certs = "0.8"
rustls-pki-types = "1.10.0"
serde = "1.0"
serde_json = "1.0"
-socket2 = "0.5.7"
+socket2 = "0.6"
sqltk = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
@@ -52,13 +51,11 @@ tokio-postgres = { version = "0.7", features = [
"with-serde_json-1",
] }
tokio-postgres-rustls = "0.13.0"
-tokio-rustls = "0.26.0"
tokio-util = { version = "0.7.13", features = ["rt"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
uuid = { version = "1.11.0", features = ["serde", "v4"] }
vitaminc-protected = "0.1.0-pre4.2"
-x509-parser = "0.17.0"
[dev-dependencies]
diff --git a/packages/cipherstash-proxy/src/connect/async_stream.rs b/packages/cipherstash-proxy/src/connect/async_stream.rs
deleted file mode 100644
index 83436a05b..000000000
--- a/packages/cipherstash-proxy/src/connect/async_stream.rs
+++ /dev/null
@@ -1,148 +0,0 @@
-use super::{configure, connect_with_retry};
-use crate::{error::Error, log::AUTHENTICATION};
-use aws_lc_rs::digest;
-use core::str;
-use oid_registry::{
- Oid, OID_HASH_SHA1, OID_NIST_HASH_SHA256, OID_NIST_HASH_SHA384, OID_NIST_HASH_SHA512,
- OID_PKCS1_SHA1WITHRSA, OID_PKCS1_SHA256WITHRSA, OID_PKCS1_SHA384WITHRSA,
- OID_PKCS1_SHA512WITHRSA, OID_SIG_ECDSA_WITH_SHA256, OID_SIG_ECDSA_WITH_SHA384, OID_SIG_ED25519,
-};
-use postgres_protocol::authentication::sasl::ChannelBinding;
-
-use std::{
- pin::Pin,
- task::{Context, Poll},
-};
-use tokio::{
- io::{split, AsyncRead, AsyncWrite, ReadBuf},
- net::{TcpListener, TcpStream},
-};
-use tokio_rustls::TlsStream;
-use tracing::debug;
-use x509_parser::prelude::{FromDer, X509Certificate};
-
-#[derive(Debug)]
-pub enum AsyncStream {
- Tcp(TcpStream),
- Tls(Box>),
-}
-
-impl AsyncStream {
- pub async fn accept(listener: &TcpListener) -> Result {
- let (stream, _) = listener.accept().await?;
- configure(&stream);
- Ok(AsyncStream::Tcp(stream))
- }
-
- pub async fn connect(addr: &str) -> Result {
- let stream = connect_with_retry(addr).await?;
- configure(&stream);
- Ok(AsyncStream::Tcp(stream))
- }
-
- pub fn split(
- self,
- ) -> (
- tokio::io::ReadHalf,
- tokio::io::WriteHalf,
- ) {
- split(self)
- }
-
- pub fn is_tls(&self) -> bool {
- matches!(self, AsyncStream::Tls(_))
- }
-
- pub fn is_tcp(&self) -> bool {
- !self.is_tls()
- }
-
- pub fn channel_binding(&self) -> ChannelBinding {
- match self {
- AsyncStream::Tcp(_) => ChannelBinding::unsupported(),
- AsyncStream::Tls(stream) => {
- let (_, session) = stream.get_ref();
- let certs = session.peer_certificates();
- match certs {
- Some(certs) if !certs.is_empty() => {
- let cert_der = &certs[0];
- X509Certificate::from_der(cert_der)
- .ok()
- .map(|(_, cert)| get_digest(&cert.signature_algorithm.algorithm))
- .map_or_else(ChannelBinding::unsupported, |algorithm| {
- let hash = digest::digest(algorithm, certs[0].as_ref());
- ChannelBinding::tls_server_end_point(hash.as_ref().into())
- })
- }
- _ => {
- debug!(
- target: AUTHENTICATION,
- msg = "Missing certificates, ChannelBinding is unsupported"
- );
- ChannelBinding::unsupported()
- }
- }
- }
- }
- }
-}
-
-///
-/// Note: SHA1 is upgraded to SHA256 as per https://datatracker.ietf.org/doc/html/rfc5929#section-4.1
-///
-fn get_digest(oid: &Oid) -> &'static digest::Algorithm {
- match oid {
- oid if oid == &OID_HASH_SHA1 => &digest::SHA256,
- oid if oid == &OID_NIST_HASH_SHA256 => &digest::SHA256,
- oid if oid == &OID_PKCS1_SHA1WITHRSA => &digest::SHA256,
- oid if oid == &OID_PKCS1_SHA256WITHRSA => &digest::SHA256,
- oid if oid == &OID_SIG_ECDSA_WITH_SHA256 => &digest::SHA256,
- oid if oid == &OID_NIST_HASH_SHA384 => &digest::SHA384,
- oid if oid == &OID_PKCS1_SHA384WITHRSA => &digest::SHA384,
- oid if oid == &OID_SIG_ECDSA_WITH_SHA384 => &digest::SHA384,
- oid if oid == &OID_NIST_HASH_SHA512 => &digest::SHA512,
- oid if oid == &OID_PKCS1_SHA512WITHRSA => &digest::SHA512,
- oid if oid == &OID_SIG_ED25519 => &digest::SHA512,
- _ => panic!("Unsupported OID"),
- }
-}
-
-impl AsyncRead for AsyncStream {
- fn poll_read(
- mut self: Pin<&mut Self>,
- cx: &mut Context<'_>,
- buf: &mut ReadBuf<'_>,
- ) -> Poll> {
- match *self {
- AsyncStream::Tcp(ref mut stream) => Pin::new(stream).poll_read(cx, buf),
- AsyncStream::Tls(ref mut stream) => Pin::new(stream).poll_read(cx, buf),
- }
- }
-}
-
-impl AsyncWrite for AsyncStream {
- fn poll_write(
- mut self: Pin<&mut Self>,
- cx: &mut Context<'_>,
- buf: &[u8],
- ) -> Poll> {
- match *self {
- AsyncStream::Tcp(ref mut stream) => Pin::new(stream).poll_write(cx, buf),
- AsyncStream::Tls(ref mut stream) => Pin::new(stream).poll_write(cx, buf),
- }
- }
-
- fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
- match *self {
- AsyncStream::Tcp(ref mut stream) => Pin::new(stream).poll_flush(cx),
- AsyncStream::Tls(ref mut stream) => Pin::new(stream).poll_flush(cx),
- }
- }
-
- fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
- match *self {
- AsyncStream::Tcp(ref mut stream) => Pin::new(stream).poll_shutdown(cx),
- AsyncStream::Tls(ref mut stream) => Pin::new(stream).poll_shutdown(cx),
- }
- }
-}
diff --git a/packages/cipherstash-proxy/src/connect/channel_writer.rs b/packages/cipherstash-proxy/src/connect/channel_writer.rs
deleted file mode 100644
index a6dc68ffb..000000000
--- a/packages/cipherstash-proxy/src/connect/channel_writer.rs
+++ /dev/null
@@ -1,109 +0,0 @@
-use bytes::BytesMut;
-use tokio::{
- io::{AsyncWrite, AsyncWriteExt},
- sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
-};
-use tracing::{debug, error};
-
-use crate::log::PROTOCOL;
-
-pub type Receiver = UnboundedReceiver;
-pub type Sender = UnboundedSender;
-
-#[derive(Debug)]
-pub struct ChannelWriter
-where
- W: AsyncWrite + Unpin,
-{
- writer: W,
- receiver: Receiver,
- sender: Sender,
- client_id: i32,
-}
-
-impl ChannelWriter
-where
- W: AsyncWrite + Unpin,
-{
- pub fn new(writer: W, client_id: i32) -> Self {
- let (sender, receiver): (UnboundedSender, UnboundedReceiver) =
- mpsc::unbounded_channel();
-
- ChannelWriter {
- writer,
- receiver,
- sender,
- client_id,
- }
- }
-
- pub async fn receive(mut self) {
- debug!(target: PROTOCOL,
- client_id = self.client_id,
- msg = "ChannelWriter task started",
- );
-
- // Drop our own sender so the channel can close when frontend/backend senders are dropped
- // Without this, we have a circular dependency: receiver waits for all senders to drop,
- // but we're holding one of them ourselves!
- drop(self.sender);
-
- while let Some(bytes) = self.receiver.recv().await {
- debug!(target: PROTOCOL,
- client_id = self.client_id,
- msg = "Writing",
- ?bytes
- );
-
- match self.writer.write_all(&bytes).await {
- Ok(_) => {
- debug!(target: PROTOCOL,
- client_id = self.client_id,
- msg = "Write complete",
- );
- }
- Err(err) => {
- error!(target: PROTOCOL,
- client_id = self.client_id,
- msg = "Write error",
- error = ?err
- );
- break;
- }
- }
- }
-
- // Channel closed - shutdown the writer to properly close the connection
- debug!(target: PROTOCOL,
- client_id = self.client_id,
- msg = "Recv loop exited - channel closed, beginning shutdown",
- );
-
- // Flush any pending writes before shutdown
- if let Err(err) = self.writer.flush().await {
- error!(target: PROTOCOL,
- client_id = self.client_id,
- msg = "Error flushing writer during shutdown",
- error = ?err
- );
- }
-
- // Shutdown the write half to send FIN and properly close the connection
- if let Err(err) = self.writer.shutdown().await {
- error!(target: PROTOCOL,
- client_id = self.client_id,
- msg = "Error shutting down writer",
- error = ?err
- );
- }
-
- debug!(target: PROTOCOL,
- client_id = self.client_id,
- msg = "Writer shutdown complete",
- );
- }
-
- pub fn sender(&self) -> Sender {
- self.sender.clone()
- }
-}
diff --git a/packages/cipherstash-proxy/src/connect/mod.rs b/packages/cipherstash-proxy/src/connect/mod.rs
index d17d97969..722145e8d 100644
--- a/packages/cipherstash-proxy/src/connect/mod.rs
+++ b/packages/cipherstash-proxy/src/connect/mod.rs
@@ -1,11 +1,5 @@
-mod async_stream;
-mod channel_writer;
-
-pub use async_stream::AsyncStream;
-pub use channel_writer::{ChannelWriter, Sender};
-
-use crate::{config::ServerConfig, error::Error, log::DEVELOPMENT, tls, DatabaseConfig};
-use socket2::TcpKeepalive;
+use crate::{config::ServerConfig, error::Error, tls, DatabaseConfig};
+use socket2::{SockRef, TcpKeepalive};
use std::time::Duration;
use tokio::{
net::{TcpListener, TcpStream},
@@ -14,13 +8,66 @@ use tokio::{
use tokio_postgres::Client;
use tracing::{debug, error, info, warn};
+const MAX_RETRY_DELAY: Duration = Duration::from_secs(2);
+const MAX_RETRY_COUNT: u32 = 3;
const TCP_USER_TIMEOUT: Duration = Duration::from_secs(10);
const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5);
const TCP_KEEPALIVE_TIME: Duration = Duration::from_secs(5);
const TCP_KEEPALIVE_RETRIES: u32 = 5;
-const MAX_RETRY_DELAY: Duration = Duration::from_secs(2);
-const MAX_RETRY_COUNT: u32 = 3;
+fn configure_tcp(stream: &TcpStream) {
+ if let Err(error) = stream.set_nodelay(true) {
+ warn!(msg = "Error configuring TCP_NODELAY", error = %error);
+ }
+
+ let socket = SockRef::from(stream);
+ #[cfg(target_os = "linux")]
+ if let Err(error) = socket.set_tcp_user_timeout(Some(TCP_USER_TIMEOUT)) {
+ warn!(msg = "Error configuring TCP_USER_TIMEOUT", error = %error);
+ }
+
+ if let Err(error) = socket.set_keepalive(true) {
+ warn!(msg = "Error enabling TCP keepalive", error = %error);
+ return;
+ }
+
+ let keepalive = TcpKeepalive::new()
+ .with_interval(TCP_KEEPALIVE_INTERVAL)
+ .with_time(TCP_KEEPALIVE_TIME)
+ .with_retries(TCP_KEEPALIVE_RETRIES);
+ if let Err(error) = socket.set_tcp_keepalive(&keepalive) {
+ warn!(msg = "Error configuring TCP keepalive", error = %error);
+ }
+}
+
+pub async fn accept(listener: &TcpListener) -> Result {
+ let (stream, _) = listener.accept().await?;
+ configure_tcp(&stream);
+ Ok(stream)
+}
+
+pub async fn connect(address: &str) -> Result {
+ debug!(msg = "Connecting to database");
+ let mut delay = Duration::from_millis(100);
+ for attempt in 0..=MAX_RETRY_COUNT {
+ match TcpStream::connect(address).await {
+ Ok(stream) => {
+ configure_tcp(&stream);
+ return Ok(stream);
+ }
+ Err(error) if attempt < MAX_RETRY_COUNT => {
+ warn!(msg = "Database connection failed; retrying", %error, attempt);
+ time::sleep(delay).await;
+ delay = (delay * 2).min(MAX_RETRY_DELAY);
+ }
+ Err(error) => {
+ error!(msg = "Could not connect to database", error = %error);
+ return Err(Error::DatabaseConnection);
+ }
+ }
+ }
+ unreachable!()
+}
pub async fn database(config: &DatabaseConfig) -> Result {
let connection_config = config.to_connection_config();
@@ -79,81 +126,3 @@ pub async fn bind_with_retry(server: &ServerConfig) -> TcpListener {
retry_count += 1;
}
}
-
-pub async fn connect_with_retry(addr: &str) -> Result {
- let mut retry_count = 0;
-
- loop {
- debug!(target: DEVELOPMENT, msg = "Connecting to database");
- match TcpStream::connect(&addr).await {
- Ok(stream) => {
- return Ok(stream);
- }
- Err(err) => {
- if retry_count > MAX_RETRY_COUNT {
- error!(msg = "Could not connect to database", retries = ?retry_count, error = err.to_string());
- return Err(Error::DatabaseConnection);
- }
- }
- };
- let sleep_duration_ms =
- (100 * 2_u64.pow(retry_count)).min(MAX_RETRY_DELAY.as_millis() as _);
- time::sleep(Duration::from_millis(sleep_duration_ms)).await;
-
- retry_count += 1;
- }
-}
-
-///
-/// Configure the tcp socket
-/// set_nodelay
-/// set_keepalive
-///
-/// Keepalive is not as important without connection pooling timeouts to deal with
-///
-pub fn configure(stream: &TcpStream) {
- let sock_ref = socket2::SockRef::from(&stream);
-
- stream.set_nodelay(true).unwrap_or_else(|err| {
- warn!(
- msg = "Error configuring nodelay for connection",
- error = err.to_string()
- );
- });
-
- #[cfg(target_os = "linux")]
- match sock_ref.set_tcp_user_timeout(Some(TCP_USER_TIMEOUT)) {
- Ok(_) => (),
- Err(err) => {
- warn!(
- msg = "Error configuring tcp_user_timeout for connection",
- error = err.to_string()
- );
- }
- }
-
- match sock_ref.set_keepalive(true) {
- Ok(_) => {
- let params = &TcpKeepalive::new()
- .with_interval(TCP_KEEPALIVE_INTERVAL)
- .with_retries(TCP_KEEPALIVE_RETRIES)
- .with_time(TCP_KEEPALIVE_TIME);
-
- match sock_ref.set_tcp_keepalive(params) {
- Ok(_) => (),
- Err(err) => {
- warn!(
- msg = "Error configuring keepalive for connection",
- error = err.to_string()
- );
- }
- }
- }
- Err(err) => {
- warn!(
- msg = "Error configuring connection",
- error = err.to_string()
- );
- }
- }
-}
diff --git a/packages/cipherstash-proxy/src/error.rs b/packages/cipherstash-proxy/src/error.rs
index dffc14935..2660f2c40 100644
--- a/packages/cipherstash-proxy/src/error.rs
+++ b/packages/cipherstash-proxy/src/error.rs
@@ -1,12 +1,12 @@
use crate::{postgresql::Column, Identifier};
-use bytes::BytesMut;
use cipherstash_client::{encryption, schema::ColumnType};
use eql_mapper::{EqlMapperError, EqlTermVariant};
use metrics_exporter_prometheus::BuildError;
use std::{io, time::Duration};
use thiserror::Error;
-const ERROR_DOC_BASE_URL: &str = "https://github.com/cipherstash/proxy/blob/main/docs/errors.md";
+pub(crate) const ERROR_DOC_BASE_URL: &str =
+ "https://github.com/cipherstash/proxy/blob/main/docs/errors.md";
const ERROR_DOC_CONFIG_URL: &str =
"https://github.com/cipherstash/proxy/blob/main/docs/how-to/index.md#configuring-proxy";
@@ -55,7 +55,7 @@ pub enum Error {
Unknown,
#[error(transparent)]
- SendError(#[from] tokio::sync::mpsc::error::SendError),
+ Send(#[from] tokio::sync::mpsc::error::SendError),
}
impl Error {
@@ -474,6 +474,21 @@ pub enum ProtocolError {
#[error("Client authentication failed. Check username and password. For help visit {}#authentication-failed-client", ERROR_DOC_BASE_URL)]
ClientAuthenticationFailed,
+ #[error("A buffered PostgreSQL DataRow was not associated with an operation")]
+ HeldDataRowMissingOperation,
+
+ #[error("Buffered PostgreSQL DataRows crossed Execute operation boundaries")]
+ HeldDataRowOperationMismatch,
+
+ #[error("A buffered PostgreSQL response was not a DataRow")]
+ HeldBackendMessageNotDataRow,
+
+ #[error("Buffered PostgreSQL DataRows were not associated with an encrypted portal")]
+ HeldDataRowsNotEncrypted,
+
+ #[error("Expected {expected} DataRow columns, received {received}")]
+ DataRowColumnCountMismatch { expected: usize, received: usize },
+
#[error("Expected {expected} parameter format codes, received {received}")]
ParameterFormatCodesMismatch { expected: usize, received: usize },
diff --git a/packages/cipherstash-proxy/src/main.rs b/packages/cipherstash-proxy/src/main.rs
index a11da143d..5cf22a728 100644
--- a/packages/cipherstash-proxy/src/main.rs
+++ b/packages/cipherstash-proxy/src/main.rs
@@ -1,5 +1,5 @@
use cipherstash_proxy::config::TandemConfig;
-use cipherstash_proxy::connect::{self, AsyncStream};
+use cipherstash_proxy::connect;
use cipherstash_proxy::error::{ConfigError, Error};
use cipherstash_proxy::prometheus::CLIENTS_ACTIVE_CONNECTIONS;
use cipherstash_proxy::proxy::Proxy;
@@ -87,7 +87,7 @@ fn main() -> Result<(), Box> {
info!(msg = "Received SIGTERM");
break;
},
- Ok(client_stream) = AsyncStream::accept(&listener) => {
+ Ok(client_stream) = connect::accept(&listener) => {
client_id += 1;
@@ -97,12 +97,12 @@ fn main() -> Result<(), Box> {
gauge!(CLIENTS_ACTIVE_CONNECTIONS).increment(1);
- match pg::handler(client_stream,context).await {
+ let result = pg::handler(client_stream,context).await;
+ gauge!(CLIENTS_ACTIVE_CONNECTIONS).decrement(1);
+
+ match result {
Ok(_) => (),
Err(err) => {
-
- gauge!(CLIENTS_ACTIVE_CONNECTIONS).decrement(1);
-
match err {
Error::ConnectionClosed => {
info!(msg = "Database connection closed by client");
diff --git a/packages/cipherstash-proxy/src/postgresql/context/mod.rs b/packages/cipherstash-proxy/src/postgresql/context/mod.rs
index d42e015cf..a4dd1ab10 100644
--- a/packages/cipherstash-proxy/src/postgresql/context/mod.rs
+++ b/packages/cipherstash-proxy/src/postgresql/context/mod.rs
@@ -4,11 +4,7 @@ pub mod portal;
pub mod statement;
pub mod statement_metadata;
pub use self::{phase_timing::PhaseTiming, portal::Portal, statement::Statement};
-use super::{
- column_mapper::ColumnMapper,
- messages::{describe::Describe, Name, Target},
- Column,
-};
+use super::{column_mapper::ColumnMapper, rewrite::Name, Column};
use crate::{
config::TandemConfig,
error::{EncryptError, Error},
@@ -22,11 +18,12 @@ use crate::{
use cipherstash_client::IdentifiedBy;
use eql_mapper::{Schema, TableResolver};
use metrics::{counter, histogram};
+use pg_proto::{Describe, DescribeTarget, OperationId, TransactionStatus};
use serde_json::json;
use sqltk::parser::ast::{Expr, Ident, ObjectName, ObjectNamePart, Set, Value, ValueWithSpan};
pub use statement_metadata::StatementMetadata;
use std::{
- collections::{HashMap, VecDeque},
+ collections::HashMap,
sync::{
atomic::{AtomicU64, Ordering},
Arc, LazyLock, RwLock,
@@ -37,12 +34,7 @@ use tokio::sync::oneshot;
use tracing::{debug, error, warn};
use uuid::Uuid;
-type DescribeQueue = Queue;
-type ExecuteQueue = Queue;
-type SessionMetricsQueue = Queue;
-type PortalQueue = Queue>;
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct SessionId(u64);
#[derive(Clone, Debug, PartialEq)]
@@ -67,26 +59,25 @@ where
column_mapper: ColumnMapper,
statements: Arc>>>,
statement_sessions: Arc>>,
- portals: Arc>>,
- describe: Arc>,
- execute: Arc>,
+ portals: Arc>>>,
+ operations: Arc>>,
schema_changed: Arc>,
- session_metrics: Arc>,
+ session_metrics: Arc>>,
table_resolver: Arc,
unsafe_disable_mapping: bool,
keyset_id: Arc>>,
session_id_counter: Arc,
+ transaction_status: Arc>,
}
/// Context for tracking an in-flight Execute operation.
///
-/// Timing data is accumulated here during backend message processing because
-/// the backend operates on the execute queue rather than having direct access
-/// to the session metrics queue. On completion via `complete_execution()`,
-/// timing is transferred to the associated SessionMetricsContext.
+/// This stores only CipherStash metadata associated with pg-proto's operation;
+/// pg-proto owns protocol ordering and backpressure.
#[derive(Clone, Debug)]
pub struct ExecuteContext {
name: Name,
+ portal: Option>,
start: Instant,
session_id: Option,
/// Server wait duration (time to first response byte).
@@ -98,9 +89,14 @@ pub struct ExecuteContext {
}
impl ExecuteContext {
- fn new(name: Name, session_id: Option) -> ExecuteContext {
+ fn new(
+ name: Name,
+ portal: Option>,
+ session_id: Option,
+ ) -> ExecuteContext {
ExecuteContext {
name,
+ portal,
start: Instant::now(),
session_id,
server_wait_duration: None,
@@ -133,6 +129,12 @@ impl ExecuteContext {
}
}
+#[derive(Clone, Debug, Default)]
+struct OperationContext {
+ describe_statement: Option>,
+ execute: Option,
+}
+
#[derive(Clone, Debug)]
pub struct SessionMetricsContext {
id: SessionId,
@@ -160,11 +162,6 @@ impl SessionMetricsContext {
}
}
-#[derive(Clone, Debug)]
-pub struct Queue {
- pub queue: VecDeque,
-}
-
impl Context
where
T: EncryptionService,
@@ -183,10 +180,9 @@ where
statements: Arc::new(RwLock::new(HashMap::new())),
statement_sessions: Arc::new(RwLock::new(HashMap::new())),
portals: Arc::new(RwLock::new(HashMap::new())),
- describe: Arc::new(RwLock::from(Queue::new())),
- execute: Arc::new(RwLock::from(Queue::new())),
+ operations: Arc::new(RwLock::new(HashMap::new())),
schema_changed: Arc::new(RwLock::from(false)),
- session_metrics: Arc::new(RwLock::from(Queue::new())),
+ session_metrics: Arc::new(RwLock::new(HashMap::new())),
table_resolver: Arc::new(TableResolver::new_editable(schema)),
client_id,
config,
@@ -197,33 +193,57 @@ where
unsafe_disable_mapping: false,
keyset_id: Arc::new(RwLock::new(None)),
session_id_counter: Arc::new(AtomicU64::new(1)),
+ transaction_status: Arc::new(RwLock::new(TransactionStatus::Idle)),
}
}
- pub fn set_describe(&mut self, describe: Describe) {
+ pub fn set_describe(&mut self, operation: OperationId, describe: Describe) {
debug!(target: CONTEXT, client_id = self.client_id, describe = ?describe);
- let _ = self.describe.write().map(|mut queue| queue.add(describe));
+ let statement = match &describe {
+ Describe {
+ name,
+ target: DescribeTarget::Portal,
+ } => self.get_portal_statement(name),
+ Describe {
+ name,
+ target: DescribeTarget::Statement,
+ } => self.get_statement(name),
+ };
+ let _ = self.operations.write().map(|mut operations| {
+ operations.entry(operation).or_default().describe_statement = statement;
+ });
}
///
/// Marks the current Describe as complete
/// Removes the Describe from the Queue
///
- pub fn complete_describe(&mut self) {
+ pub fn complete_describe(&mut self, operation: OperationId) {
debug!(target: CONTEXT, client_id = self.client_id, msg = "Describe complete");
- let _ = self.describe.write().map(|mut queue| queue.complete());
+ let _ = self.operations.write().map(|mut operations| {
+ if let Some(context) = operations.get_mut(&operation) {
+ context.describe_statement = None;
+ if context.execute.is_none() {
+ operations.remove(&operation);
+ }
+ }
+ });
}
pub fn start_session(&mut self) -> SessionId {
let id = SessionId(self.session_id_counter.fetch_add(1, Ordering::Relaxed));
let ctx = SessionMetricsContext::new(id);
- let _ = self.session_metrics.write().map(|mut queue| queue.add(ctx));
+ let _ = self
+ .session_metrics
+ .write()
+ .map(|mut sessions| sessions.insert(id, ctx));
id
}
- pub fn finish_session(&mut self) {
+ pub fn finish_session(&mut self, session_id: Option) {
debug!(target: CONTEXT, client_id = self.client_id, msg = "Session Metrics finished");
- if let Some(session) = self.get_session_metrics() {
+ let session = session_id.and_then(|id| self.session_metrics.write().ok()?.remove(&id));
+ if let Some(session) = session {
let duration = session.duration();
let metadata = &session.metadata;
@@ -287,43 +307,27 @@ where
);
}
}
-
- let _ = self
- .session_metrics
- .write()
- .map(|mut queue| queue.complete());
}
- pub fn set_execute(&mut self, name: Name, session_id: Option) {
+ pub fn set_execute(
+ &mut self,
+ operation: OperationId,
+ name: Name,
+ session_id: Option,
+ ) {
debug!(target: CONTEXT, client_id = self.client_id, execute = ?name);
- let ctx = ExecuteContext::new(name, session_id);
-
- let _ = self.execute.write().map(|mut queue| queue.add(ctx));
+ let portal = self.get_portal(&name);
+ let ctx = ExecuteContext::new(name, portal, session_id);
+ let _ = self.operations.write().map(|mut operations| {
+ operations.entry(operation).or_default().execute = Some(ctx);
+ });
}
/// Set execute state for portal, looking up session ID internally.
- pub fn set_execute_for_portal(&mut self, name: Name) {
+ pub fn set_execute_for_portal(&mut self, operation: OperationId, name: Name) {
let session_id = self.get_portal_session_id(&name);
- self.set_execute(name, session_id);
- }
-
- /// Number of entries currently queued in the `execute` queue.
- ///
- /// Test-only accessor used by the BUG-300 regression tests to assert the
- /// per-connection queues are drained (and do not grow unbounded).
- #[cfg(test)]
- pub(crate) fn execute_queue_len(&self) -> usize {
- self.execute.read().unwrap().queue.len()
- }
-
- /// Number of entries currently queued in the `session_metrics` queue.
- ///
- /// Test-only accessor used by the BUG-300 regression tests to assert the
- /// per-connection queues are drained (and do not grow unbounded).
- #[cfg(test)]
- pub(crate) fn session_metrics_queue_len(&self) -> usize {
- self.session_metrics.read().unwrap().queue.len()
+ self.set_execute(operation, name, session_id);
}
/// Marks the current Execution as Complete.
@@ -332,9 +336,8 @@ where
/// - `server_wait_duration` (time to first response byte) is recorded to the session
/// - `server_response_duration` (time receiving response data) is added to the session
///
- /// This two-phase timing pattern exists because the backend operates on the execute queue
- /// rather than having direct access to the session. Timing is accumulated in ExecuteContext
- /// during message processing, then transferred to the correct SessionMetricsContext here.
+ /// Timing is transferred to the session identified by this operation rather
+ /// than inferred from response order.
///
/// If the associated portal is Unnamed, it is closed.
///
@@ -343,10 +346,20 @@ where
/// An unnamed portal is destroyed at the end of the transaction, or as soon as the next Bind statement specifying the unnamed portal as destination is issued
///
/// https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY
- pub fn complete_execution(&mut self) {
+ pub fn complete_execution(&mut self, operation: OperationId) -> Option {
debug!(target: CONTEXT, client_id = self.client_id, msg = "Execute complete");
- if let Some(execute) = self.get_execute() {
+ let execute = self.operations.write().ok().and_then(|mut operations| {
+ let execute = operations.get_mut(&operation)?.execute.take();
+ if operations
+ .get(&operation)
+ .is_some_and(|context| context.describe_statement.is_none())
+ {
+ operations.remove(&operation);
+ }
+ execute
+ });
+ if let Some(execute) = execute {
if let Some(session_id) = execute.session_id() {
if let Some(wait) = execute.server_wait_duration() {
self.record_server_wait_duration(session_id, wait);
@@ -358,25 +371,27 @@ where
}
// Get labels from current session metadata
- let (statement_type, protocol, mapped, multi_statement) =
- if let Some(session) = self.get_session_metrics() {
- let metadata = &session.metadata;
- (
- metadata
- .statement_type
- .map(|t| t.as_label())
- .unwrap_or("unknown"),
- metadata.protocol.map(|p| p.as_label()).unwrap_or("unknown"),
- if metadata.encrypted { "true" } else { "false" },
- if metadata.multi_statement {
- "true"
- } else {
- "false"
- },
- )
- } else {
- ("unknown", "unknown", "false", "false")
- };
+ let (statement_type, protocol, mapped, multi_statement) = if let Some(session) = execute
+ .session_id()
+ .and_then(|id| self.get_session_metrics(id))
+ {
+ let metadata = &session.metadata;
+ (
+ metadata
+ .statement_type
+ .map(|t| t.as_label())
+ .unwrap_or("unknown"),
+ metadata.protocol.map(|p| p.as_label()).unwrap_or("unknown"),
+ if metadata.encrypted { "true" } else { "false" },
+ if metadata.multi_statement {
+ "true"
+ } else {
+ "false"
+ },
+ )
+ } else {
+ ("unknown", "unknown", "false", "false")
+ };
histogram!(
STATEMENTS_EXECUTION_DURATION_SECONDS,
@@ -387,12 +402,12 @@ where
)
.record(execute.duration());
- if execute.name.is_unnamed() {
- self.close_portal(&execute.name);
+ if execute.name.is_empty() {
+ self.close_portal_if_current(&execute.name, execute.portal.as_ref());
}
+ return execute.session_id();
}
-
- let _ = self.execute.write().map(|mut queue| queue.complete());
+ None
}
pub fn add_statement(&mut self, name: Name, statement: Statement) {
@@ -406,15 +421,39 @@ where
pub fn close_statement(&mut self, name: &Name) {
debug!(target: CONTEXT, client_id = self.client_id, statement = ?name);
- let _ = self
+ let statement = self
.statements
.write()
- .map(|mut guarded| guarded.remove(name));
+ .ok()
+ .and_then(|mut guarded| guarded.remove(name));
- let _ = self
+ if let Some(statement) = statement {
+ let _ = self.portals.write().map(|mut guarded| {
+ guarded.retain(|_, portal| {
+ !matches!(portal.as_ref(), Portal::Encrypted { statement: portal_statement, .. } if Arc::ptr_eq(portal_statement, &statement))
+ });
+ });
+ }
+
+ let session_id = self
.statement_sessions
.write()
- .map(|mut guarded| guarded.remove(name));
+ .ok()
+ .and_then(|mut guarded| guarded.remove(name));
+ self.finish_session(session_id);
+ }
+
+ pub fn transaction_status(&self) -> TransactionStatus {
+ self.transaction_status
+ .read()
+ .map(|status| *status)
+ .unwrap_or(TransactionStatus::Idle)
+ }
+
+ pub fn set_transaction_status(&mut self, status: TransactionStatus) {
+ if let Ok(mut current) = self.transaction_status.write() {
+ *current = status;
+ }
}
/// Close both statement and its associated portal.
@@ -425,12 +464,10 @@ where
pub fn add_portal(&mut self, name: Name, portal: Portal) {
debug!(target: CONTEXT, client_id = self.client_id, name = ?name, portal = ?portal);
- let _ = self.portals.write().map(|mut portals| {
- portals
- .entry(name)
- .or_insert_with(Queue::new)
- .add(Arc::new(portal));
- });
+ let _ = self
+ .portals
+ .write()
+ .map(|mut portals| portals.insert(name, Arc::new(portal)));
}
pub fn get_statement(&self, name: &Name) -> Option> {
@@ -462,7 +499,7 @@ where
warn!(
target: CONTEXT,
client_id = self.client_id,
- prepared_statement = %name.as_str(),
+ prepared_statement = %String::from_utf8_lossy(name),
msg = "Session lookup failed for prepared statement, using latest session"
);
}
@@ -475,10 +512,18 @@ where
///
pub fn close_portal(&mut self, name: &Name) {
debug!(target: CONTEXT, client_id = self.client_id, msg = "Close Portal", name = ?name);
+ let _ = self.portals.write().map(|mut portals| portals.remove(name));
+ }
+
+ fn close_portal_if_current(&mut self, name: &Name, expected: Option<&Arc>) {
let _ = self.portals.write().map(|mut portals| {
- portals
- .entry(name.clone())
- .and_modify(|queue| queue.complete());
+ if expected.is_some_and(|expected| {
+ portals
+ .get(name)
+ .is_some_and(|current| Arc::ptr_eq(current, expected))
+ }) {
+ portals.remove(name);
+ }
});
}
@@ -486,14 +531,12 @@ where
debug!(target: CONTEXT, client_id = self.client_id, src = "Get Portal", portal = ?name);
let portals = self.portals.read().ok()?;
- let queue = portals.get(name)?;
- queue.next().cloned()
+ portals.get(name).cloned()
}
pub fn get_portal_statement(&self, name: &Name) -> Option> {
let portals = self.portals.read().ok()?;
- let queue = portals.get(name)?;
- let portal = queue.next()?;
+ let portal = portals.get(name)?;
debug!(target: CONTEXT, client_id = self.client_id, portal = ?portal);
@@ -505,59 +548,52 @@ where
pub fn get_portal_session_id(&self, name: &Name) -> Option {
let portals = self.portals.read().ok()?;
- let queue = portals.get(name)?;
- let portal = queue.next()?;
+ let portal = portals.get(name)?;
portal.session_id()
}
- pub fn get_statement_for_row_decription(&self) -> Option> {
- if let Some(statement) = self.get_statement_from_describe() {
+ pub fn get_statement_for_operation(&self, operation: OperationId) -> Option> {
+ let operations = self.operations.read().ok()?;
+ let context = operations.get(&operation)?;
+ if let Some(statement) = &context.describe_statement {
return Some(statement.clone());
}
-
- if let Some(Portal::Encrypted { statement, .. }) = self.get_portal_from_execute().as_deref()
- {
- return Some(statement.clone());
- };
-
- None
+ match context.execute.as_ref()?.portal.as_deref()? {
+ Portal::Encrypted { statement, .. } => Some(statement.clone()),
+ Portal::Passthrough { .. } => None,
+ }
}
- pub fn get_statement_from_describe(&self) -> Option> {
- let queue = self.describe.read().ok()?;
- let describe = queue.next()?;
-
- debug!(target: CONTEXT, client_id = self.client_id, msg = "Get Statement", describe = ?describe);
-
- match describe {
- Describe {
- ref name,
- target: Target::Portal,
- } => self.get_portal_statement(name),
- Describe {
- ref name,
- target: Target::Statement,
- } => self.get_statement(name),
- }
+ pub fn get_statement_from_describe(&self, operation: OperationId) -> Option> {
+ self.operations
+ .read()
+ .ok()?
+ .get(&operation)?
+ .describe_statement
+ .clone()
}
- pub fn get_portal_from_execute(&self) -> Option> {
- let queue = self.execute.read().ok()?;
- let execute_context = queue.next()?;
- let name = &execute_context.name;
- self.get_portal(name)
+ pub fn get_portal_from_execute(&self, operation: OperationId) -> Option> {
+ self.operations
+ .read()
+ .ok()?
+ .get(&operation)?
+ .execute
+ .as_ref()?
+ .portal
+ .clone()
}
- pub fn get_execute(&self) -> Option {
- let queue = self.execute.read().ok()?;
- let execute_context = queue.next()?;
+ pub fn get_execute(&self, operation: OperationId) -> Option {
+ let operations = self.operations.read().ok()?;
+ let execute_context = operations.get(&operation)?.execute.as_ref()?;
debug!(target: CONTEXT, client_id = self.client_id, msg = "Get Execute", execute = ?execute_context);
Some(execute_context.to_owned())
}
- pub fn get_session_metrics(&self) -> Option {
- let queue = self.session_metrics.read().ok()?;
- let session_context = queue.next()?;
+ pub fn get_session_metrics(&self, session_id: SessionId) -> Option {
+ let sessions = self.session_metrics.read().ok()?;
+ let session_context = sessions.get(&session_id)?;
debug!(target: CONTEXT, client_id = self.client_id, msg = "Get Session Metrics", session_metrics = ?session_context);
Some(session_context.to_owned())
}
@@ -897,20 +933,16 @@ where
where
F: FnOnce(&mut SessionMetricsContext),
{
- if let Ok(mut queue) = self.session_metrics.write() {
- if let Some(session) = queue
- .queue
- .iter_mut()
- .find(|session| session.id() == session_id)
- {
+ if let Ok(mut sessions) = self.session_metrics.write() {
+ if let Some(session) = sessions.get_mut(&session_id) {
f(session);
}
}
}
pub fn latest_session_id(&self) -> Option {
- let queue = self.session_metrics.read().ok()?;
- queue.queue.back().map(|session| session.id())
+ let sessions = self.session_metrics.read().ok()?;
+ sessions.keys().max().copied()
}
/// Record parse phase duration for the session (first write wins)
@@ -1004,77 +1036,62 @@ where
}
/// Record server wait for first response; otherwise accumulate response time for the current execute
- pub fn record_execute_server_timing(&mut self, duration: Duration) {
- if let Ok(mut queue) = self.execute.write() {
- if let Some(execute) = queue.current_mut() {
+ pub fn record_execute_server_timing(&mut self, operation: OperationId, duration: Duration) {
+ if let Ok(mut operations) = self.operations.write() {
+ if let Some(execute) = operations
+ .get_mut(&operation)
+ .and_then(|op| op.execute.as_mut())
+ {
execute.record_server_wait_or_add_response(duration);
}
}
}
/// Add decrypt phase duration for the current execute session (if any)
- pub fn add_decrypt_duration_for_execute(&mut self, duration: Duration) {
- let session_id = self.get_execute().and_then(|execute| execute.session_id());
+ pub fn add_decrypt_duration_for_execute(&mut self, operation: OperationId, duration: Duration) {
+ let session_id = self
+ .get_execute(operation)
+ .and_then(|execute| execute.session_id());
if let Some(session_id) = session_id {
self.add_decrypt_duration(session_id, duration);
}
}
/// Add client write duration for the current execute session (if any)
- pub fn add_client_write_duration_for_execute(&mut self, duration: Duration) {
- let session_id = self.get_execute().and_then(|execute| execute.session_id());
+ pub fn add_client_write_duration_for_execute(
+ &mut self,
+ operation: OperationId,
+ duration: Duration,
+ ) {
+ let session_id = self
+ .get_execute(operation)
+ .and_then(|execute| execute.session_id());
if let Some(session_id) = session_id {
self.add_client_write_duration(session_id, duration);
}
}
}
-impl Queue {
- pub fn new() -> Self {
- Queue {
- queue: VecDeque::new(),
- }
- }
-
- pub fn complete(&mut self) {
- let _ = self.queue.pop_front();
- }
-
- pub fn next(&self) -> Option<&T> {
- self.queue.front()
- }
-
- pub fn add(&mut self, item: T) {
- self.queue.push_back(item);
- }
-
- /// Get mutable reference to the current (first) item in the queue
- pub fn current_mut(&mut self) -> Option<&mut T> {
- self.queue.front_mut()
- }
-}
-
#[cfg(test)]
mod tests {
- use super::{Context, Describe, KeysetIdentifier, Portal, Statement};
+ use super::{Context, KeysetIdentifier, Portal, Statement};
use crate::{
config::LogConfig,
error::Error,
log,
- postgresql::{
- messages::{Name, Target},
- Column,
- },
+ postgresql::{rewrite::Name, Column},
proxy::{EncryptConfig, EncryptionService},
TandemConfig,
};
use cipherstash_client::IdentifiedBy;
use eql_mapper::Schema;
+ use pg_proto::TransactionStatus;
use sqltk::parser::{dialect::PostgreSqlDialect, parser::Parser};
use std::sync::Arc;
use tokio::sync::mpsc;
use uuid::Uuid;
+ #[derive(Clone)]
struct TestService {}
#[async_trait::async_trait]
@@ -1131,108 +1148,63 @@ mod tests {
Portal::encrypted_with_format_codes(statement.clone(), vec![], None)
}
- fn get_statement(portal: Arc) -> Arc {
- match portal.as_ref() {
- Portal::Encrypted { statement, .. } => statement.clone(),
- _ => {
- panic!("Expected Encrypted Portal");
- }
- }
- }
-
#[test]
- pub fn get_statement_from_describe() {
- log::init(LogConfig::default());
-
+ fn closing_a_statement_finishes_its_metrics_session() {
let mut context = create_context();
+ let name = Name::default();
+ let session_id = context.start_session();
+ context.set_statement_session(name.clone(), session_id);
- let name = Name::from("name");
+ context.close_statement(&name);
- context.add_statement(name.clone(), statement());
-
- let statement = context.get_statement(&name).unwrap();
-
- let describe = Describe {
- name,
- target: Target::Statement,
- };
- context.set_describe(describe);
-
- let s = context.get_statement_from_describe().unwrap();
-
- assert_eq!(s, statement)
+ assert!(context.get_session_metrics(session_id).is_none());
+ assert!(context.get_statement_session(&name).is_none());
}
#[test]
- pub fn execution_flow() {
- log::init(LogConfig::default());
-
+ fn closing_a_statement_invalidates_only_its_portals() {
let mut context = create_context();
+ let closed_statement_name = Name::from("closed_statement");
+ let retained_statement_name = Name::from("retained_statement");
+ context.add_statement(closed_statement_name.clone(), statement());
+ context.add_statement(retained_statement_name.clone(), statement());
- let statement_name = Name::from("statement");
- let portal_name = Name::from("portal");
-
- // Add statement to context
- context.add_statement(statement_name.clone(), statement());
+ let closed_statement = context.get_statement(&closed_statement_name).unwrap();
+ let retained_statement = context.get_statement(&retained_statement_name).unwrap();
+ let closed_portal_name = Name::from("differently_named_portal");
+ let retained_portal_name = Name::from("retained_portal");
+ let passthrough_portal_name = Name::from("passthrough_portal");
+ context.add_portal(closed_portal_name.clone(), portal(&closed_statement));
+ context.add_portal(retained_portal_name.clone(), portal(&retained_statement));
+ context.add_portal(passthrough_portal_name.clone(), Portal::passthrough(None));
- // Get statement from context
- let statement = context.get_statement(&statement_name).unwrap();
+ context.close_statement(&closed_statement_name);
- // Add portal pointing to statement to context
- context.add_portal(portal_name.clone(), portal(&statement));
-
- // Add statement name to execute context
- context.set_execute(portal_name.clone(), None);
-
- // Portal statement should be the right statement
- let portal = context.get_portal_from_execute().unwrap();
-
- let statement = get_statement(portal);
- assert_eq!(statement, statement);
-
- // Complete the execution
- context.complete_execution();
-
- // Should be no portal for execute context
- let portal = context.get_portal_from_execute();
- assert!(portal.is_none());
-
- // Unamed portal is closed on complete
- let portal = context.get_portal(&portal_name);
- assert!(portal.is_some());
+ assert!(context.get_portal(&closed_portal_name).is_none());
+ assert!(context.get_portal(&retained_portal_name).is_some());
+ assert!(context.get_portal(&passthrough_portal_name).is_some());
}
- /// Unit test for the queue-draining primitives.
- ///
- /// `complete_execution()` / `finish_session()` are the only drains for the
- /// per-connection `execute` / `session_metrics` queues. This asserts that
- /// calling them after enqueuing a session + execute leaves both queues
- /// empty, across many iterations.
- ///
- /// Note: this exercises the primitives directly — it does *not* drive the
- /// backend passthrough path that actually caused BUG-300 (that early
- /// returned without calling these). The backend-level regression test for
- /// BUG-300 lives in `backend.rs`
- /// (`passthrough_drains_queues_on_execute_terminating_message`).
#[test]
- pub fn complete_execution_and_finish_session_drain_queues() {
- log::init(LogConfig::default());
-
+ fn transaction_status_tracks_backend_ready_state() {
let mut context = create_context();
+ assert_eq!(context.transaction_status(), TransactionStatus::Idle);
- for _ in 0..1000 {
- // Frontend: a session + execute are enqueued for every statement.
- let session_id = context.start_session();
- context.set_execute(Name::unnamed(), Some(session_id));
+ context.set_transaction_status(TransactionStatus::InTransaction);
- // Drain primitives, normally called by the backend on an
- // execute-terminating message (CommandComplete / ErrorResponse / …).
- context.complete_execution();
- context.finish_session();
- }
+ assert_eq!(
+ context.transaction_status(),
+ TransactionStatus::InTransaction
+ );
+ }
- assert_eq!(context.execute_queue_len(), 0);
- assert_eq!(context.session_metrics_queue_len(), 0);
+ fn get_statement(portal: Arc) -> Arc {
+ match portal.as_ref() {
+ Portal::Encrypted { statement, .. } => statement.clone(),
+ _ => {
+ panic!("Expected Encrypted Portal");
+ }
+ }
}
#[test]
@@ -1249,9 +1221,6 @@ mod tests {
context.add_statement(statement_name_1.clone(), statement());
context.add_statement(statement_name_2.clone(), statement());
- // Replicate pipelined execution
- // Add multiple portals with the same name
- // Pointing to different statements
let portal_name = Name::from("portal");
let statement_1 = context.get_statement(&statement_name_1).unwrap();
@@ -1260,88 +1229,10 @@ mod tests {
let statement_2 = context.get_statement(&statement_name_2).unwrap();
context.add_portal(portal_name.clone(), portal(&statement_2));
- // Execute both portals
- context.set_execute(portal_name.clone(), None);
- context.set_execute(portal_name.clone(), None);
-
- // Portal should point to first statement
- let portal = context.get_portal_from_execute().unwrap();
- let statement = get_statement(portal);
- assert_eq!(statement, statement);
-
- let portal = context.get_portal_from_execute().unwrap();
- let statement = get_statement(portal);
- assert_eq!(statement_1, statement);
-
- // Complete execution
- context.complete_execution();
-
- // Portal should point to second statement
- let portal = context.get_portal_from_execute().unwrap();
-
- let statement = get_statement(portal);
- assert_eq!(statement_1, statement);
- }
-
- #[test]
- pub fn pipeline_execution() {
- log::init(LogConfig::default());
-
- let mut context = create_context();
-
- let statement_name_1 = Name::from("statement_1");
- let portal_name_1 = Name::unnamed();
-
- let statement_name_2 = Name::from("statement_2");
- let portal_name_2 = Name::unnamed();
-
- let statement_name_3 = Name::from("statement_3");
- let portal_name_3 = Name::from("portal_3");
-
- // Add statement to context
- context.add_statement(statement_name_1.clone(), statement());
- context.add_statement(statement_name_2.clone(), statement());
- context.add_statement(statement_name_3.clone(), statement());
-
- // Create portals for each statement
- let statement_1 = context.get_statement(&statement_name_1).unwrap();
- context.add_portal(portal_name_1.clone(), portal(&statement_1));
-
- let statement_2 = context.get_statement(&statement_name_2).unwrap();
- context.add_portal(portal_name_2.clone(), portal(&statement_2));
-
- let statement_3 = context.get_statement(&statement_name_3).unwrap();
- context.add_portal(portal_name_3.clone(), portal(&statement_3));
-
- // Add portals to execute context
- context.set_execute(portal_name_1.clone(), None);
- context.set_execute(portal_name_2.clone(), None);
- context.set_execute(portal_name_3.clone(), None);
-
- // Multiple calls return the portal for the first Execution context
- let portal = context.get_portal_from_execute().unwrap();
- let statement = get_statement(portal);
- assert_eq!(statement_1, statement);
-
- let portal = context.get_portal_from_execute().unwrap();
- let statement = get_statement(portal);
- assert_eq!(statement_1, statement);
-
- // Complete the execution of the first portal
- context.complete_execution();
-
- // Returns the next portal
- let portal = context.get_portal_from_execute().unwrap();
- let statement = get_statement(portal);
- assert_eq!(statement_2, statement);
-
- // Complete the execution
- context.complete_execution();
-
- // Returns the next portal
- let portal = context.get_portal_from_execute().unwrap();
- let statement = get_statement(portal);
- assert_eq!(statement_3, statement);
+ let portal = context.get_portal(&portal_name).unwrap();
+ assert_eq!(statement_2, get_statement(portal));
+ context.close_portal(&portal_name);
+ assert!(context.get_portal(&portal_name).is_none());
}
fn parse_statement(sql: &str) -> sqltk::parser::ast::Statement {
diff --git a/packages/cipherstash-proxy/src/postgresql/data/from_sql.rs b/packages/cipherstash-proxy/src/postgresql/data/from_sql.rs
index aafd74679..9ee51d606 100644
--- a/packages/cipherstash-proxy/src/postgresql/data/from_sql.rs
+++ b/packages/cipherstash-proxy/src/postgresql/data/from_sql.rs
@@ -1,7 +1,7 @@
use crate::{
error::{Error, MappingError},
log::ENCODING,
- postgresql::{format_code::FormatCode, messages::bind::BindParam},
+ postgresql::{format_code::FormatCode, rewrite::bind::BindParam},
};
use bigdecimal::BigDecimal;
use bytes::BytesMut;
@@ -603,7 +603,7 @@ fn decimal_from_sql(
#[cfg(test)]
mod binary_json_value_tests {
use super::*;
- use crate::postgresql::{format_code::FormatCode, messages::bind::BindParam};
+ use crate::postgresql::{format_code::FormatCode, rewrite::bind::BindParam};
use bytes::BytesMut;
fn binary_param(bytes: &[u8]) -> BindParam {
@@ -733,7 +733,7 @@ mod tests {
config::LogConfig,
log,
postgresql::{
- data::bind_param_from_sql, format_code::FormatCode, messages::bind::BindParam, Column,
+ data::bind_param_from_sql, format_code::FormatCode, rewrite::bind::BindParam, Column,
},
Identifier,
};
diff --git a/packages/cipherstash-proxy/src/postgresql/diagnostics.rs b/packages/cipherstash-proxy/src/postgresql/diagnostics.rs
new file mode 100644
index 000000000..3bd0a8ddf
--- /dev/null
+++ b/packages/cipherstash-proxy/src/postgresql/diagnostics.rs
@@ -0,0 +1,127 @@
+//! CipherStash diagnostic response policy over pg-proto's wire model.
+use bytes::Bytes;
+use pg_proto::{DiagnosticField, DiagnosticResponse};
+use regex::Regex;
+use std::sync::LazyLock;
+
+pub const CODE_UNDEFINED_COLUMN: &str = "42703";
+pub const CODE_INVALID_PASSWORD: &str = "28P01";
+pub const CODE_RAISE_EXCEPTION: &str = "P0001";
+pub const CODE_SYNTAX_ERROR: &str = "42601";
+pub const CODE_INVALID_TEXT_REPRESENTATION: &str = "22P02";
+pub const CODE_IDLE_SESSION_TIMEOUT: &str = "57P05";
+pub const CODE_SYSTEM_ERROR: &str = "58000";
+
+fn response(fields: impl IntoIterator- ) -> DiagnosticResponse {
+ DiagnosticResponse {
+ fields: fields
+ .into_iter()
+ .map(|(code, value)| DiagnosticField {
+ code,
+ value: Bytes::from(value),
+ })
+ .collect(),
+ }
+}
+
+fn standard(severity: &str, code: &str, message: String) -> DiagnosticResponse {
+ response([
+ (b'S', severity.to_owned()),
+ (b'V', severity.to_owned()),
+ (b'C', code.to_owned()),
+ (b'M', message),
+ ])
+}
+
+pub fn connection_timeout(message: String) -> DiagnosticResponse {
+ standard("FATAL", CODE_IDLE_SESSION_TIMEOUT, message)
+}
+
+pub fn invalid_password(message: String) -> DiagnosticResponse {
+ standard("FATAL", CODE_INVALID_PASSWORD, message)
+}
+
+pub fn invalid_sql_statement(message: String) -> DiagnosticResponse {
+ let line = extract_line_from_parse_error(&message);
+ let position = extract_position_from_parse_error(&message);
+ let mut fields = vec![
+ (b'S', "ERROR".to_owned()),
+ (b'V', "ERROR".to_owned()),
+ (b'C', CODE_SYNTAX_ERROR.to_owned()),
+ (b'M', message),
+ ];
+ if let Some(line) = line {
+ fields.push((b'L', line.to_string()));
+ }
+ if let Some(position) = position {
+ fields.push((b'P', position.to_string()));
+ }
+ response(fields)
+}
+
+pub fn invalid_parameter(message: String, table: &str, column: &str) -> DiagnosticResponse {
+ response([
+ (b'S', "ERROR".to_owned()),
+ (b'V', "ERROR".to_owned()),
+ (b'C', CODE_INVALID_TEXT_REPRESENTATION.to_owned()),
+ (b'M', message),
+ (b't', table.to_owned()),
+ (b'c', column.to_owned()),
+ ])
+}
+
+pub fn unknown_column(message: String, table: &str, column: &str) -> DiagnosticResponse {
+ response([
+ (b'S', "ERROR".to_owned()),
+ (b'V', "ERROR".to_owned()),
+ (b'C', CODE_UNDEFINED_COLUMN.to_owned()),
+ (b'M', message),
+ (b't', table.to_owned()),
+ (b'c', column.to_owned()),
+ (b'R', "cipherstash-proxy".to_owned()),
+ ])
+}
+
+pub fn system_error(message: String) -> DiagnosticResponse {
+ standard("FATAL", CODE_SYSTEM_ERROR, message)
+}
+
+pub fn is_fatal(response: &DiagnosticResponse) -> bool {
+ response
+ .fields
+ .iter()
+ .any(|field| field.code == b'S' && field.value.as_ref() == b"FATAL")
+}
+
+fn extract_line_from_parse_error(message: &str) -> Option {
+ static RE: LazyLock = LazyLock::new(|| Regex::new(r"\s*Line:\s*(\d+)").unwrap());
+ RE.captures(message)
+ .and_then(|capture| capture.get(1)?.as_str().parse().ok())
+}
+
+fn extract_position_from_parse_error(message: &str) -> Option {
+ static RE: LazyLock = LazyLock::new(|| Regex::new(r"\s*Column:\s*(\d+)").unwrap());
+ RE.captures(message)
+ .and_then(|capture| capture.get(1)?.as_str().parse().ok())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn field(response: &DiagnosticResponse, code: u8) -> Option<&[u8]> {
+ response
+ .fields
+ .iter()
+ .find(|field| field.code == code)
+ .map(|field| field.value.as_ref())
+ }
+
+ #[test]
+ fn sql_parse_error_includes_line_and_position() {
+ let response =
+ invalid_sql_statement("sql syntax error in blah Line: 1, Column: 2".to_owned());
+ assert_eq!(field(&response, b'L'), Some(b"1".as_slice()));
+ assert_eq!(field(&response, b'P'), Some(b"2".as_slice()));
+ }
+}
diff --git a/packages/cipherstash-proxy/src/postgresql/driver.rs b/packages/cipherstash-proxy/src/postgresql/driver.rs
new file mode 100644
index 000000000..b0d2424d7
--- /dev/null
+++ b/packages/cipherstash-proxy/src/postgresql/driver.rs
@@ -0,0 +1,221 @@
+use super::{middleware::CipherStashMiddlewareFactory, Context};
+use crate::{connect, error::Error, proxy::EncryptionService, tls};
+use pg_proto::{
+ BackendHoldLimits, BoundedPipeline, CancellationPolicy, Client, ClientTlsConfig,
+ ClientTlsPolicy, ClientTlsProvider, ConnectTarget, ForwardedMessage, FrontendMessage,
+ InMemoryCancellationRegistry, InitialServerContext, Intermediary, Server, ServerIdentity,
+ ServerIdentityProvider, ServerTlsPolicy, SslMode, StartupParameters, StartupRouteResolver,
+ StaticClientCredentials, StaticMd5ServerCredentials,
+};
+use std::{convert::Infallible, sync::Arc};
+use tokio::net::TcpStream;
+use tracing::{info, warn};
+
+#[derive(Clone)]
+struct Route(String);
+
+impl StartupRouteResolver for Route {
+ type Error = Infallible;
+ async fn resolve(
+ &self,
+ _: StartupParameters,
+ _: InitialServerContext<'_, Peer>,
+ ) -> Result {
+ Ok(ConnectTarget::new(self.0.clone()))
+ }
+}
+
+#[derive(Clone)]
+struct DownstreamIdentity(ServerIdentity);
+impl ServerIdentityProvider for DownstreamIdentity {
+ type Error = Infallible;
+ fn resolve(&self) -> Result {
+ Ok(self.0.clone())
+ }
+}
+
+#[derive(Clone)]
+struct UpstreamTls {
+ server_name: rustls_pki_types::ServerName<'static>,
+ verify: bool,
+}
+impl ClientTlsProvider for UpstreamTls {
+ type Error = Error;
+ async fn resolve(&self, _: &ConnectTarget) -> Result {
+ let name = self.server_name.clone();
+ let result = rustls_native_certs::load_native_certs();
+ for error in result.errors {
+ warn!(msg = "Could not load a native TLS certificate", %error);
+ }
+ let mut roots = rustls::RootCertStore::empty();
+ for certificate in result.certs {
+ roots
+ .add(certificate)
+ .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
+ }
+ if self.verify && roots.is_empty() {
+ return Err(crate::error::ConfigError::from(
+ crate::error::TlsConfigError::InvalidCertificate,
+ )
+ .into());
+ }
+ Ok(ClientTlsConfig::new(name, roots))
+ }
+}
+
+pub async fn handler
(client_stream: TcpStream, context: Context) -> Result<(), Error>
+where
+ S: EncryptionService + Clone,
+{
+ let address = context.database_socket_address();
+ let downstream_auth = StaticMd5ServerCredentials::new(
+ context.database_username().to_owned(),
+ context.database_password(),
+ );
+ let upstream_auth = StaticClientCredentials::new(
+ context.database_username().to_owned(),
+ context.database_password(),
+ );
+
+ macro_rules! run {
+ ($server:expr, $client:expr) => {{
+ let connection_timeout = context.connection_timeout();
+ let intermediary = Intermediary::builder()
+ .server($server)
+ .client($client)
+ .startup_resolver(Route(address.clone()))
+ .cancellation(CancellationPolicy::Forward)
+ .cancellation_registry(InMemoryCancellationRegistry::default())
+ .pipeline(BoundedPipeline::new(256).expect("non-zero pipeline bound"))
+ .backend_batching(
+ BackendHoldLimits::new(4096, 64 * 1024 * 1024)
+ .expect("non-zero backend hold limits"),
+ )
+ .middleware(CipherStashMiddlewareFactory(context.clone()))
+ .build()
+ .map_err(invalid_data)?;
+ let accept = intermediary.accept(client_stream, (), ());
+ let accepted = match connection_timeout {
+ Some(duration) => tokio::time::timeout(duration, accept)
+ .await
+ .map_err(|_| Error::ConnectionTimeout { duration })?,
+ None => accept.await,
+ }
+ .map_err(invalid_data)?;
+ let mut session = match accepted {
+ pg_proto::IntermediaryAccept::Session(session) => session,
+ pg_proto::IntermediaryAccept::CancellationForwarded => return Ok(()),
+ };
+ loop {
+ let forward = session.forward_next();
+ let forwarded = match connection_timeout {
+ Some(duration) => tokio::time::timeout(duration, forward)
+ .await
+ .map_err(|_| Error::ConnectionTimeout { duration })?,
+ None => forward.await,
+ };
+ let forwarded = match forwarded {
+ Ok(forwarded) => forwarded,
+ Err(pg_proto::ForwardError::Frontend(
+ pg_proto::FrontendProjectionError::Capacity(_),
+ )) => continue,
+ Err(pg_proto::ForwardError::Middleware(error)) => return Err(error),
+ Err(error) => return Err(invalid_data(error)),
+ };
+ if matches!(
+ forwarded,
+ ForwardedMessage::Frontend(FrontendMessage::Terminate)
+ ) {
+ break;
+ }
+ }
+ Ok(())
+ }};
+ }
+
+ macro_rules! run_client {
+ ($server:expr) => {{
+ if context.database_tls_disabled() {
+ let client = Client::builder()
+ .connector(|target: &ConnectTarget| {
+ let address = target.name().to_owned();
+ async move { connect::connect(&address).await }
+ })
+ .tls(ClientTlsPolicy::Disabled)
+ .authentication(upstream_auth.clone())
+ .build()
+ .map_err(invalid_data)?;
+ run!($server, client)
+ } else {
+ let provider = UpstreamTls {
+ server_name: context.config().database.server_name()?.to_owned(),
+ verify: context.config().database.with_tls_verification,
+ };
+ let mode = upstream_ssl_mode(context.config());
+ let client = Client::builder()
+ .connector(|target: &ConnectTarget| {
+ let address = target.name().to_owned();
+ async move { connect::connect(&address).await }
+ })
+ .tls(ClientTlsPolicy::libpq(mode, provider))
+ .authentication(upstream_auth.clone())
+ .build()
+ .map_err(invalid_data)?;
+ run!($server, client)
+ }
+ }};
+ }
+
+ info!(
+ msg = "Client connected",
+ database = address,
+ client_id = context.client_id
+ );
+ if let Some(tls_config) = context.tls_config() {
+ let (config, leaf) = tls::configure_server_with_leaf(tls_config)?;
+ let identity = DownstreamIdentity(ServerIdentity::new(Arc::new(config), leaf));
+ if context.require_tls() {
+ return run_client!(Server::builder()
+ .tls(ServerTlsPolicy::Required(identity))
+ .authentication(downstream_auth)
+ .build()
+ .map_err(invalid_data)?);
+ }
+ return run_client!(Server::builder()
+ .tls(ServerTlsPolicy::Optional(identity))
+ .authentication(downstream_auth)
+ .build()
+ .map_err(invalid_data)?);
+ }
+
+ run_client!(Server::builder()
+ .tls(ServerTlsPolicy::Disabled)
+ .authentication(downstream_auth)
+ .build()
+ .map_err(invalid_data)?)
+}
+
+fn invalid_data(error: impl std::fmt::Display) -> Error {
+ std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string()).into()
+}
+
+fn upstream_ssl_mode(config: &crate::TandemConfig) -> SslMode {
+ if config.database.with_tls_verification {
+ SslMode::VerifyFull
+ } else {
+ // Preserve the proxy's historical opportunistic-TLS policy: attempt
+ // SSL, but continue in plaintext when the database rejects it.
+ SslMode::Prefer
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn upstream_tls_without_verification_is_opportunistic() {
+ let config = crate::TandemConfig::for_testing();
+ assert_eq!(upstream_ssl_mode(&config), SslMode::Prefer);
+ }
+}
diff --git a/packages/cipherstash-proxy/src/postgresql/error_handler.rs b/packages/cipherstash-proxy/src/postgresql/error_handler.rs
index 4de24e03f..2478f7b17 100644
--- a/packages/cipherstash-proxy/src/postgresql/error_handler.rs
+++ b/packages/cipherstash-proxy/src/postgresql/error_handler.rs
@@ -4,10 +4,10 @@
/// components, ensuring that all errors are properly converted to PostgreSQL
/// ErrorResponse messages and sent to clients in a protocol-compliant manner.
use crate::{
- connect::Sender,
- error::{EncryptError, Error, MappingError},
- postgresql::messages::error_response::ErrorResponse,
+ error::{EncryptError, Error, MappingError, ProtocolError, ERROR_DOC_BASE_URL},
+ postgresql::diagnostics,
};
+use pg_proto::DiagnosticResponse;
/// Trait for components that can send PostgreSQL error responses to clients.
///
@@ -15,99 +15,79 @@ use crate::{
/// frontend and backend components, providing consistent error conversion
/// and client communication.
pub trait PostgreSqlErrorHandler {
- /// Get the client sender for this component
- fn client_sender(&mut self) -> &mut Sender;
-
/// Get the client ID for logging purposes
fn client_id(&self) -> i32;
- /// Convert various error types into appropriate PostgreSQL ErrorResponse messages.
+ /// Convert various error types into PostgreSQL `DiagnosticResponse` messages.
///
/// # Error Type Mapping
///
- /// - `MappingError` -> InvalidSqlStatement error
+ /// - `MappingError::InvalidParameter` -> Invalid parameter error
+ /// - Other `MappingError` values -> Invalid SQL statement error
/// - `EncryptError::UnknownColumn` -> Unknown column error
- /// - `EncryptError::CouldNotRetrieveKey` -> Key retrieval error
+ /// - `EncryptError::CouldNotDecryptDataForKeyset` -> System error
+ /// - `EncryptError::UnknownKeysetIdentifier` -> System error
+ /// - `Error::ConnectionTimeout` -> Idle session timeout error
/// - All others -> System error
///
/// # Arguments
///
- /// * `err` - The error to be converted to a PostgreSQL ErrorResponse
- fn error_to_response(&self, err: Error) -> ErrorResponse {
+ /// * `err` - The error to be converted to a `DiagnosticResponse`
+ fn error_to_response(&self, err: Error) -> DiagnosticResponse {
match err {
Error::Mapping(MappingError::InvalidParameter(ref column)) => {
- ErrorResponse::invalid_parameter(
+ diagnostics::invalid_parameter(
err.to_string(),
&column.table_name(),
&column.column_name(),
)
}
- Error::Mapping(err) => ErrorResponse::invalid_sql_statement(err.to_string()),
+ Error::Mapping(err) => diagnostics::invalid_sql_statement(err.to_string()),
Error::Encrypt(EncryptError::UnknownColumn {
ref table,
ref column,
- }) => ErrorResponse::unknown_column(err.to_string(), table, column),
+ }) => diagnostics::unknown_column(err.to_string(), table, column),
Error::Encrypt(EncryptError::CouldNotDecryptDataForKeyset { .. }) => {
- ErrorResponse::system_error(err.to_string())
+ diagnostics::system_error(err.to_string())
}
Error::Encrypt(EncryptError::UnknownKeysetIdentifier { .. }) => {
- ErrorResponse::system_error(err.to_string())
+ diagnostics::system_error(err.to_string())
}
- Error::ConnectionTimeout { .. } => ErrorResponse::connection_timeout(err.to_string()),
- _ => ErrorResponse::system_error(err.to_string()),
+ Error::ConnectionTimeout { .. } => diagnostics::connection_timeout(err.to_string()),
+ Error::Protocol(
+ ProtocolError::HeldDataRowMissingOperation
+ | ProtocolError::HeldDataRowOperationMismatch
+ | ProtocolError::HeldBackendMessageNotDataRow
+ | ProtocolError::HeldDataRowsNotEncrypted,
+ ) => diagnostics::system_error(format!(
+ "CipherStash Proxy encountered an internal PostgreSQL protocol error. For help visit {ERROR_DOC_BASE_URL}#protocol-internal-error"
+ )),
+ _ => diagnostics::system_error(err.to_string()),
}
}
-
- /// Send an ErrorResponse message to the client.
- ///
- /// Converts the error to a PostgreSQL ErrorResponse and sends it
- /// to the client via the component's sender channel.
- ///
- /// # Arguments
- ///
- /// * `error_response` - The ErrorResponse to send to the client
- fn send_error_response(&mut self, err: Error) -> Result<(), Error>;
}
#[cfg(test)]
mod tests {
use super::*;
- use crate::postgresql::messages::error_response::{
- ErrorResponseCode, CODE_IDLE_SESSION_TIMEOUT, CODE_SYSTEM_ERROR,
- };
+ use crate::postgresql::diagnostics::{CODE_IDLE_SESSION_TIMEOUT, CODE_SYSTEM_ERROR};
use std::time::Duration;
/// Minimal implementation of PostgreSqlErrorHandler for testing the default method.
struct TestHandler;
impl PostgreSqlErrorHandler for TestHandler {
- fn client_sender(&mut self) -> &mut Sender {
- unimplemented!("not needed for error_to_response tests")
- }
-
fn client_id(&self) -> i32 {
0
}
-
- fn send_error_response(&mut self, _err: Error) -> Result<(), Error> {
- unimplemented!("not needed for error_to_response tests")
- }
}
- fn error_code(response: &ErrorResponse) -> Option<&str> {
+ fn field(response: &DiagnosticResponse, code: u8) -> Option<&str> {
response
.fields
.iter()
- .find(|f| f.code == ErrorResponseCode::Code)
- .map(|f| f.value.as_str())
- }
-
- fn error_message(response: &ErrorResponse) -> Option<&str> {
- response
- .fields
- .iter()
- .find(|f| f.code == ErrorResponseCode::Message)
- .map(|f| f.value.as_str())
+ .find(|field| field.code == code)
+ .and_then(|field| std::str::from_utf8(&field.value).ok())
}
#[test]
@@ -117,9 +97,9 @@ mod tests {
duration: Duration::from_millis(5000),
};
let response = handler.error_to_response(err);
- assert_eq!(error_code(&response), Some(CODE_IDLE_SESSION_TIMEOUT));
+ assert_eq!(field(&response, b'C'), Some(CODE_IDLE_SESSION_TIMEOUT));
assert_eq!(
- error_message(&response),
+ field(&response, b'M'),
Some("Connection timed out after 5000 ms")
);
}
@@ -129,6 +109,23 @@ mod tests {
let handler = TestHandler;
let err = Error::Unknown;
let response = handler.error_to_response(err);
- assert_eq!(error_code(&response), Some(CODE_SYSTEM_ERROR));
+ assert_eq!(field(&response, b'C'), Some(CODE_SYSTEM_ERROR));
+ }
+
+ #[test]
+ fn internal_protocol_error_does_not_expose_implementation_details() {
+ let handler = TestHandler;
+ let err = Error::Protocol(ProtocolError::HeldDataRowOperationMismatch);
+ let response = handler.error_to_response(err);
+
+ assert_eq!(field(&response, b'C'), Some(CODE_SYSTEM_ERROR));
+ assert_eq!(
+ field(&response, b'M'),
+ Some(concat!(
+ "CipherStash Proxy encountered an internal PostgreSQL protocol error. ",
+ "For help visit https://github.com/cipherstash/proxy/blob/main/docs/errors.md",
+ "#protocol-internal-error"
+ ))
+ );
}
}
diff --git a/packages/cipherstash-proxy/src/postgresql/handler.rs b/packages/cipherstash-proxy/src/postgresql/handler.rs
deleted file mode 100644
index c1fe8e50e..000000000
--- a/packages/cipherstash-proxy/src/postgresql/handler.rs
+++ /dev/null
@@ -1,394 +0,0 @@
-use super::backend::Backend;
-use super::frontend::Frontend;
-use super::protocol::StartupCode;
-use crate::connect::ChannelWriter;
-use crate::error::ConfigError;
-use crate::log::{AUTHENTICATION, PROTOCOL};
-use crate::postgresql::messages::authentication::auth::{AuthenticationMethod, SaslMechanism};
-use crate::postgresql::messages::authentication::sasl::SASLResponse;
-use crate::postgresql::messages::authentication::{
- Authentication, PasswordMessage, SASLInitialResponse,
-};
-use crate::postgresql::messages::error_response::ErrorResponse;
-use crate::postgresql::{protocol, startup};
-use crate::proxy::ZeroKms;
-use crate::{
- connect::AsyncStream,
- error::{Error, ProtocolError},
- postgresql::context::Context,
- tls,
-};
-use bytes::BytesMut;
-use md5::{Digest, Md5};
-use postgres_protocol::authentication::sasl::{ChannelBinding, ScramSha256};
-use rand::Rng;
-use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
-use tracing::{debug, error, info, warn};
-///
-///
-/// Entry point for handling postgres protocol connections
-/// Each inbound client connection is mapped to a database connection
-/// Hilarity ensues
-///
-/// Startup flow
-///
-/// Connect to database with TLS if required
-/// First message is either:
-/// - SSLRequest
-/// - ProtocolVersionNumber
-/// - CancelRequest
-///
-/// On SSLRequest
-/// Send SSLResponse
-/// Connect with TLS if configured
-///
-/// On TLS Connect
-/// Expect message containing ProtocolVersionNumber is sent
-///
-/// On CancelRequest
-/// Propagate and disconnect
-///
-/// On ProtocolVersionNumber
-/// Propagate and continue
-///
-///
-pub async fn handler(client_stream: AsyncStream, context: Context) -> Result<(), Error> {
- let mut client_stream = client_stream;
- let client_id = context.client_id;
-
- // Connect to the database server, using TLS if configured
- let stream = AsyncStream::connect(&context.database_socket_address()).await?;
- let mut database_stream = startup::with_tls(stream, context.config()).await?;
- info!(
- msg = "Client connected",
- database = context.database_socket_address(),
- client_id = client_id,
- );
-
- loop {
- let startup_message =
- match startup::read_message(&mut client_stream, context.connection_timeout()).await {
- Ok(msg) => msg,
- Err(err @ Error::ConnectionTimeout { .. }) => {
- send_timeout_error(&mut client_stream, &err).await;
- return Err(err);
- }
- Err(err) => return Err(err),
- };
-
- match &startup_message.code {
- StartupCode::SSLRequest => {
- startup::send_ssl_response(&mut client_stream, context.use_tls()).await?;
- if let Some(ref tls) = context.tls_config() {
- match client_stream {
- AsyncStream::Tcp(stream) => {
- // The Client is connecting to our Server
- let tls_stream = tls::server(stream, tls).await?;
- client_stream = AsyncStream::Tls(Box::new(tls_stream));
- }
- AsyncStream::Tls(_) => {
- unreachable!();
- }
- }
- }
- }
- StartupCode::CancelRequest => {
- database_stream.write_all(&startup_message.bytes).await?;
- return Err(Error::CancelRequest);
- }
- StartupCode::ProtocolVersionNumber => {
- database_stream.write_all(&startup_message.bytes).await?;
- break;
- }
- }
- }
-
- // Proxy -> Client Authentication
- // Uses MD5
- // SASL is not supported because I need to RTFM https://datatracker.ietf.org/doc/html/rfc5802
- //
- // Proxy -> Send AuthenticationMD5Password
- // Client -> Send PasswordMessage
- //
- {
- let salt = generate_md5_password_salt();
-
- let username = context.database_username().as_bytes();
- let password = context.database_password();
-
- let password = password.as_bytes();
-
- let hash = md5_hash(username, password, &salt);
-
- let message = Authentication::md5_password(salt);
- let bytes = BytesMut::try_from(message)?;
- client_stream.write_all(&bytes).await?;
-
- let connection_timeout = context.connection_timeout();
- let (_code, bytes) =
- match protocol::read_message(&mut client_stream, client_id, connection_timeout).await {
- Ok(result) => result,
- Err(err @ Error::ConnectionTimeout { .. }) => {
- send_timeout_error(&mut client_stream, &err).await;
- return Err(err);
- }
- Err(err) => return Err(err),
- };
-
- let password_message = PasswordMessage::try_from(&bytes)?;
-
- if hash == password_message.password {
- let message = Authentication::authentication_ok();
- debug!(target: AUTHENTICATION, msg = "Client AuthenticationOk");
- let bytes = BytesMut::try_from(message)?;
- client_stream.write_all(&bytes).await?;
- } else {
- let message = ProtocolError::ClientAuthenticationFailed.to_string();
- error!(msg = message);
-
- let message = ErrorResponse::invalid_password(message);
- let bytes = BytesMut::try_from(message)?;
- client_stream.write_all(&bytes).await?;
- }
- }
-
- // Database authentication flow
- // 1. Database -> Authentication message (SASL)
- // -> Proxy -> Auth Reponse flow with SASL
- //
- // 2. Proxy -> Auth message to the client Md5, SASL etc
- // -> Client -> Auth response
- //
-
- // First message should always be Auth
- let auth = protocol::read_auth_message(&mut database_stream, client_id).await?;
-
- match &auth.method {
- AuthenticationMethod::AuthenticationOk => {
- debug!(target: AUTHENTICATION, msg = "AuthenticationOk");
- }
- AuthenticationMethod::AuthenticationCleartextPassword => {
- debug!(target: AUTHENTICATION, msg = "AuthenticationCleartextPassword");
- let password = context.database_password();
- let message = PasswordMessage::new(password);
- let bytes = BytesMut::try_from(message)?;
- database_stream.write_all(&bytes).await?;
- }
- AuthenticationMethod::Md5Password { salt } => {
- debug!(target: AUTHENTICATION, msg = "Md5Password");
- let username = context.database_username().as_bytes();
- let password = context.database_password();
- let password = password.as_bytes();
-
- let hash = md5_hash(username, password, salt);
- let message = PasswordMessage::new(hash);
- let bytes = BytesMut::try_from(message)?;
- database_stream.write_all(&bytes).await?;
- }
- AuthenticationMethod::Sasl { .. } => {
- debug!(target: AUTHENTICATION, msg = "Sasl");
- let mechanism = auth.sasl_mechanism()?;
- sanity_check_sasl_mechanism(&mechanism, &client_stream);
-
- // Toby: I don't think we need to do anything here
- // If we are connected via TLS, we can support SCRAM-SHA-256-PLUS
- // If we are not connected via TLS, the database won't ask for SCRAM-SHA-256-PLUS
- let channel_binding = database_stream.channel_binding();
- let password = context.database_password();
- let password = password.as_bytes();
- scram_sha_256_plus_handler(&mut database_stream, mechanism, password, channel_binding)
- .await?;
- }
- AuthenticationMethod::Other { method_code, .. } => {
- debug!(target: AUTHENTICATION, msg = "UnsupportedAuthentication");
- return Err(ProtocolError::UnsupportedAuthentication {
- method_code: *method_code,
- }
- .into());
- }
- method => {
- debug!(target: AUTHENTICATION, msg = "UnexpectedStartupMessage", authentication_method = ?method);
- return Err(ProtocolError::UnexpectedStartupMessage.into());
- }
- }
-
- if context.require_tls() && !client_stream.is_tls() {
- let message = ErrorResponse::tls_required();
- let bytes = BytesMut::try_from(message)?;
- client_stream.write_all(&bytes).await?;
-
- error!(msg = "Client must connect with Transport Layer Security (TLS)");
- return Err(ConfigError::TlsRequired.into());
- }
-
- let (client_reader, client_writer) = client_stream.split();
- let (server_reader, server_writer) = database_stream.split();
-
- let channel_writer = ChannelWriter::new(client_writer, client_id);
-
- let mut frontend = Frontend::new(
- client_reader,
- channel_writer.sender(),
- server_writer,
- context.clone(),
- );
- let mut backend = Backend::new(channel_writer.sender(), server_reader, context.clone());
-
- if context.is_passthrough() {
- if context.use_structured_logging() {
- warn!(msg = "RUNNING IN PASSTHROUGH MODE");
- warn!(msg = "DATA IS NOT PROTECTED WITH ENCRYPTION");
- } else {
- warn!(msg = "========================================");
- warn!(msg = "RUNNING IN PASSTHROUGH MODE");
- warn!(msg = "DATA IS NOT PROTECTED WITH ENCRYPTION");
- warn!(msg = "========================================");
- }
- }
-
- let timeout_sender = channel_writer.sender();
- let channel_writer_task = tokio::spawn(channel_writer.receive());
-
- let client_to_server = async {
- loop {
- let result = frontend.rewrite().await;
- // Ensure the connection is terminated if the client closes the connection
- // The client ConnectionClosed error is triggered before the terminate message is passed through
- if matches!(result, Err(Error::ConnectionClosed)) {
- frontend.terminate().await?
- }
- result?;
- }
- // Unreachable, but helps the compiler understand the return type
- // TODO: extract into a function or something with type
- #[allow(unreachable_code)]
- Ok::<(), Error>(())
- };
-
- let server_to_client = async {
- loop {
- backend.rewrite().await?;
- }
- #[allow(unreachable_code)]
- Ok::<(), Error>(())
- };
-
- // Run frontend and backend tasks
- let result = tokio::try_join!(client_to_server, server_to_client);
-
- if let Err(ref err @ Error::ConnectionTimeout { .. }) = &result {
- let error_response = ErrorResponse::connection_timeout(err.to_string());
- if let Ok(bytes) = BytesMut::try_from(error_response) {
- let _ = timeout_sender.send(bytes);
- }
- // Best-effort yield to allow ChannelWriter to flush the error response
- // before the connection tears down. Not guaranteed — if the runtime doesn't
- // schedule the writer task before teardown, the client may see a connection
- // reset instead of the ErrorResponse.
- tokio::task::yield_now().await;
- }
-
- // Drop frontend and backend to drop their senders and close the channel
- // The async blocks above captured frontend/backend by reference, so they're still alive
- drop(frontend);
- drop(backend);
-
- // Wait for channel writer to finish shutdown sequence
- // The senders are now dropped, which closes the channel and allows
- // the writer task to complete its shutdown
- if let Err(err) = channel_writer_task.await {
- error!(
- client_id,
- msg = "Channel writer task panicked",
- error = ?err
- );
- }
-
- result?;
- Ok(())
-}
-
-// Keep for debugging
-fn sanity_check_sasl_mechanism(mechanism: &SaslMechanism, client_stream: &AsyncStream) {
- match mechanism {
- SaslMechanism::ScramSha256 => {
- if client_stream.is_tls() {
- debug!(
- PROTOCOL,
- msg = "Database requested SCRAM-SHA-256, but Proxy has a TLS connection"
- );
- }
- }
- SaslMechanism::ScramSha256Plus => {
- if client_stream.is_tcp() {
- debug!(
- PROTOCOL,
- msg = "Database requested SCRAM-SHA-256-PLUS, but Proxy has a TCP connection"
- );
- }
- }
- }
-}
-
-pub fn md5_hash(username: &[u8], password: &[u8], salt: &[u8; 4]) -> String {
- let mut md5 = Md5::new();
- md5.update(password);
- md5.update(username);
- let output = md5.finalize_reset();
- md5.update(format!("{output:x}"));
- md5.update(salt);
- format!("md5{:x}", md5.finalize())
-}
-
-fn generate_md5_password_salt() -> [u8; 4] {
- let mut rng = rand::rng();
- let mut bytes = [0u8; 4];
- rng.fill(&mut bytes);
- bytes
-}
-
-async fn scram_sha_256_plus_handler(
- mut stream: S,
- mechanism: SaslMechanism,
- password: &[u8],
- channel_binding: ChannelBinding,
-) -> Result<(), Error> {
- let mut scram = ScramSha256::new(password, channel_binding);
- let bytes = scram.message().to_vec();
-
- let sasl_initial_response = SASLInitialResponse::new(mechanism, bytes);
- let bytes = BytesMut::try_from(sasl_initial_response)?;
- stream.write_all(&bytes).await?;
-
- let auth = protocol::read_auth_message(&mut stream, 1).await?;
-
- let bytes = auth.sasl_continue()?;
- scram.update(bytes)?;
-
- let sasl_response = SASLResponse::new(scram.message().to_vec());
-
- let bytes = BytesMut::try_from(sasl_response)?;
- stream.write_all(&bytes).await?;
-
- let auth = protocol::read_auth_message(&mut stream, 1).await?;
- let bytes = auth.sasl_final()?;
- scram.finish(bytes)?;
-
- let auth = protocol::read_auth_message(&mut stream, 1).await?;
-
- if auth.is_ok() {
- debug!(target: AUTHENTICATION, msg = "SASL authentication successful");
- Ok(())
- } else {
- Err(ProtocolError::AuthenticationFailed.into())
- }
-}
-
-/// Best-effort send of a connection timeout ErrorResponse directly to a client stream.
-/// Used for pre-split timeout sites where no ChannelWriter exists yet.
-async fn send_timeout_error(stream: &mut S, err: &Error) {
- let error_response = ErrorResponse::connection_timeout(err.to_string());
- if let Ok(bytes) = BytesMut::try_from(error_response) {
- let _ = stream.write_all(&bytes).await;
- }
-}
diff --git a/packages/cipherstash-proxy/src/postgresql/message_buffer.rs b/packages/cipherstash-proxy/src/postgresql/message_buffer.rs
deleted file mode 100644
index 750b79b2c..000000000
--- a/packages/cipherstash-proxy/src/postgresql/message_buffer.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use super::messages::data_row::DataRow;
-
-pub struct MessageBuffer {
- // buffer: RwLock>,
- buffer: Vec,
-}
-
-impl MessageBuffer {
- /// Default number of rows to keep in the buffer.
- /// Larger rows will require more memory.
- const DEFAULT_RESPONSE_BUFFER_SIZE: usize = 4096;
-
- pub fn new() -> Self {
- Self {
- buffer: Vec::with_capacity(Self::DEFAULT_RESPONSE_BUFFER_SIZE),
- }
- }
-
- pub fn push(&mut self, row: DataRow) {
- self.buffer.push(row);
- }
-
- pub fn drain(&mut self) -> Vec {
- self.buffer.drain(..).collect()
- }
-
- pub fn clear(&mut self) {
- self.buffer.clear();
- }
-
- pub fn len(&self) -> usize {
- self.buffer.len()
- }
-
- pub fn is_empty(&self) -> bool {
- self.buffer.is_empty()
- }
-
- pub fn at_capacity(&self) -> bool {
- self.buffer.len() >= Self::DEFAULT_RESPONSE_BUFFER_SIZE
- }
-}
diff --git a/packages/cipherstash-proxy/src/postgresql/messages/authentication/auth.rs b/packages/cipherstash-proxy/src/postgresql/messages/authentication/auth.rs
deleted file mode 100644
index c96f9e119..000000000
--- a/packages/cipherstash-proxy/src/postgresql/messages/authentication/auth.rs
+++ /dev/null
@@ -1,358 +0,0 @@
-use crate::error::{Error, ProtocolError};
-use crate::postgresql::messages::{BackendCode, FrontendCode};
-use crate::postgresql::protocol::BytesMutReadString;
-use crate::SIZE_I32;
-use bytes::{Buf, BufMut, BytesMut};
-
-use std::convert::TryFrom;
-use std::ffi::CString;
-use std::fmt::{self, Display, Formatter};
-use std::io::{Cursor, Read};
-
-const SIZE_NULL_BYTE: usize = 1;
-
-pub const SCRAM_SHA_256_PLUS: &str = "SCRAM-SHA-256-PLUS";
-pub const SCRAM_SHA_256: &str = "SCRAM-SHA-256";
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub enum SaslMechanism {
- ScramSha256,
- ScramSha256Plus,
-}
-
-#[derive(Debug, Clone)]
-pub struct Authentication {
- #[allow(dead_code)]
- code: u8,
- pub method: AuthenticationMethod,
-}
-
-#[derive(Clone, Debug)]
-#[repr(i32)]
-pub enum AuthenticationMethod {
- AuthenticationOk = 0,
- AuthenticationCleartextPassword = 3,
- Md5Password { salt: [u8; 4] } = 5,
- Sasl { mechanisms: Vec } = 10,
- AuthenticationSASLContinue { bytes: Vec } = 11,
- AuthenticationSASLFinal { bytes: Vec } = 12,
- Other { method_code: i32, bytes: Vec },
-}
-
-#[derive(Clone, Debug)]
-pub struct PasswordMessage {
- code: u8,
- pub password: String,
-}
-
-impl Authentication {
- pub fn is_ok(&self) -> bool {
- matches!(self.method, AuthenticationMethod::AuthenticationOk)
- }
-
- pub fn is_sasl(&self) -> bool {
- matches!(self.method, AuthenticationMethod::Sasl { .. })
- }
-
- pub fn is_scram_sha_256_plus(&self) -> bool {
- match self.method {
- AuthenticationMethod::Sasl { ref mechanisms } => {
- mechanisms.contains(&SaslMechanism::ScramSha256Plus)
- }
- _ => false,
- }
- }
-
- ///
- /// Returns the first mechanism in the list of mechanisms
- /// If the method is not SASL, it will return an error
- /// If the method is SASL and there are no mechanisms, it will return an error
- /// - This should never happen as the server should always return at least one mechanism
- /// - If it does, it is a protocol error and the message parse should already have returned an error
- /// - This is a safety check to ensure that the server is behaving as expected
- ///
- pub fn sasl_mechanism(&self) -> Result {
- let mechanism = match self.method {
- AuthenticationMethod::Sasl { ref mechanisms } => mechanisms.first(),
- _ => None,
- };
-
- match mechanism {
- Some(m) => Ok(*m),
- None => {
- Err(ProtocolError::UnexpectedSaslAuthenticationMethod("None".to_string()).into())
- }
- }
- }
-
- pub fn sasl_continue(&self) -> Result<&Vec, Error> {
- match self.method {
- AuthenticationMethod::AuthenticationSASLContinue { ref bytes } => Ok(bytes),
- _ => Err(ProtocolError::UnexpectedAuthenticationResponse {
- expected: "SASLContinue".into(),
- received: (&self.method).into(),
- }
- .into()),
- }
- }
-
- pub fn sasl_final(&self) -> Result<&Vec, Error> {
- match self.method {
- AuthenticationMethod::AuthenticationSASLFinal { ref bytes } => Ok(bytes),
- _ => Err(ProtocolError::UnexpectedAuthenticationResponse {
- expected: "SASLFinal".into(),
- received: (&self.method).into(),
- }
- .into()),
- }
- }
-
- pub fn md5_password(salt: [u8; 4]) -> Authentication {
- Authentication {
- code: BackendCode::Authentication.into(),
- method: AuthenticationMethod::Md5Password { salt },
- }
- }
-
- pub fn authentication_ok() -> Authentication {
- Authentication {
- code: BackendCode::Authentication.into(),
- method: AuthenticationMethod::AuthenticationOk,
- }
- }
-}
-
-impl PasswordMessage {
- pub fn new(password: String) -> PasswordMessage {
- PasswordMessage {
- code: FrontendCode::PasswordMessage.into(),
- password,
- }
- }
-}
-
-impl TryFrom<&BytesMut> for Authentication {
- type Error = Error;
-
- fn try_from(bytes: &BytesMut) -> Result {
- let mut cursor = Cursor::new(bytes);
- let code = cursor.get_u8();
-
- if BackendCode::from(code) != BackendCode::Authentication {
- return Err(ProtocolError::UnexpectedMessageCode {
- expected: BackendCode::Authentication.into(),
- received: code as char,
- }
- .into());
- }
-
- let len = cursor.get_i32(); // read and progress cursor
- let method_code = cursor.get_i32();
-
- let method = match method_code {
- 0 => AuthenticationMethod::AuthenticationOk,
- 5 => {
- let mut salt = [0; 4];
- cursor.read_exact(&mut salt)?;
- AuthenticationMethod::Md5Password { salt }
- }
- 10 => {
- let mut mechanisms = Vec::new();
- let mut count = SIZE_I32 // message len
- + SIZE_I32 // method_code
- + SIZE_NULL_BYTE; // terminating null byte;
-
- while count < (len as usize) {
- let m = cursor.read_string()?;
- count += m.len() + SIZE_NULL_BYTE;
- mechanisms.push(SaslMechanism::try_from(m)?);
- }
- AuthenticationMethod::Sasl { mechanisms }
- }
- 11 => {
- let mut bytes = Vec::new();
- cursor.read_to_end(&mut bytes)?;
- AuthenticationMethod::AuthenticationSASLContinue { bytes }
- }
- 12 => {
- let mut bytes = Vec::new();
- cursor.read_to_end(&mut bytes)?;
- AuthenticationMethod::AuthenticationSASLFinal { bytes }
- }
- _ => {
- // Get any remaining bytes from the cursor
- let mut bytes = Vec::new();
- cursor.read_to_end(&mut bytes)?;
- AuthenticationMethod::Other { method_code, bytes }
- }
- };
-
- Ok(Authentication { code, method })
- }
-}
-
-impl TryFrom for BytesMut {
- type Error = Error;
-
- fn try_from(auth: Authentication) -> Result {
- let mut method_bytes = BytesMut::new();
-
- let method_code = (&auth.method).into();
- method_bytes.put_i32(method_code);
-
- match auth.method {
- AuthenticationMethod::AuthenticationOk => {}
- AuthenticationMethod::AuthenticationCleartextPassword => {}
- AuthenticationMethod::Md5Password { salt } => {
- method_bytes.put_slice(&salt);
- }
- AuthenticationMethod::Sasl { mechanisms } => {
- for m in mechanisms {
- let s = m.to_string();
- let c = CString::new(s)?;
- let s = c.as_bytes_with_nul();
- method_bytes.put_slice(s);
- }
- method_bytes.put_u8(0); // null byte
- }
- AuthenticationMethod::AuthenticationSASLContinue { bytes, .. } => {
- method_bytes.put_slice(&bytes);
- }
- AuthenticationMethod::AuthenticationSASLFinal { bytes, .. } => {
- method_bytes.put_slice(&bytes);
- }
- AuthenticationMethod::Other { bytes, .. } => {
- method_bytes.put_slice(&bytes);
- }
- }
-
- let mut bytes = BytesMut::new();
-
- let len = SIZE_I32 // message len
- + method_bytes.len(); // method_code
-
- bytes.put_u8(BackendCode::Authentication.into());
- bytes.put_i32(len as i32);
- bytes.put_slice(&method_bytes);
-
- Ok(bytes)
- }
-}
-
-impl TryFrom<&BytesMut> for PasswordMessage {
- type Error = Error;
-
- fn try_from(bytes: &BytesMut) -> Result {
- let mut cursor = Cursor::new(bytes);
- let code = cursor.get_u8();
-
- if FrontendCode::from(code) != FrontendCode::PasswordMessage {
- return Err(ProtocolError::UnexpectedMessageCode {
- expected: FrontendCode::PasswordMessage.into(),
- received: code as char,
- }
- .into());
- }
-
- let _len = cursor.get_i32(); // read and progress cursor
- let password = cursor.read_string()?;
-
- Ok(PasswordMessage { code, password })
- }
-}
-
-impl TryFrom for BytesMut {
- type Error = Error;
-
- fn try_from(password_message: PasswordMessage) -> Result {
- let mut bytes = BytesMut::new();
-
- let password = CString::new(password_message.password)?;
- let password = password.as_bytes_with_nul();
-
- let len = SIZE_I32 // message len
- + password.len(); // password
-
- bytes.put_u8(FrontendCode::PasswordMessage.into());
- bytes.put_i32(len as i32);
- bytes.put_slice(password);
-
- Ok(bytes)
- }
-}
-
-impl TryFrom for SaslMechanism {
- type Error = Error;
- fn try_from(s: String) -> Result {
- match s.as_str() {
- SCRAM_SHA_256 => Ok(SaslMechanism::ScramSha256),
- SCRAM_SHA_256_PLUS => Ok(SaslMechanism::ScramSha256Plus),
- s => Err(ProtocolError::UnexpectedSaslAuthenticationMethod(s.to_owned()).into()),
- }
- }
-}
-
-impl Display for SaslMechanism {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- let s = match self {
- SaslMechanism::ScramSha256 => SCRAM_SHA_256.to_owned(),
- SaslMechanism::ScramSha256Plus => SCRAM_SHA_256_PLUS.to_owned(),
- };
- write!(f, "{s}")
- }
-}
-
-impl From<&AuthenticationMethod> for i32 {
- fn from(method: &AuthenticationMethod) -> Self {
- match method {
- AuthenticationMethod::AuthenticationOk => 0,
- AuthenticationMethod::AuthenticationCleartextPassword => 3,
- AuthenticationMethod::Md5Password { .. } => 5,
- AuthenticationMethod::Sasl { .. } => 10,
- AuthenticationMethod::AuthenticationSASLContinue { .. } => 11,
- AuthenticationMethod::AuthenticationSASLFinal { .. } => 12,
- AuthenticationMethod::Other { method_code, .. } => *method_code,
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use bytes::BytesMut;
-
- use crate::{config::LogConfig, log};
-
- use super::Authentication;
-
- fn to_message(s: &[u8]) -> BytesMut {
- BytesMut::from(s)
- }
-
- #[test]
- pub fn parse_auth_message() {
- log::init(LogConfig::default());
-
- let bytes = to_message(b"R\0\0\0*\0\0\0\nSCRAM-SHA-256-PLUS\0SCRAM-SHA-256\0\0");
-
- let auth = Authentication::try_from(&bytes).unwrap();
-
- assert!(matches!(
- auth.method,
- super::AuthenticationMethod::Sasl { .. }
- ));
-
- let auth_bytes = BytesMut::try_from(auth).unwrap();
-
- assert_eq!(bytes, auth_bytes);
- }
-
- #[test]
- pub fn is_scram_sha_256_plus() {
- log::init(LogConfig::default());
-
- let bytes = to_message(b"R\0\0\0*\0\0\0\nSCRAM-SHA-256-PLUS\0SCRAM-SHA-256\0\0");
- let auth = Authentication::try_from(&bytes).unwrap();
-
- assert!(auth.is_scram_sha_256_plus());
- }
-}
diff --git a/packages/cipherstash-proxy/src/postgresql/messages/authentication/mod.rs b/packages/cipherstash-proxy/src/postgresql/messages/authentication/mod.rs
deleted file mode 100644
index 40ca7809a..000000000
--- a/packages/cipherstash-proxy/src/postgresql/messages/authentication/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-pub mod auth;
-pub mod sasl;
-
-pub use auth::Authentication;
-pub use auth::PasswordMessage;
-pub use sasl::SASLInitialResponse;
diff --git a/packages/cipherstash-proxy/src/postgresql/messages/authentication/sasl.rs b/packages/cipherstash-proxy/src/postgresql/messages/authentication/sasl.rs
deleted file mode 100644
index b5806619f..000000000
--- a/packages/cipherstash-proxy/src/postgresql/messages/authentication/sasl.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-use std::{
- ffi::CString,
- io::{Cursor, Read},
-};
-
-use bytes::{Buf, BufMut, BytesMut};
-
-use crate::{
- error::{Error, ProtocolError},
- postgresql::{messages::FrontendCode, protocol::BytesMutReadString},
- SIZE_I32,
-};
-
-use super::auth::{self, SaslMechanism};
-
-#[derive(Clone, Debug)]
-pub struct SASLInitialResponse {
- #[allow(dead_code)]
- code: u8,
- pub mechanism: String,
- pub response: Vec,
-}
-
-#[derive(Clone, Debug)]
-pub struct SASLResponse {
- #[allow(dead_code)]
- code: u8,
- response: Vec,
-}
-
-impl SASLInitialResponse {
- pub fn new(mechanism: SaslMechanism, response: Vec) -> Self {
- let mechanism = mechanism.to_string();
-
- SASLInitialResponse {
- code: FrontendCode::SASLInitialResponse.into(),
- mechanism,
-
- response,
- }
- }
-
- pub fn is_scram_sha_256(&self) -> bool {
- self.mechanism == auth::SCRAM_SHA_256
- }
-
- pub fn is_scram_sha_256_plus(&self) -> bool {
- self.mechanism == auth::SCRAM_SHA_256_PLUS
- }
-}
-
-impl SASLResponse {
- pub fn new(response: Vec) -> Self {
- SASLResponse {
- code: FrontendCode::SASLResponse.into(),
- response,
- }
- }
-}
-
-impl TryFrom<&BytesMut> for SASLInitialResponse {
- type Error = Error;
-
- fn try_from(bytes: &BytesMut) -> Result {
- let mut cursor = Cursor::new(bytes);
- let code = cursor.get_u8();
-
- // Note: all password messages use the 'p' code
- if code != b'p' {
- return Err(ProtocolError::UnexpectedMessageCode {
- expected: FrontendCode::SASLInitialResponse.into(),
- received: code as char,
- }
- .into());
- }
- let _len = cursor.get_i32();
- let mechanism = cursor.read_string()?;
- let _response_len = cursor.get_i32();
- let mut bytes = Vec::new();
- cursor.read_to_end(&mut bytes)?;
-
- Ok(SASLInitialResponse {
- code,
- mechanism,
- response: bytes,
- })
- }
-}
-
-impl TryFrom for BytesMut {
- type Error = Error;
-
- fn try_from(response: SASLInitialResponse) -> Result {
- let mut bytes = BytesMut::new();
-
- let mechanism = CString::new(response.mechanism)?;
- let mechanism = mechanism.as_bytes_with_nul();
-
- let response_len = response.response.len();
-
- let len = SIZE_I32 // len length
- + mechanism.len()
- + SIZE_I32 // response_len
- + response_len;
-
- bytes.put_u8(FrontendCode::SASLInitialResponse.into());
- bytes.put_i32(len as i32);
- bytes.put_slice(mechanism);
- bytes.put_i32(response_len as i32);
- bytes.put_slice(&response.response);
-
- Ok(bytes)
- }
-}
-
-impl TryFrom for BytesMut {
- type Error = Error;
-
- fn try_from(response: SASLResponse) -> Result {
- let mut bytes = BytesMut::new();
-
- let response_len = response.response.len();
-
- let len = SIZE_I32 // len length
- + response_len;
-
- bytes.put_u8(FrontendCode::SASLResponse.into());
- bytes.put_i32(len as i32);
- bytes.put_slice(&response.response);
-
- Ok(bytes)
- }
-}
diff --git a/packages/cipherstash-proxy/src/postgresql/messages/close.rs b/packages/cipherstash-proxy/src/postgresql/messages/close.rs
deleted file mode 100644
index 1a8fb11d2..000000000
--- a/packages/cipherstash-proxy/src/postgresql/messages/close.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-use crate::error::{Error, ProtocolError};
-use crate::postgresql::protocol::BytesMutReadString;
-use crate::{SIZE_I32, SIZE_U8};
-
-use bytes::{Buf, BufMut, BytesMut};
-use std::convert::TryFrom;
-use std::ffi::CString;
-use std::io::Cursor;
-
-use super::target::Target;
-use super::{FrontendCode, Name};
-
-///
-/// Close b'C' (Frontend) message.
-///
-/// See:
-///
-/// Byte1('C')
-/// Identifies the message as a Close command.
-///
-/// Int32
-/// Length of message contents in bytes, including self.
-///
-/// Byte1
-/// 'S' to close a prepared statement; or 'P' to close a portal.
-///
-/// String
-/// The name of the prepared statement or portal to close (an empty string selects the unnamed prepared statement or portal).
-
-#[derive(Debug, Clone)]
-pub(crate) struct Close {
- pub target: Target,
- pub name: Name,
-}
-
-impl TryFrom<&BytesMut> for Close {
- type Error = Error;
-
- fn try_from(bytes: &BytesMut) -> Result {
- let mut cursor = Cursor::new(bytes);
- let code = cursor.get_u8();
-
- if FrontendCode::from(code) != FrontendCode::Close {
- return Err(ProtocolError::UnexpectedMessageCode {
- expected: FrontendCode::Close.into(),
- received: code as char,
- }
- .into());
- }
-
- let _len = cursor.get_i32(); // read and progress cursor
- let target = cursor.get_u8();
- let target = Target::try_from(target)?;
- let name = cursor.read_string()?;
- let name = Name::from(name);
-
- Ok(Close { target, name })
- }
-}
-
-impl TryFrom for BytesMut {
- type Error = Error;
-
- fn try_from(close: Close) -> Result {
- let mut bytes = BytesMut::new();
-
- let name = CString::new(close.name.as_str())?;
- let name = name.as_bytes_with_nul();
-
- let len = SIZE_I32 + SIZE_U8 + name.len();
-
- bytes.put_u8(FrontendCode::Close.into());
- bytes.put_i32(len as i32);
- bytes.put_u8(close.target.into());
- bytes.put_slice(name);
-
- Ok(bytes)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{config::LogConfig, log, postgresql::messages::Name};
- use bytes::BytesMut;
- use std::convert::TryFrom;
-
- fn to_message(s: &[u8]) -> BytesMut {
- BytesMut::from(s)
- }
-
- #[test]
- pub fn test_close_statement() {
- log::init(LogConfig::default());
-
- // Close unnamed prepared statement: C\0\0\0\x06S\0
- let bytes = to_message(b"C\0\0\0\x06S\0");
- let close = Close::try_from(&bytes).unwrap();
-
- assert!(matches!(close.target, Target::Statement));
- assert!(close.name.is_unnamed());
- }
-
- #[test]
- pub fn test_close_portal() {
- log::init(LogConfig::default());
-
- // Close unnamed portal: C\0\0\0\x06P\0
- let bytes = to_message(b"C\0\0\0\x06P\0");
- let close = Close::try_from(&bytes).unwrap();
-
- assert!(matches!(close.target, Target::Portal));
- assert!(close.name.is_unnamed());
- }
-
- #[test]
- pub fn test_close_named_statement() {
- log::init(LogConfig::default());
-
- // Close named prepared statement "stmt1": C\0\0\0\x0bSstmt1\0
- let bytes = to_message(b"C\0\0\0\x0bSstmt1\0");
- let close = Close::try_from(&bytes).unwrap();
-
- assert!(matches!(close.target, Target::Statement));
- assert_eq!(close.name.as_str(), "stmt1");
- assert!(!close.name.is_unnamed());
- }
-
- #[test]
- pub fn test_close_to_bytes() {
- log::init(LogConfig::default());
-
- let close = Close {
- target: Target::Portal,
- name: Name::from("portal1"),
- };
-
- let bytes = BytesMut::try_from(close).unwrap();
- let parsed = Close::try_from(&bytes).unwrap();
-
- assert!(matches!(parsed.target, Target::Portal));
- assert_eq!(parsed.name.as_str(), "portal1");
- }
-}
diff --git a/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs b/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs
deleted file mode 100644
index 96d5cb8af..000000000
--- a/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs
+++ /dev/null
@@ -1,470 +0,0 @@
-use super::{BackendCode, NULL};
-use crate::EqlCiphertext;
-use crate::{
- error::{EncryptError, Error, ProtocolError},
- log::DECRYPT,
- postgresql::Column,
-};
-use bytes::{Buf, BufMut, BytesMut};
-use std::io::Cursor;
-use tracing::{debug, error};
-
-/// Leading byte of `jsonb`'s binary wire format. PostgreSQL has only ever
-/// emitted version 1.
-const JSONB_BINARY_VERSION: u8 = 1;
-
-#[derive(Debug, Clone)]
-pub struct DataRow {
- pub columns: Vec,
-}
-
-#[derive(Debug, Clone)]
-pub struct DataColumn {
- bytes: Option,
-}
-
-impl DataRow {
- pub fn as_ciphertext(
- &mut self,
- column_configuration: &Vec