diff --git a/.gitignore b/.gitignore index 65e7997..a8bf005 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ target cert/ .megaengine/ -tmp/* \ No newline at end of file +tmp/* +dist/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b69ac00..b616a85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,57 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "bytes 1.10.1", + "crypto-common", + "generic-array 0.14.9", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" +dependencies = [ + "aes", +] + [[package]] name = "ahash" version = "0.7.8" @@ -112,6 +163,28 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", + "zeroize", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -130,6 +203,51 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-any" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -172,6 +290,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -201,12 +334,116 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes 1.10.1", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core 0.5.6", + "bytes 1.10.1", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes 1.10.1", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes 1.10.1", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base-x" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base256emoji" version = "1.0.2" @@ -235,6 +472,30 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "bcrypt" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abaf6da45c74385272ddf00e1ac074c7d8a6c1a1dda376902bd6a427522a8b2c" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom 0.3.4", + "subtle", + "zeroize", +] + +[[package]] +name = "better_default" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b9a5040dce49a7642c97ccb1ae59567098967b5d52c29773f1299a42d23bb39" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "bigdecimal" version = "0.3.1" @@ -255,7 +516,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -266,6 +527,27 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "bitfields" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d866f92dc1574aa8da443eacb06ad8fbe4056dbc1b7c3aae508cbccd46c7e706" +dependencies = [ + "bitfields-impl", +] + +[[package]] +name = "bitfields-impl" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09459e6af3016ea58af8332e31d5da117d33a621bad7019355eefccc4a567d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "thiserror 2.0.17", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -293,6 +575,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "blake2b_simd" version = "0.5.11" @@ -301,7 +592,18 @@ checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", "arrayvec 0.5.2", - "constant_time_eq", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "constant_time_eq 0.4.2", ] [[package]] @@ -312,7 +614,21 @@ checksum = "9e461a7034e85b211a4acb57ee2e6730b32912b06c08cc242243c39fc21ae6a2" dependencies = [ "arrayref", "arrayvec 0.5.2", - "constant_time_eq", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", + "constant_time_eq 0.4.2", + "cpufeatures", ] [[package]] @@ -334,6 +650,25 @@ dependencies = [ "generic-array 0.14.9", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "1.5.7" @@ -357,6 +692,37 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "buffer-redux" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431a9cc8d7efa49bc326729264537f5e60affce816c66edf434350778c9f4f54" +dependencies = [ + "memchr", +] + +[[package]] +name = "builder-pattern" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85376b93d8efe18dd819f56505e33e7a9c0f93fb02bd761f8690026178ed6e5" +dependencies = [ + "builder-pattern-macro", + "futures", +] + +[[package]] +name = "builder-pattern-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d624ef88b39588d113f807ffb38ee968aafc388ca57dd7a9a7b82d3de1f5f4" +dependencies = [ + "bitflags 1.3.2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -391,6 +757,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -409,6 +781,34 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "camellia" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "cast5" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.44" @@ -433,7 +833,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", +] + +[[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher", ] [[package]] @@ -448,6 +857,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.42" @@ -462,6 +895,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -513,6 +957,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmac" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" +dependencies = [ + "cipher", + "dbl", + "digest 0.10.7", +] + [[package]] name = "cmake" version = "0.1.54" @@ -538,6 +993,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -556,6 +1021,27 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -606,6 +1092,30 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc24" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -627,6 +1137,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.9", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -634,9 +1156,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.9", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -664,6 +1196,81 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "cx448" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c0cf476284b03eb6c10e78787b21c7abb7d7d43cb2f02532ba6b831ed892fa" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "pkcs8", + "rand_core 0.6.4", + "serdect 0.3.0", + "sha3", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "daemonize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" +dependencies = [ + "libc", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.108", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -690,6 +1297,15 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "dbl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" +dependencies = [ + "generic-array 0.14.9", +] + [[package]] name = "der" version = "0.7.10" @@ -701,6 +1317,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.5" @@ -712,14 +1342,90 @@ dependencies = [ ] [[package]] -name = "derivative" -version = "2.2.0" +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.108", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.108", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.108", + "unicode-xid", +] + +[[package]] +name = "des" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "cipher", ] [[package]] @@ -760,12 +1466,55 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest 0.10.7", + "num-bigint-dig", + "num-traits", + "pkcs8", + "rfc6979", + "sha2 0.10.9", + "signature", + "zeroize", +] + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "eax" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" +dependencies = [ + "aead", + "cipher", + "cmac", + "ctr", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -801,6 +1550,66 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "base64ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array 0.14.9", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serde_json", + "serdect 0.2.0", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -834,6 +1643,19 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", + "portable-atomic", + "portable-atomic-util", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -858,6 +1680,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -870,6 +1703,17 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "flume" version = "0.11.1" @@ -881,6 +1725,12 @@ dependencies = [ "spin", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -925,6 +1775,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -975,6 +1826,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -996,6 +1858,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1021,6 +1884,7 @@ checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1050,6 +1914,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "git2" version = "0.16.1" @@ -1071,6 +1945,42 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "go-defer" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4053f727e8be72cb6afd9f05955676f4df66b2f6a56c5b29362403df8810062b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes 1.10.1", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1105,6 +2015,45 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hcl-edit" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88489f7cdf733b4c7798403f72d2c16fdc2b720e82c5151055f618a9b49afc1c" +dependencies = [ + "fnv", + "hcl-primitives", + "vecmap-rs", + "winnow", +] + +[[package]] +name = "hcl-primitives" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829a11d304c89e2cfe0dbb494a686bbe2b48ade17705c62cd1957b04aa4630f6" +dependencies = [ + "itoa", + "kstring", + "ryu", + "serde", + "unicode-ident", +] + +[[package]] +name = "hcl-rs" +version = "0.18.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48af7144c49a8db969e8a9d00cd470e1a446a3a73f6fa5eafc1eeb3d44d61ff4" +dependencies = [ + "hcl-edit", + "hcl-primitives", + "indexmap", + "itoa", + "serde", + "vecmap-rs", +] + [[package]] name = "heck" version = "0.4.1" @@ -1153,6 +2102,152 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes 1.10.1", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes 1.10.1", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes 1.10.1", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes 1.10.1", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.6", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes 1.10.1", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes 1.10.1", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -1258,6 +2353,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "idea" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" +dependencies = [ + "cipher", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1287,6 +2397,8 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -1300,6 +2412,40 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "ipnetwork" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c3eaab3ac0ede60ffa41add21970a7df7d91772c03383aac6c2c3d53cc716b" +dependencies = [ + "serde", +] + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1315,6 +2461,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1363,6 +2518,39 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.9", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "serde", + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1372,6 +2560,12 @@ dependencies = [ "spin", ] +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.177" @@ -1444,6 +2638,77 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libvault" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14728802093baa700a22a6906e12fe97fcee8a3edadfa30f02ce9d590e94aee4" +dependencies = [ + "anyhow", + "arc-swap", + "as-any", + "async-trait", + "base64 0.22.1", + "bcrypt", + "better_default", + "blake2b_simd 1.0.4", + "blake3", + "builder-pattern", + "chrono", + "crossbeam-channel", + "daemonize", + "dashmap", + "derivative", + "derive_more 0.99.20", + "enum-map", + "foreign-types", + "glob", + "go-defer", + "hcl-rs", + "hex", + "humantime", + "ipnetwork", + "itertools 0.14.0", + "lazy_static", + "libc", + "lockfile", + "log", + "openssl", + "openssl-sys", + "pem", + "pgp", + "priority-queue", + "radix_trie", + "rand 0.9.2", + "rand_chacha 0.3.1", + "regex", + "reqwest", + "rustls", + "rustls-pemfile", + "rustls-webpki", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "serde_yaml", + "smallvec", + "ssh-key", + "stretto", + "strum", + "strum_macros", + "tempfile", + "thiserror 2.0.17", + "tokio", + "toml", + "tonic", + "tracing", + "ureq", + "url", + "webpki-roots 0.26.11", + "x509-parser", + "zeroize", +] + [[package]] name = "libz-sys" version = "1.1.23" @@ -1477,6 +2742,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfile" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be1cf190319c74ba3e45923624626ae2e43fe42ad7e60ff38ded81044c37630" + [[package]] name = "log" version = "0.4.28" @@ -1509,6 +2780,18 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -1524,16 +2807,21 @@ name = "megaengine" version = "0.1.0" dependencies = [ "anyhow", + "axum 0.7.9", + "chacha20poly1305", "chrono", "clap", + "curve25519-dalek", "ed25519-dalek", + "futures", "git2", "hex", + "libvault", "multibase", "multihash", + "openssl", "quinn", "rand_core 0.6.4", - "rcgen", "rustls", "rustls-pemfile", "sea-orm", @@ -1542,8 +2830,11 @@ dependencies = [ "sha2 0.10.9", "sqlx", "tokio", + "tokio-stream", + "tower-http 0.5.2", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -1552,12 +2843,28 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.0" @@ -1587,7 +2894,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26aea85740a1d3014a2a4fe75fc70c5061c96eadb9d0c934cd5cb6178a3dc810" dependencies = [ - "blake2b_simd", + "blake2b_simd 0.5.11", "blake2s_simd", "bytes 0.5.6", "sha1 0.5.0", @@ -1613,6 +2920,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nom" version = "7.1.3" @@ -1623,6 +2939,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1654,6 +2979,7 @@ dependencies = [ "num-iter", "num-traits", "rand 0.8.5", + "serde", "smallvec", "zeroize", ] @@ -1694,6 +3020,49 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "ocb3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1706,6 +3075,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.75" @@ -1783,6 +3158,50 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2 0.10.9", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1806,6 +3225,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -1837,6 +3267,95 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pgp" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaffe1ec22db286599c30ae6be75b37493b558735d86c8e59ec5c38794415fe4" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "aes-kw", + "argon2", + "base64 0.22.1", + "bitfields", + "block-padding", + "blowfish", + "buffer-redux", + "byteorder", + "bytes 1.10.1", + "bzip2", + "camellia", + "cast5", + "cfb-mode", + "cipher", + "const-oid", + "crc24", + "curve25519-dalek", + "cx448", + "derive_builder", + "derive_more 2.1.1", + "des", + "digest 0.10.7", + "dsa", + "eax", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "flate2", + "generic-array 0.14.9", + "hex", + "hkdf", + "idea", + "k256", + "log", + "md-5", + "nom 8.0.0", + "num-bigint-dig", + "num-traits", + "num_enum", + "ocb3", + "p256", + "p384", + "p521", + "rand 0.8.5", + "regex", + "replace_with", + "ripemd", + "rsa", + "sha1 0.10.6", + "sha1-checked", + "sha2 0.10.9", + "sha3", + "signature", + "smallvec", + "snafu", + "twofish", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1876,6 +3395,44 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1910,13 +3467,33 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "priority-queue" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" +dependencies = [ + "equivalent", + "indexmap", + "serde", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.7", ] [[package]] @@ -2072,6 +3649,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -2131,19 +3718,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rcgen" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "yasna", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -2155,9 +3729,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2167,9 +3741,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2191,6 +3765,66 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes 1.10.1", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http 0.6.8", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.6", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2205,6 +3839,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "rkyv" version = "0.7.45" @@ -2236,9 +3879,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest 0.10.7", @@ -2248,6 +3891,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", + "sha2 0.10.9", "signature", "spki", "subtle", @@ -2285,6 +3929,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "1.1.2" @@ -2514,6 +4167,21 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.9", + "pkcs8", + "serdect 0.2.0", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2566,6 +4234,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2599,6 +4277,71 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.5.0" @@ -2616,6 +4359,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest 0.10.7", + "sha1 0.10.6", + "zeroize", +] + [[package]] name = "sha2" version = "0.7.1" @@ -2639,6 +4393,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2673,6 +4437,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "simdutf8" version = "0.1.5" @@ -2697,6 +4467,27 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "socket2" version = "0.6.1" @@ -2732,7 +4523,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ - "nom", + "nom 7.1.3", "unicode_categories", ] @@ -2764,7 +4555,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -2950,6 +4741,49 @@ dependencies = [ "uuid", ] +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "cipher", + "ssh-encoding", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2 0.10.9", +] + +[[package]] +name = "ssh-key" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" +dependencies = [ + "ed25519-dalek", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core 0.6.4", + "rsa", + "sec1", + "sha2 0.10.9", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2962,6 +4796,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stretto" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a313e115c2cd9a88d99d60386bc88641c853d468b2c3bc454c294f385fc084" +dependencies = [ + "atomic", + "crossbeam-channel", + "getrandom 0.2.16", + "parking_lot", + "rand 0.8.5", + "seahash", + "thiserror 1.0.69", + "tracing", + "wg", + "xxhash-rust", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -2984,6 +4836,22 @@ name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.108", +] [[package]] name = "subtle" @@ -3013,6 +4881,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -3024,6 +4901,27 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -3185,17 +5083,71 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes 1.10.1", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -3205,6 +5157,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_edit" version = "0.23.7" @@ -3212,7 +5178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.3", "toml_parser", "winnow", ] @@ -3226,6 +5192,106 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tonic" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +dependencies = [ + "async-trait", + "axum 0.8.8", + "base64 0.22.1", + "bytes 1.10.1", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.10.0", + "bytes 1.10.1", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes 1.10.1", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -3288,6 +5354,31 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +dependencies = [ + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twofish" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" +dependencies = [ + "cipher", +] + [[package]] name = "typenum" version = "1.19.0" @@ -3327,12 +5418,34 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unicode_categories" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "unsigned-varint" version = "0.3.3" @@ -3345,6 +5458,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "url" version = "2.5.7" @@ -3381,6 +5512,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ + "getrandom 0.3.4", "js-sys", "serde", "wasm-bindgen", @@ -3398,6 +5530,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vecmap-rs" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9758649b51083aa8008666f41c23f05abca1766aad4cc447b195dd83ef1297b" +dependencies = [ + "serde", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3414,6 +5555,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3448,6 +5598,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.105" @@ -3480,6 +5643,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -3499,6 +5672,36 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wg" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aafc5e81e847f05d6770e074faf7b1cd4a5dec9a0e88eac5d55e20fdfebee9a" +dependencies = [ + "event-listener 5.4.1", + "futures-core", + "parking_lot", + "triomphe", +] + [[package]] name = "whoami" version = "1.6.1" @@ -3559,6 +5762,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -3896,14 +6110,40 @@ dependencies = [ ] [[package]] -name = "yasna" -version = "0.5.2" +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.17", "time", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.8.1" @@ -3973,6 +6213,20 @@ 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.108", +] [[package]] name = "zerotrie" @@ -4006,3 +6260,9 @@ dependencies = [ "quote", "syn 2.0.108", ] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" diff --git a/Cargo.toml b/Cargo.toml index cd858f1..a8828b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,16 @@ rustls-pemfile = "2.2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } clap = { version = "4.3", features = ["derive"] } -rcgen = "0.13" +libvault = "0.2.2" +openssl = "0.10" chrono = { version = "0.4", features = ["serde"] } sea-orm = { version = "0.12", features = ["runtime-tokio-native-tls", "sqlx-sqlite"] } sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "sqlite"] } git2 = "0.16" +axum = "0.7" +tower-http = { version = "0.5", features = ["cors"] } +uuid = { version = "1.0", features = ["v4"] } +futures = "0.3" +tokio-stream = "0.1.18" +chacha20poly1305 = "0.10.1" +curve25519-dalek = { version = "4.1.3", features = ["legacy_compatibility"] } diff --git a/README.md b/README.md index a5ac920..eb36ffa 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ MegaEngine is a distributed peer-to-peer (P2P) network for Git repositories. It - **Bundle Transfer**: P2P transfer of Git bundle files between nodes with integrity verification - **Automatic Bundle Sync**: Periodic background task that automatically downloads bundles for external repositories - **Repository Cloning**: Clone repositories from bundles using the `repo clone` command +- **Peer-to-Peer Chat**: Send direct encrypted chat messages between nodes using the `chat send` command - **QUIC Transport**: Uses QUIC protocol for reliable, low-latency peer-to-peer communication - **Gossip Protocol**: Implements epidemic message propagation with TTL and deduplication - **Cryptographic Identity**: Each node has a unique EdDSA-based identity (`did:key` format) @@ -197,6 +198,24 @@ Replace `` with the repository ID from Step 3. The cloned repository at `./tiny` will be updated with the latest commits from the bundle. +### Step 8: Node-to-Node Chat Messaging + +After both nodes are running and connected, you can send chat messages directly by Node ID. + +**Terminal 3** - Send a message from node2 to node1: +```bash +cargo run -- --root ~/.megaengine2 chat send --to --msg "hello from node2" +``` + +Replace `` with node1's DID key from Step 1/Step 2 output. + +Example: +```bash +cargo run -- --root ~/.megaengine2 chat send --to did:key:z2DUYGZos3YrXrD4pQ9aAku2g7btumKcfTiMSyBC8btqFDJ --msg "hello" +``` + +You should see the message reception log on node1's terminal. + ## 🔐 Data Formats diff --git a/src/bundle/transfer.rs b/src/bundle/transfer.rs index 026af9b..7cf0649 100644 --- a/src/bundle/transfer.rs +++ b/src/bundle/transfer.rs @@ -5,12 +5,16 @@ use crate::util::get_node_id_last_part; use crate::util::get_repo_id_last_part; use anyhow::Context; use anyhow::Result; +use std::io::SeekFrom; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::fs; +use tokio::io::{AsyncSeekExt, AsyncWriteExt}; use tokio::sync::Mutex; use tracing::{debug, info, warn}; +const TRANSFER_CHUNK_SIZE: usize = 64 * 1024; // 64KB per chunk + /// Bundle 消息类型(用于多帧传输) #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum BundleMessageType { @@ -98,8 +102,7 @@ impl BundleTransferManager { .context("Failed to send START message")?; // 2. 分块发送数据 - const CHUNK_SIZE: usize = 64 * 1024; // 64KB per chunk - for (chunk_idx, chunk) in bundle_data.chunks(CHUNK_SIZE).enumerate() { + for (chunk_idx, chunk) in bundle_data.chunks(TRANSFER_CHUNK_SIZE).enumerate() { let chunk_msg = BundleMessageType::Chunk { repo_id: repo_id.clone(), chunk_idx: chunk_idx as u32, @@ -133,7 +136,7 @@ impl BundleTransferManager { "Bundle {} sent successfully to node {} ({} chunks)", file_name, target_node_id, - bundle_data.chunks(CHUNK_SIZE).count() + bundle_data.chunks(TRANSFER_CHUNK_SIZE).count() ); Ok(()) @@ -261,6 +264,14 @@ impl BundleTransferManager { .await .context("Failed to create bundle storage directory")?; + // 确保文件从头开始:如果存在则清空,如果不存在则创建 + let encoded_repo_id = get_repo_id_last_part(repo_id); + let file_path = dir.join(format!("{}.bundle", encoded_repo_id)); + + let _ = fs::File::create(&file_path) + .await + .context("Failed to create/truncate bundle file")?; + info!( "Bundle transfer START from {}: repo={}, file={}, size={} bytes", from, repo_id, file_name, total_size @@ -282,22 +293,35 @@ impl BundleTransferManager { let encoded_repo_id = get_repo_id_last_part(repo_id); let file_path = dir.join(format!("{}.bundle", encoded_repo_id)); - // 追加写入到文件 + // 如果文件不存在(可能是 Start 消息丢失),先创建 + if !file_path.exists() { + if let Some(parent) = file_path.parent() { + fs::create_dir_all(parent).await?; + } + } + + // 使用 Write 模式打开,不追加,而是使用 Seek let mut file = fs::OpenOptions::new() .create(true) - .append(true) + .write(true) + .truncate(false) .open(&file_path) .await - .context("Failed to open bundle file for appending")?; + .context("Failed to open bundle file")?; + + let offset = (chunk_idx as u64) * (TRANSFER_CHUNK_SIZE as u64); + file.seek(SeekFrom::Start(offset)) + .await + .context("Failed to seek to chunk position")?; - use tokio::io::AsyncWriteExt; file.write_all(&data) .await .context("Failed to write chunk data")?; info!( - "Received chunk {} ({} bytes) for repo {} from {}", + "Received chunk {} (offset {}) ({} bytes) for repo {} from {}", chunk_idx, + offset, data.len(), repo_id, from diff --git a/src/chat/mod.rs b/src/chat/mod.rs new file mode 100644 index 0000000..1f278a4 --- /dev/null +++ b/src/chat/mod.rs @@ -0,0 +1 @@ +pub mod service; diff --git a/src/chat/service.rs b/src/chat/service.rs new file mode 100644 index 0000000..50e0c7a --- /dev/null +++ b/src/chat/service.rs @@ -0,0 +1,314 @@ +use crate::gossip::message::{ + ChatAckMessage, EncryptedChatMessage, Envelope, GossipMessage, SignedMessage, +}; +use crate::node::node::Node; +use crate::node::node_id::NodeId; +use crate::storage::chat_message::MessageStatus; +use crate::transport::quic::ConnectionManager; +use crate::util::timestamp_now; +use anyhow::{anyhow, Result}; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; + +const TTL: u8 = 16; + +pub async fn start_chat_sender_task( + manager: Arc>, + my_node: Node, +) -> Result<()> { + loop { + if let Err(e) = process_pending_messages(manager.clone(), my_node.clone()).await { + tracing::error!("Failed to process pending messages: {}", e); + } + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + } +} + +async fn process_pending_messages( + manager: Arc>, + my_node: Node, +) -> Result<()> { + // 1. Find all messages with status 'Sending' + let db = crate::storage::get_db_conn().await?; + let pending_msgs = crate::storage::chat_message::Entity::find() + .filter(crate::storage::chat_message::Column::Status.eq(MessageStatus::Sending)) + .all(&db) + .await?; + + for msg in pending_msgs { + tracing::info!("Processing pending message: {}", msg.id); + + let receiver_node_id = match NodeId::from_string(&msg.to) { + Ok(id) => id, + Err(_) => { + tracing::error!("Invalid receiver node id: {}, marking failed", msg.to); + crate::storage::chat_message::update_message_status(&msg.id, MessageStatus::Failed) + .await?; + continue; + } + }; + + match try_send_pending_msg( + manager.clone(), + my_node.clone(), + receiver_node_id, + msg.content.clone(), + msg.id.clone(), + ) + .await + { + Ok(_) => { + crate::storage::chat_message::update_message_status(&msg.id, MessageStatus::Sent) + .await?; + tracing::info!("Message {} sent successfully", msg.id); + } + Err(e) => { + tracing::error!("Failed to send message {}: {}", msg.id, e); + // We can keep it as 'Sending' to retry later, or make a 'Failed' logic + // For now, retry indefinitely + } + } + } + Ok(()) +} + +async fn try_send_pending_msg( + manager: Arc>, + my_node: Node, + receiver_node_id: NodeId, + content: String, + msg_id: String, +) -> Result<()> { + // 1. Get Receiver Public Key + let receiver_keypair = receiver_node_id + .to_keypair() + .map_err(|_| anyhow!("Could not decode receiver NodeId (did:key)"))?; + let receiver_pk = receiver_keypair.verifying_key; + + // 2. Encrypt + let my_keypair = &my_node.keypair; + let encrypted_bytes = my_keypair.encrypt_to_node(&receiver_pk, content.as_bytes())?; + + // 3. Construct Message + let encrypted_chat = EncryptedChatMessage { + sender_id: my_node.node_id().clone(), + receiver_id: receiver_node_id.clone(), + msg_id: msg_id.clone(), + ciphertext: encrypted_bytes, + }; + + let message = GossipMessage::Chat(encrypted_chat); + + // 4. Sign & Broadcast/Send + let mut signed_msg = SignedMessage { + node_id: my_node.node_id().clone(), + message, + timestamp: timestamp_now(), + signature: "".to_string(), + }; + let self_hash = signed_msg.self_hash(); + let sign = my_node.sign_message(self_hash.as_slice())?; + signed_msg.signature = hex::encode(sign); + + let envelope = Envelope { + payload: signed_msg, + ttl: TTL, + }; + let data = serde_json::to_vec(&envelope)?; + + // Try to find if we are connected or have a known route? + // Gossip usually just floods peers if not knowing better. + // If we have direct connection or routing table, use it. + // For now: broadcast to all connected peers. + + // Obtain the current peer list while holding the mutex only briefly. + let peers = { + let mgr = manager.lock().await; + mgr.list_peers().await + }; + + if peers.is_empty() { + return Err(anyhow!("No peers connected to send message")); + } + + if peers.contains(&receiver_node_id) { + // Direct send to receiver; propagate any error to the caller. + let send_result = { + let mgr = manager.lock().await; + mgr.send_gossip_message(receiver_node_id.clone(), data.clone()) + .await + }; + + send_result.map_err(|e| { + anyhow!( + "Failed to send gossip message to receiver {}: {}", + receiver_node_id, + e + ) + })?; + } else { + // Broadcast to all peers; require at least one successful send. + let mut at_least_one_success = false; + let mut last_err: Option = None; + + for peer in peers { + let send_result = { + let mgr = manager.lock().await; + mgr.send_gossip_message(peer.clone(), data.clone()).await + }; + + match send_result { + Ok(()) => { + at_least_one_success = true; + } + Err(e) => { + last_err = Some(anyhow!( + "Failed to send gossip message to peer {}: {}", + peer, + e + )); + } + } + } + + if !at_least_one_success { + // If all sends failed, return the last error (or a generic one if none captured). + if let Some(err) = last_err { + return Err(err); + } else { + return Err(anyhow!( + "Failed to send gossip message to any peer (unknown error)" + )); + } + } + } + + Ok(()) +} + +pub async fn send_chat_message( + _manager: Arc>, + my_node: Node, + receiver_node_id: NodeId, + content: String, +) -> Result<()> { + // Backward compatibility or direct call wrapper + // Now just saves to DB and calls try_send immediately for responsiveness, or let scheduler handle it? + // Let's make it just save to DB. + + let msg_id = Uuid::new_v4().to_string(); + + crate::storage::chat_message::save_message( + msg_id.clone(), + my_node.node_id().to_string(), + receiver_node_id.to_string(), + content.clone(), + timestamp_now(), + MessageStatus::Sending, + ) + .await?; + + // Maybe trigger one round of processing immediately? + // For now rely on background task. + Ok(()) +} + +pub async fn process_incoming_chat( + msg: EncryptedChatMessage, + manager: Arc>, + my_node: Node, +) -> Result<()> { + // 1. Check if it's for me + if msg.receiver_id != *my_node.node_id() { + tracing::info!( + "Message not for me (target: {}), skip local forwarding and let gossip handle it", + msg.receiver_id + ); + + return Ok(()); + } + + // 2. Decrypt + let my_keypair = &my_node.keypair; + let plaintext_bytes = my_keypair.decrypt_message(&msg.ciphertext)?; + let content = String::from_utf8(plaintext_bytes)?; + + tracing::info!("Received Chat from {}: {}", msg.sender_id.0, content); + + // 3. Store + let db = crate::storage::get_db_conn().await?; + if (crate::storage::chat_message::Entity::find_by_id(msg.msg_id.clone()) + .one(&db) + .await?) + .is_none() + { + crate::storage::chat_message::save_message( + msg.msg_id.clone(), + msg.sender_id.to_string(), + my_node.node_id().to_string(), + content, + timestamp_now(), + MessageStatus::Delivered, + ) + .await?; + } + + // 4. Send ACK + let ack_msg = ChatAckMessage { + sender_id: my_node.node_id().clone(), + target_id: msg.sender_id.clone(), + msg_id: msg.msg_id.clone(), + timestamp: timestamp_now(), + signature: "".to_string(), + }; + + let gossip_msg = GossipMessage::ChatAck(ack_msg); + + let mut signed_ack = SignedMessage { + node_id: my_node.node_id().clone(), + message: gossip_msg, + timestamp: timestamp_now(), + signature: "".to_string(), + }; + let self_hash = signed_ack.self_hash(); + let sign = my_node.sign_message(self_hash.as_slice())?; + signed_ack.signature = hex::encode(sign); + + let envelope = Envelope { + payload: signed_ack, + ttl: TTL, + }; + let data = serde_json::to_vec(&envelope)?; + + let mgr = manager.lock().await; + let peers = mgr.list_peers().await; + for peer in peers { + let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; + } + + Ok(()) +} + +pub async fn process_ack( + ack: ChatAckMessage, + _manager: Arc>, + my_node: Node, +) -> Result<()> { + // 1. Check if it's for me + if ack.target_id != *my_node.node_id() { + tracing::info!( + "ACK not for me (target: {}), skip local forwarding and let gossip handle it", + ack.target_id + ); + return Ok(()); + } + + tracing::info!("Received ACK for msg {}", ack.msg_id); + + crate::storage::chat_message::update_message_status(&ack.msg_id, MessageStatus::Delivered) + .await?; + + Ok(()) +} diff --git a/src/cli/chat.rs b/src/cli/chat.rs new file mode 100644 index 0000000..44db5c6 --- /dev/null +++ b/src/cli/chat.rs @@ -0,0 +1,71 @@ +use anyhow::Result; +use clap::Subcommand; +use megaengine::node::node_id::NodeId; +use megaengine::storage::chat_message::{Entity as ChatMessage, MessageStatus}; +use megaengine::util::timestamp_now; +use sea_orm::{EntityTrait, QueryOrder}; +use uuid::Uuid; + +#[derive(Clone, Debug, Subcommand)] +pub enum ChatCommand { + /// Send a message to a node + Send { + /// Target Node ID (did:key:...) + #[arg(long)] + to: String, + /// Message content + #[arg(long)] + msg: String, + }, + /// List messages + List, +} + +pub async fn run_chat_command(cmd: ChatCommand) -> Result<()> { + match cmd { + ChatCommand::Send { to, msg } => { + // Load identity + let keypair = megaengine::storage::load_keypair()?; + let my_node_id = NodeId::from_keypair(&keypair); + + let msg_id = Uuid::new_v4().to_string(); + + // Save to DB (Queue it) + // The background node process (if running) will pick this up and send it. + // If it's not running, it will be sent next time it runs. + megaengine::storage::chat_message::save_message( + msg_id.clone(), + my_node_id.to_string(), + to.clone(), + msg, + timestamp_now(), + MessageStatus::Sending, + ) + .await?; + + println!("Message queued (ID: {}).", msg_id); + println!("It will be delivered automatically when the node service is active."); + } + ChatCommand::List => { + let db = megaengine::storage::get_db_conn().await?; + let messages = ChatMessage::find() + .order_by_desc(megaengine::storage::chat_message::Column::CreatedAt) + .all(&db) + .await?; + + println!("--- Chat History ---"); + for m in messages { + let time = if let Some(dt) = chrono::DateTime::from_timestamp(m.created_at, 0) { + dt.format("%Y-%m-%d %H:%M:%S").to_string() + } else { + m.created_at.to_string() + }; + println!( + "[{}] From: {} To: {} : {} ({:?})", + time, m.from, m.to, m.content, m.status + ); + } + } + } + Ok(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 19ea821..08b0d88 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,7 +1,9 @@ pub mod auth; +pub mod chat; pub mod node; pub mod repo; pub use auth::handle_auth; +pub use chat::run_chat_command as handle_chat; pub use node::handle_node; pub use repo::handle_repo; diff --git a/src/cli/node.rs b/src/cli/node.rs index c445ea8..e57680c 100644 --- a/src/cli/node.rs +++ b/src/cli/node.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use megaengine::mcp::start_sse_server; use megaengine::{ bundle::BundleService, node::node_addr::NodeAddr, storage, transport::config::QuicConfig, }; @@ -11,6 +12,8 @@ pub async fn handle_node_start( addr: String, cert_path: String, bootstrap_node: Option, + enable_mcp: bool, + mcp_sse_port: Option, ) -> Result<()> { tracing::info!("Starting node..."); let cert_dir = format!("{}/{}", root_path, cert_path); @@ -81,6 +84,14 @@ pub async fn handle_node_start( // 启动 Repo 同步后台任务 megaengine::repo::start_repo_sync_task().await; tracing::info!("Repo sync task started"); + + // Start Chat Sender Task + let chat_node = node.clone(); + let chat_mgr = Arc::clone(conn_mgr); + tokio::spawn(async move { + let _ = megaengine::chat::service::start_chat_sender_task(chat_mgr, chat_node).await; + }); + tracing::info!("Chat sender task started"); } else { tracing::warn!("No connection manager found, services not started"); } @@ -101,6 +112,26 @@ pub async fn handle_node_start( println!("Node address: {}", node_addr); println!("Press Ctrl+C to stop"); + if enable_mcp { + tracing::warn!( + "--mcp (stdio) is ignored during node start to avoid stdin/stdout contention; use `megaengine mcp` in a separate process" + ); + eprintln!( + "Warning: --mcp (stdio) is not started with `node start`. Run `megaengine mcp` in a separate process." + ); + } + + if let Some(port) = mcp_sse_port { + tracing::info!("MCP SSE server enabled on port {}", port); + println!("MCP SSE server enabled on port {}", port); + tokio::spawn(async move { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port)); + if let Err(e) = start_sse_server(addr).await { + tracing::error!("MCP SSE server error: {}", e); + } + }); + } + loop { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } @@ -175,7 +206,20 @@ pub async fn handle_node(root_path: String, action: crate::NodeAction) -> Result addr, cert_path, bootstrap_node, - } => handle_node_start(&root_path, alias, addr, cert_path, bootstrap_node).await, + mcp, + mcp_sse_port, + } => { + handle_node_start( + &root_path, + alias, + addr, + cert_path, + bootstrap_node, + mcp, + mcp_sse_port, + ) + .await + } crate::NodeAction::Id => handle_node_id().await, } } diff --git a/src/cli/repo.rs b/src/cli/repo.rs index 99efdc0..77a1e8a 100644 --- a/src/cli/repo.rs +++ b/src/cli/repo.rs @@ -37,11 +37,30 @@ pub async fn handle_repo_add(path: String, description: String) -> Result<()> { }; let name = megaengine::git::git_repo::repo_name_space(&path); + let language = detect_language(&path); + + // Calculate size: prefer .git directory size (repository data) over working tree size + let path_p = std::path::Path::new(&path); + let git_dir = path_p.join(".git"); + let size = if git_dir.exists() { + calculate_directory_size(&git_dir) + } else { + 0 + }; + + // Try to get latest git commit time, fallback to now if failed (e.g. empty repo) + let latest_commit_at = match megaengine::git::git_repo::get_latest_commit_time(&path) { + Ok(t) => t, + Err(_) => timestamp_now(), + }; + let desc = repo::repo::P2PDescription { creator: node_id.to_string(), name: name.clone(), description: description.clone(), - timestamp: timestamp_now(), + language: language.clone(), + latest_commit_at, + size, }; let mut repo_obj = @@ -60,20 +79,167 @@ pub async fn handle_repo_add(path: String, description: String) -> Result<()> { let mut manager = repo::repo_manager::RepoManager::new(); match manager.register_repo(repo_obj).await { - Ok(_) => tracing::info!("Repo {} added", repo_id), - Err(e) => tracing::info!("Failed to add repo: {}", e), + Ok(_) => { + tracing::info!("Repo {} added", repo_id); + println!("✅ Repository added successfully!"); + println!(" ID: {}", repo_id); + println!(" Name: {}", name); + } + Err(e) => { + tracing::error!("Failed to add repo: {}", e); + eprintln!("❌ Failed to add repository: {}", e); + } } Ok(()) } +fn detect_language(path: &str) -> String { + use std::collections::HashMap; + use std::fs; + + let mut ext_counts: HashMap = HashMap::new(); + let mut stack = vec![PathBuf::from(path)]; + let mut files_scanned = 0; + + while let Some(dir) = stack.pop() { + if files_scanned > 2000 { + break; + } // limit scanning + + if let Ok(entries) = fs::read_dir(&dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if name.starts_with('.') + || name == "target" + || name == "node_modules" + || name == "dist" + || name == "build" + { + continue; + } + if stack.len() < 50 { + stack.push(path); + } + } else if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + *ext_counts.entry(ext.to_lowercase()).or_insert(0) += 1; + files_scanned += 1; + } + } + } + } + + let mut lang_stats = HashMap::new(); + for (ext, count) in ext_counts { + let lang = match ext.as_str() { + "rs" => "Rust", + "go" => "Go", + "py" => "Python", + "js" => "JavaScript", + "ts" | "tsx" => "TypeScript", + "java" => "Java", + "c" | "h" => "C", + "cpp" | "hpp" | "cc" | "cxx" => "C++", + "cs" => "C#", + "rb" => "Ruby", + "php" => "PHP", + "html" => "HTML", + "css" | "scss" | "less" => "CSS", + "swift" => "Swift", + "kt" | "kts" => "Kotlin", + "scala" => "Scala", + "lua" => "Lua", + "sh" | "bash" | "zsh" => "Shell", + "sql" => "SQL", + "md" => "Markdown", + "json" | "yaml" | "yml" | "toml" | "xml" => "Config/Data", + _ => continue, + }; + *lang_stats.entry(lang).or_insert(0) += count; + } + + // 找出数量最多的语言,排除配置类 + lang_stats + .into_iter() + .filter(|(l, _)| *l != "Config/Data" && *l != "Markdown") + .max_by_key(|&(_, count)| count) + .map(|(lang, _)| lang.to_string()) + .unwrap_or_else(|| "Unknown".to_string()) +} + +fn calculate_directory_size(path: &std::path::Path) -> u64 { + use std::fs; + + const MAX_DEPTH: usize = 64; + const MAX_ENTRIES: u64 = 200_000; + const MAX_TOTAL_SIZE: u64 = 20 * 1024 * 1024 * 1024; // 20 GiB + + fn walk(path: &std::path::Path, depth: usize, entries_seen: &mut u64, total: &mut u64) { + if depth > MAX_DEPTH || *entries_seen >= MAX_ENTRIES || *total >= MAX_TOTAL_SIZE { + return; + } + + let Ok(entries) = fs::read_dir(path) else { + return; + }; + + for entry in entries.flatten() { + if *entries_seen >= MAX_ENTRIES || *total >= MAX_TOTAL_SIZE { + break; + } + + *entries_seen += 1; + let p = entry.path(); + + // Never follow symlinks to avoid cycles and unbounded traversal. + let Ok(meta) = fs::symlink_metadata(&p) else { + continue; + }; + + let file_type = meta.file_type(); + if file_type.is_symlink() { + continue; + } + + if file_type.is_file() { + *total = total.saturating_add(meta.len()); + } else if file_type.is_dir() { + walk(&p, depth + 1, entries_seen, total); + } + } + } + + let mut entries_seen = 0; + let mut total = 0; + walk(path, 0, &mut entries_seen, &mut total); + total +} + +fn format_bytes(bytes: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + + if bytes >= GB { + format!("{:.2} GB", bytes as f64 / GB as f64) + } else if bytes >= MB { + format!("{:.2} MB", bytes as f64 / MB as f64) + } else if bytes >= KB { + format!("{:.2} KB", bytes as f64 / KB as f64) + } else { + format!("{} B", bytes) + } +} + pub async fn handle_repo_list() -> Result<()> { match storage::repo_model::list_repos().await { Ok(repos) => { if repos.is_empty() { - println!("No repositories found"); + println!("No repositories found."); } else { - println!("Repositories:"); - println!("{}", "─".repeat(120)); + println!("Found {} repositories:", repos.len()); + println!("{}", "─".repeat(60)); for repo in repos { print_repo_info(&repo).await; } @@ -81,103 +247,113 @@ pub async fn handle_repo_list() -> Result<()> { } Err(e) => { tracing::error!("Failed to list repos: {}", e); - println!("Failed to list repositories: {}", e); + eprintln!("❌ Failed to list repositories: {}", e); } } Ok(()) } async fn print_repo_info(repo: &Repo) { - println!(" ID: {}", repo.repo_id); - println!(" Name: {}", repo.p2p_description.name); - println!(" Creator: {}", repo.p2p_description.creator); - println!(" Description: {}", repo.p2p_description.description); - println!(" Path: {}", repo.path.display()); - println!(" Bundle: {}", repo.bundle.display()); - println!(" Timestamp: {}", repo.p2p_description.timestamp); + println!("📦 Repo: {}", repo.p2p_description.name); + println!(" ID: {}", repo.repo_id); + println!(" Creator: {}", repo.p2p_description.creator); + println!(" Language: {}", repo.p2p_description.language); + if repo.p2p_description.latest_commit_at > 0 { + if let Some(dt) = chrono::DateTime::from_timestamp(repo.p2p_description.latest_commit_at, 0) + { + let local = dt.with_timezone(&chrono::Local); + println!(" Updated: {}", local.format("%Y-%m-%d %H:%M:%S")); + } + } + if repo.p2p_description.size > 0 { + println!( + " Size: {}", + format_bytes(repo.p2p_description.size) + ); + } + if !repo.p2p_description.description.is_empty() { + println!(" Description: {}", repo.p2p_description.description); + } + println!(" Path: {}", repo.path.display()); + // Bundle path only shown if it exists, to reduce clutter + if !repo.bundle.as_os_str().is_empty() { + println!(" Bundle: {}", repo.bundle.display()); + } + // Status check logic... if repo.bundle.as_os_str().is_empty() { - // No bundle path configured; avoid calling extract_bundle_refs on an empty path. - println!(" Refs: (bundle path not set)"); + println!(" Refs: (bundle not set)"); } else { match megaengine::git::pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) { Ok(local_refs) => { - if local_refs.is_empty() { - println!(" Refs: (none)"); - } else { - println!(" Refs: ({} total)", local_refs.len()); - for (ref_name, commit) in &local_refs { - println!(" - {}: {}", ref_name, commit); - } - } + let ref_count = local_refs.len(); + + // Check if up-to-date with local path + let mut status_msg = "✅ Synced".to_string(); + let mut updates = Vec::new(); - // Check for updates if this is a local repo if !repo.path.as_os_str().is_empty() && repo.path.exists() { - match megaengine::git::git_repo::read_repo_refs(repo.path.to_str().unwrap_or("")) { + match megaengine::git::git_repo::read_repo_refs( + repo.path.to_str().unwrap_or(""), + ) { Ok(current_refs) => { - // Compare current refs with local refs if current_refs != local_refs { - println!(" Status: ⚠️ HAS UPDATES"); - println!(" Updated Refs: ({} total)", current_refs.len()); + status_msg = "⚠️ Out of Sync".to_string(); for (ref_name, commit) in ¤t_refs { - let local_commit = local_refs.get(ref_name); - if local_commit != Some(commit) { - let indicator = if local_commit.is_none() { - "NEW" - } else { - "CHANGED" - }; - println!(" - {} {} : {}", indicator, ref_name, commit); + if local_refs.get(ref_name) != Some(commit) { + updates.push(format!("{} -> {}", ref_name, &commit[0..7])); } } - } else { - println!(" Status: ✅ Up-to-date"); } } - Err(e) => { - tracing::warn!("Failed to check for updates: {}", e); - println!(" Status: (failed to check: {})", e); - } + Err(_) => status_msg = "❓ Unknown (Check Failed)".to_string(), + } + } + + println!(" Refs: {} branches/tags", ref_count); + println!(" Status: {}", status_msg); + + if !updates.is_empty() { + println!(" Updates: {} pending changes", updates.len()); + for update in updates.iter().take(3) { + println!(" - {}", update); + } + if updates.len() > 3 { + println!(" - ... and {} more", updates.len() - 3); } } } - Err(e) => { - println!(" Refs: (failed to load: {})", e); - } + Err(_) => println!(" Refs: (error loading bundle)"), } } - println!("{}", "─".repeat(120)); + println!("{}", "─".repeat(60)); } pub async fn handle_repo_pull(repo_id: String) -> Result<()> { + println!("🔄 Pulling repository {}...", repo_id); match storage::repo_model::load_repo_from_db(&repo_id).await { Ok(Some(repo)) => { // Check if repo has a local path if repo.path.as_os_str().is_empty() { tracing::error!("Repository {} has no local path", repo_id); - println!("Error: Repository {} has no local path", repo_id); + eprintln!( + "❌ Error: Repository {} has no local path configured.", + repo_id + ); return Ok(()); } // Check if bundle exists if repo.bundle.as_os_str().is_empty() { tracing::error!("Repository {} has no bundle available", repo_id); - println!("Error: Repository {} has no bundle available", repo_id); + eprintln!("❌ Error: Repository {} has no bundle available.", repo_id); return Ok(()); } let path_str = match repo.path.as_os_str().to_str() { Some(s) => s, None => { - tracing::error!( - "Repository {} has a local path that is not valid UTF-8: {}", - repo_id, - repo.path.display() - ); - println!( - "Error: Repository {} has a local path that is not valid UTF-8", - repo_id - ); + eprintln!("❌ Error: Local path is not valid UTF-8."); return Ok(()); } }; @@ -185,64 +361,53 @@ pub async fn handle_repo_pull(repo_id: String) -> Result<()> { let bundle_str = match repo.bundle.as_os_str().to_str() { Some(s) => s, None => { - tracing::error!( - "Repository {} has a bundle path that is not valid UTF-8: {}", - repo_id, - repo.bundle.display() - ); - println!( - "Error: Repository {} has a bundle path that is not valid UTF-8", - repo_id - ); + eprintln!("❌ Error: Bundle path is not valid UTF-8."); return Ok(()); } }; - let result = pull_repo_from_bundle( - path_str, - bundle_str, - "master", - ); + let result = pull_repo_from_bundle(path_str, bundle_str, "master"); match result { Ok(()) => { tracing::info!("Repository {} fetched successfully from bundle", repo_id); println!("✅ Repository updated successfully!"); - println!(" Repository: {}", repo.p2p_description.name); - println!(" Path: {}", repo.path.display()); + println!(" Name: {}", repo.p2p_description.name); + println!(" Path: {}", repo.path.display()); } Err(e) => { tracing::error!("Failed to spawn fetch task: {}", e); - println!("Error: Failed to spawn fetch task: {}", e); + eprintln!("❌ Failed to update repository: {}", e); } } } Ok(None) => { tracing::error!("Repository {} not found in database", repo_id); - println!("Error: Repository {} not found", repo_id); + eprintln!("❌ Error: Repository {} not found.", repo_id); } Err(e) => { tracing::error!("Failed to query repository {}: {}", repo_id, e); - println!("Error: Failed to query repository: {}", e); + eprintln!("❌ Database error: {}", e); } } Ok(()) } pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { + println!("📥 Cloning repository {}...", repo_id); match storage::repo_model::load_repo_from_db(&repo_id).await { Ok(Some(mut repo)) => { // Check if bundle exists if repo.bundle.as_os_str().is_empty() || repo.bundle.to_string_lossy().is_empty() { tracing::error!("Repository {} has no bundle available for cloning", repo_id); - println!("Error: Repository {} has no bundle available", repo_id); + eprintln!("❌ Error: Repository {} has no bundle available.", repo_id); return Ok(()); } let bundle_path = repo.bundle.to_string_lossy().to_string(); if !std::path::Path::new(&bundle_path).exists() { tracing::error!("Bundle file not found at path: {}", bundle_path); - println!("Error: Bundle file not found at {}", bundle_path); + eprintln!("❌ Error: Bundle file not found at {}", bundle_path); return Ok(()); } @@ -256,10 +421,11 @@ pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { match restore_repo_from_bundle(&bundle_path, &output).await { Ok(_) => { tracing::info!("Repository {} cloned successfully to {}", repo_id, output); - println!("✅ Repository cloned successfully to {}", output); - println!(" Repository: {}", repo.p2p_description.name); - println!(" Creator: {}", repo.p2p_description.creator); - println!(" Description: {}", repo.p2p_description.description); + println!("✅ Repository cloned successfully!"); + println!(" Name: {}", repo.p2p_description.name); + println!(" Creator: {}", repo.p2p_description.creator); + println!(" Description: {}", repo.p2p_description.description); + println!(" Path: {}", output); // Read and save refs from the cloned repository match megaengine::git::git_repo::read_repo_refs(&output) { @@ -268,11 +434,10 @@ pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { // Save refs to the database match storage::ref_model::batch_save_refs(&repo_id, &refs).await { Ok(_) => { - tracing::info!( - "Refs saved to database for repository {}", - repo_id + println!( + " Refs: {} branches/tags imported", + refs.len() ); - println!(" Refs: {} branches/tags", refs.len()); } Err(e) => { tracing::warn!("Failed to save refs to database: {}", e); @@ -281,19 +446,14 @@ pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { } Err(e) => { tracing::warn!("Failed to read refs from cloned repository: {}", e); + println!(" ⚠️ Warning: Failed to read refs from cloned repo: {}", e); } } // Update repo path to the cloned location repo.path = PathBuf::from(&output); match storage::repo_model::save_repo_to_db(&repo).await { - Ok(_) => { - tracing::info!( - "Updated repo path to {} for repository {}", - output, - repo_id - ); - } + Ok(_) => {} Err(e) => { tracing::warn!("Failed to update repo path to database: {}", e); } @@ -301,17 +461,17 @@ pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { } Err(e) => { tracing::error!("Failed to clone repository: {}", e); - println!("Error: Failed to clone repository: {}", e); + eprintln!("❌ Failed to clone repository: {}", e); } } } Ok(None) => { tracing::error!("Repository {} not found in database", repo_id); - println!("Error: Repository {} not found", repo_id); + eprintln!("❌ Error: Repository {} not found.", repo_id); } Err(e) => { tracing::error!("Failed to query repository {}: {}", repo_id, e); - println!("Error: Failed to query repository: {}", e); + eprintln!("❌ Database error: {}", e); } } Ok(()) diff --git a/src/git/git_repo.rs b/src/git/git_repo.rs index bb9eaeb..88cd363 100644 --- a/src/git/git_repo.rs +++ b/src/git/git_repo.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use git2::{Repository, Sort}; +use git2::{BranchType, Repository, Sort}; pub fn repo_root_commit_bytes(path: &str) -> Result> { let repo = @@ -42,19 +42,18 @@ pub fn read_repo_refs(path: &str) -> Result Result Result { + let repo = + Repository::open(path).map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; + let head = repo + .head() + .map_err(|e| anyhow::anyhow!("failed to get HEAD: {}", e))?; + let commit = head + .peel_to_commit() + .map_err(|e| anyhow::anyhow!("failed to peel to commit: {}", e))?; + Ok(commit.time().seconds()) +} diff --git a/src/git/pack.rs b/src/git/pack.rs index 805f395..96f3c35 100644 --- a/src/git/pack.rs +++ b/src/git/pack.rs @@ -112,6 +112,9 @@ pub async fn restore_repo_from_bundle(bundle_path: &str, output_path: &str) -> R tokio::task::spawn_blocking(move || { // 使用 git clone 从 bundle 恢复仓库 + // 注意:从 bundle 克隆时,git clone 可能不会自动 checkout 到 HEAD, + // 特别是当 bundle 包含多个 heads 时。 + // 所以我们需要显式 clone,然后如果目录为空,尝试 checkout。 let output = Command::new("git") .arg("clone") .arg(&bundle_path) @@ -124,6 +127,23 @@ pub async fn restore_repo_from_bundle(bundle_path: &str, output_path: &str) -> R return Err(anyhow::anyhow!("git clone from bundle failed: {}", stderr)); } + // 尝试自动检出分支 (clone bundle 有时不会自动检出工作区) + // 尝试常见分支名,忽略错误(可能分支不存在) + let _ = Command::new("git") + .current_dir(&output_path) + .args(["checkout", "main"]) + .output(); + let _ = Command::new("git") + .current_dir(&output_path) + .args(["checkout", "master"]) + .output(); + + // 强制重置工作区到当前 HEAD,确保文件被检出 + let _ = Command::new("git") + .current_dir(&output_path) + .args(["reset", "--hard", "HEAD"]) + .output(); + Ok(()) }) .await @@ -201,8 +221,7 @@ pub fn pull_repo_from_bundle(repo_path: &str, bundle_path: &str, branch: &str) - return Err(anyhow::anyhow!("repository not found: {}", repo_path)); } - Repository::open(repo_path) - .map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; + Repository::open(repo_path).map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; // 构建分支引用名称,确保格式正确 let _ref_spec = if branch.starts_with("refs/") { diff --git a/src/gossip/message.rs b/src/gossip/message.rs index 17930f1..bf8978f 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -19,6 +19,33 @@ pub enum GossipMessage { NodeAnnouncement(NodeAnnouncement), /// 仓库公告 (库存公告) RepoAnnouncement(RepoAnnouncement), + /// P2P 聊天消息 + Chat(EncryptedChatMessage), + /// 聊天消息送达确认 + ChatAck(ChatAckMessage), +} + +/// 聊天消息 (加密) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncryptedChatMessage { + /// 真正的发送者 + pub sender_id: NodeId, + /// 目标接收者 + pub receiver_id: NodeId, + /// 消息 ID (用于去重) + pub msg_id: String, + /// 密文数据 (包含 ephemeral public key) + pub ciphertext: Vec, +} + +/// 聊天回执 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatAckMessage { + pub sender_id: NodeId, + pub target_id: NodeId, + pub msg_id: String, + pub timestamp: i64, + pub signature: String, } /// 节点公告 @@ -31,6 +58,12 @@ pub struct NodeAnnouncement { pub addresses: Vec, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Envelope { + pub payload: SignedMessage, + pub ttl: u8, +} + impl From for NodeAnnouncement { fn from(node: Node) -> Self { Self { @@ -60,7 +93,6 @@ pub struct SignedMessage { pub signature: String, } -#[allow(dead_code)] impl SignedMessage { pub fn new_node_sign_message(node: Node) -> Result { let message = GossipMessage::NodeAnnouncement(node.clone().into()); @@ -105,9 +137,33 @@ impl SignedMessage { Ok(sign_message) } + fn canonicalize_value(value: serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Object(map) => { + // Sort object keys to obtain a deterministic representation. + let mut entries: Vec<(String, serde_json::Value)> = map.into_iter().collect(); + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut new_map = serde_json::Map::new(); + for (k, v) in entries { + new_map.insert(k, Self::canonicalize_value(v)); + } + serde_json::Value::Object(new_map) + } + serde_json::Value::Array(vec) => { + serde_json::Value::Array(vec.into_iter().map(Self::canonicalize_value).collect()) + } + other => other, + } + } + pub fn self_hash(&self) -> Vec { let mut hasher = Sha256::new(); - let message_bytes = serde_json::to_vec(&self.message).unwrap_or_default(); + // Canonicalize JSON by recursively sorting object keys before serialization. + let message_value = + serde_json::to_value(&self.message).unwrap_or(serde_json::Value::Null); + let canonical_value = Self::canonicalize_value(message_value); + let message_bytes = serde_json::to_vec(&canonical_value).unwrap_or_default(); hasher.update(self.node_id.0.as_bytes()); hasher.update(&message_bytes); @@ -132,6 +188,8 @@ impl GossipMessage { match self { GossipMessage::NodeAnnouncement(_) => "node_announcement", GossipMessage::RepoAnnouncement(_) => "inventory_announcement", + GossipMessage::Chat(_) => "chat", + GossipMessage::ChatAck(_ack) => "chat_ack", } } @@ -140,6 +198,8 @@ impl GossipMessage { match self { GossipMessage::NodeAnnouncement(na) => &na.node_id, GossipMessage::RepoAnnouncement(ra) => &ra.node_id, + GossipMessage::Chat(c) => &c.sender_id, + GossipMessage::ChatAck(ack) => &ack.sender_id, } } } @@ -199,7 +259,9 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 1000, + size: 0, }; let repo = Repo::new( diff --git a/src/gossip/service.rs b/src/gossip/service.rs index ba70983..dc84378 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -1,4 +1,4 @@ -use crate::gossip::message::{GossipMessage, SignedMessage}; +use crate::gossip::message::{Envelope, GossipMessage, SignedMessage}; use crate::node::node::{Node, NodeInfo}; use crate::node::node_id::NodeId; use crate::repo::repo_manager::RepoManager; @@ -7,7 +7,6 @@ use crate::transport::quic::ConnectionManager; use anyhow::Result; use ed25519_dalek::Signature; use hex; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::TryInto; use std::sync::Arc; @@ -25,12 +24,6 @@ pub struct GossipService { seen: Arc>>, } -#[derive(Serialize, Deserialize, Clone, Debug)] -struct Envelope { - payload: SignedMessage, - ttl: u8, -} - impl GossipService { pub fn new( manager: Arc>, @@ -164,6 +157,17 @@ impl GossipService { } } + // Ensure outer signer identity matches the embedded payload sender identity. + // This prevents payload-level sender_id spoofing. + if signed.node_id != *signed.message.sender() { + tracing::error!( + "message sender mismatch: signed node {} != payload sender {}", + signed.node_id, + signed.message.sender() + ); + return Ok(()); + } + // process message (borrow the inner message to avoid moving) match &signed.message { GossipMessage::NodeAnnouncement(na) => { @@ -277,9 +281,11 @@ impl GossipService { // 有新的 refs 更新,清空 bundle 等待重新同步 tracing::info!( - "Detected ref updates for repo {} from node {}", + "Detected ref updates for repo {} from node {}. local refs: {:?}, remote refs: {:?}", &repo.repo_id, - ra.node_id + ra.node_id, + local_refs, + repo.refs ); // 删除旧的 bundle 文件 @@ -371,17 +377,34 @@ impl GossipService { } } } + GossipMessage::Chat(c) => { + if let Err(e) = crate::chat::service::process_incoming_chat( + c.clone(), + self.manager.clone(), + self.node.clone(), + ) + .await + { + tracing::error!("Error processing chat message: {}", e); + } + } + GossipMessage::ChatAck(ack) => { + if let Err(e) = crate::chat::service::process_ack( + ack.clone(), + self.manager.clone(), + self.node.clone(), + ) + .await + { + tracing::error!("Error processing chat ack: {}", e); + } + } } // forward if ttl > 0 if ttl > 0 { ttl -= 1; - #[derive(Serialize, Deserialize, Clone)] - struct Envelope2 { - payload: SignedMessage, - ttl: u8, - } - let fwd = Envelope2 { + let fwd = Envelope { payload: signed.clone(), ttl, }; diff --git a/src/identity/keypair.rs b/src/identity/keypair.rs index c1ca0ff..0b32e76 100644 --- a/src/identity/keypair.rs +++ b/src/identity/keypair.rs @@ -1,8 +1,15 @@ use anyhow::{anyhow, Result}; +use chacha20poly1305::{ + aead::{Aead, KeyInit}, + ChaCha20Poly1305, Nonce, +}; +use curve25519_dalek::{edwards::CompressedEdwardsY, montgomery::MontgomeryPoint, scalar::Scalar}; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use rand_core::OsRng; +use rand_core::RngCore; use serde::Deserialize; use serde::Serialize; +use sha2::{Digest, Sha256}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct KeyPair { @@ -50,6 +57,122 @@ impl KeyPair { self.verifying_key.verify(msg, sig).is_ok() } + /// Encrypt a message for a specific recipient (identified by their Ed25519 VerifyingKey) + /// Returns: Ephemeral_PK (32) + Nonce (12) + Ciphertext (N) + pub fn encrypt_to_node(&self, recipient_vk: &VerifyingKey, message: &[u8]) -> Result> { + // 1. Convert Recipient Ed25519 PK -> X25519 PK (Montgomery) + let recipient_ed_y = CompressedEdwardsY::from_slice(recipient_vk.as_bytes())?; + let recipient_ed_point = recipient_ed_y + .decompress() + .ok_or(anyhow!("Invalid Public Key Point"))?; + let recipient_mont_point = recipient_ed_point.to_montgomery(); + + // 2. Generate Ephemeral Keypair + let mut scalar_bytes = [0u8; 32]; + OsRng.fill_bytes(&mut scalar_bytes); + + // Clamp scalar bytes to align with X25519 scalar requirements. + scalar_bytes[0] &= 248; + scalar_bytes[31] &= 127; + scalar_bytes[31] |= 64; + + let ephemeral_scalar = Scalar::from_bits(scalar_bytes); + let ephemeral_point = MontgomeryPoint::mul_base(&ephemeral_scalar); + + // 3. Keep Ephemeral Public Key + let ephemeral_pk_bytes = ephemeral_point.to_bytes(); + + // 4. Calculate Shared Secret: ephemeral_secret * recipient_public + let shared_secret_point = ephemeral_scalar * recipient_mont_point; + let shared_secret_bytes = shared_secret_point.to_bytes(); + + // 5. Derive Encryption Key (Hash) + let mut hasher = Sha256::new(); + hasher.update(shared_secret_bytes); + hasher.update(ephemeral_pk_bytes); + hasher.update(recipient_mont_point.to_bytes()); + let key_hash = hasher.finalize(); + + let key = chacha20poly1305::Key::from_slice(&key_hash); + let cipher = ChaCha20Poly1305::new(key); + + // 6. Encrypt + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = chacha20poly1305::Nonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, message) + .map_err(|e| anyhow!("Encryption failed: {}", e))?; + + // 7. Pack: EphemeralPK (32) + Nonce (12) + Ciphertext + let mut result = Vec::with_capacity(32 + 12 + ciphertext.len()); + result.extend_from_slice(&ephemeral_pk_bytes); + result.extend_from_slice(nonce); + result.extend_from_slice(&ciphertext); + + Ok(result) + } + + /// Decrypt a message addressed to this keypair + pub fn decrypt_message(&self, payload: &[u8]) -> Result> { + if payload.len() < 32 + 12 { + return Err(anyhow!("Message too short")); + } + + let signing_key = self + .signing_key + .as_ref() + .ok_or(anyhow!("No private key available for decryption"))?; + + // 1. My Secret Key Conversion + let mut hasher = sha2::Sha512::new(); + hasher.update(signing_key.as_bytes()); + let h = hasher.finalize(); + + let mut clamped = [0u8; 32]; + clamped.copy_from_slice(&h[0..32]); + clamped[0] &= 248; + clamped[31] &= 127; + clamped[31] |= 64; + + let my_scalar = Scalar::from_bits(clamped); + + // 2. Parse Payload + let ephemeral_pk_bytes = &payload[0..32]; + let nonce_bytes = &payload[32..44]; + let ciphertext = &payload[44..]; + + let ephemeral_point = MontgomeryPoint(ephemeral_pk_bytes.try_into()?); + + // 3. Calculate Shared Secret: my_secret * ephemeral_public + let shared_secret_point = my_scalar * ephemeral_point; + let shared_secret_bytes = shared_secret_point.to_bytes(); + + // 4. Derive Key + let my_ed_y = CompressedEdwardsY::from_slice(self.verifying_key.as_bytes())?; + let my_ed_point = my_ed_y + .decompress() + .ok_or(anyhow!("Invalid My Public Key"))?; + let my_mont_point = my_ed_point.to_montgomery(); + + let mut hasher = Sha256::new(); + hasher.update(shared_secret_bytes); + hasher.update(ephemeral_pk_bytes); + hasher.update(my_mont_point.to_bytes()); + let key_hash = hasher.finalize(); + + let key = chacha20poly1305::Key::from_slice(&key_hash); + let cipher = ChaCha20Poly1305::new(key); + let nonce = Nonce::from_slice(nonce_bytes); + + // 5. Decrypt + let plaintext = cipher + .decrypt(nonce, ciphertext) + .map_err(|e| anyhow!("Decryption failed: {}", e))?; + + Ok(plaintext) + } + pub fn verifying_key_bytes(&self) -> [u8; 32] { *self.verifying_key.as_bytes() } @@ -91,7 +214,7 @@ mod tests { #[test] fn test_export_and_import_verifying_key() { let kp1 = KeyPair::generate().unwrap(); - let vk_bytes = kp1.verifying_key.as_bytes().clone(); + let vk_bytes = *kp1.verifying_key.as_bytes(); let kp2 = KeyPair::from_verifying_key_bytes(vk_bytes).unwrap(); let msg = b"verify test"; diff --git a/src/lib.rs b/src/lib.rs index ef483b6..d407846 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ pub mod bundle; +pub mod chat; pub mod git; pub mod gossip; pub mod identity; +pub mod mcp; pub mod node; pub mod repo; pub mod storage; diff --git a/src/main.rs b/src/main.rs index 0bdd3bc..e8271b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; mod cli; use cli::{handle_auth, handle_node, handle_repo}; +use megaengine::mcp::start_mcp_server; #[derive(Parser)] #[command(name = "megaengine")] @@ -33,6 +34,13 @@ enum Commands { #[command(subcommand)] action: RepoAction, }, + /// Chat P2P commands + Chat { + #[command(subcommand)] + action: crate::cli::chat::ChatCommand, + }, + /// Start MCP server (Stdio mode) + Mcp, } #[derive(Subcommand)] @@ -58,6 +66,14 @@ enum NodeAction { /// Bootstrap node address to connect to on startup (e.g., 127.0.0.1:9000) #[arg(long)] bootstrap_node: Option, + + /// Deprecated for node start: stdio MCP must run as a separate process via `megaengine mcp` + #[arg(long, default_value = "false")] + mcp: bool, + + /// Start MCP SSE server on the specified port (e.g., 3001) + #[arg(long)] + mcp_sse_port: Option, }, /// Print node id using stored keypair Id, @@ -104,6 +120,7 @@ async fn main() -> Result<()> { .with_env_filter(env_filter) .with_target(true) .with_level(true) + .with_writer(std::io::stderr) .init(); let cli = Cli::parse(); @@ -122,6 +139,12 @@ async fn main() -> Result<()> { Commands::Repo { action } => { handle_repo(action).await?; } + Commands::Chat { action } => { + crate::cli::handle_chat(action).await?; + } + Commands::Mcp => { + start_mcp_server().await?; + } } Ok(()) diff --git a/src/mcp/mcp_server.rs b/src/mcp/mcp_server.rs new file mode 100644 index 0000000..7269213 --- /dev/null +++ b/src/mcp/mcp_server.rs @@ -0,0 +1,397 @@ +use crate::{git::pack, storage}; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::io::{self, BufRead, BufReader, Write}; + +// --- 1. 定义符合 JSON-RPC 2.0 标准的结构 --- + +#[derive(Deserialize, Debug)] +struct JsonRpcRequest { + method: String, + params: Option, + id: Option, +} + +#[derive(Serialize, Debug)] +struct JsonRpcResponse { + jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + id: Option, +} + +#[derive(Serialize, Debug)] +struct JsonRpcError { + code: i32, + message: String, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, +} + +/// MCP Server implementation for repository operations +pub struct RepoMcpServer; + +impl RepoMcpServer { + pub fn get_tools() -> Vec { + vec![ + json!({ + "name": "list_repos", + "description": "List all repositories with their details and refs", + "inputSchema": { + "type": "object", + "properties": {}, + "required": [] + } + }), + json!({ + "name": "get_repo_details", + "description": "Get detailed information about a specific repository", + "inputSchema": { + "type": "object", + "properties": { + "repo_id": { + "type": "string", + "description": "The ID of the repository" + } + }, + "required": ["repo_id"] + } + }), + json!({ + "name": "clone_repo", + "description": "Clone a repository from its bundle to a local directory", + "inputSchema": { + "type": "object", + "properties": { + "repo_id": { + "type": "string", + "description": "The ID of the repository to clone" + }, + "output_path": { + "type": "string", + "description": "The local path where the repository should be cloned" + } + }, + "required": ["repo_id", "output_path"] + } + }), + ] + } + + pub async fn execute_tool(name: &str, args: Value) -> Result { + match name { + "list_repos" => Self::list_repos().await, + "get_repo_details" => { + let repo_id = args + .get("repo_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing repo_id parameter"))?; + Self::get_repo_details(repo_id).await + } + "clone_repo" => { + let repo_id = args + .get("repo_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing repo_id parameter"))?; + let output_path = args + .get("output_path") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing output_path parameter"))?; + Self::clone_repo(repo_id, output_path).await + } + _ => Err(anyhow::anyhow!("Unknown tool: {}", name)), + } + } + + async fn list_repos() -> Result { + match storage::repo_model::list_repos().await { + Ok(repos) => { + let repo_list: Vec = repos + .iter() + .map(|repo| { + let mut repo_info = json!({ + "repo_id": repo.repo_id, + "name": repo.p2p_description.name, + "creator": repo.p2p_description.creator, + "language": repo.p2p_description.language, + "size": repo.p2p_description.size, + "description": repo.p2p_description.description, + "path": repo.path.display().to_string(), + "bundle": repo.bundle.display().to_string(), + "latest_commit_at": repo.p2p_description.latest_commit_at, + }); + + // 恢复 refs 处理逻辑 + if !repo.bundle.as_os_str().is_empty() { + if let Ok(local_refs) = + pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) + { + let refs: Vec = local_refs + .iter() + .map(|(ref_name, commit)| { + json!({ + "name": ref_name, + "commit": commit + }) + }) + .collect(); + repo_info["refs"] = Value::Array(refs); + + let mut has_updates = false; + if !repo.path.as_os_str().is_empty() && repo.path.exists() { + if let Ok(current_refs) = crate::git::git_repo::read_repo_refs( + repo.path.to_str().unwrap_or(""), + ) { + has_updates = current_refs != local_refs; + } + } + repo_info["has_updates"] = Value::Bool(has_updates); + } + } + + repo_info + }) + .collect(); + Ok(json!({ + "content": [{ + "type": "text", + "text": serde_json::to_string(&repo_list)? + }] + })) + } + Err(e) => Err(e), + } + } + + async fn get_repo_details(repo_id: &str) -> Result { + match storage::repo_model::load_repo_from_db(repo_id).await { + Ok(Some(repo)) => { + let mut repo_info = json!({ + "repo_id": repo.repo_id, + "name": repo.p2p_description.name, + "creator": repo.p2p_description.creator, + "description": repo.p2p_description.description, + "path": repo.path.display().to_string(), + "bundle": repo.bundle.display().to_string(), + "latest_commit_at": repo.p2p_description.latest_commit_at, + }); + + // Check for updates if this is a local repo + if !repo.path.as_os_str().is_empty() && repo.path.exists() { + if let Ok(current_refs) = + crate::git::git_repo::read_repo_refs(repo.path.to_str().unwrap_or("")) + { + if let Ok(local_refs) = + pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) + { + repo_info["has_updates"] = Value::Bool(current_refs != local_refs); + + let local_ref_list: Vec = local_refs + .iter() + .map(|(ref_name, commit)| { + json!({ + "name": ref_name, + "commit": commit + }) + }) + .collect(); + repo_info["local_refs"] = Value::Array(local_ref_list); + + let current_ref_list: Vec = current_refs + .iter() + .map(|(ref_name, commit)| { + json!({ + "name": ref_name, + "commit": commit + }) + }) + .collect(); + repo_info["current_refs"] = Value::Array(current_ref_list); + } + } + } + Ok(json!({ + "content": [{ + "type": "text", + "text": serde_json::to_string_pretty(&repo_info)? + }] + })) + } + Ok(None) => Err(anyhow::anyhow!("Repository not found")), + Err(e) => Err(e), + } + } + + async fn clone_repo(repo_id: &str, output: &str) -> Result { + use std::path::PathBuf; + match storage::repo_model::load_repo_from_db(repo_id).await { + Ok(Some(mut repo)) => { + let bundle_path = repo.bundle.to_string_lossy().to_string(); + if bundle_path.is_empty() || !std::path::Path::new(&bundle_path).exists() { + return Err(anyhow::anyhow!("Bundle file not found for repository")); + } + + pack::restore_repo_from_bundle(&bundle_path, output).await?; + + // Read and save refs from the cloned repository + if let Ok(refs) = crate::git::git_repo::read_repo_refs(output) { + let _ = storage::ref_model::batch_save_refs(repo_id, &refs).await; + } + + // Update repo path to the cloned location + repo.path = PathBuf::from(output); + let _ = storage::repo_model::save_repo_to_db(&repo).await; + + Ok(json!({ + "content": [{ + "type": "text", + "text": format!("Successfully cloned repository {} to {}", repo_id, output) + }] + })) + } + Ok(None) => Err(anyhow::anyhow!("Repository not found")), + Err(e) => Err(e), + } + } +} + +pub async fn start_mcp_server() -> Result<()> { + eprintln!("MCP Repository Server started"); + + let stdin = io::stdin(); + let mut stdout = io::stdout(); + let mut reader = BufReader::new(stdin.lock()); + + let mut line = String::new(); + while reader.read_line(&mut line)? > 0 { + if line.trim().is_empty() { + line.clear(); + continue; + } + + // 1. 解析请求 + let req: JsonRpcRequest = match serde_json::from_str(&line) { + Ok(r) => r, + Err(e) => { + eprintln!("Failed to parse JSON: {}", e); + line.clear(); + continue; + } + }; + + eprintln!("Received method: {}", req.method); + + // 2. 处理请求并获取 Result 或 Error + // 注意:这里返回元组 (Option, Option) + let (result, error) = match req.method.as_str() { + // A. 初始化握手 (必须响应 initialize) + "initialize" => ( + Some(json!({ + "protocolVersion": "2024-11-05", + "capabilities": { "tools": {} }, + "serverInfo": { + "name": "megaengine-repo-mcp", + "version": "1.0" + } + })), + None, + ), + + // B. 初始化完成通知 (不需要响应) + "notifications/initialized" => { + eprintln!("Client initialized."); + line.clear(); + continue; + } + + // C. 列出工具 + "tools/list" => { + let tools = RepoMcpServer::get_tools(); + (Some(json!({ "tools": tools })), None) + } + + // D. 调用工具 + "tools/call" => handle_tool_call(&req.params).await, + + // E. 心跳 + "ping" => (Some(json!({})), None), + + // F. 未知方法 + _ => ( + None, + Some(JsonRpcError { + code: -32601, + message: format!("Method not found: {}", req.method), + data: None, + }), + ), + }; + + // 3. 构建并发送响应 + if let Some(req_id) = req.id { + let resp = JsonRpcResponse { + jsonrpc: "2.0".to_string(), + result, + error, + id: Some(req_id), + }; + + let resp_str = serde_json::to_string(&resp)?; + writeln!(stdout, "{}", resp_str)?; + stdout.flush()?; + } + + line.clear(); + } + + Ok(()) +} + +// 辅助函数:处理工具调用 +async fn handle_tool_call(params: &Option) -> (Option, Option) { + let params = match params { + Some(p) => p, + None => { + return ( + None, + Some(JsonRpcError { + code: -32602, + message: "Missing params".into(), + data: None, + }), + ) + } + }; + + let name = match params.get("name").and_then(|n| n.as_str()) { + Some(n) => n, + None => { + return ( + None, + Some(JsonRpcError { + code: -32602, + message: "Missing tool name".into(), + data: None, + }), + ) + } + }; + + let args = params.get("arguments").cloned().unwrap_or(json!({})); + + // 调用业务逻辑 + match RepoMcpServer::execute_tool(name, args).await { + Ok(res) => (Some(res), None), // 这里的 res 必须符合 CallToolResult 结构 + Err(e) => ( + None, + Some(JsonRpcError { + code: -32000, // 应用级错误 + message: e.to_string(), + data: None, + }), + ), + } +} diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs new file mode 100644 index 0000000..25385ff --- /dev/null +++ b/src/mcp/mod.rs @@ -0,0 +1,5 @@ +pub mod mcp_server; +pub mod sse_server; + +pub use mcp_server::start_mcp_server; +pub use sse_server::start_sse_server; diff --git a/src/mcp/sse_server.rs b/src/mcp/sse_server.rs new file mode 100644 index 0000000..f080d5c --- /dev/null +++ b/src/mcp/sse_server.rs @@ -0,0 +1,254 @@ +use crate::mcp::mcp_server::RepoMcpServer; +use axum::{ + extract::{Query, State}, + response::{ + sse::{Event, Sse}, + IntoResponse, + }, + routing::{get, post}, + http::Method, + Json, Router, +}; +use futures::stream::Stream; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; +use tokio::sync::{mpsc, RwLock}; +use tower_http::cors::{Any, CorsLayer}; +use uuid::Uuid; + +// App state to hold active sessions +struct AppState { + sessions: RwLock>>>, +} + +#[derive(Deserialize)] +struct SessionParam { + session_id: String, +} + +struct SessionCleanup { + state: Arc, + session_id: String, +} + +impl SessionCleanup { + fn new(state: Arc, session_id: String) -> Self { + Self { state, session_id } + } +} + +impl Drop for SessionCleanup { + fn drop(&mut self) { + let state = Arc::clone(&self.state); + let session_id = self.session_id.clone(); + + tokio::spawn(async move { + let removed = state.sessions.write().await.remove(&session_id); + if removed.is_some() { + tracing::info!("SSE session cleaned up: {}", session_id); + } + }); + } +} + +pub async fn start_sse_server(addr: SocketAddr) -> anyhow::Result<()> { + let state = Arc::new(AppState { + sessions: RwLock::new(HashMap::new()), + }); + + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods([Method::GET, Method::POST]) + .allow_headers(Any); + + let app = Router::new() + .route("/sse", get(sse_handler)) + .route("/messages", post(message_handler)) + .with_state(state) + .layer(cors); + + tracing::info!("MCP SSE Server listening on {}", addr); + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn sse_handler( + State(state): State>, +) -> Sse>> { + let session_id = Uuid::new_v4().to_string(); + let (tx, rx) = mpsc::unbounded_channel(); + + // Store the sender + state + .sessions + .write() + .await + .insert(session_id.clone(), tx.clone()); + + let stream = futures::stream::unfold( + (rx, SessionCleanup::new(state.clone(), session_id.clone())), + |(mut rx, cleanup)| async move { rx.recv().await.map(|event| (event, (rx, cleanup))) }, + ); + + // Send the endpoint event immediately + let endpoint_url = format!("/messages?session_id={}", session_id); + let _ = tx.send(Ok(Event::default().event("endpoint").data(endpoint_url))); + + tracing::info!("New SSE session connected: {}", session_id); + + Sse::new(stream) + .keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_secs(15))) +} + +async fn message_handler( + State(state): State>, + Query(params): Query, + Json(request): Json, +) -> impl IntoResponse { + let session_id = params.session_id; + + let tx = { + let sessions = state.sessions.read().await; + sessions.get(&session_id).cloned() + }; + + if let Some(tx) = tx { + // Handle the MCP request (JSON-RPC) + // We spawn a task to process it so we don't block + tokio::spawn(async move { + if let Some(method) = request.get("method").and_then(|v| v.as_str()) { + let response = match method { + "initialize" => Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": "megaengine", + "version": "0.1.0" + } + } + })), + "notifications/initialized" => { + // Client initialized, no response needed for notification + None + } + "ping" => Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": {} + })), + "tools/list" => { + let tools = RepoMcpServer::get_tools(); + Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": { + "tools": tools + } + })) + } + "tools/call" => { + if let Some(params) = request.get("params") { + let name = params.get("name").and_then(|v| v.as_str()); + let args = params.get("arguments").cloned().unwrap_or(json!({})); + + if let Some(name) = name { + match RepoMcpServer::execute_tool(name, args).await { + Ok(result_value) => Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": result_value + })), + Err(e) => Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": { + "content": [{ + "type": "text", + "text": e.to_string() + }], + "isError": true + } + })), + } + } else { + Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32602, + "message": "Missing 'name' in params" + } + })) + } + } else { + Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32602, + "message": "Missing 'params'" + } + })) + } + } + // For unknown methods, reply only when this is a request (has id). + _ => { + if request.get("id").is_some() { + Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32601, + "message": "Method not found" + } + })) + } else { + None + } + } + }; + + if let Some(response) = response { + if let Ok(data) = serde_json::to_string(&response) { + if tx + .send(Ok(Event::default().event("message").data(data))) + .is_err() + { + state.sessions.write().await.remove(&session_id); + } + } + } + } else { + tracing::warn!("Received invalid JSON-RPC request: missing method"); + let error_response = json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32600, + "message": "Invalid Request: Method missing" + } + }); + if let Ok(data) = serde_json::to_string(&error_response) { + if tx + .send(Ok(Event::default().event("message").data(data))) + .is_err() + { + state.sessions.write().await.remove(&session_id); + } + } + } + }); + + axum::http::StatusCode::ACCEPTED + } else { + axum::http::StatusCode::NOT_FOUND + } +} diff --git a/src/node/mod.rs b/src/node/mod.rs index fd5c9b9..295c983 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -2,4 +2,3 @@ pub mod node; pub mod node_addr; pub mod node_id; -pub mod node_manager; diff --git a/src/node/node.rs b/src/node/node.rs index 665298c..67fea1a 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -227,12 +227,12 @@ mod tests { // Test expiration logic std::thread::sleep(Duration::from_secs(2)); // Sleep for 2 seconds to test expiration - assert_eq!(node_routing.expired(), false); // Not expired if TTL is 24 hours + assert!(!node_routing.expired()); // Not expired if TTL is 24 hours // Manually expire the node and check node_routing.ttl = Duration::from_secs(1); // Set TTL to 1 second std::thread::sleep(Duration::from_secs(2)); // Sleep for 2 seconds to make the node expire - assert_eq!(node_routing.expired(), true); // Should be expired now + assert!(node_routing.expired()); // Should be expired now } // Test the `NodeType` enum diff --git a/src/node/node_id.rs b/src/node/node_id.rs index 87d21b0..3ed67a6 100644 --- a/src/node/node_id.rs +++ b/src/node/node_id.rs @@ -124,7 +124,7 @@ mod tests { #[test] fn test_valid_from_string() -> Result<()> { let node_id_str = "did:key:z2DXbAovGq5vNKpXVFyrhVLppMdUCmV1hCNjbUydLMEWasE"; - let node_id = NodeId::from_string(&node_id_str)?; + let node_id = NodeId::from_string(node_id_str)?; assert_eq!(node_id.0, node_id_str); Ok(()) } diff --git a/src/node/node_manager.rs b/src/node/node_manager.rs deleted file mode 100644 index 65ee83f..0000000 --- a/src/node/node_manager.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::node::{ - node::{Node, NodeRouting}, - node_id::NodeId, -}; -use std::collections::HashMap; - -#[derive(Default)] -pub struct NodeManager { - pub nodes: HashMap, -} - -impl NodeManager { - pub fn new() -> Self { - Self { - nodes: HashMap::new(), - } - } - - pub async fn insert_node(&mut self, node: &Node) { - let routing = NodeRouting::new(node.node_id().clone(), node.addresses().to_vec()); - self.nodes.insert(node.node_id().clone(), routing); - - // 持久化 NodeInfo - let _ = crate::storage::node_model::save_node_info_to_db(&node.info).await; - } - - pub fn mark_alive(&mut self, node_id: &NodeId) { - if let Some(n) = self.nodes.get_mut(node_id) { - n.refresh(); - } - } - - pub fn cleanup_expired(&mut self) { - self.nodes.retain(|_, v| !v.expired()); - } - - pub fn get_node(&self, node_id: &NodeId) -> Option<&NodeRouting> { - self.nodes.get(node_id) - } - - pub fn routing_print(&self) { - println!("Node routing table ({} entries):", self.nodes.len()); - for (id, info) in &self.nodes { - println!(" {:?} -> {:?}", id, info.addresses); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::identity::keypair::KeyPair; - use crate::node::node::NodeType; - use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - - fn create_sample_node() -> Node { - let keypair = &KeyPair::generate().unwrap(); - let node_id = NodeId::from_keypair(keypair); - let alias = "Test Node"; - let addresses = vec![SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - 8080, - )]; - let node_type = NodeType::Normal; - - Node::new(node_id, alias, addresses, node_type, keypair.clone()) - } - - #[tokio::test] - async fn test_node_manager_insert_node() { - let mut manager = NodeManager::new(); - let node = create_sample_node(); - let node_id = node.node_id().clone(); - - // Insert the node - manager.insert_node(&node).await; - - // Assert that the node is in the manager - assert_eq!(manager.nodes.len(), 1); - let node_routing = manager.get_node(&node.node_id()); - assert!(node_routing.is_some()); - assert_eq!(node_routing.unwrap().node_id, *node.node_id()); - - // Cleanup: Remove from database - let _ = crate::storage::node_model::delete_node_from_db(&node_id.to_string()).await; - } - - #[tokio::test] - async fn test_node_manager_mark_alive() { - let mut manager = NodeManager::new(); - let node = create_sample_node(); - let node_id = node.node_id().clone(); - - // Insert the node - manager.insert_node(&node).await; - - // Get the initial last_seen time - let initial_last_seen = manager.get_node(&node.node_id()).unwrap().last_seen; - - // Mark the node as alive (refresh) - manager.mark_alive(&node.node_id()); - - // Assert that the last_seen time was refreshed - let refreshed_last_seen = manager.get_node(&node.node_id()).unwrap().last_seen; - assert_ne!(initial_last_seen, refreshed_last_seen); - - // Cleanup: Remove from database - let _ = crate::storage::node_model::delete_node_from_db(&node_id.to_string()).await; - } - - #[tokio::test] - async fn test_node_manager_cleanup_expired() { - let mut manager = NodeManager::new(); - let node = create_sample_node(); - let node_id = node.node_id().clone(); - - manager.insert_node(&node).await; - assert_eq!(manager.nodes.len(), 1); - - manager.nodes.get_mut(&node.node_id()).unwrap().ttl = std::time::Duration::from_secs(1); - std::thread::sleep(std::time::Duration::from_secs(2)); - manager.cleanup_expired(); - assert_eq!(manager.nodes.len(), 0); - - // Cleanup: Remove from database - let _ = crate::storage::node_model::delete_node_from_db(&node_id.to_string()).await; - } - - #[tokio::test] - async fn test_node_manager_routing_print() { - let mut manager = NodeManager::new(); - let node1 = create_sample_node(); - let node2 = create_sample_node(); - let node_id_1 = node1.node_id().clone(); - let node_id_2 = node2.node_id().clone(); - - manager.insert_node(&node1).await; - manager.insert_node(&node2).await; - - let _ = std::panic::catch_unwind(|| { - manager.routing_print(); - }); - - // Cleanup: Remove from database - let _ = crate::storage::node_model::delete_node_from_db(&node_id_1.to_string()).await; - let _ = crate::storage::node_model::delete_node_from_db(&node_id_2.to_string()).await; - } -} diff --git a/src/repo/repo.rs b/src/repo/repo.rs index fc5ab66..64f60f6 100644 --- a/src/repo/repo.rs +++ b/src/repo/repo.rs @@ -8,7 +8,9 @@ pub struct P2PDescription { pub creator: String, pub name: String, pub description: String, - pub timestamp: i64, + pub language: String, + pub latest_commit_at: i64, + pub size: u64, } /// P2P 仓库 @@ -79,7 +81,9 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 1000, + size: 0, }; let repo = Repo::new( @@ -98,7 +102,9 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 1000, + size: 0, }; let mut repo = Repo::new( diff --git a/src/repo/repo_manager.rs b/src/repo/repo_manager.rs index 29c867c..c125cfe 100644 --- a/src/repo/repo_manager.rs +++ b/src/repo/repo_manager.rs @@ -97,7 +97,9 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 2000, + size: 0, }; let repo = Repo::new(repo_id.to_string(), desc, PathBuf::from("/tmp/test-repo")); @@ -135,7 +137,10 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo-persist".to_string(), description: "A test repository with persistence".to_string(), - timestamp: 2000, + language: "Rust".to_string(), + + latest_commit_at: 2000, + size: 0, }; let repo = Repo::new( diff --git a/src/storage/chat_message.rs b/src/storage/chat_message.rs new file mode 100644 index 0000000..64c146d --- /dev/null +++ b/src/storage/chat_message.rs @@ -0,0 +1,67 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "String(None)")] +pub enum MessageStatus { + #[sea_orm(string_value = "Sending")] + Sending, + #[sea_orm(string_value = "Sent")] + Sent, + #[sea_orm(string_value = "Delivered")] + Delivered, + #[sea_orm(string_value = "Failed")] + Failed, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "chat_messages")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: String, // UUID + pub from: String, // Sender NodeId + pub to: String, // Receiver NodeId + pub content: String, // Plaintext content (local storage is trusted for now) + pub created_at: i64, // Timestamp + pub status: MessageStatus, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +use anyhow::Result; +use sea_orm::{ActiveModelTrait, Set}; + +pub async fn save_message( + id: String, + from: String, + to: String, + content: String, + created_at: i64, + status: MessageStatus, +) -> Result<()> { + let db = crate::storage::get_db_conn().await?; + let model = ActiveModel { + id: Set(id), + from: Set(from), + to: Set(to), + content: Set(content), + created_at: Set(created_at), + status: Set(status), + }; + model.insert(&db).await?; + Ok(()) +} + +pub async fn update_message_status(msg_id: &str, status: MessageStatus) -> Result<()> { + let db = crate::storage::get_db_conn().await?; + let msg = Entity::find_by_id(msg_id).one(&db).await?; + if let Some(m) = msg { + let mut active: ActiveModel = m.into(); + active.status = Set(status); + active.update(&db).await?; + } + Ok(()) +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index f576987..fb9f841 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,9 +1,12 @@ +pub mod chat_message; pub mod node_model; pub mod ref_model; pub mod repo_model; -use anyhow::Result; -use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection}; +use anyhow::{anyhow, Result}; +use sea_orm::{ + ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbBackend, Statement, +}; use std::fs; use std::path::PathBuf; use std::time::Duration; @@ -74,89 +77,422 @@ pub fn db_path() -> PathBuf { p } -/// 初始化数据库连接并创建表 -pub async fn init_db() -> Result { - static DB: OnceCell = OnceCell::const_new(); +async fn execute_sql_ignore_duplicate_column(db: &DatabaseConnection, sql: &str) -> Result<()> { + match db.execute_unprepared(sql).await { + Ok(_) => Ok(()), + Err(err) => { + let msg = err.to_string().to_lowercase(); + if msg.contains("duplicate column name") { + return Ok(()); + } + Err(err.into()) + } + } +} + +fn escape_sqlite_literal(value: &str) -> String { + value.replace('\'', "''") +} + +async fn sqlite_query_one_i64(db: &DatabaseConnection, sql: String) -> Result { + let row = db + .query_one(Statement::from_string(DbBackend::Sqlite, sql)) + .await? + .ok_or_else(|| anyhow!("sqlite query returned no rows"))?; + Ok(row.try_get_by_index(0)?) +} + +async fn sqlite_query_one_string_opt( + db: &DatabaseConnection, + sql: String, +) -> Result> { + let Some(row) = db + .query_one(Statement::from_string(DbBackend::Sqlite, sql)) + .await? + else { + return Ok(None); + }; + + let value: Option = row.try_get_by_index(0)?; + Ok(value) +} + +async fn sqlite_has_column(db: &DatabaseConnection, table: &str, column: &str) -> Result { + let sql = format!( + "SELECT COUNT(*) FROM pragma_table_info('{}') WHERE name = '{}'", + escape_sqlite_literal(table), + escape_sqlite_literal(column) + ); + Ok(sqlite_query_one_i64(db, sql).await? > 0) +} - // 如果已经初始化,直接返回 clone - if let Some(db) = DB.get() { - return Ok(db.clone()); +async fn migrate_repos_table(db: &DatabaseConnection) -> Result<()> { + execute_sql_ignore_duplicate_column( + db, + "ALTER TABLE repos ADD COLUMN language TEXT NOT NULL DEFAULT ''", + ) + .await?; + execute_sql_ignore_duplicate_column( + db, + "ALTER TABLE repos ADD COLUMN size INTEGER NOT NULL DEFAULT 0", + ) + .await?; + execute_sql_ignore_duplicate_column( + db, + "ALTER TABLE repos ADD COLUMN latest_commit_at INTEGER NOT NULL DEFAULT 0", + ) + .await?; + + if repos_table_needs_rebuild(db).await? { + rebuild_repos_table(db).await?; } - // 延迟初始化并缓存全局连接(仅第一次会执行创建表操作) - let db_conn = DB - .get_or_init(|| async { - let db_path = db_path(); + Ok(()) +} - // 确保目录存在 - if let Some(parent) = db_path.parent() { - fs::create_dir_all(parent).ok(); - } +async fn repos_table_needs_rebuild(db: &DatabaseConnection) -> Result { + // Legacy schema had a `timestamp` column that can block inserts now that + // repo writes no longer set it. Rebuild to canonical schema when present. + sqlite_has_column(db, "repos", "timestamp").await +} - // 使用合适的 SQLite URL 格式 - let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); - - let mut opt = ConnectOptions::new(db_url); - opt.max_connections(5) - .min_connections(1) - .connect_timeout(Duration::from_secs(8)); - - let db = Database::connect(opt) - .await - .expect("failed to connect to db"); - - // 运行迁移或创建表(只在初始化时执行) - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS repos ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - creator TEXT NOT NULL, - description TEXT NOT NULL, - timestamp INTEGER NOT NULL, - path TEXT NOT NULL, - bundle TEXT NOT NULL DEFAULT '', - is_external INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - )", - ) - .await; - - // 节点表 - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS nodes ( - id TEXT PRIMARY KEY, - alias TEXT NOT NULL, - addresses TEXT NOT NULL, - node_type INTEGER NOT NULL, - version INTEGER NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - )", - ) - .await; - - // Refs 表:存储分支和标签的最新 commit - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS refs ( - id TEXT PRIMARY KEY, - repo_id TEXT NOT NULL, - ref_name TEXT NOT NULL, - commit_hash TEXT NOT NULL, - updated_at INTEGER NOT NULL, - UNIQUE(repo_id, ref_name) ON CONFLICT REPLACE - )", - ) - .await; - - db - }) +async fn rebuild_repos_table(db: &DatabaseConnection) -> Result<()> { + let has_language = sqlite_has_column(db, "repos", "language").await?; + let has_size = sqlite_has_column(db, "repos", "size").await?; + let has_latest_commit_at = sqlite_has_column(db, "repos", "latest_commit_at").await?; + let has_bundle = sqlite_has_column(db, "repos", "bundle").await?; + let has_is_external = sqlite_has_column(db, "repos", "is_external").await?; + let has_created_at = sqlite_has_column(db, "repos", "created_at").await?; + let has_updated_at = sqlite_has_column(db, "repos", "updated_at").await?; + let has_timestamp = sqlite_has_column(db, "repos", "timestamp").await?; + + let now_expr = "CAST(strftime('%s','now') AS INTEGER)"; + + let language_expr = if has_language { + "COALESCE(language, '')" + } else { + "''" + }; + + let size_expr = if has_size { "COALESCE(size, 0)" } else { "0" }; + + let latest_commit_expr = if has_latest_commit_at { + "COALESCE(latest_commit_at, 0)" + } else if has_timestamp { + "COALESCE(timestamp, 0)" + } else { + "0" + }; + + let bundle_expr = if has_bundle { + "COALESCE(bundle, '')" + } else { + "''" + }; + + let is_external_expr = if has_is_external { + "COALESCE(is_external, 0)" + } else { + "0" + }; + + let created_expr = if has_created_at { + format!("COALESCE(created_at, {now_expr})") + } else if has_timestamp { + format!("COALESCE(timestamp, {now_expr})") + } else { + now_expr.to_string() + }; + + let updated_expr = if has_updated_at { + format!("COALESCE(updated_at, {now_expr})") + } else if has_created_at { + format!("COALESCE(created_at, {now_expr})") + } else if has_timestamp { + format!("COALESCE(timestamp, {now_expr})") + } else { + now_expr.to_string() + }; + + let create_sql = "\ + CREATE TABLE repos_new (\ + id TEXT PRIMARY KEY,\ + name TEXT NOT NULL,\ + creator TEXT NOT NULL,\ + description TEXT NOT NULL,\ + language TEXT NOT NULL DEFAULT '',\ + size INTEGER NOT NULL DEFAULT 0,\ + latest_commit_at INTEGER NOT NULL DEFAULT 0,\ + path TEXT NOT NULL,\ + bundle TEXT NOT NULL DEFAULT '',\ + is_external INTEGER NOT NULL DEFAULT 0,\ + created_at INTEGER NOT NULL,\ + updated_at INTEGER NOT NULL\ + );"; + + let insert_sql = format!( + "\ + INSERT OR REPLACE INTO repos_new (\ + id,\ + name,\ + creator,\ + description,\ + language,\ + size,\ + latest_commit_at,\ + path,\ + bundle,\ + is_external,\ + created_at,\ + updated_at\ + )\ + SELECT\ + id,\ + name,\ + creator,\ + description,\ + {language_expr},\ + {size_expr},\ + {latest_commit_expr},\ + path,\ + {bundle_expr},\ + {is_external_expr},\ + {created_expr},\ + {updated_expr}\ + FROM repos\ + WHERE id IS NOT NULL\ + AND name IS NOT NULL\ + AND creator IS NOT NULL\ + AND description IS NOT NULL\ + AND path IS NOT NULL;" + ); + + let drop_sql = "DROP TABLE repos;"; + let rename_sql = "ALTER TABLE repos_new RENAME TO repos;"; + + let txn = db.begin().await?; + + txn.execute_unprepared(create_sql).await?; + txn.execute_unprepared(&insert_sql).await?; + txn.execute_unprepared(drop_sql).await?; + txn.execute_unprepared(rename_sql).await?; + + txn.commit().await?; + + Ok(()) +} + +async fn refs_table_needs_rebuild(db: &DatabaseConnection) -> Result { + let pk_sql = format!( + "SELECT group_concat(name, ',') FROM (\ + SELECT name FROM pragma_table_info('{}') WHERE pk > 0 ORDER BY pk\ + )", + escape_sqlite_literal("refs") + ); + let pk = sqlite_query_one_string_opt(db, pk_sql).await?; + let has_created_at = sqlite_has_column(db, "refs", "created_at").await?; + Ok(pk.as_deref() != Some("repo_id,ref_name") || !has_created_at) +} + +async fn rebuild_refs_table(db: &DatabaseConnection) -> Result<()> { + let refs_has_created_at = sqlite_has_column(db, "refs", "created_at").await?; + let refs_has_updated_at = sqlite_has_column(db, "refs", "updated_at").await?; + + let created_expr = if refs_has_created_at { + "COALESCE(created_at, CAST(strftime('%s','now') AS INTEGER))" + } else if refs_has_updated_at { + "COALESCE(updated_at, CAST(strftime('%s','now') AS INTEGER))" + } else { + "CAST(strftime('%s','now') AS INTEGER)" + }; + + let updated_expr = if refs_has_updated_at { + "COALESCE(updated_at, CAST(strftime('%s','now') AS INTEGER))" + } else if refs_has_created_at { + "COALESCE(created_at, CAST(strftime('%s','now') AS INTEGER))" + } else { + "CAST(strftime('%s','now') AS INTEGER)" + }; + + // Execute the migration statements one-by-one within a transaction + let txn = db.begin().await?; + + txn.execute(Statement::from_string( + DbBackend::Sqlite, + "CREATE TABLE refs_new ( + repo_id TEXT NOT NULL, + ref_name TEXT NOT NULL, + commit_hash TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (repo_id, ref_name) + )" + .to_owned(), + )) + .await?; + + let insert_sql = format!( + "INSERT OR REPLACE INTO refs_new (repo_id, ref_name, commit_hash, created_at, updated_at) + SELECT repo_id, ref_name, commit_hash, {created_expr}, {updated_expr} + FROM refs + WHERE repo_id IS NOT NULL AND ref_name IS NOT NULL" + ); + + txn.execute(Statement::from_string( + DbBackend::Sqlite, + insert_sql, + )) + .await?; + + txn.execute(Statement::from_string( + DbBackend::Sqlite, + "DROP TABLE refs".to_owned(), + )) + .await?; + + txn.execute(Statement::from_string( + DbBackend::Sqlite, + "ALTER TABLE refs_new RENAME TO refs".to_owned(), + )) + .await?; + + txn.commit().await?; + + Ok(()) +} + +async fn migrate_refs_table(db: &DatabaseConnection) -> Result<()> { + if !refs_table_needs_rebuild(db).await? { + return Ok(()); + } + rebuild_refs_table(db).await +} + +async fn ensure_schema(db: &DatabaseConnection) -> Result<()> { + db.execute_unprepared( + "CREATE TABLE IF NOT EXISTS repos ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + creator TEXT NOT NULL, + description TEXT NOT NULL, + language TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0, + latest_commit_at INTEGER NOT NULL DEFAULT 0, + path TEXT NOT NULL, + bundle TEXT NOT NULL DEFAULT '', + is_external INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )", + ) + .await?; + + db.execute_unprepared( + "CREATE TABLE IF NOT EXISTS nodes ( + id TEXT PRIMARY KEY, + alias TEXT NOT NULL, + addresses TEXT NOT NULL, + node_type INTEGER NOT NULL, + version INTEGER NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )", + ) + .await?; + + db.execute_unprepared( + "CREATE TABLE IF NOT EXISTS refs ( + repo_id TEXT NOT NULL, + ref_name TEXT NOT NULL, + commit_hash TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (repo_id, ref_name) + )", + ) + .await?; + + db.execute_unprepared( + "CREATE TABLE IF NOT EXISTS chat_messages ( + id TEXT PRIMARY KEY, + \"from\" TEXT NOT NULL, + \"to\" TEXT NOT NULL, + content TEXT NOT NULL, + created_at INTEGER NOT NULL, + status TEXT NOT NULL + )", + ) + .await?; + + migrate_repos_table(db).await?; + migrate_refs_table(db).await?; + + // Align old refs rows that may have default timestamps after ALTER/rebuild. + db.execute_unprepared( + "UPDATE refs + SET created_at = updated_at + WHERE created_at = 0 AND updated_at > 0", + ) + .await?; + + Ok(()) +} + +/// 初始化数据库连接并创建表 +pub async fn get_db_conn() -> Result { + use std::collections::HashMap; + use std::sync::Arc; + use tokio::sync::Mutex; + + static DB_POOL: OnceCell>>>> = + OnceCell::const_new(); + + let pool = DB_POOL + .get_or_init(|| async { Mutex::new(HashMap::new()) }) .await; - Ok(db_conn.clone()) + let path = db_path(); + + // 为每个数据库路径维护一个独立的初始化单元,确保每个路径的初始化只执行一次 + let cell = { + let mut map = pool.lock().await; + map.entry(path.clone()) + .or_insert_with(|| Arc::new(OnceCell::const_new())) + .clone() + }; + + // 延迟初始化并缓存全局连接(仅第一次会执行创建表操作) + let db = cell + .get_or_try_init(|| { + let db_path = path.clone(); + async move { + // 确保目录存在 + if let Some(parent) = db_path.parent() { + fs::create_dir_all(parent).ok(); + } + + // 使用合适的 SQLite URL 格式 + let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); + + let mut opt = ConnectOptions::new(db_url); + opt.max_connections(8) + .min_connections(1) + .connect_timeout(Duration::from_secs(8)) + .idle_timeout(Duration::from_secs(8)) + .sqlx_logging(false); + + let db = Database::connect(opt).await?; + + // 运行迁移/建表,兼容已有数据库结构升级 + ensure_schema(&db).await?; + + Ok::(db) + } + }) + .await? + .clone(); + + Ok(db) } /// 保存密钥对到文件(JSON) diff --git a/src/storage/node_model.rs b/src/storage/node_model.rs index 854d172..7cd8d54 100644 --- a/src/storage/node_model.rs +++ b/src/storage/node_model.rs @@ -26,7 +26,7 @@ impl ActiveModelBehavior for ActiveModel {} /// 将 NodeInfo 保存到数据库 pub async fn save_node_info_to_db(info: &NodeInfo) -> Result<()> { - let db = crate::storage::init_db().await?; + let db = crate::storage::get_db_conn().await?; let addresses_json = serde_json::to_string(&info.addresses)?; let now = chrono::Local::now().timestamp(); @@ -57,7 +57,7 @@ pub async fn save_node_info_to_db(info: &NodeInfo) -> Result<()> { /// 从数据库加载 NodeInfo pub async fn load_node_info_from_db(node_id: &str) -> Result> { - let db = crate::storage::init_db().await?; + let db = crate::storage::get_db_conn().await?; if let Some(m) = Entity::find_by_id(node_id).one(&db).await? { let addresses: Vec = serde_json::from_str(&m.addresses)?; @@ -82,14 +82,14 @@ pub async fn load_node_info_from_db(node_id: &str) -> Result> { /// 删除节点记录 pub async fn delete_node_from_db(node_id: &str) -> Result<()> { - let db = crate::storage::init_db().await?; + let db = crate::storage::get_db_conn().await?; Entity::delete_by_id(node_id).exec(&db).await?; Ok(()) } /// 列出所有节点 pub async fn list_nodes() -> Result> { - let db = crate::storage::init_db().await?; + let db = crate::storage::get_db_conn().await?; let models = Entity::find().all(&db).await?; let mut out = Vec::new(); diff --git a/src/storage/ref_model.rs b/src/storage/ref_model.rs index 15fbce1..7a2d662 100644 --- a/src/storage/ref_model.rs +++ b/src/storage/ref_model.rs @@ -2,17 +2,18 @@ use anyhow::Result; use sea_orm::entity::prelude::*; use sea_orm::{Set, Unchanged}; -use crate::storage::init_db; +use crate::storage::get_db_conn; /// Refs table entity for tracking branch and tag commits #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "refs")] pub struct Model { - #[sea_orm(primary_key)] - pub id: String, + #[sea_orm(primary_key, auto_increment = false)] pub repo_id: String, + #[sea_orm(primary_key, auto_increment = false)] pub ref_name: String, pub commit_hash: String, + pub created_at: i64, pub updated_at: i64, } @@ -23,32 +24,33 @@ impl ActiveModelBehavior for ActiveModel {} /// Save or update a ref in the database pub async fn save_ref(repo_id: &str, ref_name: &str, commit_hash: &str) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; let now = chrono::Local::now().timestamp(); - // Generate a unique ID for this ref record (repo_id + ref_name) - let id = format!("{}:{}", repo_id, ref_name); - // Check if ref already exists - let existing = Entity::find_by_id(id.clone()).one(&db).await?; + let existing = Entity::find() + .filter(Column::RepoId.eq(repo_id)) + .filter(Column::RefName.eq(ref_name)) + .one(&db) + .await?; - if let Some(_) = existing { + if let Some(existing_model) = existing { // Update existing ref let active_model = ActiveModel { - id: Unchanged(id), - repo_id: Unchanged(repo_id.to_string()), - ref_name: Unchanged(ref_name.to_string()), + repo_id: Unchanged(existing_model.repo_id), + ref_name: Unchanged(existing_model.ref_name), commit_hash: Set(commit_hash.to_string()), + created_at: Unchanged(existing_model.created_at), updated_at: Set(now), }; Entity::update(active_model).exec(&db).await?; } else { // Insert new ref let active_model = ActiveModel { - id: Set(id), repo_id: Set(repo_id.to_string()), ref_name: Set(ref_name.to_string()), commit_hash: Set(commit_hash.to_string()), + created_at: Set(now), updated_at: Set(now), }; Entity::insert(active_model).exec(&db).await?; @@ -62,29 +64,31 @@ pub async fn batch_save_refs( repo_id: &str, refs: &std::collections::HashMap, ) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; let now = chrono::Local::now().timestamp(); for (ref_name, commit_hash) in refs { - let id = format!("{}:{}", repo_id, ref_name); + let existing = Entity::find() + .filter(Column::RepoId.eq(repo_id)) + .filter(Column::RefName.eq(ref_name.as_str())) + .one(&db) + .await?; - let existing = Entity::find_by_id(id.clone()).one(&db).await?; - - if let Some(_) = existing { + if let Some(existing_model) = existing { let active_model = ActiveModel { - id: Unchanged(id), - repo_id: Unchanged(repo_id.to_string()), - ref_name: Unchanged(ref_name.clone()), + repo_id: Unchanged(existing_model.repo_id), + ref_name: Unchanged(existing_model.ref_name), commit_hash: Set(commit_hash.clone()), + created_at: Unchanged(existing_model.created_at), updated_at: Set(now), }; Entity::update(active_model).exec(&db).await?; } else { let active_model = ActiveModel { - id: Set(id), repo_id: Set(repo_id.to_string()), ref_name: Set(ref_name.clone()), commit_hash: Set(commit_hash.clone()), + created_at: Set(now), updated_at: Set(now), }; Entity::insert(active_model).exec(&db).await?; @@ -98,7 +102,7 @@ pub async fn batch_save_refs( pub async fn load_refs_for_repo( repo_id: &str, ) -> Result> { - let db = init_db().await?; + let db = get_db_conn().await?; let refs = Entity::find() .filter(Column::RepoId.eq(repo_id)) @@ -115,10 +119,14 @@ pub async fn load_refs_for_repo( /// Get a specific ref by repo_id and ref_name pub async fn get_ref(repo_id: &str, ref_name: &str) -> Result> { - let db = init_db().await?; - let id = format!("{}:{}", repo_id, ref_name); + let db = get_db_conn().await?; - if let Some(model) = Entity::find_by_id(id).one(&db).await? { + if let Some(model) = Entity::find() + .filter(Column::RepoId.eq(repo_id)) + .filter(Column::RefName.eq(ref_name)) + .one(&db) + .await? + { return Ok(Some(model.commit_hash)); } @@ -127,7 +135,7 @@ pub async fn get_ref(repo_id: &str, ref_name: &str) -> Result> { /// Delete all refs for a repository pub async fn delete_refs_for_repo(repo_id: &str) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; Entity::delete_many() .filter(Column::RepoId.eq(repo_id)) .exec(&db) @@ -137,9 +145,12 @@ pub async fn delete_refs_for_repo(repo_id: &str) -> Result<()> { /// Delete a specific ref pub async fn delete_ref(repo_id: &str, ref_name: &str) -> Result<()> { - let db = init_db().await?; - let id = format!("{}:{}", repo_id, ref_name); - Entity::delete_by_id(id).exec(&db).await?; + let db = get_db_conn().await?; + Entity::delete_many() + .filter(Column::RepoId.eq(repo_id)) + .filter(Column::RefName.eq(ref_name)) + .exec(&db) + .await?; Ok(()) } @@ -148,7 +159,7 @@ pub async fn has_refs_changed( repo_id: &str, old_refs: &std::collections::HashMap, ) -> Result { - let db = init_db().await?; + let db = get_db_conn().await?; let current_refs = Entity::find() .filter(Column::RepoId.eq(repo_id)) diff --git a/src/storage/repo_model.rs b/src/storage/repo_model.rs index fa4847b..85c3bf9 100644 --- a/src/storage/repo_model.rs +++ b/src/storage/repo_model.rs @@ -4,7 +4,7 @@ use anyhow::Result; use sea_orm::entity::prelude::*; use sea_orm::{Set, Unchanged}; -use crate::{repo::repo::Repo, storage::init_db}; +use crate::{repo::repo::Repo, storage::get_db_conn}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "repos")] @@ -14,10 +14,12 @@ pub struct Model { pub name: String, pub creator: String, pub description: String, - pub timestamp: i64, + pub language: String, pub path: String, pub bundle: String, pub is_external: bool, + pub size: i64, + pub latest_commit_at: i64, pub created_at: i64, pub updated_at: i64, } @@ -29,7 +31,7 @@ impl ActiveModelBehavior for ActiveModel {} /// 保存或更新 Repo 到数据库 pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; let now = chrono::Local::now().timestamp(); // 查询是否已存在 @@ -42,10 +44,12 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { name: Set(repo.p2p_description.name.clone()), creator: Set(repo.p2p_description.creator.clone()), description: Set(repo.p2p_description.description.clone()), - timestamp: Set(repo.p2p_description.timestamp), + language: Set(repo.p2p_description.language.clone()), path: Set(repo.path.to_string_lossy().to_string()), bundle: Set(repo.bundle.to_string_lossy().to_string()), is_external: Set(repo.is_external), + size: Set(repo.p2p_description.size as i64), + latest_commit_at: Set(repo.p2p_description.latest_commit_at), created_at: Unchanged(existing_model.created_at), updated_at: Set(now), }; @@ -57,10 +61,12 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { name: Set(repo.p2p_description.name.clone()), creator: Set(repo.p2p_description.creator.clone()), description: Set(repo.p2p_description.description.clone()), - timestamp: Set(repo.p2p_description.timestamp), + language: Set(repo.p2p_description.language.clone()), path: Set(repo.path.to_string_lossy().to_string()), bundle: Set(repo.bundle.to_string_lossy().to_string()), is_external: Set(repo.is_external), + size: Set(repo.p2p_description.size as i64), + latest_commit_at: Set(repo.p2p_description.latest_commit_at), created_at: Set(now), updated_at: Set(now), }; @@ -75,7 +81,7 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { /// 从数据库加载 Repo pub async fn load_repo_from_db(repo_id: &str) -> Result> { - let db = init_db().await?; + let db = get_db_conn().await?; // 使用 find_by_id 直接查询 if let Some(model) = Entity::find_by_id(repo_id).one(&db).await? { @@ -89,7 +95,9 @@ pub async fn load_repo_from_db(repo_id: &str) -> Result> { creator: model.creator, name: model.name, description: model.description, - timestamp: model.timestamp, + language: model.language, + latest_commit_at: model.latest_commit_at, + size: model.size as u64, }, path: PathBuf::from(model.path), bundle: PathBuf::from(model.bundle), @@ -103,7 +111,7 @@ pub async fn load_repo_from_db(repo_id: &str) -> Result> { /// 删除 Repo 从数据库 pub async fn delete_repo_from_db(repo_id: &str) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; Entity::delete_by_id(repo_id).exec(&db).await?; // Delete associated refs crate::storage::ref_model::delete_refs_for_repo(repo_id).await?; @@ -112,7 +120,7 @@ pub async fn delete_repo_from_db(repo_id: &str) -> Result<()> { /// 列出所有 Repos pub async fn list_repos() -> Result> { - let db = init_db().await?; + let db = get_db_conn().await?; let models = Entity::find().all(&db).await?; let mut repos = Vec::new(); @@ -127,7 +135,9 @@ pub async fn list_repos() -> Result> { creator: model.creator, name: model.name, description: model.description, - timestamp: model.timestamp, + language: model.language, + latest_commit_at: model.latest_commit_at, + size: model.size as u64, }, path: PathBuf::from(model.path), bundle: PathBuf::from(model.bundle), @@ -139,7 +149,7 @@ pub async fn list_repos() -> Result> { /// 更新 Repo 的 bundle 路径 pub async fn update_repo_bundle(repo_id: &str, bundle_path: &str) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; let now = chrono::Local::now().timestamp(); // 查询是否存在 @@ -152,9 +162,11 @@ pub async fn update_repo_bundle(repo_id: &str, bundle_path: &str) -> Result<()> name: Unchanged(model.name), creator: Unchanged(model.creator), description: Unchanged(model.description), - timestamp: Unchanged(model.timestamp), + language: Unchanged(model.language), path: Unchanged(model.path), is_external: Unchanged(model.is_external), + size: Unchanged(model.size), + latest_commit_at: Unchanged(model.latest_commit_at), created_at: Unchanged(model.created_at), }; Entity::update(active_model).exec(&db).await?; @@ -174,7 +186,9 @@ mod tests { creator: "did:node:test333".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 1000, + size: 0, }; let mut repo = Repo::new( @@ -212,7 +226,9 @@ mod tests { creator: "did:node:test".to_string(), name: format!("test-repo-{}", i), description: format!("Test repository {}", i), - timestamp: 1000 + i, + language: "Rust".to_string(), + latest_commit_at: 1000 + i, + size: 0, }; let repo = Repo::new( diff --git a/src/transport/cert.rs b/src/transport/cert.rs index d721680..7ce7dcf 100644 --- a/src/transport/cert.rs +++ b/src/transport/cert.rs @@ -1,10 +1,41 @@ use anyhow::{anyhow, Result}; -use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, KeyPair}; +use libvault::utils::cert::Certificate; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; use std::fs; use std::path::Path; +use std::time::{Duration, SystemTime}; + +fn build_ca_certificate() -> Result { + let mut ca = Certificate { + is_ca: true, + key_type: "rsa".to_string(), + key_bits: 2048, + not_after: SystemTime::now() + Duration::from_secs(60 * 60 * 24 * 3650), + ..Default::default() + }; + + ca.to_cert_bundle(None, None) + .map_err(|e| anyhow!("Failed to generate CA certificate: {}", e)) +} + +fn build_server_certificate(ca_cert: &X509, ca_key: &PKey) -> Result { + let mut cert = Certificate { + dns_sans: vec!["localhost".to_string()], + ip_sans: vec!["127.0.0.1".to_string(), "0.0.0.0".to_string()], + is_ca: false, + key_type: "rsa".to_string(), + key_bits: 2048, + not_after: SystemTime::now() + Duration::from_secs(60 * 60 * 24 * 3650), + ..Default::default() + }; + + cert.to_cert_bundle(Some(ca_cert), Some(ca_key)) + .map_err(|e| anyhow!("Failed to generate server certificate: {}", e)) +} /// Generate a CA certificate and save to files. -pub fn generate_ca_cert(ca_cert_path: &str, ca_key_path: &str) -> Result { +pub fn generate_ca_cert(ca_cert_path: &str, ca_key_path: &str) -> Result<()> { // Check if CA certificate already exists if Path::new(ca_cert_path).exists() && Path::new(ca_key_path).exists() { tracing::info!( @@ -12,9 +43,7 @@ pub fn generate_ca_cert(ca_cert_path: &str, ca_key_path: &str) -> Result Result Result<()> { // Check if certificate already exists @@ -88,44 +97,21 @@ pub fn generate_server_cert( tracing::info!("Generating server certificate signed by CA..."); // Read CA key - let ca_key_pem = fs::read_to_string(ca_key_path)?; + let ca_key_pem = fs::read(ca_key_path)?; // Parse CA key - let ca_keypair = - KeyPair::from_pem(&ca_key_pem).map_err(|e| anyhow!("Failed to parse CA key: {}", e))?; - - // Generate server keypair - let server_keypair = - KeyPair::generate().map_err(|e| anyhow!("Failed to generate server keypair: {}", e))?; - - // Create server certificate parameters with SANs - let mut params = CertificateParams::new(vec![ - "localhost".to_string(), - "127.0.0.1".to_string(), - "0.0.0.0".to_string(), - ]) - .map_err(|e| anyhow!("Failed to create server certificate params: {}", e))?; - - // Set server subject name - let mut dn = DistinguishedName::new(); - dn.push(DnType::CommonName, "localhost"); - dn.push(DnType::OrganizationName, "MegaEngine"); - dn.push(DnType::CountryName, "CN"); - params.distinguished_name = dn; - - // Sign server certificate with CA key - // signed_by expects (server_keypair, ca_cert_obj, ca_keypair) - let server_cert = params - .signed_by(&server_keypair, ca_cert_obj, &ca_keypair) - .map_err(|e| anyhow!("Failed to generate server certificate: {}", e))?; + let ca_key = PKey::private_key_from_pem(&ca_key_pem) + .map_err(|e| anyhow!("Failed to parse CA key: {}", e))?; + + let server_cert = build_server_certificate(ca_cert_obj, &ca_key)?; // Save server certificate - let cert_pem = server_cert.pem(); + let cert_pem = server_cert.certificate.to_pem()?; fs::write(cert_path, cert_pem)?; tracing::info!("Server certificate written to {}", cert_path); // Save server private key - let key_pem = server_keypair.serialize_pem(); + let key_pem = server_cert.private_key.private_key_to_pem_pkcs8()?; fs::write(key_path, key_pem)?; tracing::info!("Server private key written to {}", key_path); @@ -148,24 +134,10 @@ pub fn ensure_certificates(cert_path: &str, key_path: &str, ca_cert_path: &str) } // Generate CA certificate if needed (only once) - let ca_cert = match generate_ca_cert(ca_cert_path, &ca_key_path) { - Ok(cert) => cert, - Err(_) => { - // CA already exists - need to reconstruct it from files for signing - tracing::info!("CA certificate exists, reconstructing from files"); - - let ca_key_pem = fs::read_to_string(&ca_key_path)?; - let keypair = KeyPair::from_pem(&ca_key_pem)?; - let mut params = CertificateParams::new(vec![])?; - params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); - let mut dn = DistinguishedName::new(); - dn.push(DnType::CommonName, "MegaEngine CA"); - dn.push(DnType::OrganizationName, "MegaEngine"); - dn.push(DnType::CountryName, "CN"); - params.distinguished_name = dn; - params.self_signed(&keypair)? - } - }; + generate_ca_cert(ca_cert_path, &ca_key_path)?; + + let ca_cert_pem = fs::read(ca_cert_path)?; + let ca_cert = X509::from_pem(&ca_cert_pem).map_err(|e| anyhow!("Failed to parse CA cert: {}", e))?; // Generate server certificate signed by CA // If server cert and key don't both exist, regenerate them @@ -175,3 +147,123 @@ pub fn ensure_certificates(cert_path: &str, key_path: &str, ca_cert_path: &str) Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn test_dir(prefix: &str) -> std::path::PathBuf { + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("megaengine-{}-{}", prefix, ts)) + } + + #[test] + fn test_ensure_certificates_generates_files() { + let dir = test_dir("cert-generate"); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + let cert_path = dir.join("cert.pem"); + let key_path = dir.join("key.pem"); + let ca_cert_path = dir.join("ca-cert.pem"); + let ca_key_path = dir.join("ca-cert-key.pem"); + + let result = ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ); + + assert!(result.is_ok()); + assert!(cert_path.exists()); + assert!(key_path.exists()); + assert!(ca_cert_path.exists()); + assert!(ca_key_path.exists()); + + let cert_pem = std::fs::read(&cert_path).expect("read cert pem"); + let key_pem = std::fs::read(&key_path).expect("read key pem"); + let ca_cert_pem = std::fs::read(&ca_cert_path).expect("read ca cert pem"); + + assert!(X509::from_pem(&cert_pem).is_ok()); + assert!(PKey::private_key_from_pem(&key_pem).is_ok()); + assert!(X509::from_pem(&ca_cert_pem).is_ok()); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn test_ensure_certificates_is_idempotent_when_files_exist() { + let dir = test_dir("cert-idempotent"); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + let cert_path = dir.join("cert.pem"); + let key_path = dir.join("key.pem"); + let ca_cert_path = dir.join("ca-cert.pem"); + + ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ) + .expect("first ensure"); + + let cert_before = std::fs::read(&cert_path).expect("read cert before"); + let key_before = std::fs::read(&key_path).expect("read key before"); + + ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ) + .expect("second ensure"); + + let cert_after = std::fs::read(&cert_path).expect("read cert after"); + let key_after = std::fs::read(&key_path).expect("read key after"); + + assert_eq!(cert_before, cert_after); + assert_eq!(key_before, key_after); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn test_ensure_certificates_recovers_from_missing_key() { + let dir = test_dir("cert-recover"); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + let cert_path = dir.join("cert.pem"); + let key_path = dir.join("key.pem"); + let ca_cert_path = dir.join("ca-cert.pem"); + + ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ) + .expect("first ensure"); + + std::fs::remove_file(&key_path).expect("remove key file"); + assert!(cert_path.exists()); + assert!(!key_path.exists()); + + ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ) + .expect("recovery ensure"); + + assert!(cert_path.exists()); + assert!(key_path.exists()); + + let cert_pem = std::fs::read(&cert_path).expect("read cert pem"); + let key_pem = std::fs::read(&key_path).expect("read key pem"); + assert!(X509::from_pem(&cert_pem).is_ok()); + assert!(PKey::private_key_from_pem(&key_pem).is_ok()); + + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/src/transport/config.rs b/src/transport/config.rs index 97e336b..7524ab6 100644 --- a/src/transport/config.rs +++ b/src/transport/config.rs @@ -47,11 +47,12 @@ impl rustls::client::danger::ServerCertVerifier for NoServerCertificateVerificat } fn supported_verify_schemes(&self) -> Vec { - vec![ - rustls::SignatureScheme::RSA_PKCS1_SHA256, - rustls::SignatureScheme::ECDSA_NISTP256_SHA256, - rustls::SignatureScheme::ED25519, - ] + let provider = rustls::crypto::CryptoProvider::get_default() + .cloned() + .unwrap_or(Arc::new(rustls::crypto::ring::default_provider())); + provider + .signature_verification_algorithms + .supported_schemes() } } @@ -116,7 +117,14 @@ impl QuicConfig { client_crypto.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); client_crypto.enable_early_data = false; - let client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); + let mut client_config = + ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); + + let mut transport_config = TransportConfig::default(); + transport_config.max_idle_timeout(Some(IdleTimeout::from(VarInt::from_u32(300_000)))); + transport_config.keep_alive_interval(Some(Duration::from_secs(30))); + client_config.transport_config(Arc::new(transport_config)); + Ok(client_config) } diff --git a/src/transport/quic.rs b/src/transport/quic.rs index 5694752..1ba4aa9 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -31,7 +31,6 @@ pub struct ConnectionManager { endpoint: Arc, connection_tx: mpsc::Sender, connections: Arc>>>, - // 区分 Gossip 消息(控制流)和数据传输流 gossip_sender: GossipMessageSender, data_sender: DataMessageSender, } @@ -236,8 +235,11 @@ impl ConnectionManager { let mut dead_nodes = Vec::new(); for (node_id, conn) in conns.iter() { - if conn.connection.close_reason().is_some() { - info!("Connection to node[{}] closed", node_id); + if let Some(reason) = conn.connection.close_reason() { + info!( + "Connection to node[{}] closed, reason: {:?}", + node_id, reason + ); dead_nodes.push(node_id.clone()); } } @@ -385,10 +387,15 @@ mod tests { identity::keypair::KeyPair, node::node::{Node, NodeType}, }; - use std::sync::Once; + use std::sync::{Once, OnceLock}; use tokio::time::Duration; static RUSTLS_INIT: Once = Once::new(); + static TEST_SERIAL_LOCK: OnceLock> = OnceLock::new(); + + fn serial_lock() -> &'static tokio::sync::Mutex<()> { + TEST_SERIAL_LOCK.get_or_init(|| tokio::sync::Mutex::new(())) + } fn init() { // Install ring crypto provider only once per test process. @@ -397,6 +404,29 @@ mod tests { }); } + fn cleanup_test_certs() { + let files = [ + "cert/cert.pem", + "cert/key.pem", + "cert/cert2.pem", + "cert/key2.pem", + "cert/ca-cert.pem", + "cert/ca-cert-key.pem", + "cert/no-shared-ca-cert1.pem", + "cert/no-shared-ca-key1.pem", + "cert/no-shared-ca-cert2.pem", + "cert/no-shared-ca-key2.pem", + "cert/no-shared-ca1.pem", + "cert/no-shared-ca1-key.pem", + "cert/no-shared-ca2.pem", + "cert/no-shared-ca2-key.pem", + ]; + + for file in files { + let _ = std::fs::remove_file(file); + } + } + // Mock configuration for the tests fn mock_quic_config() -> QuicConfig { // tracing subscriber may only be initialized once per process; ignore error if already set. @@ -438,10 +468,42 @@ mod tests { ) } + fn mock_quic_config_no_shared_ca_1() -> QuicConfig { + let _ = crate::transport::cert::ensure_certificates( + "cert/no-shared-ca-cert1.pem", + "cert/no-shared-ca-key1.pem", + "cert/no-shared-ca1.pem", + ); + + QuicConfig::new( + "0.0.0.0:0".parse().unwrap(), + "cert/no-shared-ca-cert1.pem".to_string(), + "cert/no-shared-ca-key1.pem".to_string(), + "cert/no-shared-ca1.pem".to_string(), + ) + } + + fn mock_quic_config_no_shared_ca_2() -> QuicConfig { + let _ = crate::transport::cert::ensure_certificates( + "cert/no-shared-ca-cert2.pem", + "cert/no-shared-ca-key2.pem", + "cert/no-shared-ca2.pem", + ); + + QuicConfig::new( + "0.0.0.0:0".parse().unwrap(), + "cert/no-shared-ca-cert2.pem".to_string(), + "cert/no-shared-ca-key2.pem".to_string(), + "cert/no-shared-ca2.pem".to_string(), + ) + } + // Test the `server` method #[tokio::test] async fn test_server_creation() { + let _guard = serial_lock().lock().await; init(); + cleanup_test_certs(); let config = mock_quic_config(); let manager = ConnectionManager::run_server(config).await; @@ -450,12 +512,15 @@ mod tests { tokio::time::sleep(Duration::from_millis(500)).await; let quic_transport = manager.unwrap(); assert!(quic_transport.connections.lock().await.is_empty()); + cleanup_test_certs(); } // Test the `connect` method #[tokio::test] async fn test_client_connection() { + let _guard = serial_lock().lock().await; init(); + cleanup_test_certs(); let keypair1 = KeyPair::generate().expect("generate keypair"); let keypair2 = KeyPair::generate().expect("generate keypair"); @@ -509,11 +574,14 @@ mod tests { assert!(connections1.contains_key(&node2.node_id().clone())); assert!(connections2.contains_key(&node1.node_id().clone())); + cleanup_test_certs(); } #[tokio::test] async fn test_send_message() { + let _guard = serial_lock().lock().await; init(); + cleanup_test_certs(); let keypair1 = KeyPair::generate().expect("generate keypair"); let keypair2 = KeyPair::generate().expect("generate keypair"); @@ -570,5 +638,64 @@ mod tests { .await .unwrap(); tokio::time::sleep(Duration::from_millis(500)).await; + cleanup_test_certs(); + } + + #[tokio::test] + async fn test_client_connection_without_shared_ca() { + let _guard = serial_lock().lock().await; + init(); + cleanup_test_certs(); + let keypair1 = KeyPair::generate().expect("generate keypair"); + let keypair2 = KeyPair::generate().expect("generate keypair"); + + let config1 = mock_quic_config_no_shared_ca_1(); + let manager1 = ConnectionManager::run_server(config1).await; + assert!(manager1.is_ok()); + let manager1 = manager1.unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + + let addr1 = manager1.endpoint.local_addr().expect("get local addr"); + let addr1 = format!("127.0.0.1:{}", addr1.port()).parse().unwrap(); + let node1 = Node::new( + NodeId::from_keypair(&keypair1), + "", + vec![addr1], + NodeType::Normal, + keypair1.clone(), + ); + + let config2 = mock_quic_config_no_shared_ca_2(); + let manager2 = ConnectionManager::run_server(config2).await; + assert!(manager2.is_ok()); + let manager2 = manager2.unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + + let addr2 = manager2.endpoint.local_addr().expect("get local addr"); + let addr2 = format!("127.0.0.1:{}", addr2.port()).parse().unwrap(); + let node2 = Node::new( + NodeId::from_keypair(&keypair2), + "", + vec![addr2], + NodeType::Normal, + keypair2.clone(), + ); + + let result = manager2 + .connect( + node2.node_id().clone(), + node1.node_id().clone(), + node1.addresses().to_vec(), + ) + .await; + assert!(result.is_ok()); + + tokio::time::sleep(Duration::from_millis(500)).await; + + let connections1 = manager1.connections.lock().await; + let connections2 = manager2.connections.lock().await; + assert!(connections1.contains_key(&node2.node_id().clone())); + assert!(connections2.contains_key(&node1.node_id().clone())); + cleanup_test_certs(); } } diff --git a/tests/bundle_two_nodes.rs b/tests/bundle_two_nodes.rs index a561f06..9329727 100644 --- a/tests/bundle_two_nodes.rs +++ b/tests/bundle_two_nodes.rs @@ -65,19 +65,43 @@ async fn test_bundle_transfer_between_two_nodes() { .with_test_writer() .try_init(); + let cert_dir = std::env::current_dir() + .unwrap() + .join("tmp/bundle_two_nodes_certs"); + fs::remove_dir_all(&cert_dir).ok(); + fs::create_dir_all(&cert_dir).expect("Failed to create test cert directory"); + + let sender_cert_path = cert_dir + .join("cert_sender.pem") + .to_string_lossy() + .to_string(); + let sender_key_path = cert_dir + .join("key_sender.pem") + .to_string_lossy() + .to_string(); + let receiver_cert_path = cert_dir + .join("cert_receiver.pem") + .to_string_lossy() + .to_string(); + let receiver_key_path = cert_dir + .join("key_receiver.pem") + .to_string_lossy() + .to_string(); + let ca_cert_path = cert_dir.join("ca-cert.pem").to_string_lossy().to_string(); + println!("📋 Step 1: Setting up certificates"); // Ensure certificates exist megaengine::transport::cert::ensure_certificates( - "cert/cert_sender.pem", - "cert/key_sender.pem", - "cert/ca-cert.pem", + &sender_cert_path, + &sender_key_path, + &ca_cert_path, ) .expect("Failed to ensure sender certificates"); megaengine::transport::cert::ensure_certificates( - "cert/cert_receiver.pem", - "cert/key_receiver.pem", - "cert/ca-cert.pem", + &receiver_cert_path, + &receiver_key_path, + &ca_cert_path, ) .expect("Failed to ensure receiver certificates"); @@ -116,15 +140,15 @@ async fn test_bundle_transfer_between_two_nodes() { // Start QUIC servers let sender_config = QuicConfig::new( sender_addr, - "cert/cert_sender.pem".to_string(), - "cert/key_sender.pem".to_string(), - "cert/ca-cert.pem".to_string(), + sender_cert_path.clone(), + sender_key_path.clone(), + ca_cert_path.clone(), ); let receiver_config = QuicConfig::new( receiver_addr, - "cert/cert_receiver.pem".to_string(), - "cert/key_receiver.pem".to_string(), - "cert/ca-cert.pem".to_string(), + receiver_cert_path.clone(), + receiver_key_path.clone(), + ca_cert_path.clone(), ); sender_node @@ -271,14 +295,14 @@ async fn test_bundle_transfer_between_two_nodes() { // 提取最后一段(NodeId 格式是 "did:key:xxx") let last_segment = sender_node_id_str .split(':') - .last() + .next_back() .unwrap_or(&sender_node_id_str); - let encoded_sender_id = last_segment.replace(':', "_").replace('/', "_"); + let encoded_sender_id = last_segment.replace([':', '/'], "_"); // repo_id 会被处理为最后一段(用 : 分割) let repo_id_last_part = "test_transfer_repo" .split(':') - .last() + .next_back() .unwrap_or("test_transfer_repo") .to_string(); @@ -332,7 +356,7 @@ async fn test_bundle_transfer_between_two_nodes() { // Check commit history let output = Command::new("git") .current_dir(restored_repo_path.to_str().unwrap()) - .args(&["log", "--oneline"]) + .args(["log", "--oneline"]) .output() .expect("Failed to get git log"); @@ -368,6 +392,7 @@ async fn test_bundle_transfer_between_two_nodes() { fs::remove_dir_all(&sender_bundle_storage).ok(); fs::remove_dir_all(&receiver_bundle_storage).ok(); fs::remove_file(&bundle_path).ok(); + fs::remove_dir_all(&cert_dir).ok(); println!("✅ Cleanup completed"); println!("\n========================================"); @@ -388,10 +413,8 @@ async fn test_bundle_transfer_between_two_nodes() { if receiver_bundle_storage.exists() { println!(" - Contents of receiver storage:"); if let Ok(entries) = fs::read_dir(&receiver_bundle_storage) { - for entry in entries { - if let Ok(entry) = entry { - println!(" - {}", entry.path().display()); - } + for entry in entries.flatten() { + println!(" - {}", entry.path().display()); } } } @@ -406,6 +429,7 @@ async fn test_bundle_transfer_between_two_nodes() { fs::remove_dir_all(&sender_bundle_storage).ok(); fs::remove_dir_all(&receiver_bundle_storage).ok(); fs::remove_file(&bundle_path).ok(); + fs::remove_dir_all(&cert_dir).ok(); panic!("Bundle reception failed"); } @@ -417,4 +441,7 @@ async fn test_bundle_transfer_between_two_nodes() { let _ = megaengine::storage::node_model::delete_node_from_db(&receiver_node.node_id().to_string()) .await; + + // 清理生成的证书目录(包含 CA 文件) + fs::remove_dir_all(&cert_dir).ok(); } diff --git a/tests/git_pack.rs b/tests/git_pack.rs index 59eb3f2..1c42c5c 100644 --- a/tests/git_pack.rs +++ b/tests/git_pack.rs @@ -190,7 +190,7 @@ fn test_pack_repo_bundle() { // Check that main branch exists let output = Command::new("git") .current_dir(repo2_str) - .args(&["branch", "-a"]) + .args(["branch", "-a"]) .output() .expect("Failed to list branches"); let branches = String::from_utf8_lossy(&output.stdout); @@ -213,7 +213,7 @@ fn test_pack_repo_bundle() { // Check commit history let output = Command::new("git") .current_dir(repo2_str) - .args(&["log", "--oneline"]) + .args(["log", "--oneline"]) .output() .expect("Failed to get log"); let log = String::from_utf8_lossy(&output.stdout); @@ -226,7 +226,7 @@ fn test_pack_repo_bundle() { // Check tags let output = Command::new("git") .current_dir(repo2_str) - .args(&["tag"]) + .args(["tag"]) .output() .expect("Failed to list tags"); let tags = String::from_utf8_lossy(&output.stdout); diff --git a/tests/gossip_three_nodes.rs b/tests/gossip_three_nodes.rs index b8b2cb8..e0366ae 100644 --- a/tests/gossip_three_nodes.rs +++ b/tests/gossip_three_nodes.rs @@ -21,8 +21,8 @@ async fn test_gossip_three_nodes_message_relay() { // 生成或确保证书存在 megaengine::transport::cert::ensure_certificates( - "cert/cert.pem", - "cert/key.pem", + "cert/cert1.pem", + "cert/key1.pem", "cert/ca-cert.pem", ) .expect("ensure certificates"); @@ -59,8 +59,8 @@ async fn test_gossip_three_nodes_message_relay() { // 4. 启动 QUIC server let config1 = QuicConfig::new( addr1, - "cert/cert.pem".to_string(), - "cert/key.pem".to_string(), + "cert/cert1.pem".to_string(), + "cert/key1.pem".to_string(), "cert/ca-cert.pem".to_string(), ); let config2 = QuicConfig::new( @@ -80,7 +80,6 @@ async fn test_gossip_three_nodes_message_relay() { node3.start_quic_server(config3).await.unwrap(); // 5. 启动 gossip 和 bundle 服务 - let bundle_storage = std::path::PathBuf::from("./data/test_bundles"); let gossip1 = Arc::new(GossipService::new( Arc::clone(node1.connection_manager.as_ref().unwrap()), node1.clone(), @@ -156,4 +155,14 @@ async fn test_gossip_three_nodes_message_relay() { let _ = node_model::delete_node_from_db(&node_id_1).await; let _ = node_model::delete_node_from_db(&node_id_2).await; let _ = node_model::delete_node_from_db(&node_id_3).await; + + // 清理生成的证书文件 + let _ = std::fs::remove_file("cert/cert1.pem"); + let _ = std::fs::remove_file("cert/key1.pem"); + let _ = std::fs::remove_file("cert/cert2.pem"); + let _ = std::fs::remove_file("cert/key2.pem"); + let _ = std::fs::remove_file("cert/cert3.pem"); + let _ = std::fs::remove_file("cert/key3.pem"); + let _ = std::fs::remove_file("cert/ca-cert.pem"); + let _ = std::fs::remove_file("cert/ca-cert-key.pem"); }