Compare commits
11 Commits
5482283727
...
752b91d329
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
752b91d329 | ||
|
|
02847ef1db | ||
|
|
1090359951 | ||
|
|
f5ab554d6b | ||
|
|
cef4ff1289 | ||
|
|
5a59f56319 | ||
|
|
beea8854ce | ||
|
|
7989f7ba4b | ||
|
|
7416f37cec | ||
|
|
677e88980b | ||
|
|
c89f01b718 |
301
Cargo.lock
generated
301
Cargo.lock
generated
@ -304,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common 0.1.7",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1191,7 +1191,7 @@ version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1210,7 +1210,7 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1653,17 +1653,6 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-models"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0940496e5c83c54f3b753d5317daec82e8edac71c33aaa1f666d76f518de2444"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
"pastey",
|
||||
"rand 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core2"
|
||||
version = "0.4.0"
|
||||
@ -1782,7 +1771,7 @@ version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
@ -1794,7 +1783,7 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"typenum",
|
||||
]
|
||||
@ -2198,7 +2187,7 @@ dependencies = [
|
||||
"crypto-bigint",
|
||||
"digest 0.10.7",
|
||||
"ff",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"group",
|
||||
"hkdf",
|
||||
"pem-rfc7468 0.7.0",
|
||||
@ -2662,17 +2651,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "1.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"rustversion",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@ -3112,43 +3090,6 @@ dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hax-lib"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74d9ba66d1739c68e0219b2b2238b5c4145f491ebf181b9c6ab561a19352ae86"
|
||||
dependencies = [
|
||||
"hax-lib-macros",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hax-lib-macros"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24ba777a231a58d1bce1d68313fa6b6afcc7966adef23d60f45b8a2b9b688bf1"
|
||||
dependencies = [
|
||||
"hax-lib-macros-types",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hax-lib-macros-types"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "867e19177d7425140b417cd27c2e05320e727ee682e98368f88b7194e80ad515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers"
|
||||
version = "0.4.1"
|
||||
@ -3423,7 +3364,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3663,7 +3604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3686,9 +3627,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "internal-russh-forked-ssh-key"
|
||||
version = "0.6.11+upstream-0.6.7"
|
||||
version = "0.6.9+upstream-0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a77eae781ed6a7709fb15b64862fcca13d886b07c7e2786f5ed34e5e2b9187"
|
||||
checksum = "fb5af01d366561582e9ea5f841837cc1d8e37e7142a32f33a43801e81863cba5"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"bcrypt-pbkdf",
|
||||
@ -3696,6 +3637,7 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"hex",
|
||||
"hmac",
|
||||
"num-bigint-dig",
|
||||
"p256",
|
||||
"p384",
|
||||
"p521",
|
||||
@ -4088,72 +4030,6 @@ version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-intrinsics"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc9ee7ef66569dd7516454fe26de4e401c0c62073929803486b96744594b9632"
|
||||
dependencies = [
|
||||
"core-models",
|
||||
"hax-lib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-ml-kem"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bb6a88086bf11bd2ec90926c749c4a427f2e59841437dbdede8cde8a96334ab"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
"libcrux-intrinsics",
|
||||
"libcrux-platform",
|
||||
"libcrux-secrets",
|
||||
"libcrux-sha3",
|
||||
"libcrux-traits",
|
||||
"rand 0.9.2",
|
||||
"tls_codec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-platform"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db82d058aa76ea315a3b2092f69dfbd67ddb0e462038a206e1dcd73f058c0778"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-secrets"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4dbbf6bc9f2bc0f20dc3bea3e5c99adff3bdccf6d2a40488963da69e2ec307"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-sha3"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2400bec764d1c75b8a496d5747cffe32f1fb864a12577f0aca2f55a92021c962"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
"libcrux-intrinsics",
|
||||
"libcrux-platform",
|
||||
"libcrux-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-traits"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9adfd58e79d860f6b9e40e35127bfae9e5bd3ade33201d1347459011a2add034"
|
||||
dependencies = [
|
||||
"libcrux-secrets",
|
||||
"rand 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.12"
|
||||
@ -4920,21 +4796,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pageant"
|
||||
version = "0.2.0"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595"
|
||||
checksum = "2c6f0e349ea8dea1b50aa17c082777d30df133d89898c7568a615354772d3731"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"delegate",
|
||||
"futures",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"windows",
|
||||
"windows-strings",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6099,16 +5972,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "russh"
|
||||
version = "0.55.0"
|
||||
version = "0.50.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b4d036bb45d7bbe99dbfef4ec60eaeb614708d22ff107124272f8ef6b54548"
|
||||
checksum = "02d8075561703e70dab7b095b2c13597cde37f5be94af0849fa4e51c315020d0"
|
||||
dependencies = [
|
||||
"aes 0.8.4",
|
||||
"aes-gcm 0.10.3",
|
||||
"bitflags",
|
||||
"block-padding",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"cbc",
|
||||
"chacha20 0.9.1",
|
||||
"ctr 0.9.2",
|
||||
"curve25519-dalek",
|
||||
"data-encoding",
|
||||
@ -6119,29 +5994,30 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"elliptic-curve",
|
||||
"enum_dispatch",
|
||||
"flate2",
|
||||
"futures",
|
||||
"generic-array 1.3.5",
|
||||
"generic-array",
|
||||
"getrandom 0.2.17",
|
||||
"hex-literal",
|
||||
"hmac",
|
||||
"home",
|
||||
"inout 0.1.4",
|
||||
"internal-russh-forked-ssh-key",
|
||||
"libcrux-ml-kem",
|
||||
"log",
|
||||
"md5",
|
||||
"num-bigint",
|
||||
"once_cell",
|
||||
"p256",
|
||||
"p384",
|
||||
"p521",
|
||||
"pageant",
|
||||
"pbkdf2",
|
||||
"pkcs1",
|
||||
"pkcs5",
|
||||
"pkcs8",
|
||||
"poly1305 0.8.0",
|
||||
"rand 0.8.5",
|
||||
"rand_core 0.6.4",
|
||||
"ring",
|
||||
"rsa",
|
||||
"russh-cryptovec",
|
||||
"russh-util",
|
||||
"sec1 0.7.3",
|
||||
@ -6153,6 +6029,7 @@ dependencies = [
|
||||
"subtle",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"typenum",
|
||||
"yasna",
|
||||
"zeroize",
|
||||
@ -6160,22 +6037,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "russh-cryptovec"
|
||||
version = "0.52.0"
|
||||
version = "0.50.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb0ed583ff0f6b4aa44c7867dd7108df01b30571ee9423e250b4cc939f8c6cf"
|
||||
checksum = "1fcb7c127135848b47715b5bcb13d8a27ccd86ce1de1c15eab5982df91fe279a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"ssh-encoding 0.2.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "russh-util"
|
||||
version = "0.52.0"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6"
|
||||
checksum = "c698d702527b51a82e64de98d506e9c1e83a063035d41df4f2354499ec090b79"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"tokio",
|
||||
@ -6594,7 +6470,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct 0.2.0",
|
||||
"der",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
@ -7117,7 +6993,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
@ -7402,7 +7278,7 @@ dependencies = [
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7585,27 +7461,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b"
|
||||
dependencies = [
|
||||
"tls_codec_derive",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec_derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.50.0"
|
||||
@ -8329,6 +8184,16 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core 0.58.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
@ -8336,7 +8201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
@ -8347,7 +8212,20 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement 0.58.0",
|
||||
"windows-interface 0.58.0",
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8356,11 +8234,11 @@ version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8369,11 +8247,22 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
@ -8385,6 +8274,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
@ -8408,10 +8308,19 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@ -8421,6 +8330,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
@ -8880,20 +8799,6 @@ name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
|
||||
@ -98,7 +98,7 @@ prost = "0.14.3"
|
||||
prost-build = "0.14.3"
|
||||
qdrant-client = "1.17.0"
|
||||
rand = "0.10.0"
|
||||
russh = { version = "0.55.0", default-features = false }
|
||||
russh = { version = "0.50.0", default-features = false, features = [] }
|
||||
hmac = { version = "0.12.1", features = ["std"] }
|
||||
sha1_smol = "1.0.1"
|
||||
rsa = { version = "0.9.7", package = "rsa" }
|
||||
|
||||
@ -379,10 +379,12 @@ async fn poll_push_streams(streams: &mut PushStreams) -> Option<WsPushEvent> {
|
||||
}
|
||||
return Some(WsPushEvent::RoomMessage { room_id, event });
|
||||
}
|
||||
Some(Err(_)) => {
|
||||
streams.remove(&room_id);
|
||||
}
|
||||
None => {
|
||||
Some(Err(_)) | None => {
|
||||
// Stream closed/error — remove and re-subscribe to avoid
|
||||
// spinning on a closed stream. The manager keeps the
|
||||
// broadcast sender alive so re-subscribing gets the latest
|
||||
// receiver. Multiple rapid errors are handled by the
|
||||
// manager's existing retry/cleanup logic.
|
||||
streams.remove(&room_id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ chrono = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
num_cpus = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
russh = { workspace = true, features = ["flate2", "ring", "legacy-ed25519-pkcs8-parser"] }
|
||||
russh = { workspace = true, features = ["legacy-ed25519-pkcs8-parser"] }
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
|
||||
@ -48,26 +48,31 @@ impl GitHttpHandler {
|
||||
"git-upload-pack" => "upload-pack",
|
||||
"git-receive-pack" => "receive-pack",
|
||||
_ => {
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid service"));
|
||||
return Err(actix_web::error::ErrorBadRequest("Invalid service"));
|
||||
}
|
||||
};
|
||||
|
||||
let output = tokio::process::Command::new("git")
|
||||
.arg(git_cmd)
|
||||
.arg("--stateless-rpc")
|
||||
.arg("--advertise-refs")
|
||||
.arg(&self.storage_path)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
actix_web::error::ErrorInternalServerError(format!("Failed to execute git: {}", e))
|
||||
})?;
|
||||
let output = tokio::time::timeout(GIT_OPERATION_TIMEOUT, async {
|
||||
tokio::process::Command::new("git")
|
||||
.arg(git_cmd)
|
||||
.arg("--stateless-rpc")
|
||||
.arg("--advertise-refs")
|
||||
.arg(&self.storage_path)
|
||||
.output()
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(|_| actix_web::error::ErrorInternalServerError("Git info-refs timeout"))?
|
||||
.map_err(|e| {
|
||||
actix_web::error::ErrorInternalServerError(format!("Failed to execute git: {}", e))
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Ok(
|
||||
HttpResponse::InternalServerError().body(format!("Git command failed: {}", stderr))
|
||||
);
|
||||
return Err(actix_web::error::ErrorInternalServerError(format!(
|
||||
"Git command failed: {}",
|
||||
stderr
|
||||
)));
|
||||
}
|
||||
|
||||
let mut response_body = Vec::new();
|
||||
@ -128,21 +133,17 @@ impl GitHttpHandler {
|
||||
|
||||
// Reject oversized pre-PACK data to prevent memory exhaustion
|
||||
if pre_pack.len() + bytes.len() > PRE_PACK_LIMIT {
|
||||
return Ok(HttpResponse::PayloadTooLarge()
|
||||
.insert_header(("Content-Type", "text/plain"))
|
||||
.body(format!(
|
||||
"Ref negotiation exceeds {} byte limit",
|
||||
PRE_PACK_LIMIT
|
||||
)));
|
||||
return Err(actix_web::error::ErrorPayloadTooLarge(format!(
|
||||
"Ref negotiation exceeds {} byte limit",
|
||||
PRE_PACK_LIMIT
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(pos) = bytes.windows(4).position(|w| w == PACK_SIG) {
|
||||
pre_pack.extend_from_slice(&bytes[..pos]);
|
||||
|
||||
if let Err(msg) = check_branch_protection(&branch_protects, &pre_pack) {
|
||||
return Ok(HttpResponse::Forbidden()
|
||||
.insert_header(("Content-Type", "text/plain"))
|
||||
.body(msg));
|
||||
return Err(actix_web::error::ErrorForbidden(msg));
|
||||
}
|
||||
|
||||
let remaining: ByteStream = Box::pin(stream! {
|
||||
@ -201,9 +202,10 @@ impl GitHttpHandler {
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.insert_header(("Content-Type", "text/plain"))
|
||||
.body(format!("Git command failed: {}", stderr)));
|
||||
return Err(actix_web::error::ErrorInternalServerError(format!(
|
||||
"Git command failed: {}",
|
||||
stderr
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
|
||||
@ -12,6 +12,8 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const LFS_AUTH_TOKEN_EXPIRY: u64 = 3600;
|
||||
/// Maximum LFS object size in bytes (50 GiB, matching GitHub/Gitea default).
|
||||
const LFS_MAX_OBJECT_SIZE: i64 = 50 * 1024 * 1024 * 1024;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct BatchRequest {
|
||||
@ -128,6 +130,15 @@ impl LfsHandler {
|
||||
)));
|
||||
}
|
||||
|
||||
for obj in &req.objects {
|
||||
if obj.size > LFS_MAX_OBJECT_SIZE {
|
||||
return Err(GitError::InvalidOid(format!(
|
||||
"Object size {} exceeds maximum allowed size {}",
|
||||
obj.size, LFS_MAX_OBJECT_SIZE
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let oids: Vec<&str> = req.objects.iter().map(|o| o.oid.as_str()).collect();
|
||||
|
||||
// Single batch query for all OIDs
|
||||
@ -260,6 +271,15 @@ impl LfsHandler {
|
||||
while let Some(chunk) = payload.next().await {
|
||||
let chunk = chunk.map_err(|e| GitError::Internal(format!("Payload error: {}", e)))?;
|
||||
size += chunk.len() as i64;
|
||||
// Hard limit: abort if we exceed the max LFS object size.
|
||||
// This prevents unbounded disk usage from a malicious or misbehaving client.
|
||||
if size > LFS_MAX_OBJECT_SIZE {
|
||||
let _ = tokio::fs::remove_file(&temp_path).await;
|
||||
return Err(GitError::InvalidOid(format!(
|
||||
"Object size exceeds maximum allowed size {}",
|
||||
LFS_MAX_OBJECT_SIZE
|
||||
)));
|
||||
}
|
||||
hasher.update(&chunk);
|
||||
if let Err(e) = file.write_all(&chunk).await {
|
||||
let _ = tokio::fs::remove_file(&temp_path).await;
|
||||
|
||||
@ -4,8 +4,6 @@ use db::cache::AppCache;
|
||||
use db::database::AppDatabase;
|
||||
use slog::{Logger, error, info};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
pub mod auth;
|
||||
pub mod handler;
|
||||
@ -99,27 +97,12 @@ pub async fn run_http(config: AppConfig, logger: Logger) -> anyhow::Result<()> {
|
||||
.bind("0.0.0.0:8021")?
|
||||
.run();
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let server = server;
|
||||
let logger_shutdown = logger.clone();
|
||||
let server_handle = tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
result = server => {
|
||||
if let Err(e) = result {
|
||||
error!(&logger_shutdown, "HTTP server error: {}", e);
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx => {
|
||||
info!(&logger_shutdown, "HTTP server shutting down");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::signal::ctrl_c().await?;
|
||||
info!(&logger, "Received shutdown signal");
|
||||
drop(shutdown_tx);
|
||||
|
||||
let _ = timeout(Duration::from_secs(5), server_handle).await;
|
||||
// Await the server. Actix-web handles Ctrl+C gracefully by default:
|
||||
// workers finish in-flight requests then exit (graceful shutdown).
|
||||
let result = server.await;
|
||||
if let Err(e) = result {
|
||||
error!(&logger, "HTTP server error: {}", e);
|
||||
}
|
||||
|
||||
info!(&logger, "Git HTTP server stopped");
|
||||
Ok(())
|
||||
|
||||
@ -36,6 +36,12 @@ struct RateLimitBucket {
|
||||
reset_time: Instant,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum BucketOp {
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
|
||||
pub struct RateLimiter {
|
||||
buckets: Arc<RwLock<HashMap<String, RateLimitBucket>>>,
|
||||
config: RateLimitConfig,
|
||||
@ -51,23 +57,23 @@ impl RateLimiter {
|
||||
|
||||
pub async fn is_ip_read_allowed(&self, ip: &str) -> bool {
|
||||
let key = format!("ip:read:{}", ip);
|
||||
self.is_allowed(&key, self.config.read_requests_per_window)
|
||||
self.is_allowed(&key, BucketOp::Read, self.config.read_requests_per_window)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn is_ip_write_allowed(&self, ip: &str) -> bool {
|
||||
let key = format!("ip:write:{}", ip);
|
||||
self.is_allowed(&key, self.config.write_requests_per_window)
|
||||
self.is_allowed(&key, BucketOp::Write, self.config.write_requests_per_window)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn is_repo_write_allowed(&self, ip: &str, repo_path: &str) -> bool {
|
||||
let key = format!("repo:write:{}:{}", ip, repo_path);
|
||||
self.is_allowed(&key, self.config.write_requests_per_window)
|
||||
self.is_allowed(&key, BucketOp::Write, self.config.write_requests_per_window)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn is_allowed(&self, key: &str, limit: u32) -> bool {
|
||||
async fn is_allowed(&self, key: &str, op: BucketOp, limit: u32) -> bool {
|
||||
let now = Instant::now();
|
||||
let mut buckets = self.buckets.write().await;
|
||||
|
||||
@ -85,12 +91,19 @@ impl RateLimiter {
|
||||
bucket.reset_time = now + Duration::from_secs(self.config.window_secs);
|
||||
}
|
||||
|
||||
// Use read_count for both read/write since we don't distinguish in bucket
|
||||
if bucket.read_count >= limit {
|
||||
let over_limit = match op {
|
||||
BucketOp::Read => bucket.read_count >= limit,
|
||||
BucketOp::Write => bucket.write_count >= limit,
|
||||
};
|
||||
|
||||
if over_limit {
|
||||
return false;
|
||||
}
|
||||
|
||||
bucket.read_count += 1;
|
||||
match op {
|
||||
BucketOp::Read => bucket.read_count += 1,
|
||||
BucketOp::Write => bucket.write_count += 1,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ pub async fn info_refs(
|
||||
.ok_or_else(|| actix_web::error::ErrorBadRequest("Missing service parameter"))?;
|
||||
|
||||
if service_param != "git-upload-pack" && service_param != "git-receive-pack" {
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid service"));
|
||||
return Err(actix_web::error::ErrorBadRequest("Invalid service"));
|
||||
}
|
||||
|
||||
let path_inner = path.into_inner();
|
||||
|
||||
@ -163,19 +163,12 @@ impl russh::server::Handler for SSHandle {
|
||||
Ok(Auth::UnsupportedMethod)
|
||||
}
|
||||
|
||||
async fn auth_password(&mut self, user: &str, token: &str) -> Result<Auth, Self::Error> {
|
||||
async fn auth_password(&mut self, _user: &str, token: &str) -> Result<Auth, Self::Error> {
|
||||
let client_info = self
|
||||
.client_addr
|
||||
.map(|addr| format!("{}", addr))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
if user != "git" {
|
||||
warn!(
|
||||
self.logger,
|
||||
"auth_password rejected: invalid username '{}', client: {}", user, client_info
|
||||
);
|
||||
return Err(russh::Error::NotAuthenticated);
|
||||
}
|
||||
|
||||
if token.is_empty() {
|
||||
warn!(
|
||||
@ -423,11 +416,54 @@ impl russh::server::Handler for SSHandle {
|
||||
|
||||
async fn channel_open_session(
|
||||
&mut self,
|
||||
_: Channel<Msg>,
|
||||
_: &mut Session,
|
||||
channel: Channel<Msg>,
|
||||
session: &mut Session,
|
||||
) -> Result<bool, Self::Error> {
|
||||
let client_info = self
|
||||
.client_addr
|
||||
.map(|addr| format!("{}", addr))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
info!(self.logger, "{}", format!("channel_open_session channel={:?} client={}", channel, client_info));
|
||||
let _ = session.flush().ok();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn pty_request(
|
||||
&mut self,
|
||||
channel: ChannelId,
|
||||
term: &str,
|
||||
col_width: u32,
|
||||
row_height: u32,
|
||||
_pix_width: u32,
|
||||
_pix_height: u32,
|
||||
_modes: &[(russh::Pty, u32)],
|
||||
session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
let client_info = self
|
||||
.client_addr
|
||||
.map(|addr| format!("{}", addr))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
warn!(self.logger, "{}", format!("pty_request not supported channel={:?} term={} cols={} rows={} client={}", channel, term, col_width, row_height, client_info));
|
||||
let _ = session.flush().ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subsystem_request(
|
||||
&mut self,
|
||||
channel: ChannelId,
|
||||
name: &str,
|
||||
session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
let client_info = self
|
||||
.client_addr
|
||||
.map(|addr| format!("{}", addr))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
info!(self.logger, "{}", format!("subsystem_request channel={:?} subsystem={} client={}", channel, name, client_info));
|
||||
// git-clients may send "subsystem" for git protocol over ssh.
|
||||
// We don't use subsystem; exec_request handles it directly.
|
||||
let _ = session.flush().ok();
|
||||
Ok(())
|
||||
}
|
||||
async fn data(
|
||||
&mut self,
|
||||
channel: ChannelId,
|
||||
@ -811,20 +847,45 @@ fn parse_repo_path(path: &str) -> Option<(&str, &str)> {
|
||||
fn build_git_command(service: GitService, path: PathBuf) -> tokio::process::Command {
|
||||
let mut cmd = tokio::process::Command::new("git");
|
||||
|
||||
let canonical_path = path.canonicalize().unwrap_or(path);
|
||||
cmd.current_dir(canonical_path);
|
||||
// Canonicalize only for validation; if it fails, fall back to the raw path.
|
||||
// Using canonicalize for current_dir is safe since we validate repo existence
|
||||
// before reaching this point.
|
||||
let cwd = match path.canonicalize() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
// Log and continue with the raw path — the git process will fail
|
||||
// with a clear "repository not found" message rather than panicking here.
|
||||
let _ = e;
|
||||
path.clone()
|
||||
}
|
||||
};
|
||||
cmd.current_dir(cwd);
|
||||
|
||||
match service {
|
||||
GitService::UploadPack => cmd.arg("upload-pack"),
|
||||
GitService::ReceivePack => cmd.arg("receive-pack"),
|
||||
GitService::UploadArchive => cmd.arg("upload-archive"),
|
||||
};
|
||||
GitService::UploadPack => { cmd.arg("upload-pack"); }
|
||||
GitService::ReceivePack => { cmd.arg("receive-pack"); }
|
||||
GitService::UploadArchive => { cmd.arg("upload-archive"); }
|
||||
}
|
||||
|
||||
cmd.arg(".")
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||||
.env("GIT_NO_REPLACE_OBJECTS", "1")
|
||||
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
||||
.env("GIT_CONFIG_SYSTEM", "/dev/null");
|
||||
.env("GIT_NO_REPLACE_OBJECTS", "1");
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
cmd.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
||||
.env("GIT_CONFIG_SYSTEM", "/dev/null");
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, /dev/null doesn't exist. Set invalid paths so git
|
||||
// ignores them without crashing. GIT_CONFIG_NOSYSTEM already disables
|
||||
// the system config.
|
||||
let nul = "NUL";
|
||||
cmd.env("GIT_CONFIG_GLOBAL", nul)
|
||||
.env("GIT_CONFIG_SYSTEM", nul);
|
||||
}
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
@ -864,6 +925,8 @@ where
|
||||
Fwd: FnMut(&'a Handle, ChannelId, CryptoVec) -> Fut,
|
||||
{
|
||||
const BUF_SIZE: usize = 1024 * 32;
|
||||
const MAX_RETRIES: usize = 5;
|
||||
const RETRY_DELAY: u64 = 10; // ms
|
||||
|
||||
let mut buf = [0u8; BUF_SIZE];
|
||||
loop {
|
||||
@ -874,12 +937,20 @@ where
|
||||
}
|
||||
|
||||
let mut chunk = CryptoVec::from_slice(&buf[..read]);
|
||||
let mut retries = 0;
|
||||
loop {
|
||||
match fwd(session_handle, chan_id, chunk).await {
|
||||
Ok(()) => break,
|
||||
Err(unsent) => {
|
||||
retries += 1;
|
||||
if retries >= MAX_RETRIES {
|
||||
// Give up — connection is likely broken. Returning Ok (not Err)
|
||||
// so the outer task can clean up gracefully without logging
|
||||
// a spurious error for a normal disconnection.
|
||||
return Ok(());
|
||||
}
|
||||
chunk = unsent;
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
sleep(Duration::from_millis(RETRY_DELAY)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,6 +145,10 @@ impl SSHHandle {
|
||||
self.logger.clone(),
|
||||
token_service,
|
||||
);
|
||||
|
||||
// Start the rate limiter cleanup background task so the HashMap
|
||||
// doesn't grow unbounded over time.
|
||||
let _cleanup = server.rate_limiter.clone().start_cleanup();
|
||||
let ssh_port = self.app.ssh_port()?;
|
||||
let bind_addr = format!("0.0.0.0:{}", ssh_port);
|
||||
let public_host = self.app.ssh_domain()?;
|
||||
|
||||
@ -25,6 +25,7 @@ struct RateLimitState {
|
||||
reset_time: Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RateLimiter {
|
||||
limits: Arc<RwLock<HashMap<String, RateLimitState>>>,
|
||||
config: RateLimitConfig,
|
||||
@ -131,4 +132,10 @@ impl SshRateLimiter {
|
||||
.is_allowed(&format!("repo_access:{}:{}", user_id, repo_path))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start a background cleanup task that removes expired entries every 5 minutes.
|
||||
/// Prevents unbounded HashMap growth in the underlying RateLimiter.
|
||||
pub fn start_cleanup(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
|
||||
RateLimiter::start_cleanup(Arc::new(self.limiter.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ use crate::error::RoomError;
|
||||
use crate::metrics::RoomMetrics;
|
||||
use crate::types::NotificationEvent;
|
||||
|
||||
const BROADCAST_CAPACITY: usize = 10000;
|
||||
const BROADCAST_CAPACITY: usize = 100_000;
|
||||
const SHUTDOWN_CHANNEL_CAPACITY: usize = 16;
|
||||
const CONNECTION_COOLDOWN: Duration = Duration::from_secs(30);
|
||||
const MAX_CONNECTIONS_PER_ROOM: usize = 50000;
|
||||
@ -185,6 +185,13 @@ impl RoomConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut stream_map = self.room_stream_inner.write().await;
|
||||
for room_id in &idle_room_ids {
|
||||
stream_map.remove(room_id);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut txs = self.room_shutdown_txs.write().await;
|
||||
for room_id in &idle_room_ids {
|
||||
@ -236,6 +243,10 @@ impl RoomConnectionManager {
|
||||
let mut map = self.room_inner.write().await;
|
||||
map.remove(&room_id);
|
||||
}
|
||||
{
|
||||
let mut stream_map = self.room_stream_inner.write().await;
|
||||
stream_map.remove(&room_id);
|
||||
}
|
||||
{
|
||||
let mut txs = self.room_shutdown_txs.write().await;
|
||||
txs.remove(&room_id);
|
||||
|
||||
@ -1121,25 +1121,46 @@ impl RoomService {
|
||||
let mut conn = cache.conn().await.map_err(|e| {
|
||||
RoomError::Internal(format!("failed to get redis connection for seq: {}", e))
|
||||
})?;
|
||||
let seq: i64 = redis::cmd("INCR")
|
||||
// Atomically increment and check via Lua: INCR first, then if Redis was
|
||||
// externally set to a higher value, jump to max+1. This prevents concurrent
|
||||
// requests from getting duplicate seqs — the Lua script runs as one atomic unit.
|
||||
let seq: i64 = redis::cmd("EVAL")
|
||||
.arg(
|
||||
r#"
|
||||
local current = redis.call('INCR', KEYS[1])
|
||||
local stored = redis.call('GET', KEYS[1])
|
||||
if stored and tonumber(stored) > current then
|
||||
local next = tonumber(stored) + 1
|
||||
redis.call('SET', KEYS[1], next)
|
||||
return next
|
||||
end
|
||||
return current
|
||||
"#,
|
||||
)
|
||||
.arg(1)
|
||||
.arg(&seq_key)
|
||||
.query_async(&mut conn)
|
||||
.await
|
||||
.map_err(|e| RoomError::Internal(format!("INCR seq: {}", e)))?;
|
||||
.map_err(|e| RoomError::Internal(format!("seq Lua script: {}", e)))?;
|
||||
|
||||
// Reconciliation check: if DB is ahead of Redis (e.g. server restart wiped
|
||||
// Redis), bump Redis to stay in sync. This query is only hit on the rare
|
||||
// cross-server handoff case, not on every request.
|
||||
use models::rooms::room_message::{Column as RmCol, Entity as RoomMessage};
|
||||
use sea_orm::EntityTrait;
|
||||
let db_seq: Option<Option<i64>> = RoomMessage::find()
|
||||
let db_seq: Option<Option<Option<i64>>> = RoomMessage::find()
|
||||
.filter(RmCol::Room.eq(room_id))
|
||||
.select_only()
|
||||
.column_as(RmCol::Seq.max(), "max_seq")
|
||||
.into_tuple::<Option<i64>>()
|
||||
.into_tuple::<Option<Option<i64>>>()
|
||||
.one(db)
|
||||
.await?
|
||||
.map(|r| r);
|
||||
let db_seq = db_seq.flatten().unwrap_or(0);
|
||||
let db_seq = db_seq.flatten().flatten().unwrap_or(0);
|
||||
|
||||
if db_seq >= seq {
|
||||
let _: i64 = redis::cmd("SET")
|
||||
// Another server handled this room while we were idle — catch up.
|
||||
let _: String = redis::cmd("SET")
|
||||
.arg(&seq_key)
|
||||
.arg(db_seq + 1)
|
||||
.query_async(&mut conn)
|
||||
|
||||
@ -277,6 +277,13 @@ export function RoomChatPanel({ room, isAdmin, onClose, onDelete }: RoomChatPane
|
||||
? (wsError ?? 'Connection error')
|
||||
: null;
|
||||
|
||||
// Visual connection status dot (Discord-style)
|
||||
const statusDotColor = wsStatus === 'open'
|
||||
? 'bg-green-500'
|
||||
: wsStatus === 'connecting'
|
||||
? 'bg-yellow-400 animate-pulse'
|
||||
: 'bg-red-500';
|
||||
|
||||
const handleSend = useCallback(
|
||||
(content: string) => {
|
||||
sendMessage(content, 'text', replyingTo?.id ?? undefined);
|
||||
@ -380,6 +387,10 @@ export function RoomChatPanel({ room, isAdmin, onClose, onDelete }: RoomChatPane
|
||||
<div className="flex items-center gap-2">
|
||||
<Hash className="h-5 w-5 text-muted-foreground" />
|
||||
<h1 className="truncate text-base font-semibold text-foreground">{room.room_name}</h1>
|
||||
<span
|
||||
className={cn('h-2 w-2 rounded-full', statusDotColor)}
|
||||
title={connectionLabel ?? 'Connected'}
|
||||
/>
|
||||
{!room.public && (
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">
|
||||
Private
|
||||
|
||||
@ -98,7 +98,9 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
|
||||
const isOwner = user?.uid === getSenderUserUid(message);
|
||||
const isRevoked = !!message.revoked;
|
||||
const isFailed = message.isOptimisticError === true;
|
||||
const isPending = message.id.startsWith('temp-');
|
||||
// True for messages that haven't been confirmed by the server yet.
|
||||
// Handles both the old 'temp-' prefix and the new isOptimistic flag.
|
||||
const isPending = message.isOptimistic === true || message.id.startsWith('temp-') || message.id.startsWith('optimistic-');
|
||||
|
||||
// Get streaming content if available
|
||||
const displayContent = isStreaming && streamingMessages?.has(message.id)
|
||||
@ -213,6 +215,7 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
|
||||
grouped ? 'py-0.5' : 'py-2',
|
||||
!isSystem && 'hover:bg-muted/30',
|
||||
isSystem && 'border-l-2 border-amber-500/60 bg-amber-500/5',
|
||||
(isPending || isFailed) && 'opacity-60',
|
||||
)}
|
||||
>
|
||||
{/* Avatar */}
|
||||
@ -414,7 +417,7 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
|
||||
)}
|
||||
|
||||
{/* Action toolbar - inline icons when wide, collapsed to dropdown when narrow */}
|
||||
{!isEditing && !isRevoked && (
|
||||
{!isEditing && !isRevoked && !isPending && (
|
||||
<div className="flex items-start gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{isNarrow ? (
|
||||
/* Narrow: all actions in dropdown */
|
||||
|
||||
@ -60,6 +60,8 @@ export type MessageWithMeta = RoomMessageResponse & {
|
||||
display_content?: string;
|
||||
is_streaming?: boolean;
|
||||
isOptimisticError?: boolean;
|
||||
/** True for messages sent by the current user that haven't been confirmed by the server */
|
||||
isOptimistic?: boolean;
|
||||
reactions?: ReactionGroup[];
|
||||
};
|
||||
|
||||
@ -246,7 +248,11 @@ export function RoomProvider({
|
||||
if (client.getStatus() !== 'open') {
|
||||
await client.connect();
|
||||
}
|
||||
await client.subscribeRoom(activeRoomId);
|
||||
// Re-check: activeRoomId may have changed while we were waiting for connect.
|
||||
// Use activeRoomIdRef to get the *current* room, not the stale closure value.
|
||||
const roomId = activeRoomIdRef.current;
|
||||
if (!roomId) return;
|
||||
await client.subscribeRoom(roomId);
|
||||
loadMoreRef.current?.(null);
|
||||
};
|
||||
setup();
|
||||
@ -393,9 +399,27 @@ export function RoomProvider({
|
||||
// Use ref to get current activeRoomId to avoid stale closure
|
||||
if (payload.room_id === activeRoomIdRef.current) {
|
||||
setMessages((prev) => {
|
||||
// Deduplicate by both ID (for normal) and seq (for optimistic replacement)
|
||||
if (prev.some((m) => m.id === payload.id)) {
|
||||
return prev;
|
||||
}
|
||||
// Also check if there's an optimistic message with the same seq that should be replaced
|
||||
const optimisticIdx = prev.findIndex(
|
||||
(m) => m.isOptimistic && m.seq === payload.seq && m.seq !== 0,
|
||||
);
|
||||
if (optimisticIdx !== -1) {
|
||||
// Replace optimistic message with confirmed one
|
||||
const confirmed: MessageWithMeta = {
|
||||
...wsMessageToUiMessage(payload),
|
||||
reactions: prev[optimisticIdx].reactions,
|
||||
};
|
||||
const next = [...prev];
|
||||
next[optimisticIdx] = confirmed;
|
||||
// Remove optimistic from IDB, save confirmed
|
||||
deleteMessageFromIdb(prev[optimisticIdx].id).catch(() => {});
|
||||
saveMessage(confirmed).catch(() => {});
|
||||
return next;
|
||||
}
|
||||
const newMsg = wsMessageToUiMessage(payload);
|
||||
let updated = [...prev, newMsg];
|
||||
updated.sort((a, b) => a.seq - b.seq);
|
||||
@ -476,13 +500,16 @@ export function RoomProvider({
|
||||
const client = wsClientRef.current;
|
||||
if (!client) return;
|
||||
|
||||
// Optimistic update: set edited_at immediately
|
||||
// Capture original edited_at for rollback if fetch fails
|
||||
let rollbackEditedAt: string | null = null;
|
||||
setMessages((prev) => {
|
||||
const msg = prev.find((m) => m.id === payload.message_id);
|
||||
rollbackEditedAt = msg?.edited_at ?? null;
|
||||
const updated = prev.map((m) =>
|
||||
m.id === payload.message_id ? { ...m, edited_at: payload.edited_at } : m,
|
||||
);
|
||||
const msg = updated.find((m) => m.id === payload.message_id);
|
||||
if (msg) saveMessage(msg).catch(() => {});
|
||||
const saved = updated.find((m) => m.id === payload.message_id);
|
||||
if (saved) saveMessage(saved).catch(() => {});
|
||||
return updated;
|
||||
});
|
||||
|
||||
@ -502,12 +529,19 @@ export function RoomProvider({
|
||||
: m,
|
||||
);
|
||||
// Persist to IndexedDB
|
||||
const msg = merged.find((m) => m.id === payload.message_id);
|
||||
if (msg) saveMessage(msg).catch(() => {});
|
||||
const saved = merged.find((m) => m.id === payload.message_id);
|
||||
if (saved) saveMessage(saved).catch(() => {});
|
||||
return merged;
|
||||
});
|
||||
} catch {
|
||||
// Silently ignore - the optimistic update already applied
|
||||
// Revert edited_at if the fetch failed
|
||||
if (rollbackEditedAt !== null) {
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === payload.message_id ? { ...m, edited_at: rollbackEditedAt! } : m,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onMessageRevoked: async (payload) => {
|
||||
@ -575,11 +609,17 @@ export function RoomProvider({
|
||||
}, [wsToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wsClientRef.current) return;
|
||||
wsClientRef.current.connect().catch((e) => {
|
||||
// NOTE: intentionally omitted [wsClient] from deps.
|
||||
// In React StrictMode the component mounts twice — if wsClient were a dep,
|
||||
// the first mount's effect would connect client-1, then StrictMode cleanup
|
||||
// would disconnect it, then the second mount's effect would connect client-2,
|
||||
// then immediately the first mount's *second* cleanup would fire and
|
||||
// disconnect client-2 — leaving WS unconnected. Using a ref for the initial
|
||||
// connect avoids this. The client is always ready by the time this runs.
|
||||
wsClientRef.current?.connect().catch((e) => {
|
||||
console.error('[RoomContext] WS connect error:', e);
|
||||
});
|
||||
}, [wsClient]);
|
||||
}, []);
|
||||
|
||||
const connectWs = useCallback(async () => {
|
||||
const client = wsClientRef.current;
|
||||
@ -764,32 +804,113 @@ export function RoomProvider({
|
||||
[activeRoomId],
|
||||
);
|
||||
|
||||
// Guard against double-sending while a previous send is in-flight.
|
||||
// Without this, rapid clicking can queue multiple optimistic messages and
|
||||
// create duplicate sends on the server.
|
||||
const sendingRef = useRef(false);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (content: string, contentType = 'text', inReplyTo?: string) => {
|
||||
const client = wsClientRef.current;
|
||||
if (!activeRoomId || !client) return;
|
||||
await client.messageCreate(activeRoomId, content, {
|
||||
contentType,
|
||||
inReplyTo,
|
||||
});
|
||||
if (sendingRef.current) return;
|
||||
sendingRef.current = true;
|
||||
|
||||
// Optimistic update: add message immediately so user sees it instantly
|
||||
const optimisticId = `optimistic-${crypto.randomUUID()}`;
|
||||
const optimisticMsg: MessageWithMeta = {
|
||||
id: optimisticId,
|
||||
room: activeRoomId,
|
||||
seq: 0,
|
||||
sender_type: 'member',
|
||||
sender_id: user?.uid ?? null,
|
||||
content,
|
||||
content_type: contentType,
|
||||
send_at: new Date().toISOString(),
|
||||
display_content: content,
|
||||
is_streaming: false,
|
||||
isOptimistic: true,
|
||||
thread: inReplyTo,
|
||||
thread_id: inReplyTo,
|
||||
in_reply_to: inReplyTo,
|
||||
reactions: [],
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, optimisticMsg]);
|
||||
// Persist optimistic message to IndexedDB so it's not lost on refresh
|
||||
saveMessage(optimisticMsg).catch(() => {});
|
||||
|
||||
try {
|
||||
const confirmedMsg = await client.messageCreate(activeRoomId, content, {
|
||||
contentType,
|
||||
inReplyTo,
|
||||
});
|
||||
// Replace optimistic message with server-confirmed one
|
||||
setMessages((prev) => {
|
||||
const without = prev.filter((m) => m.id !== optimisticId);
|
||||
const confirmed: MessageWithMeta = {
|
||||
...confirmedMsg,
|
||||
thread_id: confirmedMsg.thread,
|
||||
display_content: confirmedMsg.content,
|
||||
is_streaming: false,
|
||||
isOptimistic: false,
|
||||
reactions: [],
|
||||
};
|
||||
// Remove optimistic from IDB
|
||||
deleteMessageFromIdb(optimisticId).catch(() => {});
|
||||
// Save confirmed to IDB
|
||||
saveMessage(confirmed).catch(() => {});
|
||||
return [...without, confirmed];
|
||||
});
|
||||
} catch (err) {
|
||||
// Mark optimistic message as failed
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === optimisticId ? { ...m, isOptimisticError: true } : m,
|
||||
),
|
||||
);
|
||||
handleRoomError('Send message', err);
|
||||
} finally {
|
||||
sendingRef.current = false;
|
||||
}
|
||||
},
|
||||
[activeRoomId],
|
||||
[activeRoomId, user],
|
||||
);
|
||||
|
||||
const editMessage = useCallback(
|
||||
async (messageId: string, content: string) => {
|
||||
const client = wsClientRef.current;
|
||||
if (!client) return;
|
||||
await client.messageUpdate(messageId, content);
|
||||
setMessages((prev) =>
|
||||
prev.map((m) => (m.id === messageId ? { ...m, content, display_content: content } : m)),
|
||||
);
|
||||
// Persist to IndexedDB
|
||||
|
||||
// Capture original content for rollback on server rejection
|
||||
let rollbackContent: string | null = null;
|
||||
setMessages((prev) => {
|
||||
const msg = prev.find((m) => m.id === messageId);
|
||||
if (msg) saveMessage({ ...msg, content, display_content: content }).catch(() => {});
|
||||
return prev;
|
||||
rollbackContent = msg?.content ?? null;
|
||||
return prev.map((m) =>
|
||||
m.id === messageId ? { ...m, content, display_content: content } : m,
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await client.messageUpdate(messageId, content);
|
||||
// Persist updated content to IndexedDB
|
||||
setMessages((prev) => {
|
||||
const msg = prev.find((m) => m.id === messageId);
|
||||
if (msg) saveMessage(msg).catch(() => {});
|
||||
return prev;
|
||||
});
|
||||
} catch (err) {
|
||||
// Rollback optimistic update on server rejection
|
||||
if (rollbackContent !== null) {
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === messageId ? { ...m, content: rollbackContent!, display_content: rollbackContent! } : m,
|
||||
),
|
||||
);
|
||||
}
|
||||
handleRoomError('Edit message', err);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -798,10 +919,25 @@ export function RoomProvider({
|
||||
async (messageId: string) => {
|
||||
const client = wsClientRef.current;
|
||||
if (!client) return;
|
||||
await client.messageRevoke(messageId);
|
||||
setMessages((prev) => prev.filter((m) => m.id !== messageId));
|
||||
// Persist to IndexedDB
|
||||
deleteMessageFromIdb(messageId).catch(() => {});
|
||||
|
||||
// Optimistic removal: hide message immediately
|
||||
let rollbackMsg: MessageWithMeta | null = null;
|
||||
setMessages((prev) => {
|
||||
rollbackMsg = prev.find((m) => m.id === messageId) ?? null;
|
||||
return prev.filter((m) => m.id !== messageId);
|
||||
});
|
||||
|
||||
try {
|
||||
await client.messageRevoke(messageId);
|
||||
deleteMessageFromIdb(messageId).catch(() => {});
|
||||
} catch (err) {
|
||||
// Rollback: restore message on server rejection
|
||||
if (rollbackMsg) {
|
||||
setMessages((prev) => [...prev, rollbackMsg!]);
|
||||
saveMessage(rollbackMsg!).catch(() => {});
|
||||
}
|
||||
handleRoomError('Delete message', err);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
@ -916,15 +916,17 @@ export class RoomWsClient {
|
||||
for (const roomId of this.subscribedRooms) {
|
||||
try {
|
||||
await this.request('room.subscribe', { room_id: roomId });
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err) {
|
||||
// Resubscribe failure is non-fatal — messages still arrive via REST poll.
|
||||
// Log at warn level so operators can observe patterns (e.g. auth expiry).
|
||||
console.warn(`[RoomWs] resubscribe room failed (will retry on next reconnect): ${roomId}`, err);
|
||||
}
|
||||
}
|
||||
for (const projectName of this.subscribedProjects) {
|
||||
try {
|
||||
await this.request('project.subscribe', { project_name: projectName });
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err) {
|
||||
console.warn(`[RoomWs] resubscribe project failed (will retry on next reconnect): ${projectName}`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -932,10 +934,13 @@ export class RoomWsClient {
|
||||
private scheduleReconnect(): void {
|
||||
if (!this.shouldReconnect) return;
|
||||
|
||||
const delay = Math.min(
|
||||
this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempt),
|
||||
this.reconnectMaxDelay,
|
||||
);
|
||||
// Exponential backoff with full jitter (uniform random within the backoff window).
|
||||
// Without jitter, all disconnected clients reconnect at exactly the same time
|
||||
// (thundering herd) after a server restart, overwhelming it.
|
||||
const baseDelay = this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempt);
|
||||
const cappedDelay = Math.min(baseDelay, this.reconnectMaxDelay);
|
||||
const jitter = Math.random() * cappedDelay;
|
||||
const delay = Math.floor(jitter);
|
||||
this.reconnectAttempt++;
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user