diff --git a/Cargo.lock b/Cargo.lock index 7c209eb87..96ae63e0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,16 +17,6 @@ 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 = [ - "crypto-common 0.1.7", - "generic-array 0.14.7", -] - [[package]] name = "aead" version = "0.6.0-rc.5" @@ -57,21 +47,6 @@ dependencies = [ "cfg-if", "cipher 0.5.0-rc.3", "cpufeatures", - "zeroize", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead 0.5.2", - "aes 0.8.4", - "cipher 0.4.4", - "ctr 0.9.2", - "ghash 0.5.1", - "subtle", ] [[package]] @@ -80,13 +55,12 @@ version = "0.11.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f5c07f414d7dc0755870f84c7900425360288d24e0eae4836f9dee19a30fa5f" dependencies = [ - "aead 0.6.0-rc.5", + "aead", "aes 0.9.0-rc.2", "cipher 0.5.0-rc.3", - "ctr 0.10.0-rc.2", - "ghash 0.6.0-rc.3", + "ctr", + "ghash", "subtle", - "zeroize", ] [[package]] @@ -233,25 +207,13 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +checksum = "9ded5f9a03ac8f24d1b8a25101ee812cd32cdc8c50a4c50237de2c4915850e73" dependencies = [ "rustversion", ] -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2 0.10.6", - "cpufeatures", - "password-hash 0.5.0", -] - [[package]] name = "argon2" version = "0.6.0-rc.6" @@ -261,7 +223,7 @@ dependencies = [ "base64ct", "blake2 0.11.0-rc.3", "cpufeatures", - "password-hash 0.6.0-rc.10", + "password-hash", ] [[package]] @@ -861,7 +823,7 @@ dependencies = [ "hmac 0.12.1", "http 0.2.12", "http 1.4.0", - "p256 0.11.1", + "p256", "percent-encoding", "ring", "sha2 0.10.9", @@ -1174,12 +1136,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - [[package]] name = "base16ct" version = "1.0.0" @@ -1208,17 +1164,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bcrypt-pbkdf" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" -dependencies = [ - "blowfish", - "pbkdf2 0.12.2", - "sha2 0.10.9", -] - [[package]] name = "bigdecimal" version = "0.4.10" @@ -1243,9 +1188,6 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] [[package]] name = "blake2" @@ -1287,7 +1229,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1297,26 +1239,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" dependencies = [ "hybrid-array", - "zeroize", -] - -[[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.7", -] - -[[package]] -name = "blowfish" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" -dependencies = [ - "byteorder", - "cipher 0.4.4", ] [[package]] @@ -1440,15 +1362,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "cc" version = "1.2.55" @@ -1479,17 +1392,6 @@ 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 0.4.4", - "cpufeatures", -] - [[package]] name = "chacha20" version = "0.10.0-rc.6" @@ -1500,7 +1402,6 @@ dependencies = [ "cipher 0.5.0-rc.3", "cpufeatures", "rand_core 0.10.0-rc-3", - "zeroize", ] [[package]] @@ -1509,10 +1410,10 @@ version = "0.11.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c662d31454533832974f2b2b3fcbd552ed3cde94c95e614a5039d297dd97076f" dependencies = [ - "aead 0.6.0-rc.5", - "chacha20 0.10.0-rc.6", + "aead", + "chacha20", "cipher 0.5.0-rc.3", - "poly1305 0.9.0-rc.3", + "poly1305", ] [[package]] @@ -1585,7 +1486,6 @@ dependencies = [ "block-buffer 0.11.0", "crypto-common 0.2.0-rc.9", "inout 0.2.2", - "zeroize", ] [[package]] @@ -1828,17 +1728,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-models" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0940496e5c83c54f3b753d5317daec82e8edac71c33aaa1f666d76f518de2444" -dependencies = [ - "hax-lib", - "pastey 0.1.1", - "rand 0.9.2", -] - [[package]] name = "core_affinity" version = "0.8.3" @@ -1852,9 +1741,9 @@ dependencies = [ [[package]] name = "cpp_demangle" -version = "0.4.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" dependencies = [ "cfg-if", ] @@ -1915,9 +1804,9 @@ dependencies = [ [[package]] name = "criterion" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ "alloca", "anes", @@ -1940,9 +1829,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools 0.13.0", @@ -2003,7 +1892,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", "subtle", "zeroize", @@ -2015,10 +1904,8 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array 0.14.7", "rand_core 0.6.4", "subtle", - "zeroize", ] [[package]] @@ -2040,7 +1927,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array 0.14.7", + "generic-array", "typenum", ] @@ -2087,15 +1974,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "ctr" version = "0.10.0-rc.2" @@ -2114,48 +1992,6 @@ dependencies = [ "cmov", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto 0.2.9", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek" -version = "5.0.0-pre.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ae8b2fe5e4995d7fd08a7604e794dc569a65ed19659f5939d529813ed816d38" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest 0.11.0-rc.5", - "fiat-crypto 0.3.0", - "rustc_version", - "subtle", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "darling" version = "0.14.4" @@ -2983,17 +2819,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" -[[package]] -name = "delegate" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "der" version = "0.6.1" @@ -3004,17 +2829,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "pem-rfc7468 0.7.0", - "zeroize", -] - [[package]] name = "der" version = "0.8.0-rc.10" @@ -3022,7 +2836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653" dependencies = [ "const-oid 0.10.2", - "pem-rfc7468 1.0.0", + "pem-rfc7468", "zeroize", ] @@ -3135,15 +2949,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "des" -version = "0.9.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512ca722eff02fa73c43e5136f440c46f861d41f9dd7761c1f2817a5ca5d9ad7" -dependencies = [ - "cipher 0.5.0-rc.3", -] - [[package]] name = "diff" version = "0.1.13" @@ -3157,7 +2962,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", "crypto-common 0.1.7", "subtle", ] @@ -3237,7 +3041,7 @@ dependencies = [ "chrono", "flatbuffers", "futures", - "md5 0.8.0", + "md5", "rand 0.10.0-rc.6", "rcgen", "reqwest 0.13.1", @@ -3270,71 +3074,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ "der 0.6.1", - "elliptic-curve 0.12.3", - "rfc6979 0.3.1", + "elliptic-curve", + "rfc6979", "signature 1.6.4", ] -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der 0.7.10", - "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", - "spki 0.7.3", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", -] - -[[package]] -name = "ed25519" -version = "3.0.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "594435fe09e345ee388e4e8422072ff7dfeca8729389fbd997b3f5504c44cd47" -dependencies = [ - "signature 3.0.0-rc.6", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek 4.1.3", - "ed25519 2.2.3", - "rand_core 0.6.4", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", -] - -[[package]] -name = "ed25519-dalek" -version = "3.0.0-pre.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b9f613e0c236c699bf70d39f825594d9b03aadfd8dd856ea40685f782a4ef2" -dependencies = [ - "curve25519-dalek 5.0.0-pre.4", - "ed25519 3.0.0-rc.2", - "sha2 0.11.0-rc.3", - "subtle", -] - [[package]] name = "either" version = "1.15.0" @@ -3351,33 +3095,12 @@ dependencies = [ "crypto-bigint 0.4.9", "der 0.6.1", "digest 0.10.7", - "ff 0.12.1", - "generic-array 0.14.7", - "group 0.12.1", + "ff", + "generic-array", + "group", "pkcs8 0.9.0", "rand_core 0.6.4", - "sec1 0.3.0", - "subtle", - "zeroize", -] - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", - "digest 0.10.7", - "ff 0.13.1", - "generic-array 0.14.7", - "group 0.13.0", - "hkdf", - "pem-rfc7468 0.7.0", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sec1 0.7.3", + "sec1", "subtle", "zeroize", ] @@ -3391,18 +3114,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "enumset" version = "1.1.10" @@ -3546,28 +3257,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "fiat-crypto" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" - [[package]] name = "filetime" version = "0.2.27" @@ -3615,9 +3304,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -3659,18 +3348,6 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "flurry" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf5efcf77a4da27927d3ab0509dec5b0954bb3bc59da5a1de9e52642ebd4cdf9" -dependencies = [ - "ahash", - "num_cpus", - "parking_lot", - "seize", -] - [[package]] name = "fnv" version = "1.0.7" @@ -3833,18 +3510,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", -] - -[[package]] -name = "generic-array" -version = "1.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" -dependencies = [ - "generic-array 0.14.7", - "rustversion", - "typenum", ] [[package]] @@ -3899,23 +3564,13 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval 0.6.2", -] - [[package]] name = "ghash" version = "0.6.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "333de57ed9494a40df4bbb866752b100819dde0d18f2264c48f5a08a85fe673d" dependencies = [ - "polyval 0.7.0-rc.3", + "polyval", ] [[package]] @@ -4101,7 +3756,7 @@ dependencies = [ "http-body 1.0.1", "hyper", "lazy_static", - "md5 0.8.0", + "md5", "percent-encoding", "pin-project", "prost 0.14.3", @@ -4155,18 +3810,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff 0.12.1", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff 0.13.1", + "ff", "rand_core 0.6.4", "subtle", ] @@ -4246,43 +3890,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "hax-lib" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d9ba66d1739c68e0219b2b2238b5c4145f491ebf181b9c6ab561a19352ae86" -dependencies = [ - "hax-lib-macros", - "num-bigint", - "num-traits", -] - -[[package]] -name = "hax-lib-macros" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba777a231a58d1bce1d68313fa6b6afcc7966adef23d60f45b8a2b9b688bf1" -dependencies = [ - "hax-lib-macros-types", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "hax-lib-macros-types" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "867e19177d7425140b417cd27c2e05320e727ee682e98368f88b7194e80ad515" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_json", - "uuid", -] - [[package]] name = "heapless" version = "0.8.0" @@ -4311,12 +3918,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hex-literal" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" - [[package]] name = "hex-simd" version = "0.8.0" @@ -4333,15 +3934,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9040319a6910b901d5d49cbada4a99db52836a1b63228a05f7e2b7f8feef89b1" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac 0.12.1", -] - [[package]] name = "hmac" version = "0.12.1" @@ -4444,12 +4036,11 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hybrid-array" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41fb3dc24fe72c2e3a4685eed55917c2fb228851257f4a8f2d985da9443c3e5" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" dependencies = [ "typenum", - "zeroize", ] [[package]] @@ -4753,8 +4344,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -4772,37 +4362,6 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" -[[package]] -name = "internal-russh-forked-ssh-key" -version = "0.6.16+upstream-0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe44f2bbd99fcb302e246e2d6bcf51aeda346d02a365f80296a07a8c711b6da6" -dependencies = [ - "argon2 0.5.3", - "bcrypt-pbkdf", - "digest 0.11.0-rc.5", - "ecdsa 0.16.9", - "ed25519-dalek 2.2.0", - "hex", - "hmac 0.12.1", - "num-bigint-dig", - "p256 0.13.2", - "p384", - "p521", - "rand_core 0.6.4", - "rsa", - "sec1 0.7.3", - "sha1 0.10.6", - "sha1 0.11.0-rc.3", - "sha2 0.10.9", - "signature 2.2.0", - "signature 3.0.0-rc.6", - "ssh-cipher 0.2.0", - "ssh-encoding 0.2.0", - "subtle", - "zeroize", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -5040,9 +4599,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin 0.9.8", -] [[package]] name = "lexical-core" @@ -5113,72 +4669,6 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" -[[package]] -name = "libcrux-intrinsics" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9ee7ef66569dd7516454fe26de4e401c0c62073929803486b96744594b9632" -dependencies = [ - "core-models", - "hax-lib", -] - -[[package]] -name = "libcrux-ml-kem" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6a88086bf11bd2ec90926c749c4a427f2e59841437dbdede8cde8a96334ab" -dependencies = [ - "hax-lib", - "libcrux-intrinsics", - "libcrux-platform", - "libcrux-secrets", - "libcrux-sha3", - "libcrux-traits", - "rand 0.9.2", - "tls_codec", -] - -[[package]] -name = "libcrux-platform" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db82d058aa76ea315a3b2092f69dfbd67ddb0e462038a206e1dcd73f058c0778" -dependencies = [ - "libc", -] - -[[package]] -name = "libcrux-secrets" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4dbbf6bc9f2bc0f20dc3bea3e5c99adff3bdccf6d2a40488963da69e2ec307" -dependencies = [ - "hax-lib", -] - -[[package]] -name = "libcrux-sha3" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2400bec764d1c75b8a496d5747cffe32f1fb864a12577f0aca2f55a92021c962" -dependencies = [ - "hax-lib", - "libcrux-intrinsics", - "libcrux-platform", - "libcrux-traits", -] - -[[package]] -name = "libcrux-traits" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9adfd58e79d860f6b9e40e35127bfae9e5bd3ade33201d1347459011a2add034" -dependencies = [ - "libcrux-secrets", - "rand 0.9.2", -] - [[package]] name = "libloading" version = "0.8.9" @@ -5444,12 +4934,6 @@ dependencies = [ "digest 0.11.0-rc.5", ] -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - [[package]] name = "md5" version = "0.8.0" @@ -5730,23 +5214,6 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "serde", - "smallvec", ] [[package]] @@ -5942,12 +5409,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -6087,46 +5548,8 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ - "ecdsa 0.14.8", - "elliptic-curve 0.12.3", - "sha2 0.10.9", -] - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "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 0.16.9", - "elliptic-curve 0.13.8", - "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 0.2.0", - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder", - "rand_core 0.6.4", + "ecdsa", + "elliptic-curve", "sha2 0.10.9", ] @@ -6140,25 +5563,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "pageant" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595" -dependencies = [ - "byteorder", - "bytes", - "delegate", - "futures", - "log", - "rand 0.8.5", - "sha2 0.10.9", - "thiserror 1.0.69", - "tokio", - "windows", - "windows-strings", -] - [[package]] name = "parking" version = "2.2.1" @@ -6225,17 +5629,6 @@ dependencies = [ "zstd", ] -[[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 = "password-hash" version = "0.6.0-rc.10" @@ -6252,12 +5645,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - [[package]] name = "pastey" version = "0.2.1" @@ -6318,15 +5705,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "pem-rfc7468" version = "1.0.0" @@ -6425,21 +5803,6 @@ dependencies = [ "spki 0.8.0-rc.4", ] -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "aes 0.8.4", - "cbc", - "der 0.7.10", - "pbkdf2 0.12.2", - "scrypt", - "sha2 0.10.9", - "spki 0.7.3", -] - [[package]] name = "pkcs8" version = "0.9.0" @@ -6450,18 +5813,6 @@ dependencies = [ "spki 0.6.0", ] -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "pkcs5", - "rand_core 0.6.4", - "spki 0.7.3", -] - [[package]] name = "pkcs8" version = "0.11.0-rc.10" @@ -6512,17 +5863,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" -[[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 0.5.1", -] - [[package]] name = "poly1305" version = "0.9.0-rc.3" @@ -6530,20 +5870,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c0749ae91cfe6e68c77c4d48802d9720ee06aed3f7100a38975fb0962d50bc" dependencies = [ "cpufeatures", - "universal-hash 0.6.0-rc.4", - "zeroize", -] - -[[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 0.5.1", + "universal-hash", ] [[package]] @@ -6554,7 +5881,7 @@ checksum = "1ad60831c19edda4b20878a676595c357e93a9b4e6dca2ba98d75b01066b317b" dependencies = [ "cfg-if", "cpufeatures", - "universal-hash 0.6.0-rc.4", + "universal-hash", ] [[package]] @@ -6661,15 +5988,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve 0.13.8", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -6992,24 +6310,13 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] @@ -7019,22 +6326,12 @@ version = "0.10.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bccc05ac8fad6ee391f3cc6725171817eed960345e2fb42ad229d486c1ca2d98" dependencies = [ - "chacha20 0.10.0-rc.6", + "chacha20", "getrandom 0.4.0-rc.0", "rand_core 0.10.0-rc-3", "serde", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -7099,7 +6396,7 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "x509-parser 0.18.0", + "x509-parser 0.18.1", "yasna", ] @@ -7192,9 +6489,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -7204,9 +6501,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -7215,15 +6512,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" @@ -7323,16 +6620,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac 0.12.1", - "subtle", -] - [[package]] name = "rgb" version = "0.8.52" @@ -7366,7 +6653,7 @@ dependencies = [ "base64", "chrono", "futures", - "pastey 0.2.1", + "pastey", "pin-project-lite", "rmcp-macros", "schemars 1.2.1", @@ -7423,7 +6710,6 @@ dependencies = [ "pkcs1", "pkcs8 0.11.0-rc.10", "rand_core 0.10.0-rc-3", - "sha2 0.11.0-rc.3", "signature 3.0.0-rc.6", "spki 0.8.0-rc.4", "zeroize", @@ -7450,109 +6736,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "russh" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01fe22d10a0e39c1134a971d5b8db8a40357b48ef22d81fa8d6eac22202dd782" -dependencies = [ - "aes 0.8.4", - "aws-lc-rs", - "bitflags 2.10.0", - "block-padding", - "byteorder", - "bytes", - "cbc", - "ctr 0.9.2", - "curve25519-dalek 4.1.3", - "data-encoding", - "delegate", - "der 0.7.10", - "digest 0.10.7", - "ecdsa 0.16.9", - "ed25519-dalek 2.2.0", - "elliptic-curve 0.13.8", - "enum_dispatch", - "futures", - "generic-array 1.3.5", - "getrandom 0.2.17", - "hex-literal", - "hmac 0.12.1", - "home", - "inout 0.1.4", - "internal-russh-forked-ssh-key", - "libcrux-ml-kem", - "log", - "md5 0.7.0", - "num-bigint", - "p256 0.13.2", - "p384", - "p521", - "pageant", - "pbkdf2 0.12.2", - "pkcs1", - "pkcs5", - "pkcs8 0.10.2", - "rand 0.9.2", - "rand_core 0.10.0-rc-3", - "rsa", - "russh-cryptovec", - "russh-util", - "sec1 0.7.3", - "sha1 0.10.6", - "sha2 0.10.9", - "signature 2.2.0", - "spki 0.7.3", - "ssh-encoding 0.2.0", - "subtle", - "thiserror 1.0.69", - "tokio", - "typenum", - "zeroize", -] - -[[package]] -name = "russh-cryptovec" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb0ed583ff0f6b4aa44c7867dd7108df01b30571ee9423e250b4cc939f8c6cf" -dependencies = [ - "libc", - "log", - "nix 0.29.0", - "ssh-encoding 0.2.0", - "winapi", -] - -[[package]] -name = "russh-sftp" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb94393cafad0530145b8f626d8687f1ee1dedb93d7ba7740d6ae81868b13b5" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "chrono", - "flurry", - "log", - "serde", - "thiserror 2.0.18", - "tokio", - "tokio-util", -] - -[[package]] -name = "russh-util" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6" -dependencies = [ - "chrono", - "tokio", - "wasm-bindgen", - "wasm-bindgen-futures", -] - [[package]] name = "rust-embed" version = "8.11.0" @@ -7613,7 +6796,7 @@ dependencies = [ name = "rustfs" version = "0.0.5" dependencies = [ - "aes-gcm 0.11.0-rc.2", + "aes-gcm", "anyhow", "astral-tokio-tar", "async-trait", @@ -7643,9 +6826,8 @@ dependencies = [ "jiff", "libc", "libsystemd", - "libunftp", "matchit 0.9.1", - "md5 0.8.0", + "md5", "metrics", "mimalloc", "mime_guess", @@ -7655,8 +6837,6 @@ dependencies = [ "rand 0.10.0-rc.6", "reqwest 0.13.1", "rmp-serde", - "russh", - "russh-sftp", "rust-embed", "rustfs-appauth", "rustfs-audit", @@ -7673,6 +6853,7 @@ dependencies = [ "rustfs-notify", "rustfs-obs", "rustfs-policy", + "rustfs-protocols", "rustfs-protos", "rustfs-rio", "rustfs-s3select-api", @@ -7691,7 +6872,6 @@ dependencies = [ "serial_test", "shadow-rs", "socket2", - "ssh-key", "starshard", "subtle", "sysinfo", @@ -7801,8 +6981,8 @@ dependencies = [ name = "rustfs-crypto" version = "0.0.5" dependencies = [ - "aes-gcm 0.11.0-rc.2", - "argon2 0.6.0-rc.6", + "aes-gcm", + "argon2", "cfg-if", "chacha20poly1305", "jsonwebtoken", @@ -7978,13 +7158,13 @@ dependencies = [ name = "rustfs-kms" version = "0.0.5" dependencies = [ - "aes-gcm 0.11.0-rc.2", + "aes-gcm", "arc-swap", "async-trait", "base64", "chacha20poly1305", "jiff", - "md5 0.8.0", + "md5", "moka", "rand 0.10.0-rc.6", "reqwest 0.13.1", @@ -8136,6 +7316,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "rustfs-protocols" +version = "0.0.5" +dependencies = [ + "async-trait", + "base64", + "bytes", + "futures", + "futures-util", + "libunftp", + "rustfs-credentials", + "rustfs-iam", + "rustfs-policy", + "rustfs-utils", + "rustls", + "s3s", + "serde_json", + "suppaftp", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + [[package]] name = "rustfs-protos" version = "0.0.5" @@ -8153,7 +7359,7 @@ dependencies = [ name = "rustfs-rio" version = "0.0.5" dependencies = [ - "aes-gcm 0.11.0-rc.2", + "aes-gcm", "base64", "bytes", "crc-fast", @@ -8602,15 +7808,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "same-file" version = "1.0.6" @@ -8682,17 +7879,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "pbkdf2 0.12.2", - "salsa20", - "sha2 0.10.9", -] - [[package]] name = "sdd" version = "3.0.10" @@ -8707,36 +7893,12 @@ checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ "base16ct 0.1.1", "der 0.6.1", - "generic-array 0.14.7", + "generic-array", "pkcs8 0.9.0", "subtle", "zeroize", ] -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct 0.2.0", - "der 0.7.10", - "generic-array 0.14.7", - "pkcs8 0.10.2", - "subtle", - "zeroize", -] - -[[package]] -name = "sec1" -version = "0.8.0-rc.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2400ed44a13193820aa528a19f376c3843141a8ce96ff34b11104cc79763f2" -dependencies = [ - "base16ct 1.0.0", - "hybrid-array", -] - [[package]] name = "security-framework" version = "3.5.1" @@ -8760,12 +7922,6 @@ dependencies = [ "libc", ] -[[package]] -name = "seize" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "689224d06523904ebcc9b482c6a3f4f7fb396096645c4cd10c0d2ff7371a34d3" - [[package]] name = "semver" version = "1.0.27" @@ -9052,7 +8208,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", "rand_core 0.6.4", ] @@ -9243,16 +8398,6 @@ dependencies = [ "der 0.6.1", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - [[package]] name = "spki" version = "0.8.0-rc.4" @@ -9285,83 +8430,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "ssh-cipher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" -dependencies = [ - "aes 0.8.4", - "aes-gcm 0.10.3", - "cbc", - "chacha20 0.9.1", - "cipher 0.4.4", - "ctr 0.9.2", - "poly1305 0.8.0", - "ssh-encoding 0.2.0", - "subtle", -] - -[[package]] -name = "ssh-cipher" -version = "0.3.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ca7fe5fcf2f30c6fcbad76c65c0aef40a09087ef9092eae072383c7d959200" -dependencies = [ - "aes 0.9.0-rc.2", - "aes-gcm 0.11.0-rc.2", - "chacha20 0.10.0-rc.6", - "cipher 0.5.0-rc.3", - "des", - "poly1305 0.9.0-rc.3", - "ssh-encoding 0.3.0-rc.4", - "zeroize", -] - -[[package]] -name = "ssh-encoding" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" -dependencies = [ - "base64ct", - "bytes", - "pem-rfc7468 0.7.0", - "sha2 0.10.9", -] - -[[package]] -name = "ssh-encoding" -version = "0.3.0-rc.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a544d1b898f016dd32083ec3a926a5bff0deb6e43c691db6eb39fa11b7c9e" -dependencies = [ - "base64ct", - "crypto-bigint 0.7.0-rc.18", - "digest 0.11.0-rc.5", - "pem-rfc7468 1.0.0", - "subtle", - "zeroize", -] - -[[package]] -name = "ssh-key" -version = "0.7.0-rc.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1b3dd9b51062c9dfd6339675bcc2bab19e91400a08cc167227e9184e99d715" -dependencies = [ - "ed25519-dalek 3.0.0-pre.4", - "rand_core 0.10.0-rc-3", - "rsa", - "sec1 0.8.0-rc.13", - "sha2 0.11.0-rc.3", - "signature 3.0.0-rc.6", - "ssh-cipher 0.3.0-rc.5", - "ssh-encoding 0.3.0-rc.4", - "subtle", - "zeroize", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -9459,9 +8527,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "suppaftp" -version = "7.1.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69a15b325bbe0a1f85de3dbf988a3a14e9cd321537dffcbf6641381dd6d7586f" +checksum = "939df458aae4df73ba0de3063941ec9ff85bfff67d4dac7e482c20637a52ac8a" dependencies = [ "async-trait", "chrono", @@ -9556,9 +8624,9 @@ dependencies = [ [[package]] name = "symbolic-common" -version = "12.17.1" +version = "12.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520cf51c674f8b93d533f80832babe413214bb766b6d7cb74ee99ad2971f8467" +checksum = "751a2823d606b5d0a7616499e4130a516ebd01a44f39811be2b9600936509c23" dependencies = [ "debugid", "memmap2", @@ -9568,9 +8636,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.17.1" +version = "12.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f0de2ee0ffa2641e17ba715ad51d48b9259778176517979cb38b6aa86fa7425" +checksum = "79b237cfbe320601dd24b4ac817a5b68bb28f5508e33f08d42be0682cadc8ac9" dependencies = [ "cpp_demangle", "rustc-demangle", @@ -9821,9 +8889,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -9845,9 +8913,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -9897,27 +8965,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tls_codec" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "tokio" version = "1.49.0" @@ -10367,16 +9414,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.7", - "subtle", -] - [[package]] name = "universal-hash" version = "0.6.0-rc.4" @@ -10644,18 +9681,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -11113,9 +10150,9 @@ dependencies = [ [[package]] name = "x509-parser" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs", "data-encoding", @@ -11191,18 +10228,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.37" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.37" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", @@ -11295,7 +10332,7 @@ dependencies = [ "crc32fast", "deflate64", "flate2", - "generic-array 0.14.7", + "generic-array", "getrandom 0.3.4", "hmac 0.12.1", "indexmap 2.13.0", @@ -11313,9 +10350,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.5" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" +checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index 357c8b31e..da7e3d0a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ members = [ "crates/utils", # Utility functions and helpers "crates/workers", # Worker thread pools and task scheduling "crates/zip", # ZIP file handling and compression + "crates/protocols", # Protocol implementations (FTPS, SFTP, etc.) ] resolver = "2" @@ -58,6 +59,7 @@ homepage = "https://rustfs.com" description = "RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. " keywords = ["RustFS", "Minio", "object-storage", "filesystem", "s3"] categories = ["web-programming", "development-tools", "filesystem", "network-programming"] +authors = ["RustFS Team"] [workspace.lints.rust] unsafe_code = "deny" @@ -97,6 +99,7 @@ rustfs-targets = { path = "crates/targets", version = "0.0.5" } rustfs-utils = { path = "crates/utils", version = "0.0.5" } rustfs-workers = { path = "crates/workers", version = "0.0.5" } rustfs-zip = { path = "./crates/zip", version = "0.0.5" } +rustfs-protocols = { path = "crates/protocols", version = "0.0.5" } # Async Runtime and Networking async-channel = "2.5.0" @@ -271,11 +274,8 @@ opentelemetry-semantic-conventions = { version = "0.31.0", features = ["semconv_ opentelemetry-stdout = { version = "0.31.0" } # FTP and SFTP -libunftp = { version = "0.21.0" } -russh = { version = "0.57.0", features = ["aws-lc-rs", "rsa"], default-features = false } -russh-sftp = { version = "2.1.1" } -ssh-key = { version = "0.7.0-rc.6", features = ["std", "rsa", "ed25519"] } -suppaftp = { version = "7.1.0", features = ["tokio", "tokio-rustls", "rustls"] } +libunftp = { version = "0.21.0", features = ["experimental"] } +suppaftp = { version = "8.0.1", features = ["tokio", "tokio-rustls-aws-lc-rs"] } rcgen = "0.14.7" # Performance Analysis and Memory Profiling diff --git a/crates/config/src/constants/protocols.rs b/crates/config/src/constants/protocols.rs index 52881be37..8649fe698 100644 --- a/crates/config/src/constants/protocols.rs +++ b/crates/config/src/constants/protocols.rs @@ -14,27 +14,34 @@ //! Protocol server configuration constants -/// Default FTPS server bind address -pub const DEFAULT_FTPS_ADDRESS: &str = "0.0.0.0:8021"; +/// Default FTP server bind address (non-encrypted) +pub const DEFAULT_FTP_ADDRESS: &str = "0.0.0.0:8021"; -/// Default SFTP server bind address -pub const DEFAULT_SFTP_ADDRESS: &str = "0.0.0.0:8022"; +/// Default FTPS server bind address (FTP over TLS) +pub const DEFAULT_FTPS_ADDRESS: &str = "0.0.0.0:8022"; + +/// Default FTP passive ports range (optional) +pub const DEFAULT_FTP_PASSIVE_PORTS: Option<&str> = None; /// Default FTPS passive ports range (optional) pub const DEFAULT_FTPS_PASSIVE_PORTS: Option<&str> = None; +/// Default FTP external IP (auto-detected) +pub const DEFAULT_FTP_EXTERNAL_IP: Option<&str> = None; + /// Default FTPS external IP (auto-detected) pub const DEFAULT_FTPS_EXTERNAL_IP: Option<&str> = None; /// Environment variable names +pub const ENV_FTP_ENABLE: &str = "RUSTFS_FTP_ENABLE"; +pub const ENV_FTP_ADDRESS: &str = "RUSTFS_FTP_ADDRESS"; +pub const ENV_FTP_PASSIVE_PORTS: &str = "RUSTFS_FTP_PASSIVE_PORTS"; +pub const ENV_FTP_EXTERNAL_IP: &str = "RUSTFS_FTP_EXTERNAL_IP"; + pub const ENV_FTPS_ENABLE: &str = "RUSTFS_FTPS_ENABLE"; pub const ENV_FTPS_ADDRESS: &str = "RUSTFS_FTPS_ADDRESS"; -pub const ENV_FTPS_CERTS_FILE: &str = "RUSTFS_FTPS_CERTS_FILE"; -pub const ENV_FTPS_KEY_FILE: &str = "RUSTFS_FTPS_KEY_FILE"; +pub const ENV_FTPS_TLS_ENABLED: &str = "RUSTFS_FTPS_TLS_ENABLED"; +pub const ENV_FTPS_CERTS_DIR: &str = "RUSTFS_FTPS_CERTS_DIR"; +pub const ENV_FTPS_CA_FILE: &str = "RUSTFS_FTPS_CA_FILE"; pub const ENV_FTPS_PASSIVE_PORTS: &str = "RUSTFS_FTPS_PASSIVE_PORTS"; pub const ENV_FTPS_EXTERNAL_IP: &str = "RUSTFS_FTPS_EXTERNAL_IP"; - -pub const ENV_SFTP_ENABLE: &str = "RUSTFS_SFTP_ENABLE"; -pub const ENV_SFTP_ADDRESS: &str = "RUSTFS_SFTP_ADDRESS"; -pub const ENV_SFTP_HOST_KEY: &str = "RUSTFS_SFTP_HOST_KEY"; -pub const ENV_SFTP_AUTHORIZED_KEYS: &str = "RUSTFS_SFTP_AUTHORIZED_KEYS"; diff --git a/crates/e2e_test/Cargo.toml b/crates/e2e_test/Cargo.toml index 234d4a2b8..a0fa5b78e 100644 --- a/crates/e2e_test/Cargo.toml +++ b/crates/e2e_test/Cargo.toml @@ -23,6 +23,10 @@ rust-version.workspace = true [lints] workspace = true +[features] +default = [] +ftps = [] + [dependencies] rustfs-ecstore.workspace = true rustfs-common.workspace = true @@ -52,7 +56,7 @@ rand = { workspace = true } chrono = { workspace = true } md5 = { workspace = true } sha2 = { workspace = true } -suppaftp.workspace = true +suppaftp = { workspace = true, features = ["tokio", "rustls-aws-lc-rs"] } rcgen.workspace = true anyhow.workspace = true rustls.workspace = true diff --git a/crates/e2e_test/src/common.rs b/crates/e2e_test/src/common.rs index 666e2a83f..aab8bf283 100644 --- a/crates/e2e_test/src/common.rs +++ b/crates/e2e_test/src/common.rs @@ -81,6 +81,14 @@ fn build_rustfs_binary() { let mut cmd = Command::new("cargo"); cmd.current_dir(&workspace).args(["build", "--bin", "rustfs"]); + // Read features from environment variable for e2e tests + if let Ok(features) = std::env::var("RUSTFS_BUILD_FEATURES") + && !features.is_empty() + { + cmd.arg("--features").arg(&features); + info!("Building with features: {}", features); + } + if !cfg!(debug_assertions) { cmd.arg("--release"); } diff --git a/crates/e2e_test/src/protocols/README.md b/crates/e2e_test/src/protocols/README.md index 4a2fafe3e..31b1b85e6 100644 --- a/crates/e2e_test/src/protocols/README.md +++ b/crates/e2e_test/src/protocols/README.md @@ -1,6 +1,6 @@ # Protocol E2E Tests -FTPS and SFTP protocol end-to-end tests for RustFS. +FTPS protocol end-to-end tests for RustFS. ## Prerequisites @@ -21,12 +21,7 @@ brew install sshpass openssh Run all protocol tests: ```bash -cargo test --package e2e_test test_protocol_core_suite -- --test-threads=1 --nocapture -``` - -Run only FTPS tests: -```bash -cargo test --package e2e_test test_ftps_core_operations -- --test-threads=1 --nocapture +RUSTFS_BUILD_FEATURES=ftps cargo test --package e2e_test test_protocol_core_suite -- --test-threads=1 --nocapture ``` ## Test Coverage @@ -41,4 +36,5 @@ cargo test --package e2e_test test_ftps_core_operations -- --test-threads=1 --no - cd nonexistent bucket (should fail) - delete object - cdup -- rmdir delete bucket \ No newline at end of file +- rmdir delete bucket + diff --git a/crates/e2e_test/src/protocols/ftps_core.rs b/crates/e2e_test/src/protocols/ftps_core.rs index 97656cd23..3193d2cc3 100644 --- a/crates/e2e_test/src/protocols/ftps_core.rs +++ b/crates/e2e_test/src/protocols/ftps_core.rs @@ -36,15 +36,26 @@ const FTPS_ADDRESS: &str = "127.0.0.1:9021"; pub async fn test_ftps_core_operations() -> Result<()> { let env = ProtocolTestEnvironment::new().map_err(|e| anyhow::anyhow!("{}", e))?; - // Generate and write certificate - let cert = generate_simple_self_signed(vec!["localhost".to_string(), "127.0.0.1".to_string()])?; - let cert_path = PathBuf::from(&env.temp_dir).join("ftps.crt"); - let key_path = PathBuf::from(&env.temp_dir).join("ftps.key"); + let cert_dir = PathBuf::from(&env.temp_dir).join("ftps_certs"); + tokio::fs::create_dir_all(&cert_dir).await?; - let cert_pem = cert.cert.pem(); - let key_pem = cert.signing_key.serialize_pem(); - tokio::fs::write(&cert_path, &cert_pem).await?; - tokio::fs::write(&key_path, &key_pem).await?; + // Generate default certificate for root directory + let default_cert = generate_simple_self_signed(vec!["localhost".to_string(), "127.0.0.1".to_string()])?; + let default_cert_path = cert_dir.join("rustfs_cert.pem"); + let default_key_path = cert_dir.join("rustfs_key.pem"); + tokio::fs::write(&default_cert_path, default_cert.cert.pem()).await?; + tokio::fs::write(&default_key_path, default_cert.signing_key.serialize_pem()).await?; + + // Create subdirectory for domain-specific certificate + let example_domain_dir = cert_dir.join("example1.com"); + tokio::fs::create_dir_all(&example_domain_dir).await?; + let domain_cert = generate_simple_self_signed(vec!["example1.com".to_string()])?; + let domain_cert_path = example_domain_dir.join("rustfs_cert.pem"); + let domain_key_path = example_domain_dir.join("rustfs_key.pem"); + tokio::fs::write(&domain_cert_path, domain_cert.cert.pem()).await?; + tokio::fs::write(&domain_key_path, domain_cert.signing_key.serialize_pem()).await?; + + info!("Generated 2 certificates in {:?}", cert_dir); // Start server manually info!("Starting FTPS server on {}", FTPS_ADDRESS); @@ -52,8 +63,7 @@ pub async fn test_ftps_core_operations() -> Result<()> { let mut server_process = Command::new(&binary_path) .env("RUSTFS_FTPS_ENABLE", "true") .env("RUSTFS_FTPS_ADDRESS", FTPS_ADDRESS) - .env("RUSTFS_FTPS_CERTS_FILE", cert_path.to_str().unwrap()) - .env("RUSTFS_FTPS_KEY_FILE", key_path.to_str().unwrap()) + .env("RUSTFS_FTPS_CERTS_DIR", cert_dir.to_str().unwrap()) .arg(&env.temp_dir) .spawn()?; @@ -73,7 +83,7 @@ pub async fn test_ftps_core_operations() -> Result<()> { let mut root_store = RootCertStore::empty(); // Add the self-signed certificate to the trust store for e2e // Note: In a real environment, you'd use proper root certificates - let cert_pem = cert.cert.pem(); + let cert_pem = default_cert.cert.pem(); let cert_der = rustls_pemfile::certs(&mut Cursor::new(cert_pem)) .collect::, _>>() .map_err(|e| anyhow::anyhow!("Failed to parse cert: {}", e))?; diff --git a/crates/e2e_test/src/protocols/mod.rs b/crates/e2e_test/src/protocols/mod.rs index 19f7ab903..18589fd6c 100644 --- a/crates/e2e_test/src/protocols/mod.rs +++ b/crates/e2e_test/src/protocols/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Protocol tests for FTPS and SFTP +//! Protocol tests for FTPS pub mod ftps_core; pub mod test_env; diff --git a/crates/e2e_test/src/protocols/test_runner.rs b/crates/e2e_test/src/protocols/test_runner.rs index 8492fa555..57794aa2f 100644 --- a/crates/e2e_test/src/protocols/test_runner.rs +++ b/crates/e2e_test/src/protocols/test_runner.rs @@ -59,12 +59,9 @@ struct TestDefinition { impl ProtocolTestSuite { /// Create default test suite pub fn new() -> Self { - let tests = vec![ - TestDefinition { - name: "test_ftps_core_operations".to_string(), - }, - // TestDefinition { name: "test_sftp_core_operations".to_string() }, - ]; + let tests = vec![TestDefinition { + name: "test_ftps_core_operations".to_string(), + }]; Self { tests } } @@ -86,10 +83,6 @@ impl ProtocolTestSuite { info!("=== Starting FTPS Module Test ==="); "FTPS core operations (put, ls, mkdir, rmdir, delete)" } - "test_sftp_core_operations" => { - info!("=== Starting SFTP Module Test ==="); - "SFTP core operations (put, ls, mkdir, rmdir, delete)" - } _ => "", }; @@ -128,7 +121,6 @@ impl ProtocolTestSuite { async fn run_single_test(&self, test_def: &TestDefinition) -> Result<(), Box> { match test_def.name.as_str() { "test_ftps_core_operations" => test_ftps_core_operations().await.map_err(|e| e.into()), - // "test_sftp_core_operations" => test_sftp_core_operations().await.map_err(|e| e.into()), _ => Err(format!("Test {} not implemented", test_def.name).into()), } } diff --git a/crates/protocols/Cargo.toml b/crates/protocols/Cargo.toml new file mode 100644 index 000000000..f71971e79 --- /dev/null +++ b/crates/protocols/Cargo.toml @@ -0,0 +1,69 @@ +# Copyright 2024 RustFS Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[package] +name = "rustfs-protocols" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Protocol implementations for RustFS (FTPS, SFTP, etc.)" +keywords = ["ftp", "sftp", "protocol", "storage", "rustfs"] +categories = ["network-programming", "filesystem"] + +[features] +default = [] +ftps = ["libunftp", "suppaftp", "rustls"] + +[dependencies] +# Core RustFS dependencies +rustfs-iam = { workspace = true } +rustfs-credentials = { workspace = true } +rustfs-policy = { workspace = true } +rustfs-utils = { workspace = true } + +# Async dependencies +tokio = { workspace = true, features = ["fs", "io-util", "sync", "time"] } +tracing = { workspace = true } +futures = { workspace = true } +futures-util = { workspace = true } + +# Error handling +thiserror = { workspace = true } + +# Serialization +serde_json = { workspace = true } + +# Utilities +async-trait = { workspace = true } +base64 = { workspace = true } +uuid = { workspace = true } +tokio-util = { workspace = true, features = ["io"] } +time = { workspace = true } +bytes = { workspace = true } + +# S3 API dependencies +s3s = { workspace = true } + +# FTPS specific dependencies (optional) +libunftp = { workspace = true, optional = true } +suppaftp = { workspace = true, optional = true } +rustls = { workspace = true, optional = true } + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/rustfs/src/protocols/client/mod.rs b/crates/protocols/src/common/client/mod.rs similarity index 92% rename from rustfs/src/protocols/client/mod.rs rename to crates/protocols/src/common/client/mod.rs index b88e013c8..88a2db630 100644 --- a/rustfs/src/protocols/client/mod.rs +++ b/crates/protocols/src/common/client/mod.rs @@ -13,3 +13,5 @@ // limitations under the License. pub mod s3; + +pub use s3::StorageBackend as S3StorageBackend; diff --git a/crates/protocols/src/common/client/s3.rs b/crates/protocols/src/common/client/s3.rs new file mode 100644 index 000000000..5b9abd4be --- /dev/null +++ b/crates/protocols/src/common/client/s3.rs @@ -0,0 +1,74 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +use s3s::dto::*; + +#[async_trait] +pub trait StorageBackend: Send + Sync { + /// Error type for this storage backend + type Error: std::error::Error + Send + Sync + 'static; + /// Get object content and metadata + async fn get_object( + &self, + bucket: &str, + key: &str, + access_key: &str, + secret_key: &str, + start_pos: Option, + ) -> Result; + async fn get_object_range( + &self, + bucket: &str, + key: &str, + access_key: &str, + secret_key: &str, + start_pos: u64, + length: u64, + ) -> Result; + /// Put object content with metadata + async fn put_object(&self, input: PutObjectInput, access_key: &str, secret_key: &str) + -> Result; + /// Delete an object + async fn delete_object( + &self, + bucket: &str, + key: &str, + access_key: &str, + secret_key: &str, + ) -> Result; + /// Get object metadata without content + async fn head_object( + &self, + bucket: &str, + key: &str, + access_key: &str, + secret_key: &str, + ) -> Result; + /// Check if bucket exists and get metadata + async fn head_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result; + /// List objects in a bucket with pagination + async fn list_objects_v2( + &self, + input: ListObjectsV2Input, + access_key: &str, + secret_key: &str, + ) -> Result; + /// List all buckets (requires authentication) + async fn list_buckets(&self, access_key: &str, secret_key: &str) -> Result; + /// Create a new bucket + async fn create_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result; + /// Delete a bucket (must be empty) + async fn delete_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result; +} diff --git a/rustfs/src/protocols/gateway/action.rs b/crates/protocols/src/common/gateway.rs similarity index 53% rename from rustfs/src/protocols/gateway/action.rs rename to crates/protocols/src/common/gateway.rs index e0a84c448..0a584ea69 100644 --- a/rustfs/src/protocols/gateway/action.rs +++ b/crates/protocols/src/common/gateway.rs @@ -12,7 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. +use rustfs_credentials; use rustfs_policy::policy::action::S3Action as PolicyS3Action; +use serde_json; +use std::collections::HashMap; +use thiserror::Error; +use tracing::error; + +use super::session::SessionContext; + +/// Authorization errors +#[derive(Debug, Error)] +pub enum AuthorizationError { + #[error("Access denied")] + AccessDenied, +} /// S3 actions that can be performed through the gateway #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -29,6 +43,7 @@ pub enum S3Action { PutObject, DeleteObject, HeadObject, + CopyObject, // Multipart operations CreateMultipartUpload, @@ -43,9 +58,6 @@ pub enum S3Action { PutBucketAcl, GetObjectAcl, PutObjectAcl, - - // Other operations - CopyObject, } impl From for PolicyS3Action { @@ -108,3 +120,101 @@ impl S3Action { } } } + +/// Check if operation is supported for the given protocol +pub fn is_operation_supported(protocol: super::session::Protocol, action: &S3Action) -> bool { + match protocol { + super::session::Protocol::Ftps => match action { + // Bucket operations + S3Action::CreateBucket => true, + S3Action::DeleteBucket => true, + + // Object operations + S3Action::GetObject => true, // RETR command + S3Action::PutObject => true, // STOR and APPE commands both map to PutObject + S3Action::DeleteObject => true, // DELE command + S3Action::HeadObject => true, // SIZE command + + // Multipart operations + S3Action::CreateMultipartUpload => false, + S3Action::UploadPart => false, + S3Action::CompleteMultipartUpload => false, + S3Action::AbortMultipartUpload => false, + S3Action::ListMultipartUploads => false, + S3Action::ListParts => false, + + // ACL operations + S3Action::GetBucketAcl => false, + S3Action::PutBucketAcl => false, + S3Action::GetObjectAcl => false, + S3Action::PutObjectAcl => false, + + // Other operations + S3Action::CopyObject => false, // No native copy support in FTPS + S3Action::ListBucket => true, // LIST command + S3Action::ListBuckets => true, // LIST at root level + S3Action::HeadBucket => true, // Can check if directory exists + }, + } +} + +/// Check if a principal is allowed to perform an S3 action +pub async fn is_authorized(session_context: &SessionContext, action: &S3Action, bucket: &str, object: Option<&str>) -> bool { + let iam_sys = match rustfs_iam::get() { + Ok(sys) => sys, + Err(e) => { + error!("IAM system unavailable: {}", e); + return false; + } + }; + + // Create policy arguments + let mut claims = HashMap::new(); + claims.insert( + "principal".to_string(), + serde_json::Value::String(session_context.principal.access_key().to_string()), + ); + + let policy_action: rustfs_policy::policy::action::Action = action.clone().into(); + + // Check if user is the owner (admin) + let is_owner = if let Some(global_cred) = rustfs_credentials::get_global_action_cred() { + session_context.principal.access_key() == global_cred.access_key + } else { + false + }; + + let args = rustfs_policy::policy::Args { + account: session_context.principal.access_key(), + groups: &session_context.principal.user_identity.credentials.groups, + action: policy_action, + bucket, + conditions: &HashMap::new(), + is_owner, + object: object.unwrap_or(""), + claims: &claims, + deny_only: false, + }; + + iam_sys.is_allowed(&args).await +} + +/// Authorize an operation and return an error if not authorized +pub async fn authorize_operation( + session_context: &SessionContext, + action: &S3Action, + bucket: &str, + object: Option<&str>, +) -> Result<(), AuthorizationError> { + // check if the operation is supported + if !is_operation_supported(session_context.protocol, action) { + return Err(AuthorizationError::AccessDenied); + } + + // check IAM authorization + if is_authorized(session_context, action, bucket, object).await { + Ok(()) + } else { + Err(AuthorizationError::AccessDenied) + } +} diff --git a/rustfs/src/protocols/gateway/mod.rs b/crates/protocols/src/common/mod.rs similarity index 69% rename from rustfs/src/protocols/gateway/mod.rs rename to crates/protocols/src/common/mod.rs index 4dbccd154..b0934f95f 100644 --- a/rustfs/src/protocols/gateway/mod.rs +++ b/crates/protocols/src/common/mod.rs @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Gateway module for protocol implementations +pub mod client; +pub mod gateway; +pub mod session; -pub mod action; -pub mod adapter; -pub mod authorize; -pub mod error; -pub mod restrictions; +pub use client::s3::StorageBackend as S3StorageBackend; +pub use gateway::{AuthorizationError, S3Action, authorize_operation, is_operation_supported}; +pub use session::{ProtocolPrincipal, SessionContext}; diff --git a/rustfs/src/protocols/session/context.rs b/crates/protocols/src/common/session.rs similarity index 72% rename from rustfs/src/protocols/session/context.rs rename to crates/protocols/src/common/session.rs index e0809a00c..e7a44d63b 100644 --- a/rustfs/src/protocols/session/context.rs +++ b/crates/protocols/src/common/session.rs @@ -12,16 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Session context for protocol implementations - -use crate::protocols::session::principal::ProtocolPrincipal; +use rustfs_policy::auth::UserIdentity; use std::net::IpAddr; +use std::sync::Arc; /// Protocol types -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Protocol { Ftps, - Sftp, +} + +/// Protocol principal representing an authenticated user +#[derive(Debug, Clone)] +pub struct ProtocolPrincipal { + /// User identity from IAM system + pub user_identity: Arc, +} + +impl ProtocolPrincipal { + pub fn new(user_identity: Arc) -> Self { + Self { user_identity } + } + pub fn access_key(&self) -> &str { + &self.user_identity.credentials.access_key + } } /// Session context for protocol operations @@ -29,10 +43,8 @@ pub enum Protocol { pub struct SessionContext { /// The protocol principal (authenticated user) pub principal: ProtocolPrincipal, - /// The protocol type pub protocol: Protocol, - /// The source IP address pub source_ip: IpAddr, } diff --git a/crates/protocols/src/constants.rs b/crates/protocols/src/constants.rs new file mode 100644 index 000000000..10ec093f9 --- /dev/null +++ b/crates/protocols/src/constants.rs @@ -0,0 +1,58 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Path and file system constants +pub mod paths { + /// Universal path constants + pub const ROOT_PATH: &str = "/"; + pub const CURRENT_DIR: &str = "."; + pub const PARENT_DIR: &str = ".."; + pub const PATH_SEPARATOR: &str = "/"; + + /// File mode and permission constants + pub const DIR_MODE: u32 = 0o040000; + pub const FILE_MODE: u32 = 0o100000; + pub const DIR_PERMISSIONS: u32 = 0o755; + pub const FILE_PERMISSIONS: u32 = 0o644; +} + +/// Network constants +pub mod network { + /// Default network addresses + pub const DEFAULT_SOURCE_IP: &str = "0.0.0.0"; + pub const DEFAULT_ADDR: &str = "0.0.0.0:0"; + + /// Authentication constants + pub const AUTH_SUFFIX_SVC: &str = "=svc"; + pub const AUTH_SUFFIX_LDAP: &str = "=ldap"; + pub const AUTH_FAILURE_DELAY_MS: u64 = 300; +} + +/// FTPS constants +#[cfg(feature = "ftps")] +pub mod ftps { + pub const PORT_RANGE_SEPARATOR: &str = "-"; + pub const PASSIVE_PORTS_PART_COUNT: usize = 2; +} + +/// Default configuration values +pub mod defaults { + /// Default protocol addresses + #[cfg(feature = "ftps")] + pub const DEFAULT_FTPS_ADDRESS: &str = "0.0.0.0:8021"; + + /// Default FTPS passive port range + #[cfg(feature = "ftps")] + pub const DEFAULT_FTPS_PASSIVE_PORTS: &str = "40000-50000"; +} diff --git a/crates/protocols/src/ftps/config.rs b/crates/protocols/src/ftps/config.rs new file mode 100644 index 000000000..f38c5b802 --- /dev/null +++ b/crates/protocols/src/ftps/config.rs @@ -0,0 +1,121 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::constants::ftps; +use std::fmt::Debug; +use std::net::SocketAddr; +use thiserror::Error; + +/// FTPS server initialization error +#[derive(Debug, Error)] +pub enum FtpsInitError { + #[error("failed to bind address {0}")] + Bind(#[from] std::io::Error), + #[error("server error: {0}")] + Server(#[from] libunftp::ServerError), + #[error("invalid FTPS configuration: {0}")] + InvalidConfig(String), +} + +/// FTPS server configuration +#[derive(Debug, Clone)] +pub struct FtpsConfig { + /// Server bind address + pub bind_addr: SocketAddr, + /// Passive port range (e.g., "40000-50000") + pub passive_ports: Option, + /// External IP address for passive mode + pub external_ip: Option, + /// Whether FTPS is required + pub ftps_required: bool, + /// Whether TLS is enabled (default: true) + pub tls_enabled: bool, + /// Certificate directory path (supports multiple certificates) + pub cert_dir: Option, + /// CA certificate file path for client certificate verification + pub ca_file: Option, +} + +impl FtpsConfig { + /// Validates the configuration + pub async fn validate(&self) -> Result<(), FtpsInitError> { + if self.ftps_required && self.cert_dir.is_none() { + return Err(FtpsInitError::InvalidConfig( + "FTPS is required but certificate directory is missing".to_string(), + )); + } + + if let Some(path) = &self.cert_dir + && !tokio::fs::try_exists(path).await.unwrap_or(false) + { + return Err(FtpsInitError::InvalidConfig(format!("Certificate directory not found: {}", path))); + } + + // Validate CA file exists if specified + if let Some(path) = &self.ca_file + && !tokio::fs::try_exists(path).await.unwrap_or(false) + { + return Err(FtpsInitError::InvalidConfig(format!("CA file not found: {}", path))); + } + + // Validate passive ports format + if self.passive_ports.is_some() { + self.parse_passive_ports()?; + } + + Ok(()) + } + + /// Parse passive ports range from string format "start-end" + pub fn parse_passive_ports(&self) -> Result, FtpsInitError> { + match &self.passive_ports { + Some(ports) => { + let parts: Vec<&str> = ports.split(ftps::PORT_RANGE_SEPARATOR).collect(); + if parts.len() != ftps::PASSIVE_PORTS_PART_COUNT { + return Err(FtpsInitError::InvalidConfig(format!( + "Invalid passive ports format: {}, expected 'start-end'", + ports + ))); + } + + let start = parts[0] + .parse::() + .map_err(|e| FtpsInitError::InvalidConfig(format!("Invalid start port: {}", e)))?; + let end = parts[1] + .parse::() + .map_err(|e| FtpsInitError::InvalidConfig(format!("Invalid end port: {}", e)))?; + if start > end { + return Err(FtpsInitError::InvalidConfig("Start port cannot be greater than end port".to_string())); + } + + Ok(start..=end) + } + None => Err(FtpsInitError::InvalidConfig("No passive ports configured".to_string())), + } + } +} + +impl Default for FtpsConfig { + fn default() -> Self { + Self { + bind_addr: crate::constants::defaults::DEFAULT_FTPS_ADDRESS.parse().unwrap(), + passive_ports: Some(crate::constants::defaults::DEFAULT_FTPS_PASSIVE_PORTS.to_string()), + external_ip: None, + ftps_required: false, + tls_enabled: true, + cert_dir: None, + ca_file: None, + } + } +} diff --git a/crates/protocols/src/ftps/driver.rs b/crates/protocols/src/ftps/driver.rs new file mode 100644 index 000000000..c965172d8 --- /dev/null +++ b/crates/protocols/src/ftps/driver.rs @@ -0,0 +1,573 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::common::client::s3::StorageBackend as S3StorageBackend; +use crate::common::gateway::S3Action; +use crate::common::gateway::authorize_operation; +use async_trait::async_trait; +use futures_util::stream; +use libunftp::storage::{Error, ErrorKind, Fileinfo, Metadata, Result, StorageBackend}; +use rustfs_utils::path; +use s3s::dto::*; +use std::fmt::Debug; +use std::path::{Path, PathBuf}; +use tokio::io::AsyncRead; +use tracing::{debug, error}; + +/// FTPS metadata implementation +#[derive(Debug, Clone)] +pub struct FtpsMetadata { + /// File size in bytes + pub size: u64, + /// Modification time + pub modified: Option, + /// Whether this is a directory + pub is_dir: bool, +} + +impl Metadata for FtpsMetadata { + fn len(&self) -> u64 { + self.size + } + fn is_dir(&self) -> bool { + self.is_dir + } + fn is_file(&self) -> bool { + !self.is_dir + } + fn is_symlink(&self) -> bool { + false + } + fn modified(&self) -> Result { + self.modified + .ok_or_else(|| Error::new(ErrorKind::PermanentFileNotAvailable, "No modification time available")) + } + fn gid(&self) -> u32 { + 0 + } + fn uid(&self) -> u32 { + 0 + } +} + +/// FTPS storage driver implementation +pub struct FtpsDriver { + /// Storage backend for S3 operations + storage: S, +} + +impl Debug for FtpsDriver +where + S: S3StorageBackend + Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FtpsDriver").field("storage", &"StorageBackend").finish() + } +} + +impl FtpsDriver +where + S: S3StorageBackend + Debug, +{ + /// Create a new FTPS driver with the given storage backend + pub fn new(storage: S) -> Self { + Self { storage } + } + + /// List all buckets (for root path) + async fn list_buckets( + &self, + session_context: &crate::common::session::SessionContext, + ) -> Result as libunftp::storage::StorageBackend>::Metadata>>> + { + match authorize_operation(session_context, &S3Action::ListBuckets, "", None).await { + Ok(_) => {} + Err(_e) => { + return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied")); + } + } + + let mut list_result = Vec::new(); + match self + .storage + .list_buckets( + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + ) + .await + { + Ok(output) => { + if let Some(buckets) = output.buckets { + for bucket in buckets { + if let Some(ref bucket_name) = bucket.name { + let metadata = FtpsMetadata { + size: 0, + modified: bucket.creation_date.map(|dt| { + let offset_dt: time::OffsetDateTime = dt.into(); + std::time::SystemTime::from(offset_dt) + }), + is_dir: true, + }; + + list_result.push(Fileinfo { + path: PathBuf::from(bucket_name), + metadata, + }); + } + } + } + + Ok(list_result) + } + Err(_) => Err(Error::new(ErrorKind::PermanentFileNotAvailable, "List failed")), + } + } + + fn parse_s3_path(&self, path: &str) -> std::result::Result<(String, Option), String> { + let cleaned_path = path::clean(path); + let (bucket, object) = path::path_to_bucket_object(&cleaned_path); + let key = if object.is_empty() { None } else { Some(object) }; + + Ok((bucket, key)) + } +} + +#[async_trait] +impl StorageBackend for FtpsDriver +where + S: S3StorageBackend + Debug, +{ + type Metadata = FtpsMetadata; + + async fn metadata + Send>(&self, user: &super::server::FtpsUser, path: P) -> Result { + let path_str = path.as_ref().to_string_lossy(); + let session_context = &user.session_context; + + let (bucket, key) = self + .parse_s3_path(&path_str) + .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; + + if let Some(key) = key { + match self + .storage + .head_object( + &bucket, + &key, + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + ) + .await + { + Ok(output) => { + let size = output.content_length.unwrap_or(0) as u64; + let modified = output.last_modified.map(|dt| { + // Convert s3s Timestamp to SystemTime + let offset_dt: time::OffsetDateTime = dt.into(); + std::time::SystemTime::from(offset_dt) + }); + + Ok(FtpsMetadata { + size, + modified, + is_dir: false, + }) + } + Err(e) => { + error!("Failed to get metadata for '{}': {}", path_str, e); + Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Metadata failed", e))) + } + } + } else { + // Directory metadata - use HeadBucket + let bucket_clone = bucket.clone(); + match self + .storage + .head_bucket( + &bucket, + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + ) + .await + { + Ok(_) => Ok(FtpsMetadata { + size: 0, + modified: Some(std::time::SystemTime::now()), + is_dir: true, + }), + Err(e) => { + error!("Failed to get bucket metadata for '{}': {}", bucket_clone, e); + Err(Error::new( + ErrorKind::PermanentFileNotAvailable, + format!("{}: {}", "Bucket metadata failed", e), + )) + } + } + } + } + + async fn list + Send>( + &self, + user: &super::server::FtpsUser, + path: P, + ) -> Result>> { + let path_str = path.as_ref().to_string_lossy(); + + // Get session context from user + let session_context = &user.session_context; + + // Check if this is root path listing + if path_str == "/" || path_str == "/." { + return self.list_buckets(session_context).await; + } + + let (bucket, prefix) = self + .parse_s3_path(&path_str) + .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; + + // Authorize the operation + authorize_operation(session_context, &S3Action::ListBucket, &bucket, prefix.as_deref()) + .await + .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; + + let list_input = ListObjectsV2Input::builder() + .bucket(bucket) + .prefix(prefix.map(|p| p.to_string())) + .delimiter(Some("/".to_string())) + .build() + .map_err(|e| { + Error::new(ErrorKind::PermanentFileNotAvailable, format!("Failed to build ListObjectsV2Input: {}", e)) + })?; + + match self + .storage + .list_objects_v2( + list_input, + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + ) + .await + { + Ok(output) => { + let mut fileinfos = Vec::new(); + + // Add files (objects) + if let Some(objects) = output.contents { + for obj in objects { + if let Some(key) = obj.key { + let filename = PathBuf::from(key.as_str()); + let size = obj.size.unwrap_or(0) as u64; + let modified = obj.last_modified.map(|dt: s3s::dto::Timestamp| { + // Convert s3s Timestamp to SystemTime + let offset_dt: time::OffsetDateTime = dt.into(); + std::time::SystemTime::from(offset_dt) + }); + + let metadata = FtpsMetadata { + size, + modified, + is_dir: false, + }; + + fileinfos.push(Fileinfo { + path: filename, + metadata, + }); + } + } + } + + // Add directories (common prefixes) + if let Some(common_prefixes) = output.common_prefixes { + for prefix in common_prefixes { + if let Some(prefix_str) = prefix.prefix { + let dir_name = PathBuf::from(prefix_str.as_str().trim_end_matches('/')); + let metadata = FtpsMetadata { + size: 0, + modified: Some(std::time::SystemTime::now()), + is_dir: true, + }; + + fileinfos.push(Fileinfo { + path: dir_name, + metadata, + }); + } + } + } + + Ok(fileinfos) + } + Err(e) => { + error!("Failed to list '{}': {}", path_str, e); + Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "List failed", e))) + } + } + } + + async fn get + Send>( + &self, + user: &super::server::FtpsUser, + path: P, + start_pos: u64, + ) -> Result> { + let path_str = path.as_ref().to_string_lossy(); + let session_context = &user.session_context; + + let (bucket, key) = self + .parse_s3_path(&path_str) + .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; + + let key = key.ok_or_else(|| Error::new(ErrorKind::PermanentFileNotAvailable, "Cannot get directory"))?; + + match self + .storage + .get_object( + &bucket, + &key, + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + Some(start_pos), // Pass start_pos for range request + ) + .await + { + Ok(output) => { + let body = output + .body + .ok_or_else(|| Error::new(ErrorKind::PermanentFileNotAvailable, "No body in response"))?; + + use futures_util::StreamExt; + let mut data = Vec::new(); + let mut stream = body; + while let Some(chunk_result) = stream.next().await { + match chunk_result { + Ok(bytes) => data.extend_from_slice(&bytes), + Err(e) => { + error!("Error reading stream: {}", e); + return Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("Stream error: {}", e))); + } + } + } + + Ok(Box::new(std::io::Cursor::new(data))) + } + Err(e) => { + error!("Failed to get '{}': {}", path_str, e); + Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Get failed", e))) + } + } + } + + async fn put + Send + Debug, R: tokio::io::AsyncRead + Send + Sync + Unpin + 'static>( + &self, + user: &super::server::FtpsUser, + bytes: R, + path: P, + start_pos: u64, + ) -> Result { + let path_str = path.as_ref().to_string_lossy(); + let session_context = &user.session_context; + + let (bucket, key) = self + .parse_s3_path(&path_str) + .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; + + let key = key.ok_or_else(|| Error::new(ErrorKind::PermanentFileNotAvailable, "Cannot put to directory"))?; + + // Check if this is an append operation (start_pos > 0) + if start_pos > 0 { + return Err(Error::new( + ErrorKind::CommandNotImplemented, + "Append operations (start_pos > 0) are not supported with S3 backend", + )); + } + + // Authorize the operation + authorize_operation(session_context, &S3Action::PutObject, &bucket, Some(&key)) + .await + .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; + + // Convert AsyncRead to bytes + let bytes_vec = { + let mut buffer = Vec::new(); + let mut reader = bytes; + tokio::io::copy(&mut reader, &mut buffer) + .await + .map_err(|e| Error::new(ErrorKind::TransientFileNotAvailable, e.to_string()))?; + buffer + }; + + let file_size = bytes_vec.len(); + + let mut put_builder = PutObjectInput::builder(); + put_builder.set_bucket(bucket.clone()); + put_builder.set_key(key.clone()); + put_builder.set_content_length(Some(file_size as i64)); + + // Create StreamingBlob with known size + let data_bytes = bytes::Bytes::from(bytes_vec); + let stream = stream::once(async move { Ok::(data_bytes) }); + let streaming_blob = s3s::dto::StreamingBlob::wrap(stream); + put_builder.set_body(Some(streaming_blob)); + let put_input = put_builder + .build() + .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build PutObjectInput"))?; + + match self + .storage + .put_object( + put_input, + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + ) + .await + { + Ok(_output) => { + Ok(file_size as u64) // Return the size of the uploaded object + } + Err(e) => { + error!("FTPS put - S3 error details: {:?}", e); + Err(Error::new( + ErrorKind::PermanentFileNotAvailable, + format!("Failed to upload object: {:?}", e), + )) + } + } + } + + async fn del + Send>(&self, user: &super::server::FtpsUser, path: P) -> Result<()> { + let path_str = path.as_ref().to_string_lossy(); + let session_context = &user.session_context; + debug!("FTPS delete request for user '{}' path '{}'", user.username, path_str); + + let (bucket, key) = self + .parse_s3_path(&path_str) + .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; + + if let Some(key) = key { + // Delete file + match self + .storage + .delete_object( + &bucket, + &key, + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + ) + .await + { + Ok(_) => Ok(()), + Err(e) => { + error!("Failed to delete file '{}': {}", path_str, e); + Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("Delete failed: {}", e))) + } + } + } else { + // Delete directory (bucket) - not supported in typical FTP + Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Directory deletion not supported")) + } + } + + async fn mkd + Send>(&self, user: &super::server::FtpsUser, path: P) -> Result<()> { + let path_str = path.as_ref().to_string_lossy(); + let session_context = &user.session_context; + debug!("FTPS mkdir request for user '{}' path '{}'", user.username, path_str); + + let (bucket, _key) = self + .parse_s3_path(&path_str) + .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; + + // Create bucket for directory + match self + .storage + .create_bucket( + &bucket, + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + ) + .await + { + Ok(_) => { + debug!("Successfully created directory/bucket '{}'", path_str); + Ok(()) + } + Err(e) => { + error!("Failed to create directory/bucket '{}': {}", path_str, e); + Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("Mkdir failed: {}", e))) + } + } + } + + async fn rmd + Send>(&self, user: &super::server::FtpsUser, path: P) -> Result<()> { + let path_str = path.as_ref().to_string_lossy(); + let session_context = &user.session_context; + + let (bucket, _key) = self + .parse_s3_path(&path_str) + .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; + + // Delete bucket for directory + match self + .storage + .delete_bucket( + &bucket, + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + ) + .await + { + Ok(_) => { + debug!("Successfully removed directory/bucket '{}'", path_str); + Ok(()) + } + Err(e) => { + error!("Failed to remove directory/bucket '{}': {}", path_str, e); + Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("Rmdir failed: {}", e))) + } + } + } + + async fn cwd + Send>(&self, user: &super::server::FtpsUser, path: P) -> Result<()> { + let path_str = path.as_ref().to_string_lossy(); + let session_context = &user.session_context; + + let (bucket, _key) = self + .parse_s3_path(&path_str) + .map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?; + + // Check if bucket exists + match self + .storage + .head_bucket( + &bucket, + &session_context.principal.user_identity.credentials.access_key, + &session_context.principal.user_identity.credentials.secret_key, + ) + .await + { + Ok(_) => Ok(()), + Err(e) => { + error!("CWD to '{}' failed: {}", path_str, e); + Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("CWD failed: {}", e))) + } + } + } + + async fn rename + Send>(&self, user: &super::server::FtpsUser, from: P, to: P) -> Result<()> { + let from_str = from.as_ref().to_string_lossy(); + let to_str = to.as_ref().to_string_lossy(); + debug!("FTPS rename request for user '{}' from '{}' to '{}'", user.username, from_str, to_str); + + Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Atomic rename not supported in S3")) + } +} diff --git a/rustfs/src/protocols/ftps/mod.rs b/crates/protocols/src/ftps/mod.rs similarity index 94% rename from rustfs/src/protocols/ftps/mod.rs rename to crates/protocols/src/ftps/mod.rs index 4467d12f6..fb55c7478 100644 --- a/rustfs/src/protocols/ftps/mod.rs +++ b/crates/protocols/src/ftps/mod.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! FTPS protocol implementation - +pub mod config; pub mod driver; pub mod server; diff --git a/rustfs/src/protocols/ftps/server.rs b/crates/protocols/src/ftps/server.rs similarity index 59% rename from rustfs/src/protocols/ftps/server.rs rename to crates/protocols/src/ftps/server.rs index 94239ffb6..21fbbe1c2 100644 --- a/rustfs/src/protocols/ftps/server.rs +++ b/crates/protocols/src/ftps/server.rs @@ -12,27 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::protocols::ftps::driver::FtpsDriver; -use crate::protocols::session::context::{Protocol as SessionProtocol, SessionContext}; -use crate::protocols::session::principal::ProtocolPrincipal; -use libunftp::{ - ServerError, - auth::{AuthenticationError, UserDetail}, - options::FtpsRequired, -}; +use super::config::{FtpsConfig, FtpsInitError}; +use super::driver::FtpsDriver; +use crate::common::client::s3::StorageBackend; +use crate::common::session::{Protocol, ProtocolPrincipal, SessionContext}; +use crate::constants::{network::DEFAULT_SOURCE_IP, paths::ROOT_PATH}; +use libunftp::auth::{AuthenticationError, UserDetail}; +use libunftp::options::FtpsRequired; use std::fmt::{Debug, Display, Formatter}; -use std::net::{IpAddr, SocketAddr}; +use std::net::IpAddr; use std::path::Path; use std::sync::Arc; -use thiserror::Error; use tokio::sync::broadcast; use tracing::{debug, error, info, warn}; -const ROOT_PATH: &str = "/"; -const DEFAULT_SOURCE_IP: &str = "0.0.0.0"; -const PORT_RANGE_SEPARATOR: &str = "-"; -const PASSIVE_PORTS_PART_COUNT: usize = 2; - /// FTPS user implementation #[derive(Debug, Clone)] pub struct FtpsUser { @@ -59,106 +52,22 @@ impl Display for FtpsUser { } } -/// FTPS server initialization error -#[derive(Debug, Error)] -pub enum FtpsInitError { - #[error("failed to bind address {0}")] - Bind(#[from] std::io::Error), - - #[error("server error: {0}")] - Server(#[from] ServerError), - - #[error("invalid FTPS configuration: {0}")] - InvalidConfig(String), -} - -/// FTPS server configuration -#[derive(Debug, Clone)] -pub struct FtpsConfig { - /// Server bind address - pub bind_addr: SocketAddr, - /// Passive port range (e.g., "40000-50000") - pub passive_ports: Option, - /// External IP address for passive mode - pub external_ip: Option, - /// Whether FTPS is required - pub ftps_required: bool, - /// Certificate file path - pub cert_file: Option, - /// Private key file path - pub key_file: Option, -} - -impl FtpsConfig { - /// Validates the configuration - pub async fn validate(&self) -> Result<(), FtpsInitError> { - if self.ftps_required && (self.cert_file.is_none() || self.key_file.is_none()) { - return Err(FtpsInitError::InvalidConfig( - "FTPS is required but certificate or key file is missing".to_string(), - )); - } - - if let Some(path) = &self.cert_file - && !tokio::fs::try_exists(path).await.unwrap_or(false) - { - return Err(FtpsInitError::InvalidConfig(format!("Certificate file not found: {}", path))); - } - - if let Some(path) = &self.key_file - && !tokio::fs::try_exists(path).await.unwrap_or(false) - { - return Err(FtpsInitError::InvalidConfig(format!("Key file not found: {}", path))); - } - - // Validate passive ports format - if self.passive_ports.is_some() { - self.parse_passive_ports()?; - } - - Ok(()) - } - - /// Parse passive ports range from string format "start-end" - fn parse_passive_ports(&self) -> Result, FtpsInitError> { - match &self.passive_ports { - Some(ports) => { - let parts: Vec<&str> = ports.split(PORT_RANGE_SEPARATOR).collect(); - if parts.len() != PASSIVE_PORTS_PART_COUNT { - return Err(FtpsInitError::InvalidConfig(format!( - "Invalid passive ports format: {}, expected 'start-end'", - ports - ))); - } - - let start = parts[0] - .parse::() - .map_err(|e| FtpsInitError::InvalidConfig(format!("Invalid start port: {}", e)))?; - let end = parts[1] - .parse::() - .map_err(|e| FtpsInitError::InvalidConfig(format!("Invalid end port: {}", e)))?; - - if start > end { - return Err(FtpsInitError::InvalidConfig("Start port cannot be greater than end port".to_string())); - } - - Ok(start..=end) - } - None => Err(FtpsInitError::InvalidConfig("No passive ports configured".to_string())), - } - } -} - /// FTPS server implementation -pub struct FtpsServer { +pub struct FtpsServer { /// Server configuration config: FtpsConfig, + /// S3 storage backend + storage: S, } -impl FtpsServer { +impl FtpsServer +where + S: StorageBackend + Clone + Send + Sync + 'static + std::fmt::Debug, +{ /// Create a new FTPS server - pub async fn new(config: FtpsConfig) -> Result { + pub async fn new(config: FtpsConfig, storage: S) -> Result { config.validate().await?; - Ok(Self { config }) + Ok(Self { config, storage }) } /// Start the FTPS server @@ -168,8 +77,11 @@ impl FtpsServer { pub async fn start(&self, mut shutdown_rx: broadcast::Receiver<()>) -> Result<(), FtpsInitError> { info!("Initializing FTPS server on {}", self.config.bind_addr); - let mut server_builder = - libunftp::ServerBuilder::with_authenticator(Box::new(FtpsDriver::new), Arc::new(FtpsAuthenticator::new())); + let storage_clone = self.storage.clone(); + let mut server_builder = libunftp::ServerBuilder::with_authenticator( + Box::new(move || FtpsDriver::new(storage_clone.clone())), + Arc::new(FtpsAuthenticator::new()), + ); // Configure passive ports for data connections if let Some(passive_ports) = &self.config.passive_ports { @@ -186,19 +98,50 @@ impl FtpsServer { server_builder = server_builder.passive_host(external_ip.as_str()); } + // Configure both active and passive mode support + use libunftp::options::ActivePassiveMode; + server_builder = server_builder.active_passive_mode(ActivePassiveMode::ActiveAndPassive); + info!("FTPS server configured for both active and passive mode support"); + // Configure FTPS / TLS - if let Some(cert) = &self.config.cert_file { - if let Some(key) = &self.config.key_file { - debug!("Enabling FTPS with cert: {} and key: {}", cert, key); - server_builder = server_builder.ftps(cert, key); + if self.config.tls_enabled { + if let Some(cert_dir) = &self.config.cert_dir { + debug!("Enabling FTPS with multi-certificate support from directory: {}", cert_dir); + + // Load all certificates from directory + let cert_key_pairs = rustfs_utils::load_all_certs_from_directory(cert_dir) + .map_err(|e| FtpsInitError::InvalidConfig(format!("Failed to load certificates: {}", e)))?; + + if cert_key_pairs.is_empty() { + return Err(FtpsInitError::InvalidConfig("No valid certificates found in directory".into())); + } + + debug!("Loaded {} certificates for FTPS", cert_key_pairs.len()); + + // Create multi-certificate resolver with SNI support + let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs) + .map_err(|e| FtpsInitError::InvalidConfig(format!("Failed to create certificate resolver: {}", e)))?; + + // Build ServerConfig with SNI support + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + + let server_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_cert_resolver(std::sync::Arc::new(resolver)); + + server_builder = server_builder.ftps_manual::(std::sync::Arc::new(server_config)); if self.config.ftps_required { info!("FTPS is explicitly required for all connections"); server_builder = server_builder.ftps_required(FtpsRequired::All, FtpsRequired::All); } + } else if self.config.ftps_required { + return Err(FtpsInitError::InvalidConfig( + "FTPS required but certificate directory not provided".into(), + )); } - } else if self.config.ftps_required { - return Err(FtpsInitError::InvalidConfig("FTPS required but certificates not provided".into())); + } else { + info!("TLS disabled, running in plain FTP mode"); } // Build the server instance @@ -245,6 +188,11 @@ impl FtpsServer { pub fn config(&self) -> &FtpsConfig { &self.config } + + /// Get storage backend + pub fn storage(&self) -> &S { + &self.storage + } } /// FTPS authenticator implementation @@ -265,20 +213,15 @@ impl libunftp::auth::Authenticator for FtpsAuthenticator { use rustfs_credentials::Credentials as S3Credentials; use rustfs_iam::get; - debug!("FTPS authentication attempt for user: {}", username); - // Access IAM system let iam_sys = get().map_err(|e| { error!("IAM system unavailable during FTPS auth: {}", e); AuthenticationError::ImplPropagated("Internal authentication service unavailable".to_string(), Some(Box::new(e))) })?; - // Map FTP credentials to S3 Credentials structure - // Note: FTP PASSWORD is treated as S3 SECRET KEY let s3_creds = S3Credentials { access_key: username.to_string(), secret_key: creds.password.clone().unwrap_or_default(), - // Fields below are not used for authentication verification, but for struct compliance session_token: String::new(), expiration: None, status: String::new(), @@ -304,18 +247,14 @@ impl libunftp::auth::Authenticator for FtpsAuthenticator { AuthenticationError::BadUser })?; - // Constant time comparison is preferred if available, but for now simple eq if !identity.credentials.secret_key.eq(&s3_creds.secret_key) { warn!("FTPS login failed: Invalid secret key for '{}'", username); return Err(AuthenticationError::BadPassword); } - // Policy conditions relying on `aws:SourceIp` will currently not work correctly for FTP. - // TODO: Investigate wrapping the authenticator or using Proxy Protocol metadata if available in future libunftp versions. let source_ip: IpAddr = DEFAULT_SOURCE_IP.parse().unwrap(); - let session_context = - SessionContext::new(ProtocolPrincipal::new(Arc::new(identity.clone())), SessionProtocol::Ftps, source_ip); + let session_context = SessionContext::new(ProtocolPrincipal::new(Arc::new(identity.clone())), Protocol::Ftps, source_ip); let ftps_user = FtpsUser { username: username.to_string(), diff --git a/rustfs/src/protocols/session/mod.rs b/crates/protocols/src/lib.rs similarity index 64% rename from rustfs/src/protocols/session/mod.rs rename to crates/protocols/src/lib.rs index b206c80c2..1dee33ec7 100644 --- a/rustfs/src/protocols/session/mod.rs +++ b/crates/protocols/src/lib.rs @@ -12,7 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Session management for protocol implementations +#![deny(unsafe_code)] -pub mod context; -pub mod principal; +pub mod common; +pub mod constants; + +#[cfg(feature = "ftps")] +pub mod ftps; + +pub use common::session::Protocol; +pub use common::{AuthorizationError, ProtocolPrincipal, S3Action, SessionContext, authorize_operation}; + +#[cfg(feature = "ftps")] +pub use ftps::{config::FtpsConfig, server::FtpsServer}; diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 113dc2c57..68ce4479b 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -33,6 +33,7 @@ path = "src/main.rs" [features] default = ["metrics"] metrics = [] +ftps = ["rustfs-protocols/ftps"] [lints] workspace = true @@ -54,6 +55,7 @@ rustfs-madmin = { workspace = true } rustfs-notify = { workspace = true } rustfs-obs = { workspace = true } rustfs-policy = { workspace = true } +rustfs-protocols = { workspace = true } rustfs-protos = { workspace = true } rustfs-rio.workspace = true rustfs-s3select-api = { workspace = true } @@ -132,12 +134,6 @@ aes-gcm = { workspace = true } # Observability and Metrics metrics = { workspace = true } -# FTP and SFTP Libraries -libunftp = { workspace = true } -russh = { workspace = true } -russh-sftp = { workspace = true } -ssh-key = { workspace = true } - [target.'cfg(target_os = "linux")'.dependencies] libsystemd.workspace = true diff --git a/rustfs/src/init.rs b/rustfs/src/init.rs index 1c7c284e2..7f6542d36 100644 --- a/rustfs/src/init.rs +++ b/rustfs/src/init.rs @@ -317,131 +317,160 @@ pub(crate) fn init_buffer_profile_system(opt: &config::Opt) { } } +/// Initialize the FTP system +/// +/// This function initializes the FTP server (non-encrypted) if enabled in the configuration. +#[cfg(feature = "ftps")] +#[instrument(skip_all)] +pub async fn init_ftp_system() -> Result>, Box> { + { + use crate::protocols::ProtocolStorageClient; + use rustfs_config::{DEFAULT_FTP_ADDRESS, ENV_FTP_ADDRESS, ENV_FTP_ENABLE, ENV_FTP_EXTERNAL_IP, ENV_FTP_PASSIVE_PORTS}; + use rustfs_protocols::constants::defaults::DEFAULT_FTPS_PASSIVE_PORTS; + use rustfs_protocols::{FtpsConfig, FtpsServer}; + // Check if FTP is enabled + let ftp_enable = rustfs_utils::get_env_bool(ENV_FTP_ENABLE, false); + if !ftp_enable { + debug!("FTP system is disabled"); + return Ok(None); + } + + // Parse FTP address - force IPv4 for libunftp compatibility + let ftp_address_str = rustfs_utils::get_env_str(ENV_FTP_ADDRESS, DEFAULT_FTP_ADDRESS); + let addr = rustfs_utils::net::parse_and_resolve_address(&ftp_address_str) + .map_err(|e| format!("Invalid FTP address '{ftp_address_str}': {e}"))?; + // Force IPv4 binding to avoid libunftp IPv6 compatibility issues + let addr = if addr.is_ipv6() { + std::net::SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), addr.port()) + } else { + addr + }; + + // Get FTP configuration from environment variables + let passive_ports = + rustfs_utils::get_env_opt_str(ENV_FTP_PASSIVE_PORTS).or_else(|| Some(DEFAULT_FTPS_PASSIVE_PORTS.to_string())); // Default passive ports range + let external_ip = rustfs_utils::get_env_opt_str(ENV_FTP_EXTERNAL_IP); + + // Create FTP configuration (TLS disabled, FTPS not required) + let config = FtpsConfig { + bind_addr: addr, + passive_ports, + external_ip, + ftps_required: false, + tls_enabled: false, + cert_dir: None, + ca_file: None, + }; + + // Validate FTP configuration + config.validate().await?; + + // Create FTP server with protocol storage client + let fs = crate::storage::ecfs::FS::new(); + let storage_client = ProtocolStorageClient::new(fs); + let server: FtpsServer = FtpsServer::new(config, storage_client).await?; + + // Log server configuration + info!( + "FTP server configured on {} with passive ports {:?}", + server.config().bind_addr, + server.config().passive_ports + ); + + // Start FTP server in background task with proper shutdown support + let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel(1); + + tokio::spawn(async move { + if let Err(e) = server.start(shutdown_rx).await { + error!("FTP server error: {}", e); + } + info!("FTP server shutdown completed"); + }); + + info!("FTP system initialized successfully"); + Ok(Some(shutdown_tx)) + } +} + /// Initialize the FTPS system /// /// This function initializes the FTPS server if enabled in the configuration. /// It sets up the FTPS server with the appropriate configuration and starts -/// the server in a background task. -/// -/// MINIO CONSTRAINT: FTPS server MUST follow the same lifecycle management -/// as other services and MUST integrate with the global shutdown system. +/// the server in a background task.s +#[cfg(feature = "ftps")] #[instrument(skip_all)] -pub async fn init_ftp_system( - shutdown_tx: tokio::sync::broadcast::Sender<()>, -) -> Result<(), Box> { - use crate::protocols::ftps::server::{FtpsConfig, FtpsServer}; - use std::net::SocketAddr; - - // Check if FTPS is enabled - let ftps_enable = rustfs_utils::get_env_bool(rustfs_config::ENV_FTPS_ENABLE, false); - if !ftps_enable { - debug!("FTPS system is disabled"); - return Ok(()); - } - - // Parse FTPS address - let ftps_address_str = rustfs_utils::get_env_str(rustfs_config::ENV_FTPS_ADDRESS, rustfs_config::DEFAULT_FTPS_ADDRESS); - let addr: SocketAddr = ftps_address_str - .parse() - .map_err(|e| format!("Invalid FTPS address '{ftps_address_str}': {e}"))?; - - // Get FTPS configuration from environment variables - let cert_file = rustfs_utils::get_env_opt_str(rustfs_config::ENV_FTPS_CERTS_FILE); - let key_file = rustfs_utils::get_env_opt_str(rustfs_config::ENV_FTPS_KEY_FILE); - let passive_ports = rustfs_utils::get_env_opt_str(rustfs_config::ENV_FTPS_PASSIVE_PORTS); - let external_ip = rustfs_utils::get_env_opt_str(rustfs_config::ENV_FTPS_EXTERNAL_IP); - - // Create FTPS configuration - let config = FtpsConfig { - bind_addr: addr, - passive_ports, - external_ip, - ftps_required: true, - cert_file, - key_file, - }; - - // Create FTPS server - let server = FtpsServer::new(config).await?; - - // Log server configuration - info!( - "FTPS server configured on {} with passive ports {:?}", - server.config().bind_addr, - server.config().passive_ports - ); - - // Start FTPS server in background task - let shutdown_rx = shutdown_tx.subscribe(); - tokio::spawn(async move { - if let Err(e) = server.start(shutdown_rx).await { - error!("FTPS server error: {}", e); +pub async fn init_ftps_system() -> Result>, Box> { + { + use crate::protocols::ProtocolStorageClient; + use rustfs_config::{ + DEFAULT_FTPS_ADDRESS, ENV_FTPS_ADDRESS, ENV_FTPS_CA_FILE, ENV_FTPS_CERTS_DIR, ENV_FTPS_ENABLE, ENV_FTPS_EXTERNAL_IP, + ENV_FTPS_PASSIVE_PORTS, ENV_FTPS_TLS_ENABLED, + }; + use rustfs_protocols::constants::defaults::DEFAULT_FTPS_PASSIVE_PORTS; + use rustfs_protocols::{FtpsConfig, FtpsServer}; + // Check if FTPS is enabled + let ftps_enable = rustfs_utils::get_env_bool(ENV_FTPS_ENABLE, false); + if !ftps_enable { + debug!("FTPS system is disabled"); + return Ok(None); } - }); - info!("FTPS system initialized successfully"); - Ok(()) -} + // Parse FTPS address - force IPv4 for libunftp compatibility + let ftps_address_str = rustfs_utils::get_env_str(ENV_FTPS_ADDRESS, DEFAULT_FTPS_ADDRESS); + let addr = rustfs_utils::net::parse_and_resolve_address(&ftps_address_str) + .map_err(|e| format!("Invalid FTPS address '{ftps_address_str}': {e}"))?; + // Force IPv4 binding to avoid libunftp IPv6 compatibility issues + let addr = if addr.is_ipv6() { + std::net::SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), addr.port()) + } else { + addr + }; -/// Initialize the SFTP system -/// -/// This function initializes the SFTP server if enabled in the configuration. -/// It sets up the SFTP server with the appropriate configuration and starts -/// the server in a background task. -/// -/// MINIO CONSTRAINT: SFTP server MUST follow the same lifecycle management -/// as other services and MUST integrate with the global shutdown system. -#[instrument(skip_all)] -pub async fn init_sftp_system( - shutdown_tx: tokio::sync::broadcast::Sender<()>, -) -> Result<(), Box> { - use crate::protocols::sftp::server::{SftpConfig, SftpServer}; - use std::net::SocketAddr; + // Get FTPS configuration from environment variables + let tls_enabled = rustfs_utils::get_env_bool(ENV_FTPS_TLS_ENABLED, true); + let cert_dir = rustfs_utils::get_env_opt_str(ENV_FTPS_CERTS_DIR); + let ca_file = rustfs_utils::get_env_opt_str(ENV_FTPS_CA_FILE); + let passive_ports = + rustfs_utils::get_env_opt_str(ENV_FTPS_PASSIVE_PORTS).or_else(|| Some(DEFAULT_FTPS_PASSIVE_PORTS.to_string())); // Default passive ports range + let external_ip = rustfs_utils::get_env_opt_str(ENV_FTPS_EXTERNAL_IP); - // Check if SFTP is enabled - let sftp_enable = rustfs_utils::get_env_bool(rustfs_config::ENV_SFTP_ENABLE, false); - if !sftp_enable { - debug!("SFTP system is disabled"); - return Ok(()); + // Create FTPS configuration + let config = FtpsConfig { + bind_addr: addr, + passive_ports, + external_ip, + ftps_required: true, + tls_enabled, + cert_dir, + ca_file, + }; + + // Validate FTPS configuration + config.validate().await?; + + // Create FTPS server with protocol storage client + let fs = crate::storage::ecfs::FS::new(); + let storage_client = ProtocolStorageClient::new(fs); + let server: FtpsServer = FtpsServer::new(config, storage_client).await?; + + // Log server configuration + info!( + "FTPS server configured on {} with passive ports {:?}", + server.config().bind_addr, + server.config().passive_ports + ); + + // Start FTPS server in background task with proper shutdown support + let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel(1); + + tokio::spawn(async move { + if let Err(e) = server.start(shutdown_rx).await { + error!("FTPS server error: {}", e); + } + info!("FTPS server shutdown completed"); + }); + + info!("FTPS system initialized successfully"); + Ok(Some(shutdown_tx)) } - - // Parse SFTP address - let sftp_address_str = rustfs_utils::get_env_str(rustfs_config::ENV_SFTP_ADDRESS, rustfs_config::DEFAULT_SFTP_ADDRESS); - let addr: SocketAddr = sftp_address_str - .parse() - .map_err(|e| format!("Invalid SFTP address '{sftp_address_str}': {e}"))?; - - // Get SFTP configuration from environment variables - let host_key = rustfs_utils::get_env_opt_str(rustfs_config::ENV_SFTP_HOST_KEY); - let authorized_keys = rustfs_utils::get_env_opt_str(rustfs_config::ENV_SFTP_AUTHORIZED_KEYS); - - // Create SFTP configuration - let config = SftpConfig { - bind_addr: addr, - require_key_auth: false, // TODO: Add key auth configuration - cert_file: None, // CA certificates for client certificate authentication - key_file: host_key, // SFTP server host key - authorized_keys_file: authorized_keys, // Pre-loaded authorized SSH public keys - }; - - // Create SFTP server - let server = SftpServer::new(config)?; - - // Log server configuration - info!( - "SFTP server configured on {} with key auth requirement: {}", - server.config().bind_addr, - server.config().require_key_auth - ); - - // Start SFTP server in background task - let shutdown_rx = shutdown_tx.subscribe(); - tokio::spawn(async move { - if let Err(e) = server.start(shutdown_rx).await { - error!("SFTP server error: {}", e); - } - }); - - info!("SFTP system initialized successfully"); - Ok(()) } diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 267fd7a5d..2eaa84501 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -19,6 +19,7 @@ mod error; mod init; mod license; mod profiling; +#[cfg(feature = "ftps")] mod protocols; mod server; mod storage; @@ -27,9 +28,12 @@ mod version; // Ensure the correct path for parse_license is imported use crate::init::{ - add_bucket_notification_configuration, init_buffer_profile_system, init_ftp_system, init_kms_system, init_sftp_system, - init_update_check, print_server_info, + add_bucket_notification_configuration, init_buffer_profile_system, init_kms_system, init_update_check, print_server_info, }; + +#[cfg(feature = "ftps")] +use crate::init::{init_ftp_system, init_ftps_system}; + use crate::server::{ SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, init_cert, init_event_notifier, shutdown_event_notifier, start_audit_system, start_http_server, stop_audit_system, wait_for_shutdown, @@ -287,14 +291,45 @@ async fn run(opt: config::Opt) -> Result<()> { // Initialize KMS system if enabled init_kms_system(&opt).await?; - // Create a shutdown channel for FTP/SFTP services - let (ftp_sftp_shutdown_tx, _) = tokio::sync::broadcast::channel(1); - // Initialize FTP system if enabled - init_ftp_system(ftp_sftp_shutdown_tx.clone()).await.map_err(Error::other)?; + #[cfg(feature = "ftps")] + let ftp_shutdown_tx = match init_ftp_system().await { + Ok(Some(tx)) => { + info!("FTP system initialized successfully"); + Some(tx) + } + Ok(None) => { + info!("FTP system disabled"); + None + } + Err(e) => { + error!("Failed to initialize FTP system: {}", e); + return Err(Error::other(e)); + } + }; - // Initialize SFTP system if enabled - init_sftp_system(ftp_sftp_shutdown_tx.clone()).await.map_err(Error::other)?; + #[cfg(not(feature = "ftps"))] + let ftp_shutdown_tx: Option> = None; + + // Initialize FTPS system if enabled + #[cfg(feature = "ftps")] + let ftps_shutdown_tx = match init_ftps_system().await { + Ok(Some(tx)) => { + info!("FTPS system initialized successfully"); + Some(tx) + } + Ok(None) => { + info!("FTPS system disabled"); + None + } + Err(e) => { + error!("Failed to initialize FTPS system: {}", e); + return Err(Error::other(e)); + } + }; + + #[cfg(not(feature = "ftps"))] + let ftps_shutdown_tx: Option> = None; // Initialize buffer profiling system init_buffer_profile_system(&opt); @@ -386,11 +421,27 @@ async fn run(opt: config::Opt) -> Result<()> { match wait_for_shutdown().await { #[cfg(unix)] ShutdownSignal::CtrlC | ShutdownSignal::Sigint | ShutdownSignal::Sigterm => { - handle_shutdown(&state_manager, s3_shutdown_tx, console_shutdown_tx, ftp_sftp_shutdown_tx, ctx.clone()).await; + handle_shutdown( + &state_manager, + s3_shutdown_tx, + console_shutdown_tx, + ftp_shutdown_tx, + ftps_shutdown_tx, + ctx.clone(), + ) + .await; } #[cfg(not(unix))] ShutdownSignal::CtrlC => { - handle_shutdown(&state_manager, s3_shutdown_tx, console_shutdown_tx, ftp_sftp_shutdown_tx, ctx.clone()).await; + handle_shutdown( + &state_manager, + s3_shutdown_tx, + console_shutdown_tx, + ftp_shutdown_tx, + ftps_shutdown_tx, + ctx.clone(), + ) + .await; } } @@ -403,7 +454,8 @@ async fn handle_shutdown( state_manager: &ServiceStateManager, s3_shutdown_tx: Option>, console_shutdown_tx: Option>, - ftp_sftp_shutdown_tx: tokio::sync::broadcast::Sender<()>, + ftp_shutdown_tx: Option>, + ftps_shutdown_tx: Option>, ctx: CancellationToken, ) { ctx.cancel(); @@ -439,6 +491,23 @@ async fn handle_shutdown( ); } + // Shutdown FTP and FTPS servers + if let Some(ftp_shutdown_tx) = ftp_shutdown_tx { + info!( + target: "rustfs::main::handle_shutdown", + "Shutting down FTP server..." + ); + let _ = ftp_shutdown_tx.send(()); + } + + if let Some(ftps_shutdown_tx) = ftps_shutdown_tx { + info!( + target: "rustfs::main::handle_shutdown", + "Shutting down FTPS server..." + ); + let _ = ftps_shutdown_tx.send(()); + } + // Stop the notification system info!( target: "rustfs::main::handle_shutdown", @@ -477,9 +546,6 @@ async fn handle_shutdown( // Wait for the worker thread to complete the cleaning work tokio::time::sleep(SHUTDOWN_TIMEOUT).await; - // Send shutdown signal to FTP/SFTP services - let _ = ftp_sftp_shutdown_tx.send(()); - // the last updated status is stopped state_manager.update(ServiceState::Stopped); info!( diff --git a/rustfs/src/protocols/README.md b/rustfs/src/protocols/README.md deleted file mode 100644 index 1aa0c893a..000000000 --- a/rustfs/src/protocols/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# RustFS Protocols - -RustFS provides multiple protocol interfaces for accessing object storage, including: - -- **FTPS** - File Transfer Protocol over TLS for traditional file transfers -- **SFTP** - SSH File Transfer Protocol for secure file transfers with key-based authentication - -## Quick Start - -### Enable Protocols on Startup - -```bash -# Start RustFS with FTPS enabled -export RUSTFS_FTPS_ENABLE=true -export RUSTFS_FTPS_CERTS_FILE=/path/to/cert.pem -export RUSTFS_FTPS_KEY_FILE=/path/to/key.pem -rustfs --address 0.0.0.0:9000 --access-key rustfsadmin --secret-key rustfsadmin /data - -# Start RustFS with SFTP enabled -export RUSTFS_SFTP_ENABLE=true -export RUSTFS_SFTP_HOST_KEY=/path/to/host_key -export RUSTFS_SFTP_AUTHORIZED_KEYS=/path/to/authorized_keys -rustfs --address 0.0.0.0:9000 --access-key rustfsadmin --secret-key rustfsadmin /data -``` - -## Protocol Details - -### FTPS -- **Port**: 8021 (default) -- **Protocol**: FTP over TLS (FTPS) -- **Authentication**: Access Key / Secret Key (same as S3) -- **Features**: - - File upload/download - - Directory listing - - File deletion - - Passive mode data connections -- **Limitations**: - - Cannot create/delete buckets (use S3 API) - - No file rename/copy operations - - No multipart upload - -### SFTP -- **Port**: 8022 (default) -- **Protocol**: SSH File Transfer Protocol -- **Authentication**: - - Password (Access Key / Secret Key) - - SSH Public Key (recommended) - - SSH Certificate (optional) -- **Features**: - - File upload/download - - Directory listing and manipulation - - File deletion - - Bucket creation/deletion via mkdir/rmdir -- **Limitations**: - - No file rename/copy operations - - No multipart upload - - No symlinks or file attributes modification - -- **Documentation: [SFTP README](./sftp/README.md)** - -## Architecture - -``` -┌─────────────────────────────────────────┐ -│ RustFS Client │ -│ (FTPS Client, SFTP Client) │ -└─────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────┐ -│ Protocol Gateway Layer │ -│ - Action Mapping │ -│ - Authorization (IAM Policy) │ -│ - Operation Support Check │ -└─────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────┐ -│ Internal S3 Client │ -│ (ProtocolS3Client) │ -└─────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────┐ -│ Storage Layer (ECStore) │ -│ - Object Storage │ -│ - Erasure Coding │ -│ - Metadata Management │ -└─────────────────────────────────────────┘ -``` - -## Security - -### Encryption -- **FTPS**: TLS 1.2/1.3 for all control and data connections -- **SFTP**: SSH2 protocol with modern cipher suites (Ed25519, RSA, ECDSA) - -### Authentication -All protocols share the same IAM-based authentication system: -- **Access Key**: Username identifier -- **Secret Key**: Password/API key -- **SSH Public Keys**: For SFTP key-based authentication -- **IAM Policies**: Fine-grained access control - -### Authorization -Unified authorization based on IAM policies: -- Supports `s3:*` action namespace -- Condition-based policies (IP, time, etc.) -- Bucket-level and object-level permissions - -## Troubleshooting - -### FTPS Connection Issues -```bash -# Check TLS certificate -openssl s_client -connect localhost:21 -starttls ftp - -# Test with lftp -lftp -u username,password -e "set ssl:verify-certificate no; ls; bye" ftps://localhost -``` - -### SFTP Connection Issues -```bash -# Verbose SFTP connection -sftp -vvv -o StrictHostKeyChecking=no -o LogLevel=DEBUG3 user@localhost - -# Check SSH host key -ssh-keygen -l -f /path/to/host_key -``` - -## Configuration Reference - -### FTPS Configuration - -| Environment Variable | Description | Default | -|---------------------|-------------|---------| -| `RUSTFS_FTPS_ENABLE` | Enable FTPS server | `false` | -| `RUSTFS_FTPS_ADDRESS` | FTPS bind address | `0.0.0.0:8021` | -| `RUSTFS_FTPS_CERTS_FILE` | TLS certificate file | - | -| `RUSTFS_FTPS_KEY_FILE` | TLS private key file | - | -| `RUSTFS_FTPS_PASSIVE_PORTS` | Passive port range | - | -| `RUSTFS_FTPS_EXTERNAL_IP` | External IP for NAT | - | - -### SFTP Configuration - -| Environment Variable | Description | Default | -|---------------------|-------------|---------| -| `RUSTFS_SFTP_ENABLE` | Enable SFTP server | `false` | -| `RUSTFS_SFTP_ADDRESS` | SFTP bind address | `0.0.0.0:8022` | -| `RUSTFS_SFTP_HOST_KEY` | SSH host key file | - | -| `RUSTFS_SFTP_AUTHORIZED_KEYS` | Authorized keys file | - | - -## See Also - -- [FTPS README](./ftps/README.md) - Detailed FTPS usage -- [SFTP README](./sftp/README.md) - Detailed SFTP usage -- [RustFS Documentation](https://rustfs.com/docs/) -- [IAM Policy Reference](https://rustfs.com/docs/iam-policies) \ No newline at end of file diff --git a/rustfs/src/protocols/client.rs b/rustfs/src/protocols/client.rs new file mode 100644 index 000000000..65347d84f --- /dev/null +++ b/rustfs/src/protocols/client.rs @@ -0,0 +1,462 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::storage::ecfs::FS; +use http::{HeaderMap, Method}; +use rustfs_credentials; +use s3s::dto::*; +use s3s::{S3, S3Request, S3Result}; +use tokio_stream::Stream; +use tracing::trace; + +/// Request parameters for creating S3 requests +#[derive(Debug)] +struct RequestParams<'a> { + bucket: Option, + object: Option, + access_key: &'a str, + secret_key: &'a str, +} + +/// Protocol storage client that implements the StorageBackend trait +#[derive(Clone, Debug)] +pub struct ProtocolStorageClient { + /// FS instance for storage operations + fs: FS, +} + +impl ProtocolStorageClient { + /// Create a new protocol storage client + pub fn new(fs: FS) -> Self { + Self { fs } + } + + /// Create a proper S3Request with ReqInfo extension for authorization + async fn create_request( + &self, + input: T, + method: Method, + uri: http::Uri, + params: RequestParams<'_>, + ) -> S3Result> { + let mut extensions = http::Extensions::default(); + + let is_owner = if let Some(global_cred) = rustfs_credentials::get_global_action_cred() { + params.access_key == global_cred.access_key + } else { + false + }; + + let credentials = Some(s3s::auth::Credentials { + access_key: params.access_key.to_string(), + secret_key: params.secret_key.to_string().into(), + }); + + extensions.insert(crate::storage::access::ReqInfo { + cred: Some(rustfs_credentials::Credentials { + access_key: params.access_key.to_string(), + secret_key: params.secret_key.to_string(), + session_token: String::new(), + expiration: None, + status: String::new(), + parent_user: String::new(), + groups: None, + claims: None, + name: None, + description: None, + }), + is_owner, + bucket: params.bucket, + object: params.object, + version_id: None, + region: None, + }); + + let req = S3Request { + input, + method, + uri, + headers: HeaderMap::default(), + extensions, + credentials, + region: None, + service: None, + trailing_headers: None, + }; + Ok(req) + } +} + +#[async_trait::async_trait] +impl rustfs_protocols::common::client::s3::StorageBackend for ProtocolStorageClient { + type Error = s3s::S3Error; + + async fn get_object( + &self, + bucket: &str, + key: &str, + access_key: &str, + secret_key: &str, + start_pos: Option, + ) -> Result { + trace!( + "Protocol storage client GetObject request: bucket={}, key={}, start_pos={:?}", + bucket, key, start_pos + ); + + let mut builder = GetObjectInput::builder().bucket(bucket.to_string()).key(key.to_string()); + + // Add range header if start_pos is specified + if let Some(start) = start_pos { + let range = s3s::dto::Range::Int { + first: start, + last: None, // Read to end of file + }; + builder = builder.range(Some(range)); + } + + let input = builder.build().map_err(|e| { + s3s::S3Error::with_message(s3s::S3ErrorCode::InvalidRequest, format!("Failed to build GetObjectInput: {}", e)) + })?; + + let uri: http::Uri = format!("/{}{}", bucket, key).parse().unwrap_or_default(); + let req = self + .create_request( + input, + Method::GET, + uri, + RequestParams { + bucket: Some(bucket.to_string()), + object: Some(key.to_string()), + access_key, + secret_key, + }, + ) + .await?; + + match self.fs.get_object(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } + + async fn put_object( + &self, + input: PutObjectInput, + access_key: &str, + secret_key: &str, + ) -> Result { + trace!("Protocol storage client PutObject request: bucket={}, key={:?}", input.bucket, input.key); + + let bucket = input.bucket.clone(); + let key = input.key.clone(); + let uri: http::Uri = format!("/{}{}", bucket, key).parse().unwrap_or_default(); + + let mut headers = HeaderMap::default(); + if let Some(ref body) = input.body { + let (lower, upper) = body.size_hint(); + if let Some(len) = upper { + headers.insert("content-length", len.to_string().parse().unwrap()); + } else if lower > 0 { + headers.insert("content-length", lower.to_string().parse().unwrap()); + } + } + + let req = self + .create_request( + input, + Method::PUT, + uri, + RequestParams { + bucket: Some(bucket), + object: Some(key), + access_key, + secret_key, + }, + ) + .await?; + let req = S3Request { headers, ..req }; + + match self.fs.put_object(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } + + async fn delete_object( + &self, + bucket: &str, + key: &str, + access_key: &str, + secret_key: &str, + ) -> Result { + trace!("Protocol storage client DeleteObject request: bucket={}, key={}", bucket, key); + + let input = DeleteObjectInput::builder() + .bucket(bucket.to_string()) + .key(key.to_string()) + .build() + .map_err(|e| { + s3s::S3Error::with_message(s3s::S3ErrorCode::InvalidRequest, format!("Failed to build DeleteObjectInput: {}", e)) + })?; + + let uri: http::Uri = format!("/{}{}", bucket, key).parse().unwrap_or_default(); + let req = self + .create_request( + input, + Method::DELETE, + uri, + RequestParams { + bucket: Some(bucket.to_string()), + object: Some(key.to_string()), + access_key, + secret_key, + }, + ) + .await?; + + match self.fs.delete_object(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } + + async fn head_object( + &self, + bucket: &str, + key: &str, + access_key: &str, + secret_key: &str, + ) -> Result { + trace!("Protocol storage client HeadObject request: bucket={}, key={}", bucket, key); + + let input = HeadObjectInput::builder() + .bucket(bucket.to_string()) + .key(key.to_string()) + .build() + .map_err(|e| { + s3s::S3Error::with_message(s3s::S3ErrorCode::InvalidRequest, format!("Failed to build HeadObjectInput: {}", e)) + })?; + + let uri: http::Uri = format!("/{}{}", bucket, key).parse().unwrap_or_default(); + let req = self + .create_request( + input, + Method::HEAD, + uri, + RequestParams { + bucket: Some(bucket.to_string()), + object: Some(key.to_string()), + access_key, + secret_key, + }, + ) + .await?; + + match self.fs.head_object(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } + + async fn head_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result { + trace!("Protocol storage client HeadBucket request: bucket={}", bucket); + + let input = HeadBucketInput::builder().bucket(bucket.to_string()).build().map_err(|e| { + s3s::S3Error::with_message(s3s::S3ErrorCode::InvalidRequest, format!("Failed to build HeadBucketInput: {}", e)) + })?; + + let uri: http::Uri = format!("/{}", bucket).parse().unwrap_or_default(); + let req = self + .create_request( + input, + Method::HEAD, + uri, + RequestParams { + bucket: Some(bucket.to_string()), + object: None, + access_key, + secret_key, + }, + ) + .await?; + + match self.fs.head_bucket(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } + + async fn list_objects_v2( + &self, + input: ListObjectsV2Input, + access_key: &str, + secret_key: &str, + ) -> Result { + trace!("Protocol storage client ListObjectsV2 request: bucket={}", input.bucket); + + let bucket = input.bucket.clone(); + let uri: http::Uri = format!("/{}?list-type=2", bucket).parse().unwrap_or_default(); + let req = self + .create_request( + input, + Method::GET, + uri, + RequestParams { + bucket: Some(bucket), + object: None, + access_key, + secret_key, + }, + ) + .await?; + + match self.fs.list_objects_v2(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } + + async fn list_buckets(&self, access_key: &str, secret_key: &str) -> Result { + trace!("Protocol storage client ListBuckets request: access_key={}", access_key); + + let input = ListBucketsInput::builder().build().map_err(|e| { + s3s::S3Error::with_message(s3s::S3ErrorCode::InvalidRequest, format!("Failed to build ListBucketsInput: {}", e)) + })?; + + let req = self + .create_request( + input, + Method::GET, + http::Uri::from_static("/"), + RequestParams { + bucket: None, + object: None, + access_key, + secret_key, + }, + ) + .await?; + + match self.fs.list_buckets(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } + + async fn create_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result { + trace!("Protocol storage client CreateBucket request: bucket={}", bucket); + + let input = CreateBucketInput::builder().bucket(bucket.to_string()).build().map_err(|e| { + s3s::S3Error::with_message(s3s::S3ErrorCode::InvalidRequest, format!("Failed to build CreateBucketInput: {}", e)) + })?; + + let uri: http::Uri = format!("/{}", bucket).parse().unwrap_or_default(); + let req = self + .create_request( + input, + Method::PUT, + uri, + RequestParams { + bucket: Some(bucket.to_string()), + object: None, + access_key, + secret_key, + }, + ) + .await?; + + match self.fs.create_bucket(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } + + async fn get_object_range( + &self, + bucket: &str, + key: &str, + access_key: &str, + secret_key: &str, + start_pos: u64, + length: u64, + ) -> Result { + trace!( + "Protocol storage client GetObjectRange request: bucket={}, key={}, start={}, length={}", + bucket, key, start_pos, length + ); + + let range = s3s::dto::Range::Int { + first: start_pos, + last: Some(start_pos + length - 1), + }; + + let input = GetObjectInput::builder() + .bucket(bucket.to_string()) + .key(key.to_string()) + .range(Some(range)) + .build() + .map_err(|e| { + s3s::S3Error::with_message(s3s::S3ErrorCode::InvalidRequest, format!("Failed to build GetObjectInput: {}", e)) + })?; + + let uri: http::Uri = format!("/{}{}", bucket, key).parse().unwrap_or_default(); + let req = self + .create_request( + input, + Method::GET, + uri, + RequestParams { + bucket: Some(bucket.to_string()), + object: Some(key.to_string()), + access_key, + secret_key, + }, + ) + .await?; + + match self.fs.get_object(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } + + async fn delete_bucket(&self, bucket: &str, access_key: &str, secret_key: &str) -> Result { + trace!("Protocol storage client DeleteBucket request: bucket={}", bucket); + + let input = DeleteBucketInput::builder().bucket(bucket.to_string()).build().map_err(|e| { + s3s::S3Error::with_message(s3s::S3ErrorCode::InvalidRequest, format!("Failed to build DeleteBucketInput: {}", e)) + })?; + + let uri: http::Uri = format!("/{}", bucket).parse().unwrap_or_default(); + let req = self + .create_request( + input, + Method::DELETE, + uri, + RequestParams { + bucket: Some(bucket.to_string()), + object: None, + access_key, + secret_key, + }, + ) + .await?; + + match self.fs.delete_bucket(req).await { + Ok(response) => Ok(response.output), + Err(e) => Err(e), + } + } +} diff --git a/rustfs/src/protocols/client/s3.rs b/rustfs/src/protocols/client/s3.rs deleted file mode 100644 index 361951b87..000000000 --- a/rustfs/src/protocols/client/s3.rs +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::storage::ecfs::FS; -use http::{HeaderMap, Method}; -use rustfs_credentials; -use s3s::dto::*; -use s3s::{S3, S3Request, S3Result}; -use tokio_stream::Stream; -use tracing::trace; - -/// S3 client for internal protocol use -pub struct ProtocolS3Client { - /// FS instance for internal operations - fs: FS, - /// Access key for the client - access_key: String, -} - -impl ProtocolS3Client { - /// Create a new protocol S3 client - pub fn new(fs: FS, access_key: String) -> Self { - Self { fs, access_key } - } - - /// Get object - maps to S3 GetObject - pub async fn get_object(&self, input: GetObjectInput) -> S3Result { - trace!( - "Protocol S3 client GetObject request: bucket={}, key={:?}, access_key={}", - input.bucket, input.key, self.access_key - ); - // Go through standard S3 API path - let uri: http::Uri = format!("/{}{}", input.bucket, input.key.as_str()).parse().unwrap_or_default(); - let req = S3Request { - input, - method: Method::GET, - uri, - headers: HeaderMap::default(), - extensions: http::Extensions::default(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; - let resp = self.fs.get_object(req).await?; - Ok(resp.output) - } - - /// Put object - maps to S3 PutObject - pub async fn put_object(&self, input: PutObjectInput) -> S3Result { - trace!( - "Protocol S3 client PutObject request: bucket={}, key={:?}, access_key={}", - input.bucket, input.key, self.access_key - ); - let uri: http::Uri = format!("/{}{}", input.bucket, input.key.as_str()).parse().unwrap_or_default(); - - // Set required headers for put operation - let mut headers = HeaderMap::default(); - if let Some(ref body) = input.body { - let (lower, upper) = body.size_hint(); - if let Some(len) = upper { - headers.insert("content-length", len.to_string().parse().unwrap()); - } else if lower > 0 { - headers.insert("content-length", lower.to_string().parse().unwrap()); - } - } - - let req = S3Request { - input, - method: Method::PUT, - uri, - headers, - extensions: http::Extensions::default(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; - let resp = self.fs.put_object(req).await?; - Ok(resp.output) - } - - /// Delete object - maps to S3 DeleteObject - pub async fn delete_object(&self, input: DeleteObjectInput) -> S3Result { - trace!( - "Protocol S3 client DeleteObject request: bucket={}, key={:?}, access_key={}", - input.bucket, input.key, self.access_key - ); - let uri: http::Uri = format!("/{}{}", input.bucket, input.key.as_str()).parse().unwrap_or_default(); - let req = S3Request { - input, - method: Method::DELETE, - uri, - headers: HeaderMap::default(), - extensions: http::Extensions::default(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; - let resp = self.fs.delete_object(req).await?; - Ok(resp.output) - } - - /// Head object - maps to S3 HeadObject - pub async fn head_object(&self, input: HeadObjectInput) -> S3Result { - trace!( - "Protocol S3 client HeadObject request: bucket={}, key={:?}, access_key={}", - input.bucket, input.key, self.access_key - ); - let uri: http::Uri = format!("/{}{}", input.bucket, input.key.as_str()).parse().unwrap_or_default(); - let req = S3Request { - input, - method: Method::HEAD, - uri, - headers: HeaderMap::default(), - extensions: http::Extensions::default(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; - let resp = self.fs.head_object(req).await?; - Ok(resp.output) - } - - /// Head bucket - maps to S3 HeadBucket - pub async fn head_bucket(&self, input: HeadBucketInput) -> S3Result { - trace!( - "Protocol S3 client HeadBucket request: bucket={}, access_key={}", - input.bucket, self.access_key - ); - let uri: http::Uri = format!("/{}", input.bucket).parse().unwrap_or_default(); - let req = S3Request { - input, - method: Method::HEAD, - uri, - headers: HeaderMap::default(), - extensions: http::Extensions::default(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; - let resp = self.fs.head_bucket(req).await?; - Ok(resp.output) - } - - /// List objects v2 - maps to S3 ListObjectsV2 - pub async fn list_objects_v2(&self, input: ListObjectsV2Input) -> S3Result { - trace!( - "Protocol S3 client ListObjectsV2 request: bucket={}, access_key={}", - input.bucket, self.access_key - ); - let uri: http::Uri = format!("/{}?list-type=2", input.bucket).parse().unwrap_or_default(); - let req = S3Request { - input, - method: Method::GET, - uri, - headers: HeaderMap::default(), - extensions: http::Extensions::default(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; - let resp = self.fs.list_objects_v2(req).await?; - Ok(resp.output) - } - - /// List buckets - maps to S3 ListBuckets - /// Note: This requires credentials and ReqInfo because list_buckets performs credential validation - pub async fn list_buckets(&self, input: ListBucketsInput, secret_key: &str) -> S3Result { - trace!("Protocol S3 client ListBuckets request: access_key={}", self.access_key); - - // Create proper credentials with the real secret key from authentication - let credentials = Some(s3s::auth::Credentials { - access_key: self.access_key.clone(), - secret_key: secret_key.to_string().into(), - }); - - // Check if user is the owner (admin) - let is_owner = if let Some(global_cred) = rustfs_credentials::get_global_action_cred() { - self.access_key == global_cred.access_key - } else { - false - }; - - // Create ReqInfo for authorization (required by list_buckets) - let mut extensions = http::Extensions::default(); - extensions.insert(crate::storage::access::ReqInfo { - cred: Some(rustfs_credentials::Credentials { - access_key: self.access_key.clone(), - secret_key: secret_key.to_string(), - session_token: String::new(), - expiration: None, - status: String::new(), - parent_user: String::new(), - groups: None, - claims: None, - name: None, - description: None, - }), - is_owner, - bucket: None, - object: None, - version_id: None, - region: None, - }); - - let req = S3Request { - input, - method: Method::GET, - uri: http::Uri::from_static("/"), - headers: HeaderMap::default(), - extensions, - credentials, - region: None, - service: None, - trailing_headers: None, - }; - let resp = self.fs.list_buckets(req).await?; - Ok(resp.output) - } - - /// Create bucket - maps to S3 CreateBucket - pub async fn create_bucket(&self, input: CreateBucketInput) -> S3Result { - trace!( - "Protocol S3 client CreateBucket request: bucket={:?}, access_key={}", - input.bucket, self.access_key - ); - let bucket_str = input.bucket.as_str(); - let uri: http::Uri = format!("/{}", bucket_str).parse().unwrap_or_default(); - let req = S3Request { - input, - method: Method::PUT, - uri, - headers: HeaderMap::default(), - extensions: http::Extensions::default(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; - let resp = self.fs.create_bucket(req).await?; - Ok(resp.output) - } - - /// Delete bucket - maps to S3 DeleteBucket - pub async fn delete_bucket(&self, input: DeleteBucketInput) -> S3Result { - trace!( - "Protocol S3 client DeleteBucket request: bucket={}, access_key={}", - input.bucket, self.access_key - ); - let uri: http::Uri = format!("/{}", input.bucket).parse().unwrap_or_default(); - let req = S3Request { - input, - method: Method::DELETE, - uri, - headers: HeaderMap::default(), - extensions: http::Extensions::default(), - credentials: None, - region: None, - service: None, - trailing_headers: None, - }; - let resp = self.fs.delete_bucket(req).await?; - Ok(resp.output) - } -} diff --git a/rustfs/src/protocols/ftps/driver.rs b/rustfs/src/protocols/ftps/driver.rs deleted file mode 100644 index a23dfddca..000000000 --- a/rustfs/src/protocols/ftps/driver.rs +++ /dev/null @@ -1,909 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! FTPS driver implementation -//! -//! This module provides the FTPS driver that integrates with libunftp -//! and translates FTP operations to S3 actions through the gateway. - -use crate::protocols::client::s3::ProtocolS3Client; -use crate::protocols::gateway::action::S3Action; -use crate::protocols::gateway::adapter::is_operation_supported; -use crate::protocols::gateway::authorize::authorize_operation; -use crate::protocols::gateway::error::map_s3_error_to_ftps; -use crate::protocols::gateway::restrictions::{get_s3_equivalent_operation, is_ftp_feature_supported}; -use crate::protocols::session::context::SessionContext; -use async_trait::async_trait; -use futures::stream; -use futures_util::TryStreamExt; -use libunftp::storage::{Error, ErrorKind, Fileinfo, Metadata, Result, StorageBackend}; -use rustfs_utils::path; -use s3s::dto::StreamingBlob; -use s3s::dto::{GetObjectInput, PutObjectInput}; -use std::fmt::Debug; -use std::path::{Path, PathBuf}; -use tokio::io::AsyncRead; -use tokio_util::io::StreamReader; -use tracing::{debug, error, info, trace}; - -/// FTPS storage driver implementation -#[derive(Debug, Clone)] -pub struct FtpsDriver { - fs: crate::storage::ecfs::FS, -} - -impl FtpsDriver { - /// Create a new FTPS driver - pub fn new() -> Self { - let fs = crate::storage::ecfs::FS {}; - Self { fs } - } - - /// Validate FTP feature support - fn validate_feature_support(&self, feature: &str) -> Result<()> { - if !is_ftp_feature_supported(feature) { - let error_msg = if let Some(s3_equivalent) = get_s3_equivalent_operation(feature) { - format!("Unsupported FTP feature: {}. S3 equivalent: {}", feature, s3_equivalent) - } else { - format!("Unsupported FTP feature: {}", feature) - }; - error!("{}", error_msg); - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, error_msg)); - } - Ok(()) - } - - /// Get SessionContext from User - fn get_session_context_from_user(&self, user: &super::server::FtpsUser) -> Result { - Ok(user.session_context.clone()) - } - - /// Create ProtocolS3Client for the given user - fn create_s3_client_for_user(&self, user: &super::server::FtpsUser) -> Result { - let session_context = &user.session_context; - let s3_client = ProtocolS3Client::new(self.fs.clone(), session_context.access_key().to_string()); - Ok(s3_client) - } - - /// List all buckets (for root path) - async fn list_buckets( - &self, - user: &super::server::FtpsUser, - session_context: &SessionContext, - ) -> Result>> { - let s3_client = self.create_s3_client_for_user(user)?; - - let action = S3Action::ListBuckets; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - error!("FTPS LIST - ListBuckets operation not supported for FTPS protocol"); - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - match authorize_operation(session_context, &action, "", None).await { - Ok(_) => debug!("FTPS LIST - ListBuckets authorization successful"), - Err(e) => { - error!("FTPS LIST - ListBuckets authorization failed: {}", e); - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied")); - } - } - - let mut list_result = Vec::new(); - - // List all buckets - let input = s3s::dto::ListBucketsInput::builder() - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build ListBucketsInput"))?; - - // Get the real secret key from the authenticated user - let secret_key = &session_context.principal.user_identity.credentials.secret_key; - debug!( - "FTPS LIST - calling S3 list_buckets with access_key: {}", - session_context.principal.access_key() - ); - - match s3_client.list_buckets(input, secret_key).await { - Ok(output) => { - debug!( - "FTPS LIST - S3 list_buckets succeeded, buckets count: {:?}", - output.buckets.as_ref().map(|b| b.len()).unwrap_or(0) - ); - if let Some(buckets) = output.buckets { - for bucket in buckets { - if let Some(ref bucket_name) = bucket.name { - debug!("FTPS LIST - found bucket: '{}'", bucket_name); - - let metadata = FtpsMetadata { - size: 0, - is_directory: true, - modification_time: bucket - .creation_date - .map(|t| { - let offset_datetime: time::OffsetDateTime = t.into(); - offset_datetime.unix_timestamp() as u64 - }) - .unwrap_or(0), - }; - - list_result.push(Fileinfo { - path: PathBuf::from(bucket_name), - metadata, - }); - } - } - } - - Ok(list_result) - } - Err(e) => { - error!("FTPS LIST - Failed to list buckets: {}", e); - let protocol_error = map_s3_error_to_ftps(&e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, protocol_error)) - } - } - } - - /// Create bucket - async fn create_bucket(&self, user: &super::server::FtpsUser, session_context: &SessionContext, bucket: &str) -> Result<()> { - let s3_client = self.create_s3_client_for_user(user)?; - - let action = S3Action::CreateBucket; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - error!("FTPS CREATE_BUCKET - operation not supported for FTPS protocol"); - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - match authorize_operation(session_context, &action, bucket, None).await { - Ok(_) => debug!("FTPS CREATE_BUCKET - authorization successful"), - Err(e) => { - error!("FTPS CREATE_BUCKET - authorization failed: {}", e); - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied")); - } - } - - // Create bucket - let mut input_builder = s3s::dto::CreateBucketInput::builder(); - input_builder.set_bucket(bucket.to_string()); - let input = input_builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build CreateBucketInput"))?; - - match s3_client.create_bucket(input).await { - Ok(_) => { - debug!("FTPS CREATE_BUCKET - successfully created bucket: '{}'", bucket); - Ok(()) - } - Err(e) => { - error!("FTPS CREATE_BUCKET - failed to create bucket: '{}', error: {}", bucket, e); - let protocol_error = map_s3_error_to_ftps(&e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, protocol_error)) - } - } - } - - /// Get bucket and key from path - fn parse_path(&self, path_str: &str) -> Result<(String, Option)> { - debug!("FTPS parse_path - input: '{}'", path_str); - let (bucket, object) = path::path_to_bucket_object(path_str); - - let key = if object.is_empty() { None } else { Some(object) }; - - debug!("FTPS parse_path - bucket: '{}', key: {:?}", bucket, key); - Ok((bucket, key)) - } -} - -#[async_trait] -impl StorageBackend for FtpsDriver { - type Metadata = FtpsMetadata; - - /// Get file metadata - async fn metadata + Send + Debug>(&self, user: &super::server::FtpsUser, path: P) -> Result { - trace!("FTPS metadata request for path: {:?}", path); - - let s3_client = self.create_s3_client_for_user(user)?; - - let path_str = path.as_ref().to_string_lossy(); - let (bucket, key) = self.parse_path(&path_str)?; - - if let Some(object_key) = key { - // Object metadata request - let action = S3Action::HeadObject; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - let session_context = self.get_session_context_from_user(user)?; - // Log the operation for audit purposes - debug!( - "FTPS operation authorized: user={}, action={}, bucket={}, object={}, source_ip={}", - session_context.access_key(), - action.as_str(), - bucket, - object_key, - session_context.source_ip - ); - - authorize_operation(&session_context, &action, &bucket, Some(&object_key)) - .await - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; - - let mut builder = s3s::dto::HeadObjectInput::builder(); - builder.set_bucket(bucket.clone()); - builder.set_key(object_key.clone()); - let input = builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build HeadObjectInput"))?; - - match s3_client.head_object(input).await { - Ok(output) => { - let metadata = FtpsMetadata { - size: output.content_length.unwrap_or(0) as u64, - is_directory: false, - modification_time: output - .last_modified - .map(|t| { - let offset_datetime: time::OffsetDateTime = t.into(); - offset_datetime.unix_timestamp() as u64 - }) - .unwrap_or(0), - }; - Ok(metadata) - } - Err(e) => { - error!("Failed to get object metadata: {}", e); - Err(map_s3_error_to_ftps(&e)) - } - } - } else { - // Bucket metadata request - let action = S3Action::HeadBucket; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - let session_context = self.get_session_context_from_user(user)?; - authorize_operation(&session_context, &action, &bucket, None) - .await - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; - - let mut builder = s3s::dto::HeadBucketInput::builder(); - builder.set_bucket(bucket.clone()); - let input = builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build HeadBucketInput"))?; - - match s3_client.head_bucket(input).await { - Ok(_) => { - let metadata = FtpsMetadata { - size: 0, - is_directory: true, - modification_time: 0, - }; - Ok(metadata) - } - Err(e) => { - error!("Failed to get bucket metadata: {}", e); - Err(map_s3_error_to_ftps(&e)) - } - } - } - } - - /// Get directory listing - async fn list + Send + Debug>( - &self, - user: &super::server::FtpsUser, - path: P, - ) -> Result>> { - info!("FTPS LIST request - user: {}, raw path: {:?}", user.username, path); - - let s3_client = self.create_s3_client_for_user(user)?; - let session_context = self.get_session_context_from_user(user)?; - - let path_str = path.as_ref().to_string_lossy(); - info!("FTPS LIST - parsing path: '{}'", path_str); - - // Check if this is root path listing - if path_str == "/" || path_str == "/." { - debug!("FTPS LIST - root path listing (including /.), using ListBuckets"); - return self.list_buckets(user, &session_context).await; - } - - // Handle paths ending with /., e.g., /testbucket/. - // Remove trailing /. to get the actual path - let cleaned_path = if let Some(stripped) = path_str.strip_suffix("/.") { - info!("FTPS LIST - path ends with /., removing trailing /."); - stripped - } else { - &path_str - }; - - let (bucket, prefix) = self.parse_path(cleaned_path)?; - debug!("FTPS LIST - parsed bucket: '{}', prefix: {:?}", bucket, prefix); - - // Validate feature support - self.validate_feature_support("LIST command")?; - - let action = S3Action::ListBucket; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - debug!("FTPS LIST - authorizing operation for bucket: '{}', prefix: {:?}", bucket, prefix); - match authorize_operation(&session_context, &action, &bucket, prefix.as_deref()).await { - Ok(_) => debug!("FTPS LIST - authorization successful"), - Err(e) => { - error!("FTPS LIST - authorization failed: {}", e); - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied")); - } - } - - let mut list_result = Vec::new(); - - // List objects with prefix - let mut builder = s3s::dto::ListObjectsV2Input::builder(); - builder.set_bucket(bucket.clone()); - builder.set_prefix(prefix.clone()); - builder.set_delimiter(Option::from("/".to_string())); - let input = builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build ListObjectsV2Input"))?; - - match s3_client.list_objects_v2(input).await { - Ok(output) => { - // Add directories (common prefixes) - if let Some(common_prefixes) = output.common_prefixes { - for prefix_info in common_prefixes { - if let Some(key) = prefix_info.prefix { - let dir_name = key.trim_end_matches('/').to_string(); - - let metadata = FtpsMetadata { - size: 0, - is_directory: true, - modification_time: 0, - }; - - list_result.push(Fileinfo { - path: PathBuf::from(dir_name), - metadata, - }); - } - } - } - - // Add files (objects) - if let Some(contents) = output.contents { - for object in contents { - if let Some(key) = object.key { - let file_name = key; - - let metadata = FtpsMetadata { - size: object.size.unwrap_or(0) as u64, - is_directory: false, - modification_time: object - .last_modified - .map(|t| { - let offset_datetime: time::OffsetDateTime = t.into(); - offset_datetime.unix_timestamp() as u64 - }) - .unwrap_or(0), - }; - - list_result.push(Fileinfo { - path: PathBuf::from(file_name), - metadata, - }); - } - } - } - - Ok(list_result) - } - Err(e) => { - error!("Failed to list objects: {}", e); - let protocol_error = map_s3_error_to_ftps(&e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, protocol_error)) - } - } - } - - /// Get file - async fn get + Send + Debug>( - &self, - user: &super::server::FtpsUser, - path: P, - start_pos: u64, - ) -> Result> { - trace!("FTPS get request for path: {:?} at position: {}", path, start_pos); - - let s3_client = self.create_s3_client_for_user(user)?; - let session_context = self.get_session_context_from_user(user)?; - - let path_str = path.as_ref().to_string_lossy(); - let (bucket, key) = self.parse_path(&path_str)?; - - if key.is_none() { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Cannot read bucket as file")); - } - - let object_key = key.unwrap(); - - let action = S3Action::GetObject; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - authorize_operation(&session_context, &action, &bucket, Some(&object_key)) - .await - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; - - let mut builder = GetObjectInput::builder(); - builder.set_bucket(bucket); - builder.set_key(object_key); - - if start_pos > 0 - && let Ok(range) = s3s::dto::Range::parse(&format!("bytes={}-", start_pos)) - { - builder.set_range(Some(range)); - } - - let input = builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build GetObjectInput"))?; - - match s3_client.get_object(input).await { - Ok(output) => { - if let Some(body) = output.body { - let stream = body.map_err(std::io::Error::other); - let reader = StreamReader::new(stream); - Ok(Box::new(reader)) - } else { - Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Empty object body")) - } - } - Err(e) => { - let protocol_error = map_s3_error_to_ftps(&e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, protocol_error)) - } - } - } - - /// Put file - async fn put + Send + Debug, R: AsyncRead + Send + Sync + Unpin + 'static>( - &self, - user: &super::server::FtpsUser, - input: R, - path: P, - start_pos: u64, - ) -> Result { - trace!("FTPS put request for path: {:?} at position: {}", path, start_pos); - - let s3_client = self.create_s3_client_for_user(user)?; - let session_context = self.get_session_context_from_user(user)?; - - let path_str = path.as_ref().to_string_lossy(); - let (bucket, key) = self.parse_path(&path_str)?; - - if key.is_none() { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Cannot write to bucket directly")); - } - - let object_key = key.unwrap(); - - // Check for append operation (not supported) - if start_pos > 0 { - self.validate_feature_support("APPE command (file append)")?; - } - - let action = S3Action::PutObject; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - authorize_operation(&session_context, &action, &bucket, Some(&object_key)) - .await - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; - - // Convert AsyncRead to bytes - let bytes_vec = { - let mut buffer = Vec::new(); - let mut reader = input; - tokio::io::copy(&mut reader, &mut buffer) - .await - .map_err(|e| Error::new(ErrorKind::TransientFileNotAvailable, e.to_string()))?; - buffer - }; - - let file_size = bytes_vec.len(); - - let mut put_builder = PutObjectInput::builder(); - put_builder.set_bucket(bucket.clone()); - put_builder.set_key(object_key.clone()); - put_builder.set_content_length(Some(file_size as i64)); - - // Create StreamingBlob with known size - let data_bytes = bytes::Bytes::from(bytes_vec); - let stream = stream::once(async move { Ok::(data_bytes) }); - let streaming_blob = StreamingBlob::wrap(stream); - - put_builder.set_body(Some(streaming_blob)); - let put_input = put_builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build PutObjectInput"))?; - - match s3_client.put_object(put_input).await { - Ok(output) => { - debug!("Successfully put object: {:?}", output); - // Return the size of the uploaded object - Ok(file_size as u64) - } - Err(e) => { - error!("FTPS put - S3 error details: {:?}", e); - let protocol_error = map_s3_error_to_ftps(&e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, protocol_error)) - } - } - } - - /// Delete file - async fn del + Send + Debug>(&self, user: &super::server::FtpsUser, path: P) -> Result<()> { - trace!("FTPS delete request for path: {:?}", path); - - let s3_client = self.create_s3_client_for_user(user)?; - let session_context = self.get_session_context_from_user(user)?; - - let path_str = path.as_ref().to_string_lossy(); - let (bucket, key) = self.parse_path(&path_str)?; - - if key.is_none() { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Cannot delete bucket")); - } - - let object_key = key.unwrap(); - - let action = S3Action::DeleteObject; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - authorize_operation(&session_context, &action, &bucket, Some(&object_key)) - .await - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; - - let mut builder = s3s::dto::DeleteObjectInput::builder(); - builder.set_bucket(bucket); - builder.set_key(object_key); - let input = builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build DeleteObjectInput"))?; - - match s3_client.delete_object(input).await { - Ok(_) => { - debug!("Successfully deleted object"); - Ok(()) - } - Err(e) => { - error!("Failed to delete object: {}", e); - let protocol_error = map_s3_error_to_ftps(&e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, protocol_error)) - } - } - } - - /// Create directory - async fn mkd + Send + Debug>(&self, user: &super::server::FtpsUser, path: P) -> Result<()> { - let s3_client = self.create_s3_client_for_user(user)?; - let session_context = self.get_session_context_from_user(user)?; - - let path_str = path.as_ref().to_string_lossy(); - let (bucket, key) = self.parse_path(&path_str)?; - - let dir_key = if let Some(k) = key { - // Creating directory inside bucket - path::retain_slash(&k) - } else { - // Creating bucket - use CreateBucket action instead of PutObject - debug!("FTPS MKDIR - Creating bucket: '{}'", bucket); - return self.create_bucket(user, &session_context, &bucket).await; - }; - - let action = S3Action::PutObject; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - authorize_operation(&session_context, &action, &bucket, Some(&dir_key)) - .await - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; - - // Create directory marker object - let mut input_builder = PutObjectInput::builder(); - input_builder.set_bucket(bucket); - input_builder.set_key(dir_key); - input_builder.set_body(Some(StreamingBlob::from(s3s::Body::from(Vec::new())))); - let input = input_builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build PutObjectInput"))?; - - match s3_client.put_object(input).await { - Ok(_) => { - debug!("Successfully created directory marker"); - Ok(()) - } - Err(e) => { - error!("Failed to create directory marker: {}", e); - let protocol_error = map_s3_error_to_ftps(&e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, protocol_error)) - } - } - } - - async fn rename + Send + Debug>(&self, _user: &super::server::FtpsUser, _from: P, _to: P) -> Result<()> { - // Rename/copy operations are not supported in FTPS - Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Rename operation not supported")) - } - - /// Remove directory - async fn rmd + Send + Debug>(&self, user: &super::server::FtpsUser, path: P) -> Result<()> { - debug!("FTPS RMD request for path: {:?}", path); - - let s3_client = self.create_s3_client_for_user(user)?; - let session_context = self.get_session_context_from_user(user)?; - - let path_str = path.as_ref().to_string_lossy(); - let (bucket, key) = self.parse_path(&path_str)?; - - if let Some(key) = key { - // Remove directory inside bucket - let dir_key = path::retain_slash(&key); - - let action = S3Action::DeleteObject; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation - authorize_operation(&session_context, &action, &bucket, Some(&dir_key)) - .await - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; - - // Save references for debug output after build - let bucket_for_log = bucket.clone(); - let dir_key_for_log = dir_key.clone(); - - let mut builder = s3s::dto::DeleteObjectInput::builder(); - builder = builder.bucket(bucket); - builder = builder.key(dir_key); - let input = builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build DeleteObjectInput"))?; - - match s3_client.delete_object(input).await { - Ok(_) => { - debug!( - "FTPS RMD - successfully removed directory marker: '{}' in bucket '{}'", - dir_key_for_log, bucket_for_log - ); - Ok(()) - } - Err(e) => { - error!("FTPS RMD - failed to remove directory marker: {}", e); - let protocol_error = map_s3_error_to_ftps(&e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, protocol_error)) - } - } - } else { - // Delete bucket - check if bucket is empty first - debug!("FTPS RMD - attempting to delete bucket: '{}'", bucket); - - let action = S3Action::DeleteBucket; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - authorize_operation(&session_context, &action, &bucket, None) - .await - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; - - // Check if bucket is empty - let list_input = s3s::dto::ListObjectsV2Input { - bucket: bucket.clone(), - max_keys: Some(1), - ..Default::default() - }; - - match s3_client.list_objects_v2(list_input).await { - Ok(output) => { - if let Some(objects) = output.contents - && !objects.is_empty() - { - debug!("FTPS RMD - bucket '{}' is not empty, cannot delete", bucket); - return Err(Error::new( - ErrorKind::PermanentFileNotAvailable, - format!("Bucket '{}' is not empty", bucket), - )); - } - } - Err(e) => { - debug!("FTPS RMD - failed to list objects: {}", e); - } - } - - // Bucket is empty, delete it - let delete_bucket_input = s3s::dto::DeleteBucketInput { - bucket: bucket.clone(), - ..Default::default() - }; - - match s3_client.delete_bucket(delete_bucket_input).await { - Ok(_) => { - debug!("FTPS RMD - successfully deleted bucket: '{}'", bucket); - Ok(()) - } - Err(e) => { - error!("FTPS RMD - failed to delete bucket '{}': {}", bucket, e); - let protocol_error = map_s3_error_to_ftps(&e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, protocol_error)) - } - } - } - } - - /// Change working directory - async fn cwd + Send + Debug>(&self, user: &super::server::FtpsUser, path: P) -> Result<()> { - debug!("FTPS cwd request for path: {:?}", path); - - let session_context = self.get_session_context_from_user(user)?; - let path_str = path.as_ref().to_string_lossy(); - info!("FTPS cwd - received path: '{}'", path_str); - - // Handle special cases - if path_str == "/" || path_str == "/." { - // cd to root directory - always allowed - debug!("FTPS cwd - changing to root directory"); - return Ok(()); - } - - if path_str == "." { - // cd . - stay in current directory - debug!("FTPS cwd - staying in current directory"); - return Ok(()); - } - - if path_str == ".." { - // cd .. from root directory should fail - error!("FTPS cwd - cannot go above root directory"); - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Cannot go above root directory")); - } - - // Parse the path - let (bucket, key) = self.parse_path(&path_str)?; - debug!("FTPS cwd - parsed bucket: '{}', key: {:?}", bucket, key); - - // S3 does not support hierarchical directories - you can only cd to bucket root - // Exception: key being "." means stay in current place (handled earlier), but path::clean may have converted it - if key.is_some() && key.as_ref().map(|k| k != ".").unwrap_or(true) { - error!( - "FTPS cwd - S3 does not support multi-level directories, cannot cd to path with key: {:?}", - key - ); - return Err(Error::new( - ErrorKind::PermanentFileNotAvailable, - "S3 does not support multi-level directories. Use absolute path to switch buckets.", - )); - } - - // Validate feature support - self.validate_feature_support("CWD command")?; - - // Verify that the bucket exists by trying to list it - let s3_client = self.create_s3_client_for_user(user)?; - let action = S3Action::HeadBucket; - if !is_operation_supported(crate::protocols::session::context::Protocol::Ftps, &action) { - return Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Operation not supported")); - } - - // Authorize the operation first - authorize_operation(&session_context, &action, &bucket, None) - .await - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?; - - // Check if bucket actually exists - let mut builder = s3s::dto::HeadBucketInput::builder(); - builder.set_bucket(bucket.clone()); - let input = builder - .build() - .map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Failed to build HeadBucketInput"))?; - - match s3_client.head_bucket(input).await { - Ok(_) => { - debug!("FTPS cwd - bucket '{}' exists and is accessible", bucket); - Ok(()) - } - Err(e) => { - error!("FTPS cwd - bucket '{}' does not exist or access denied: {}", bucket, e); - Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("Bucket '{}' not found", bucket))) - } - } - } -} - -/// FTPS metadata implementation -#[derive(Debug, Clone)] -pub struct FtpsMetadata { - /// File size in bytes - size: u64, - /// Whether this is a directory - is_directory: bool, - /// Last modification time (Unix timestamp) - modification_time: u64, -} - -impl Metadata for FtpsMetadata { - /// Get file size - fn len(&self) -> u64 { - self.size - } - - /// Check if file is empty - fn is_empty(&self) -> bool { - self.size == 0 - } - - /// Check if this is a directory - fn is_dir(&self) -> bool { - self.is_directory - } - - /// Check if this is a file - fn is_file(&self) -> bool { - !self.is_directory - } - - /// Check if file is a symbolic link (stub implementation) - /// - /// S3 doesn't support symbolic links - fn is_symlink(&self) -> bool { - false - } - - /// Get last modification time - fn modified(&self) -> Result { - Ok(std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(self.modification_time)) - } - - /// Get file permissions (stub implementation) - fn gid(&self) -> u32 { - 0 - } - - /// Get file permissions (stub implementation) - fn uid(&self) -> u32 { - 0 - } - - /// Get file permissions (stub implementation) - fn links(&self) -> u64 { - 1 - } -} diff --git a/rustfs/src/protocols/gateway/adapter.rs b/rustfs/src/protocols/gateway/adapter.rs deleted file mode 100644 index 217157916..000000000 --- a/rustfs/src/protocols/gateway/adapter.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Protocol to S3 action adapter - -use super::action::S3Action; -use crate::protocols::session::context::Protocol; - -pub fn is_operation_supported(protocol: Protocol, action: &S3Action) -> bool { - match protocol { - Protocol::Ftps => match action { - // Bucket operations: FTPS has no native bucket commands, but gateway allows create/delete - S3Action::CreateBucket => true, - S3Action::DeleteBucket => true, - - // Object operations: All file operations supported - S3Action::GetObject => true, // RETR command - S3Action::PutObject => true, // STOR and APPE commands both map to PutObject - S3Action::DeleteObject => true, // DELE command - S3Action::HeadObject => true, // SIZE command - - // Multipart operations: FTPS has no native multipart upload support - S3Action::CreateMultipartUpload => false, - S3Action::UploadPart => false, - S3Action::CompleteMultipartUpload => false, - S3Action::AbortMultipartUpload => false, - S3Action::ListMultipartUploads => false, - S3Action::ListParts => false, - - // ACL operations: FTPS has no native ACL support - S3Action::GetBucketAcl => false, - S3Action::PutBucketAcl => false, - S3Action::GetObjectAcl => false, - S3Action::PutObjectAcl => false, - - // Other operations - S3Action::CopyObject => false, // No native copy support in FTPS - S3Action::ListBucket => true, // LIST command - S3Action::ListBuckets => true, // LIST at root level - S3Action::HeadBucket => true, // Can check if directory exists - }, - Protocol::Sftp => match action { - // Bucket operations: SFTP can create/delete buckets via mkdir/rmdir - S3Action::CreateBucket => true, - S3Action::DeleteBucket => true, - - // Object operations: All file operations supported - S3Action::GetObject => true, // RealPath + Open + Read - S3Action::PutObject => true, // Open + Write - S3Action::DeleteObject => true, // Remove - S3Action::HeadObject => true, // Stat/Fstat - - // Multipart operations: SFTP has no native multipart upload support - S3Action::CreateMultipartUpload => false, - S3Action::UploadPart => false, - S3Action::CompleteMultipartUpload => false, - S3Action::AbortMultipartUpload => false, - S3Action::ListMultipartUploads => false, - S3Action::ListParts => false, - - // ACL operations: SFTP has no native ACL support - S3Action::GetBucketAcl => false, - S3Action::PutBucketAcl => false, - S3Action::GetObjectAcl => false, - S3Action::PutObjectAcl => false, - - // Other operations - S3Action::CopyObject => false, // No remote copy, only local rename - S3Action::ListBucket => true, // Readdir - S3Action::ListBuckets => true, // Readdir at root - S3Action::HeadBucket => true, // Stat on directory - }, - } -} diff --git a/rustfs/src/protocols/gateway/authorize.rs b/rustfs/src/protocols/gateway/authorize.rs deleted file mode 100644 index 2e065a148..000000000 --- a/rustfs/src/protocols/gateway/authorize.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::action::S3Action; -use super::adapter::is_operation_supported; -use crate::protocols::session::context::SessionContext; -use rustfs_credentials; -use rustfs_iam::get; -use rustfs_policy::policy::Args; -use std::collections::HashMap; -use tracing::{debug, error}; - -/// Check if a principal is allowed to perform an S3 action -pub async fn is_authorized(session_context: &SessionContext, action: &S3Action, bucket: &str, object: Option<&str>) -> bool { - let iam_sys = match get() { - Ok(sys) => sys, - Err(e) => { - error!("IAM system unavailable: {}", e); - return false; - } - }; - - // Create policy arguments - let mut claims = HashMap::new(); - claims.insert( - "principal".to_string(), - serde_json::Value::String(session_context.principal.access_key().to_string()), - ); - - let policy_action: rustfs_policy::policy::action::Action = action.clone().into(); - - // Check if user is the owner (admin) - let is_owner = if let Some(global_cred) = rustfs_credentials::get_global_action_cred() { - session_context.principal.access_key() == global_cred.access_key - } else { - false - }; - - let args = Args { - account: session_context.principal.access_key(), - groups: &session_context.principal.user_identity.credentials.groups, - action: policy_action, - bucket, - conditions: &HashMap::new(), - is_owner, - object: object.unwrap_or(""), - claims: &claims, - deny_only: false, - }; - - debug!( - "FTPS AUTH - Checking authorization: account={}, action={:?}, bucket='{}', object={:?}", - args.account, args.action, args.bucket, args.object - ); - - let allowed = iam_sys.is_allowed(&args).await; - debug!("FTPS AUTH - Authorization result: {}", allowed); - allowed -} - -/// Unified authorization entry point for all protocols -pub async fn authorize_operation( - session_context: &SessionContext, - action: &S3Action, - bucket: &str, - object: Option<&str>, -) -> Result<(), AuthorizationError> { - // First check if the operation is supported - if !is_operation_supported(session_context.protocol.clone(), action) { - return Err(AuthorizationError::AccessDenied); - } - - // Then check IAM authorization - if is_authorized(session_context, action, bucket, object).await { - Ok(()) - } else { - Err(AuthorizationError::AccessDenied) - } -} - -/// Authorization errors -#[derive(Debug, thiserror::Error)] -pub enum AuthorizationError { - #[error("Access denied")] - AccessDenied, -} diff --git a/rustfs/src/protocols/gateway/error.rs b/rustfs/src/protocols/gateway/error.rs deleted file mode 100644 index ab4782ed8..000000000 --- a/rustfs/src/protocols/gateway/error.rs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// FTP error code constants -pub mod ftp_errors { - pub const FILE_NOT_FOUND: &str = "550 File not found"; - pub const DIRECTORY_NOT_FOUND: &str = "550 Directory not found"; - pub const PERMISSION_DENIED: &str = "550 Permission denied"; - pub const DIRECTORY_NOT_EMPTY: &str = "550 Directory not empty"; - pub const DIRECTORY_ALREADY_EXISTS: &str = "550 Directory already exists"; - pub const INVALID_DIRECTORY_NAME: &str = "553 Invalid directory name"; - pub const INVALID_FILE_NAME: &str = "553 Invalid file name"; - pub const INVALID_REQUEST: &str = "501 Invalid request"; - pub const INTERNAL_SERVER_ERROR: &str = "421 Internal server error"; -} - -// FTP error messages mapping -pub fn map_s3_error_to_ftp_string(s3_error: &s3s::S3Error) -> String { - match s3_error.code() { - s3s::S3ErrorCode::NoSuchKey => ftp_errors::FILE_NOT_FOUND.to_string(), - s3s::S3ErrorCode::NoSuchBucket => ftp_errors::DIRECTORY_NOT_FOUND.to_string(), - s3s::S3ErrorCode::AccessDenied => ftp_errors::PERMISSION_DENIED.to_string(), - s3s::S3ErrorCode::BucketNotEmpty => ftp_errors::DIRECTORY_NOT_EMPTY.to_string(), - s3s::S3ErrorCode::BucketAlreadyExists => ftp_errors::DIRECTORY_ALREADY_EXISTS.to_string(), - s3s::S3ErrorCode::InvalidBucketName => ftp_errors::INVALID_DIRECTORY_NAME.to_string(), - s3s::S3ErrorCode::InvalidObjectState => ftp_errors::INVALID_FILE_NAME.to_string(), - s3s::S3ErrorCode::InvalidRequest => ftp_errors::INVALID_REQUEST.to_string(), - s3s::S3ErrorCode::InternalError => ftp_errors::INTERNAL_SERVER_ERROR.to_string(), - _ => ftp_errors::INTERNAL_SERVER_ERROR.to_string(), - } -} - -/// Map S3Error to FTPS libunftp Error -pub fn map_s3_error_to_ftps(s3_error: &s3s::S3Error) -> libunftp::storage::Error { - use libunftp::storage::{Error, ErrorKind}; - - match s3_error.code() { - s3s::S3ErrorCode::NoSuchKey | s3s::S3ErrorCode::NoSuchBucket => { - Error::new(ErrorKind::PermanentFileNotAvailable, map_s3_error_to_ftp_string(s3_error)) - } - s3s::S3ErrorCode::AccessDenied => Error::new(ErrorKind::PermissionDenied, map_s3_error_to_ftp_string(s3_error)), - s3s::S3ErrorCode::InvalidRequest | s3s::S3ErrorCode::InvalidBucketName | s3s::S3ErrorCode::InvalidObjectState => { - Error::new(ErrorKind::PermanentFileNotAvailable, map_s3_error_to_ftp_string(s3_error)) - } - _ => Error::new(ErrorKind::PermanentFileNotAvailable, map_s3_error_to_ftp_string(s3_error)), - } -} - -/// Map S3Error directly to SFTP StatusCode -pub fn map_s3_error_to_sftp_status(s3_error: &s3s::S3Error) -> russh_sftp::protocol::StatusCode { - use russh_sftp::protocol::StatusCode; - - match s3_error.code() { - s3s::S3ErrorCode::NoSuchKey => StatusCode::NoSuchFile, // SSH_FX_NO_SUCH_FILE (2) - s3s::S3ErrorCode::NoSuchBucket => StatusCode::NoSuchFile, // SSH_FX_NO_SUCH_FILE (2) - s3s::S3ErrorCode::AccessDenied => StatusCode::PermissionDenied, // SSH_FX_PERMISSION_DENIED (3) - s3s::S3ErrorCode::BucketNotEmpty => StatusCode::Failure, // SSH_FX_DIR_NOT_EMPTY (21) - s3s::S3ErrorCode::BucketAlreadyExists => StatusCode::Failure, // SSH_FX_FILE_ALREADY_EXISTS (17) - s3s::S3ErrorCode::InvalidBucketName => StatusCode::Failure, // SSH_FX_INVALID_FILENAME (22) - s3s::S3ErrorCode::InvalidObjectState => StatusCode::Failure, // SSH_FX_INVALID_FILENAME (22) - s3s::S3ErrorCode::InvalidRequest => StatusCode::OpUnsupported, // SSH_FX_OP_UNSUPPORTED (5) - s3s::S3ErrorCode::InternalError => StatusCode::Failure, // SSH_FX_FAILURE (4) - _ => StatusCode::Failure, // SSH_FX_FAILURE as default - } -} diff --git a/rustfs/src/protocols/gateway/restrictions.rs b/rustfs/src/protocols/gateway/restrictions.rs deleted file mode 100644 index c67eeb619..000000000 --- a/rustfs/src/protocols/gateway/restrictions.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Unsupported FTP features list -pub const UNSUPPORTED_FTP_FEATURES: &[&str] = &[ - // Atomic rename operations (must be implemented via CopyObject+DeleteObject) - "Atomic RNFR/RNTO rename", - // File append operations (S3 does not support native append) - "APPE command (file append)", - // POSIX permission operations (S3 uses ACLs and Policies) - "chmod command", - "chown command", - // Symbolic links (S3 object storage does not support) - "SYMLINK creation", - // Hard links (S3 object storage does not support) - "HARD LINK creation", - // File locking (S3 does not support filesystem-level locking) - "File locking mechanism", - // Direct directory rename (must be implemented via object copy) - "Directory atomic rename", -]; - -/// Check if an FTP feature is supported -pub fn is_ftp_feature_supported(feature: &str) -> bool { - !UNSUPPORTED_FTP_FEATURES.contains(&feature) -} - -/// Get S3 equivalent operation for unsupported features -pub fn get_s3_equivalent_operation(unsupported_feature: &str) -> Option<&'static str> { - match unsupported_feature { - "Atomic RNFR/RNTO rename" | "SSH_FXP_RENAME atomic rename" | "Directory atomic rename" => { - Some("Use CopyObject + DeleteObject to implement rename") - } - "APPE command (file append)" | "SSH_FXP_OPEN append mode" => Some("Use PutObject to overwrite the entire object"), - "chmod command" - | "chown command" - | "SSH_FXP_SETSTAT permission modification" - | "SSH_FXP_FSETSTAT permission modification" => Some("Use S3 ACLs or Bucket Policies to manage permissions"), - "SYMLINK creation" | "SSH_FXP_SYMLINK creation" => Some("S3 object storage does not support symbolic links"), - "File locking mechanism" | "SSH_FXP_BLOCK file locking" => { - Some("Use S3 object versioning or conditional writes for concurrency control") - } - _ => None, - } -} diff --git a/rustfs/src/protocols/mod.rs b/rustfs/src/protocols/mod.rs index e546c0e81..c072afd3b 100644 --- a/rustfs/src/protocols/mod.rs +++ b/rustfs/src/protocols/mod.rs @@ -13,7 +13,5 @@ // limitations under the License. pub mod client; -pub mod ftps; -pub mod gateway; -pub mod session; -pub mod sftp; + +pub use client::ProtocolStorageClient; diff --git a/rustfs/src/protocols/session/principal.rs b/rustfs/src/protocols/session/principal.rs deleted file mode 100644 index 5182df1c6..000000000 --- a/rustfs/src/protocols/session/principal.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use rustfs_policy::auth::UserIdentity; -use std::sync::Arc; - -/// Protocol principal representing an authenticated user -#[derive(Debug, Clone)] -pub struct ProtocolPrincipal { - /// User identity from IAM system - pub user_identity: Arc, -} - -impl ProtocolPrincipal { - /// Create a new protocol principal - pub fn new(user_identity: Arc) -> Self { - Self { user_identity } - } - - /// Get the access key for this principal - pub fn access_key(&self) -> &str { - &self.user_identity.credentials.access_key - } -} diff --git a/rustfs/src/protocols/sftp/handler.rs b/rustfs/src/protocols/sftp/handler.rs deleted file mode 100644 index 51d9516b4..000000000 --- a/rustfs/src/protocols/sftp/handler.rs +++ /dev/null @@ -1,928 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::protocols::client::s3::ProtocolS3Client; -use crate::protocols::gateway::action::S3Action; -use crate::protocols::gateway::authorize::authorize_operation; -use crate::protocols::gateway::error::map_s3_error_to_sftp_status; -use crate::protocols::session::context::SessionContext; -use futures::TryStreamExt; -use russh_sftp::protocol::{Attrs, Data, File, FileAttributes, Handle, Name, OpenFlags, Status, StatusCode, Version}; -use russh_sftp::server::Handler; -use rustfs_utils::path; -use s3s::S3ErrorCode; -use s3s::dto::{DeleteBucketInput, DeleteObjectInput, GetObjectInput, ListObjectsV2Input, PutObjectInput, StreamingBlob}; -use std::collections::HashMap; -use std::future::Future; -use std::path::PathBuf; -use std::sync::Arc; -use std::sync::atomic::{AtomicU32, Ordering}; -use tokio::fs::{File as TokioFile, OpenOptions}; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; -use tokio::sync::RwLock; -use tokio_util::io::StreamReader; -use tracing::{debug, error, trace}; -use uuid::Uuid; - -const INITIAL_HANDLE_ID: u32 = 1; -const ROOT_PATH: &str = "/"; -const CURRENT_DIR: &str = "."; -const PARENT_DIR: &str = ".."; -const HANDLE_ID_PREFIX: &str = "handle_"; -const PATH_SEPARATOR: &str = "/"; -const PERMISSION_DENIED_PATH: &str = ".."; -const DIR_MODE: u32 = 0o040000; -const FILE_MODE: u32 = 0o100000; -const DIR_PERMISSIONS: u32 = 0o755; -const FILE_PERMISSIONS: u32 = 0o644; - -/// State associated with an open file handle -#[derive(Debug)] -enum HandleState { - Read { - path: String, - bucket: String, - key: String, - }, - Write { - path: String, - bucket: String, - key: String, - temp_file_path: PathBuf, - file_handle: Option, - }, - Dir { - path: String, - files: Vec, - offset: usize, - }, -} - -#[derive(Clone)] -pub struct SftpHandler { - session_context: SessionContext, - handles: Arc>>, - next_handle_id: Arc, - temp_dir: PathBuf, - current_dir: Arc>, - fs: crate::storage::ecfs::FS, -} - -impl SftpHandler { - pub fn new(session_context: SessionContext) -> Self { - let fs = crate::storage::ecfs::FS {}; - - Self { - session_context, - handles: Arc::new(RwLock::new(HashMap::new())), - next_handle_id: Arc::new(AtomicU32::new(INITIAL_HANDLE_ID)), - temp_dir: std::env::temp_dir(), - current_dir: Arc::new(RwLock::new(ROOT_PATH.to_string())), - fs, - } - } - - fn create_s3_client(&self) -> ProtocolS3Client { - ProtocolS3Client::new(self.fs.clone(), self.session_context.access_key().to_string()) - } - - fn parse_path(&self, path_str: &str) -> Result<(String, Option), StatusCode> { - if path_str.contains(PERMISSION_DENIED_PATH) { - return Err(StatusCode::PermissionDenied); - } - - // Clean the path to normalize - let cleaned_path = path::clean(path_str); - - let (bucket, object) = path::path_to_bucket_object(&cleaned_path); - - let key = if object.is_empty() { None } else { Some(object) }; - - debug!( - "SFTP parse_path - input: '{}', cleaned: '{}', bucket: '{}', key: {:?}", - path_str, cleaned_path, bucket, key - ); - Ok((bucket, key)) - } - - fn generate_handle_id(&self) -> String { - let id = self.next_handle_id.fetch_add(1, Ordering::Relaxed); - format!("{}{}", HANDLE_ID_PREFIX, id) - } - - /// Convert relative path to absolute path based on current directory - async fn resolve_path(&self, path_str: &str) -> String { - let current = self.current_dir.read().await; - - if path_str.starts_with(PATH_SEPARATOR) { - // Absolute path - return path::clean(path_str).to_string(); - } - - // Relative path - if path_str == CURRENT_DIR { - current.clone() - } else if path_str == PARENT_DIR { - if *current == ROOT_PATH { - ROOT_PATH.to_string() - } else { - let parent = std::path::Path::new(&*current) - .parent() - .map(|p| p.to_str().unwrap()) - .unwrap_or(ROOT_PATH); - path::clean(parent).to_string() - } - } else { - // Join current directory with path - let joined = if *current == ROOT_PATH { - format!("{}{}", PATH_SEPARATOR, path_str.trim_start_matches(PATH_SEPARATOR)) - } else { - format!( - "{}{}{}", - current.trim_end_matches(PATH_SEPARATOR), - PATH_SEPARATOR, - path_str.trim_start_matches(PATH_SEPARATOR) - ) - }; - path::clean(&joined).to_string() - } - } - - async fn cleanup_state(&self, state: HandleState) { - if let HandleState::Write { temp_file_path, .. } = state { - let _ = tokio::fs::remove_file(temp_file_path).await; - } - } - - async fn do_stat(&self, path: String) -> Result { - debug!("SFTP do_stat - input path: '{}'", path); - - let (bucket, key_opt) = self.parse_path(&path)?; - - if bucket.is_empty() { - let mut attrs = FileAttributes::default(); - attrs.set_dir(true); - attrs.size = Some(0); - let current_mode = attrs.permissions.unwrap_or(0); - attrs.permissions = Some(current_mode | DIR_MODE | DIR_PERMISSIONS); - return Ok(attrs); - } - - let action = if key_opt.is_none() { - S3Action::HeadBucket - } else { - S3Action::HeadObject - }; - - debug!("SFTP do_stat - parsed bucket: '{}', key: {:?}, action: {:?}", bucket, key_opt, action); - - authorize_operation(&self.session_context, &action, &bucket, key_opt.as_deref()) - .await - .map_err(|_| StatusCode::PermissionDenied)?; - - let s3_client = self.create_s3_client(); - - match action { - S3Action::HeadBucket => { - let input = s3s::dto::HeadBucketInput { - bucket, - ..Default::default() - }; - - match s3_client.head_bucket(input).await { - Ok(_) => { - let mut attrs = FileAttributes::default(); - attrs.set_dir(true); - attrs.size = Some(0); - attrs.permissions = Some(DIR_PERMISSIONS | DIR_MODE); - attrs.mtime = Some(0); - Ok(attrs) - } - Err(_) => Err(StatusCode::NoSuchFile), - } - } - - S3Action::HeadObject => { - let key = key_opt.expect("key_opt should be Some for HeadObject action"); - let input = s3s::dto::HeadObjectInput { - bucket, - key, - ..Default::default() - }; - - match s3_client.head_object(input).await { - Ok(out) => { - let mut attrs = FileAttributes::default(); - attrs.set_dir(false); - attrs.size = Some(out.content_length.unwrap_or(0) as u64); - - if let Some(lm) = out.last_modified { - let dt = time::OffsetDateTime::from(lm); - attrs.mtime = Some(dt.unix_timestamp() as u32); - } - - attrs.permissions = Some(FILE_PERMISSIONS | FILE_MODE); - Ok(attrs) - } - Err(_) => Err(StatusCode::NoSuchFile), - } - } - - _ => { - error!("SFTP do_stat - Unexpected action type"); - Err(StatusCode::Failure) - } - } - } -} - -impl Handler for SftpHandler { - type Error = StatusCode; - - fn unimplemented(&self) -> Self::Error { - StatusCode::OpUnsupported - } - - async fn init(&mut self, version: u32, _extensions: HashMap) -> Result { - trace!("SFTP Init version: {}", version); - Ok(Version::new()) - } - - fn open( - &mut self, - id: u32, - filename: String, - pflags: OpenFlags, - _attrs: FileAttributes, - ) -> impl Future> + Send { - let this = self.clone(); - - async move { - debug!("SFTP Open: {} (flags: {:?})", filename, pflags); - - // Resolve relative path to absolute path - let resolved_filename = this.resolve_path(&filename).await; - - let (bucket, key_opt) = this.parse_path(&resolved_filename)?; - - if bucket.is_empty() { - return Err(StatusCode::PermissionDenied); // Cannot open root directory as file - } - - let key = key_opt.ok_or(StatusCode::PermissionDenied)?; // Cannot open bucket as file - - let handle_id = this.generate_handle_id(); - let state; - - if pflags.contains(OpenFlags::WRITE) || pflags.contains(OpenFlags::CREATE) || pflags.contains(OpenFlags::TRUNCATE) { - let action = S3Action::PutObject; - authorize_operation(&this.session_context, &action, &bucket, Some(&key)) - .await - .map_err(|_| StatusCode::PermissionDenied)?; - - if pflags.contains(OpenFlags::APPEND) { - return Err(StatusCode::OpUnsupported); - } - - let temp_filename = format!("rustfs-sftp-{}.tmp", Uuid::new_v4()); - let temp_path = this.temp_dir.join(temp_filename); - - let file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .open(&temp_path) - .await - .map_err(|e| { - error!("Failed to create temp file: {}", e); - StatusCode::Failure - })?; - - state = HandleState::Write { - path: filename.clone(), - bucket, - key, - temp_file_path: temp_path, - file_handle: Some(file), - }; - } else { - let action = S3Action::GetObject; - authorize_operation(&this.session_context, &action, &bucket, Some(&key)) - .await - .map_err(|_| StatusCode::PermissionDenied)?; - - state = HandleState::Read { - path: filename.clone(), - bucket, - key, - }; - } - - this.handles.write().await.insert(handle_id.clone(), state); - Ok(Handle { id, handle: handle_id }) - } - } - - fn close(&mut self, id: u32, handle: String) -> impl Future> + Send { - let this = self.clone(); - async move { - let state = this.handles.write().await.remove(&handle); - - match state { - Some(HandleState::Write { - bucket, - key, - temp_file_path, - mut file_handle, - .. - }) => { - let mut file = file_handle.take().ok_or(StatusCode::Failure)?; - - if let Err(e) = file.flush().await { - error!("Flush to disk failed: {}", e); - let _ = tokio::fs::remove_file(&temp_file_path).await; - return Err(StatusCode::Failure); - } - - let metadata = file.metadata().await.map_err(|e| { - error!("Failed to get metadata: {}", e); - StatusCode::Failure - })?; - let file_size = metadata.len(); - - if let Err(e) = file.seek(std::io::SeekFrom::Start(0)).await { - error!("Seek temp file failed: {}", e); - let _ = tokio::fs::remove_file(&temp_file_path).await; - return Err(StatusCode::Failure); - } - - let s3_client = this.create_s3_client(); - - let stream = tokio_util::io::ReaderStream::new(file); - let body = StreamingBlob::wrap(stream); - - let input = PutObjectInput::builder() - .bucket(bucket.clone()) - .key(key.clone()) - .body(Option::from(body)) - .content_length(Option::from(file_size as i64)) // 告诉 S3 文件多大 - .build() - .unwrap(); - - let result = match s3_client.put_object(input).await { - Ok(_) => Status { - id, - status_code: StatusCode::Ok, - error_message: "Success".into(), - language_tag: "en".into(), - }, - Err(e) => { - error!("S3 PutObject failed: {}", e); - let status_code = map_s3_error_to_sftp_status(&e); - return Err(status_code); - } - }; - - let _ = tokio::fs::remove_file(&temp_file_path).await; - Ok(result) - } - Some(state) => { - this.cleanup_state(state).await; - Ok(Status { - id, - status_code: StatusCode::Ok, - error_message: "Success".into(), - language_tag: "en".into(), - }) - } - None => Err(StatusCode::NoSuchFile), - } - } - } - - fn read(&mut self, id: u32, handle: String, offset: u64, len: u32) -> impl Future> + Send { - let this = self.clone(); - async move { - let (bucket, key) = { - let guard = this.handles.read().await; - match guard.get(&handle) { - Some(HandleState::Read { bucket, key, .. }) => (bucket.clone(), key.clone()), - Some(_) => return Err(StatusCode::OpUnsupported), - None => return Err(StatusCode::NoSuchFile), - } - }; - - let s3_client = this.create_s3_client(); - let range_end = offset + (len as u64) - 1; - - let mut builder = GetObjectInput::builder(); - builder.set_bucket(bucket); - builder.set_key(key); - - if offset > 0 - && let Ok(range) = s3s::dto::Range::parse(&format!("bytes={}-{}", offset, range_end)) - { - builder.set_range(Some(range)); - } - - let input = builder.build().map_err(|_| StatusCode::Failure)?; - - match s3_client.get_object(input).await { - Ok(output) => { - let mut data = Vec::new(); - if let Some(body) = output.body { - let stream = body.map_err(std::io::Error::other); - let mut reader = StreamReader::new(stream); - reader.read_to_end(&mut data).await.map_err(|_| StatusCode::Failure)?; - } - Ok(Data { id, data }) - } - Err(e) => match e.code() { - S3ErrorCode::InvalidRange => Err(StatusCode::Eof), - _ => Err(map_s3_error_to_sftp_status(&e)), - }, - } - } - } - - fn write( - &mut self, - id: u32, - handle: String, - offset: u64, - data: Vec, - ) -> impl Future> + Send { - let this = self.clone(); - async move { - let mut guard = this.handles.write().await; - - if let Some(HandleState::Write { file_handle, .. }) = guard.get_mut(&handle) { - if let Some(file) = file_handle { - if let Err(e) = file.seek(std::io::SeekFrom::Start(offset)).await { - error!("File seek failed: {}", e); - return Err(StatusCode::Failure); - } - - if let Err(e) = file.write_all(&data).await { - error!("File write failed: {}", e); - return Err(StatusCode::Failure); - } - - Ok(Status { - id, - status_code: StatusCode::Ok, - error_message: "Success".into(), - language_tag: "en".into(), - }) - } else { - Err(StatusCode::Failure) - } - } else { - Err(StatusCode::NoSuchFile) - } - } - } - - fn lstat(&mut self, id: u32, path: String) -> impl Future> + Send { - let this = self.clone(); - async move { - let resolved = this.resolve_path(&path).await; - let attrs = this.do_stat(resolved).await?; - Ok(Attrs { id, attrs }) - } - } - - fn fstat(&mut self, id: u32, handle: String) -> impl Future> + Send { - let this = self.clone(); - async move { - let path = { - let guard = this.handles.read().await; - match guard.get(&handle) { - Some(HandleState::Read { path, .. }) => path.clone(), - Some(HandleState::Write { path, .. }) => path.clone(), - Some(HandleState::Dir { path, .. }) => path.clone(), - None => return Err(StatusCode::NoSuchFile), - } - }; - let attrs = this.do_stat(path).await?; - Ok(Attrs { id, attrs }) - } - } - - fn opendir(&mut self, id: u32, path: String) -> impl Future> + Send { - let this = self.clone(); - async move { - debug!("SFTP Opendir START: path='{}'", path); - - // Resolve relative path to absolute path - let resolved_path = this.resolve_path(&path).await; - debug!("SFTP Opendir - resolved path: '{}'", resolved_path); - - // Handle root directory case - list all buckets - if resolved_path == "/" || resolved_path == "/." { - debug!("SFTP Opendir - listing root directory (all buckets)"); - let action = S3Action::ListBuckets; - authorize_operation(&this.session_context, &action, "", None) - .await - .map_err(|_| StatusCode::PermissionDenied)?; - - // List all buckets - let s3_client = this.create_s3_client(); - - let input = s3s::dto::ListBucketsInput::builder() - .build() - .map_err(|_| StatusCode::Failure)?; - - let secret_key = &this.session_context.principal.user_identity.credentials.secret_key; - let output = s3_client.list_buckets(input, secret_key).await.map_err(|e| { - error!("SFTP Opendir - failed to list buckets: {}", e); - StatusCode::Failure - })?; - - let mut files = Vec::new(); - if let Some(buckets) = output.buckets { - for bucket in buckets { - if let Some(bucket_name) = bucket.name { - let mut attrs = FileAttributes::default(); - attrs.set_dir(true); - attrs.permissions = Some(0o755); - files.push(File { - filename: bucket_name.clone(), - longname: format!("drwxr-xr-x 2 0 0 0 Dec 28 18:54 {}", bucket_name), - attrs, - }); - } - } - } - - let handle_id = this.generate_handle_id(); - let mut guard = this.handles.write().await; - guard.insert( - handle_id.clone(), - HandleState::Dir { - path: "/".to_string(), - files, - offset: 0, - }, - ); - return Ok(Handle { id, handle: handle_id }); - } - - // Handle bucket directory listing - let (bucket, key_prefix) = this.parse_path(&resolved_path)?; - debug!("SFTP Opendir - bucket: '{}', key_prefix: {:?}", bucket, key_prefix); - - let action = S3Action::ListBucket; - authorize_operation(&this.session_context, &action, &bucket, key_prefix.as_deref()) - .await - .map_err(|_| StatusCode::PermissionDenied)?; - - let mut builder = s3s::dto::ListObjectsV2Input::builder(); - builder.set_bucket(bucket.clone()); - - let prefix = if let Some(ref p) = key_prefix { - path::retain_slash(p) - } else { - String::new() - }; - - if !prefix.is_empty() { - builder.set_prefix(Some(prefix)); - } - builder.set_delimiter(Some("/".to_string())); - - let s3_client = this.create_s3_client(); - let input = builder.build().map_err(|_| StatusCode::Failure)?; - - let mut files = Vec::new(); - match s3_client.list_objects_v2(input).await { - Ok(output) => { - if let Some(prefixes) = output.common_prefixes { - for p in prefixes { - if let Some(prefix_str) = p.prefix { - let name = prefix_str - .trim_end_matches('/') - .split('/') - .next_back() - .unwrap_or("") - .to_string(); - if !name.is_empty() { - let mut attrs = FileAttributes::default(); - attrs.set_dir(true); - attrs.permissions = Some(0o755); - files.push(File { - filename: name.clone(), - longname: format!("drwxr-xr-x 1 rustfs rustfs 0 Jan 1 1970 {}", name), - attrs, - }); - } - } - } - } - if let Some(contents) = output.contents { - for obj in contents { - if let Some(key) = obj.key { - if key.ends_with('/') { - continue; - } - let name = key.split('/').next_back().unwrap_or("").to_string(); - let size = obj.size.unwrap_or(0) as u64; - let mut attrs = FileAttributes { - size: Some(size), - permissions: Some(0o644), - ..Default::default() - }; - if let Some(lm) = obj.last_modified { - let dt = time::OffsetDateTime::from(lm); - attrs.mtime = Some(dt.unix_timestamp() as u32); - } - files.push(File { - filename: name.clone(), - longname: format!("-rw-r--r-- 1 rustfs rustfs {} Jan 1 1970 {}", size, name), - attrs, - }); - } - } - } - } - Err(e) => { - error!("S3 List failed: {}", e); - return Err(StatusCode::Failure); - } - } - - let handle_id = this.generate_handle_id(); - this.handles - .write() - .await - .insert(handle_id.clone(), HandleState::Dir { path, files, offset: 0 }); - - Ok(Handle { id, handle: handle_id }) - } - } - - fn readdir(&mut self, id: u32, handle: String) -> impl Future> + Send { - let this = self.clone(); - async move { - let mut guard = this.handles.write().await; - - if let Some(HandleState::Dir { files, offset, .. }) = guard.get_mut(&handle) { - debug!("SFTP Readdir - handle: {}, offset: {}, total files: {}", handle, offset, files.len()); - for (i, f) in files.iter().enumerate() { - debug!("SFTP Readdir - file[{}]: filename='{}', longname='{}'", i, f.filename, f.longname); - } - - if *offset >= files.len() { - debug!("SFTP Readdir - offset {} >= files length {}, returning empty", offset, files.len()); - return Ok(Name { id, files: Vec::new() }); - } - let chunk = files[*offset..].to_vec(); - debug!("SFTP Readdir - returning {} files (offset {})", chunk.len(), offset); - *offset = files.len(); - Ok(Name { id, files: chunk }) - } else { - debug!("SFTP Readdir - handle '{}' not found or not a directory handle", handle); - Err(StatusCode::NoSuchFile) - } - } - } - - fn remove(&mut self, id: u32, filename: String) -> impl Future> + Send { - let this = self.clone(); - async move { - // Resolve relative path to absolute path - let resolved_filename = this.resolve_path(&filename).await; - - let (bucket, key_opt) = this.parse_path(&resolved_filename)?; - - if let Some(key) = key_opt { - // Delete object - let action = S3Action::DeleteObject; - authorize_operation(&this.session_context, &action, &bucket, Some(&key)) - .await - .map_err(|_| StatusCode::PermissionDenied)?; - - let input = DeleteObjectInput { - bucket, - key, - ..Default::default() - }; - - let s3_client = this.create_s3_client(); - s3_client.delete_object(input).await.map_err(|e| { - error!("SFTP REMOVE - failed to delete object: {}", e); - StatusCode::Failure - })?; - - Ok(Status { - id, - status_code: StatusCode::Ok, - error_message: "Success".into(), - language_tag: "en".into(), - }) - } else { - // Delete bucket - check if bucket is empty first - debug!("SFTP REMOVE - attempting to delete bucket: '{}'", bucket); - - let action = S3Action::DeleteBucket; - authorize_operation(&this.session_context, &action, &bucket, None) - .await - .map_err(|_| StatusCode::PermissionDenied)?; - - let s3_client = this.create_s3_client(); - - // Check if bucket is empty - let list_input = ListObjectsV2Input { - bucket: bucket.clone(), - max_keys: Some(1), - ..Default::default() - }; - - match s3_client.list_objects_v2(list_input).await { - Ok(output) => { - if let Some(objects) = output.contents - && !objects.is_empty() - { - debug!("SFTP REMOVE - bucket '{}' is not empty, cannot delete", bucket); - return Ok(Status { - id, - status_code: StatusCode::Failure, - error_message: format!("Bucket '{}' is not empty", bucket), - language_tag: "en".into(), - }); - } - } - Err(e) => { - debug!("SFTP REMOVE - failed to list objects: {}", e); - } - } - - // Bucket is empty, delete it - let delete_bucket_input = DeleteBucketInput { - bucket: bucket.clone(), - ..Default::default() - }; - - match s3_client.delete_bucket(delete_bucket_input).await { - Ok(_) => { - debug!("SFTP REMOVE - successfully deleted bucket: '{}'", bucket); - Ok(Status { - id, - status_code: StatusCode::Ok, - error_message: "Success".into(), - language_tag: "en".into(), - }) - } - Err(e) => { - error!("SFTP REMOVE - failed to delete bucket '{}': {}", bucket, e); - Ok(Status { - id, - status_code: StatusCode::Failure, - error_message: format!("Failed to delete bucket: {}", e), - language_tag: "en".into(), - }) - } - } - } - } - } - fn mkdir( - &mut self, - id: u32, - path: String, - _attrs: FileAttributes, - ) -> impl Future> + Send { - let this = self.clone(); - async move { - let (bucket, key_opt) = this.parse_path(&path)?; - - if let Some(key) = key_opt { - // Create directory inside bucket - let dir_key = path::retain_slash(&key); - - let action = S3Action::PutObject; - authorize_operation(&this.session_context, &action, &bucket, Some(&dir_key)) - .await - .map_err(|_| StatusCode::PermissionDenied)?; - - let s3_client = this.create_s3_client(); - let empty_stream = futures::stream::empty::>(); - let body = StreamingBlob::wrap(empty_stream); - let input = PutObjectInput { - bucket, - key: dir_key, - body: Some(body), - ..Default::default() - }; - - match s3_client.put_object(input).await { - Ok(_) => Ok(Status { - id, - status_code: StatusCode::Ok, - error_message: "Directory created".into(), - language_tag: "en".into(), - }), - Err(e) => { - error!("SFTP Failed to create directory: {}", e); - Ok(Status { - id, - status_code: StatusCode::Failure, - error_message: format!("Failed to create directory: {}", e), - language_tag: "en".into(), - }) - } - } - } else { - // Create bucket - debug!("SFTP mkdir - Creating bucket: '{}'", bucket); - - let action = S3Action::CreateBucket; - authorize_operation(&this.session_context, &action, &bucket, None) - .await - .map_err(|_| StatusCode::PermissionDenied)?; - - let s3_client = this.create_s3_client(); - let input = s3s::dto::CreateBucketInput { - bucket, - ..Default::default() - }; - - match s3_client.create_bucket(input).await { - Ok(_) => Ok(Status { - id, - status_code: StatusCode::Ok, - error_message: "Bucket created".into(), - language_tag: "en".into(), - }), - Err(e) => { - error!("SFTP Failed to create bucket: {}", e); - Ok(Status { - id, - status_code: StatusCode::Failure, - error_message: format!("Failed to create bucket: {}", e), - language_tag: "en".into(), - }) - } - } - } - } - } - - fn rmdir(&mut self, id: u32, path: String) -> impl Future> + Send { - self.remove(id, path) - } - - fn realpath(&mut self, id: u32, path: String) -> impl Future> + Send { - let this = self.clone(); - async move { - let resolved = this.resolve_path(&path).await; - debug!("SFTP Realpath - input: '{}', resolved: '{}'", path, resolved); - - // Check if this path is a directory and get proper attributes - let attrs = this.do_stat(resolved.clone()).await.unwrap_or_else(|_| { - let mut default_attrs = FileAttributes::default(); - // Assume it's a directory if stat fails (for root path) - default_attrs.set_dir(true); - default_attrs - }); - - Ok(Name { - id, - files: vec![File { - filename: resolved.clone(), - longname: format!( - "{:?} {:>4} {:>6} {:>6} {:>8} {} {}", - if attrs.is_dir() { "drwxr-xr-x" } else { "-rw-r--r--" }, - 1, - "rustfs", - "rustfs", - attrs.size.unwrap_or(0), - "Jan 1 1970", - resolved.split('/').next_back().unwrap_or(&resolved) - ), - attrs, - }], - }) - } - } - - fn stat(&mut self, id: u32, path: String) -> impl Future> + Send { - let this = self.clone(); - async move { - let attrs = this.do_stat(path).await?; - Ok(Attrs { id, attrs }) - } - } -} diff --git a/rustfs/src/protocols/sftp/mod.rs b/rustfs/src/protocols/sftp/mod.rs deleted file mode 100644 index fe790dd62..000000000 --- a/rustfs/src/protocols/sftp/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! SFTP protocol implementation -pub mod handler; -pub mod server; diff --git a/rustfs/src/protocols/sftp/server.rs b/rustfs/src/protocols/sftp/server.rs deleted file mode 100644 index 8cdde4c0d..000000000 --- a/rustfs/src/protocols/sftp/server.rs +++ /dev/null @@ -1,707 +0,0 @@ -// Copyright 2024 RustFS Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::protocols::session::context::{Protocol as SessionProtocol, SessionContext}; -use crate::protocols::session::principal::ProtocolPrincipal; -use crate::protocols::sftp::handler::SftpHandler; -use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; -use russh::ChannelId; -use russh::keys::{Algorithm, HashAlg, PrivateKey, PublicKey, PublicKeyBase64}; -use russh::server::{Auth, Handler, Server as RusshServer, Session}; -use ssh_key::Certificate; -use ssh_key::certificate::CertType; -use std::borrow::Cow; -use std::collections::HashMap; -use std::future::Future; -use std::net::SocketAddr; -use std::path::Path; -use std::sync::{Arc, Mutex}; -use std::time::SystemTime; -use tokio::sync::mpsc; -use tracing::{debug, error, info, warn}; - -const DEFAULT_ADDR: &str = "0.0.0.0:0"; -const AUTH_SUFFIX_SVC: &str = "=svc"; -const AUTH_SUFFIX_LDAP: &str = "=ldap"; -const SSH_KEY_TYPE_RSA: &str = "ssh-rsa"; -const SSH_KEY_TYPE_ED25519: &str = "ssh-ed25519"; -const SSH_KEY_TYPE_ECDSA: &str = "ecdsa-"; -const SFTP_SUBSYSTEM: &str = "sftp"; -const CRITICAL_OPTION_SOURCE_ADDRESS: &str = "source-address"; -const AUTH_FAILURE_DELAY_MS: u64 = 300; -const SFTP_BUFFER_SIZE: usize = 65536; -const SFTP_READ_BUF_SIZE: usize = 32 * 1024; - -type ServerError = Box; - -#[derive(Debug, Clone)] -pub struct SftpConfig { - pub bind_addr: SocketAddr, - pub require_key_auth: bool, - pub cert_file: Option, - pub key_file: Option, - pub authorized_keys_file: Option, -} - -#[derive(Clone)] -pub struct SftpServer { - config: SftpConfig, - key_pair: Arc, - trusted_certificates: Arc>, - authorized_keys: Arc>, -} - -impl SftpServer { - pub fn new(config: SftpConfig) -> Result { - let key_pair = if let Some(key_file) = &config.key_file { - let path = Path::new(key_file); - russh::keys::load_secret_key(path, None)? - } else { - warn!("No host key provided, generating random key (not recommended for production)."); - use russh::keys::signature::rand_core::OsRng; - let mut rng = OsRng; - PrivateKey::random(&mut rng, Algorithm::Ed25519)? - }; - - let trusted_certificates = if let Some(cert_file) = &config.cert_file { - info!("Loading trusted CA certificates from: {}", cert_file); - load_trusted_certificates(cert_file)? - } else { - if config.require_key_auth { - warn!("Key auth required but no CA certs provided."); - } - Vec::new() - }; - - let authorized_keys = if let Some(auth_keys_file) = &config.authorized_keys_file { - info!("Loading authorized SSH public keys from: {}", auth_keys_file); - load_authorized_keys(auth_keys_file).unwrap_or_else(|e| { - error!("Failed to load authorized keys from {}: {}", auth_keys_file, e); - Vec::new() - }) - } else { - info!("No authorized keys file provided, will use IAM for key validation."); - Vec::new() - }; - - info!("Loaded {} authorized SSH public key(s)", authorized_keys.len()); - - Ok(Self { - config, - key_pair: Arc::new(key_pair), - trusted_certificates: Arc::new(trusted_certificates), - authorized_keys: Arc::new(authorized_keys), - }) - } - - pub async fn start(&self, mut shutdown_rx: tokio::sync::broadcast::Receiver<()>) -> Result<(), ServerError> { - info!("Starting SFTP server on {}", self.config.bind_addr); - - let config = Arc::new(self.make_ssh_config()); - let socket = tokio::net::TcpListener::bind(&self.config.bind_addr).await?; - let server_stub = self.clone(); - - loop { - tokio::select! { - accept_res = socket.accept() => { - match accept_res { - Ok((stream, addr)) => { - let config = config.clone(); - let server_instance = server_stub.clone(); - tokio::spawn(async move { - let handler = SftpConnectionHandler::new(addr, server_instance.trusted_certificates.clone(), server_instance.authorized_keys.clone()); - if let Err(e) = russh::server::run_stream(config, stream, handler).await { - debug!("SFTP session closed from {}: {}", addr, e); - } - }); - } - Err(e) => error!("Failed to accept SFTP connection: {}", e), - } - } - _ = shutdown_rx.recv() => { - info!("SFTP server shutting down"); - break; - } - } - } - Ok(()) - } - - fn make_ssh_config(&self) -> russh::server::Config { - let mut config = russh::server::Config::default(); - config.keys.push(self.key_pair.as_ref().clone()); - - config.preferred.key = Cow::Borrowed(&[ - Algorithm::Ed25519, - Algorithm::Rsa { hash: None }, - Algorithm::Rsa { - hash: Some(HashAlg::Sha256), - }, - Algorithm::Rsa { - hash: Some(HashAlg::Sha512), - }, - ]); - - config - } - - pub fn config(&self) -> &SftpConfig { - &self.config - } -} - -impl RusshServer for SftpServer { - type Handler = SftpConnectionHandler; - - fn new_client(&mut self, peer_addr: Option) -> Self::Handler { - let addr = peer_addr.unwrap_or_else(|| DEFAULT_ADDR.parse().unwrap()); - SftpConnectionHandler::new(addr, self.trusted_certificates.clone(), self.authorized_keys.clone()) - } -} - -struct ConnectionState { - client_ip: SocketAddr, - identity: Option, - trusted_certificates: Arc>, - authorized_keys: Arc>, - sftp_channels: HashMap>>, -} - -#[derive(Clone)] -pub struct SftpConnectionHandler { - state: Arc>, -} - -impl SftpConnectionHandler { - fn new(client_ip: SocketAddr, trusted_certificates: Arc>, authorized_keys: Arc>) -> Self { - Self { - state: Arc::new(Mutex::new(ConnectionState { - client_ip, - identity: None, - trusted_certificates, - authorized_keys, - sftp_channels: HashMap::new(), - })), - } - } -} - -impl Handler for SftpConnectionHandler { - type Error = ServerError; - - fn auth_password(&mut self, user: &str, password: &str) -> impl Future> + Send { - let raw_user = user.to_string(); - let password = password.to_string(); - let state = self.state.clone(); - - async move { - use rustfs_credentials::Credentials as S3Credentials; - use rustfs_iam::get; - - let (username, suffix) = parse_auth_username(&raw_user); - if let Some(s) = suffix { - debug!("Detected auth suffix '{}' for user '{}'", s, username); - } - - let iam_sys = get().map_err(|e| format!("IAM system unavailable: {}", e))?; - - let s3_creds = S3Credentials { - access_key: username.to_string(), - secret_key: password.clone(), - session_token: String::new(), - expiration: None, - status: String::new(), - parent_user: String::new(), - groups: None, - claims: None, - name: None, - description: None, - }; - - let (user_identity, is_valid) = iam_sys - .check_key(&s3_creds.access_key) - .await - .map_err(|e| format!("IAM check failed: {}", e))?; - - if !is_valid { - warn!("Invalid AccessKey: {}", username); - tokio::time::sleep(std::time::Duration::from_millis(AUTH_FAILURE_DELAY_MS)).await; - return Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }); - } - - if let Some(identity) = user_identity { - if identity.credentials.secret_key != s3_creds.secret_key { - warn!("Invalid SecretKey for user: {}", username); - tokio::time::sleep(std::time::Duration::from_millis(AUTH_FAILURE_DELAY_MS)).await; - return Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }); - } - - { - let mut guard = state.lock().unwrap(); - guard.identity = Some(identity); - } - debug!("User {} authenticated successfully via password", username); - Ok(Auth::Accept) - } else { - Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }) - } - } - } - - fn auth_publickey(&mut self, user: &str, key: &PublicKey) -> impl Future> + Send { - let raw_user = user.to_string(); - let key = key.clone(); - let state = self.state.clone(); - - async move { - debug!("SFTP public key auth request for user: {}", raw_user); - - let trusted_cas = { - let guard = state.lock().unwrap(); - guard.trusted_certificates.clone() - }; - - if !trusted_cas.is_empty() { - match validate_ssh_certificate(&key, &trusted_cas, &raw_user) { - Ok(true) => { - let (username, _) = parse_auth_username(&raw_user); - - use rustfs_iam::get; - let iam_sys = get().map_err(|e| format!("IAM system unavailable: {}", e))?; - - let (user_identity, is_valid) = iam_sys - .check_key(username) - .await - .map_err(|e| format!("IAM lookup error: {}", e))?; - - if is_valid && user_identity.is_some() { - { - let mut guard = state.lock().unwrap(); - guard.identity = user_identity; - } - info!("User {} authenticated via SSH certificate", username); - Ok(Auth::Accept) - } else { - warn!("Valid certificate presented, but user '{}' does not exist in IAM", username); - Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }) - } - } - Ok(false) => Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }), - Err(e) => { - error!("SSH certificate validation error: {}", e); - Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }) - } - } - } else { - let (username, _) = parse_auth_username(&raw_user); - - use russh::keys::PublicKeyBase64; - - let client_key_bytes = key.public_key_bytes(); - let client_key_openssh = BASE64.encode(&client_key_bytes); - - let authorized_keys_clone = { - let guard = state.lock().unwrap(); - guard.authorized_keys.clone() - }; - - if !authorized_keys_clone.is_empty() { - debug!("Checking against {} pre-loaded authorized key(s)", authorized_keys_clone.len()); - - for authorized_key in authorized_keys_clone.iter() { - if authorized_key.contains(&client_key_openssh) - || authorized_key == &client_key_openssh - || compare_keys(authorized_key, &client_key_openssh) - { - use rustfs_iam::get; - if let Ok(iam_sys) = get() { - match iam_sys.check_key(username).await { - Ok((user_identity, is_valid)) => { - if is_valid && user_identity.is_some() { - let mut guard = state.lock().unwrap(); - guard.identity = user_identity; - info!("User {} authenticated via pre-loaded authorized key (IAM verified)", username); - return Ok(Auth::Accept); - } - } - Err(e) => { - error!("IAM lookup error: {}", e); - } - } - } - warn!( - "Key matched pre-loaded authorized keys, but IAM verification failed for user '{}'", - username - ); - } - } - } - - use rustfs_iam::get; - match get() { - Ok(iam_sys) => match iam_sys.check_key(username).await { - Ok((user_identity, is_valid)) => { - if is_valid { - if let Some(identity) = user_identity { - let authorized_keys = identity.get_ssh_public_keys(); - - if authorized_keys.is_empty() { - warn!("User '{}' found in IAM but has no SSH public keys registered", username); - return Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }); - } - - let key_valid = authorized_keys.iter().any(|authorized_key| { - authorized_key.contains(&client_key_openssh) - || authorized_key == &client_key_openssh - || compare_keys(authorized_key, &client_key_openssh) - }); - - if key_valid { - { - let mut guard = state.lock().unwrap(); - guard.identity = Some(identity); - } - info!("User {} authenticated via public key from IAM", username); - Ok(Auth::Accept) - } else { - warn!("Public key auth failed: client key not in IAM for user '{}'", username); - Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }) - } - } else { - warn!("Public key auth failed: user '{}' not found in IAM", username); - Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }) - } - } else { - warn!("Public key auth failed: user '{}' not valid in IAM", username); - Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }) - } - } - Err(e) => { - error!("IAM lookup error: {}", e); - Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }) - } - }, - Err(e) => { - error!("IAM system unavailable: {}", e); - Ok(Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }) - } - } - } - } - } - - async fn channel_open_session( - &mut self, - _channel: russh::Channel, - _session: &mut Session, - ) -> Result { - Ok(true) - } - - fn data( - &mut self, - channel_id: ChannelId, - data: &[u8], - _session: &mut Session, - ) -> impl Future> + Send { - let state = self.state.clone(); - let data = data.to_vec(); - - async move { - let sender = { - let guard = state.lock().unwrap(); - guard.sftp_channels.get(&channel_id).cloned() - }; - - if let Some(tx) = sender { - let _ = tx.send(data); - } - Ok(()) - } - } - - fn subsystem_request( - &mut self, - channel_id: ChannelId, - name: &str, - session: &mut Session, - ) -> impl Future> + Send { - let name = name.to_string(); - let state = self.state.clone(); - let session_handle = session.handle(); - - async move { - if name == SFTP_SUBSYSTEM { - let (identity, client_ip) = { - let guard = state.lock().unwrap(); - if let Some(id) = &guard.identity { - (id.clone(), guard.client_ip) - } else { - error!("SFTP subsystem requested but user not authenticated"); - return Ok(()); - } - }; - - debug!("Initializing SFTP subsystem for user: {}", identity.credentials.access_key); - - let context = - SessionContext::new(ProtocolPrincipal::new(Arc::new(identity)), SessionProtocol::Sftp, client_ip.ip()); - - let (client_pipe, server_pipe) = tokio::io::duplex(SFTP_BUFFER_SIZE); - let (mut client_read, mut client_write) = tokio::io::split(client_pipe); - - let (tx, mut rx) = mpsc::unbounded_channel::>(); - { - let mut guard = state.lock().unwrap(); - guard.sftp_channels.insert(channel_id, tx); - } - - tokio::spawn(async move { - use tokio::io::AsyncWriteExt; - while let Some(data) = rx.recv().await { - if let Err(e) = client_write.write_all(&data).await { - debug!("SFTP input pipe closed: {}", e); - break; - } - } - }); - - let sftp_handler = SftpHandler::new(context); - tokio::spawn(async move { - russh_sftp::server::run(server_pipe, sftp_handler).await; - debug!("SFTP handler finished"); - }); - - let session_handle = session_handle.clone(); - tokio::spawn(async move { - use tokio::io::AsyncReadExt; - let mut buf = vec![0u8; SFTP_READ_BUF_SIZE]; - loop { - match client_read.read(&mut buf).await { - Ok(0) => break, - Ok(n) => { - let data: Vec = buf[..n].to_vec(); - if session_handle.data(channel_id, data.into()).await.is_err() { - break; - } - } - Err(e) => { - error!("Error reading from SFTP output: {}", e); - break; - } - } - } - let _ = session_handle.close(channel_id).await; - }); - } - Ok(()) - } - } -} - -fn load_trusted_certificates(ca_cert_path: &str) -> Result, ServerError> { - let path = Path::new(ca_cert_path); - if !path.exists() { - return Err(format!("CA certificate file not found: {}", ca_cert_path).into()); - } - - let contents = std::fs::read_to_string(path)?; - let mut keys = Vec::new(); - - for line in contents.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - match ssh_key::PublicKey::from_openssh(line) { - Ok(key) => keys.push(key), - Err(e) => warn!("Skipping invalid CA key line in {}: {}", ca_cert_path, e), - } - } - - info!("Loaded {} trusted CA certificates from {}", keys.len(), ca_cert_path); - Ok(keys) -} - -fn load_authorized_keys(auth_keys_path: &str) -> Result, ServerError> { - let path = Path::new(auth_keys_path); - if !path.exists() { - return Err(format!("Authorized keys file not found: {}", auth_keys_path).into()); - } - - let contents = std::fs::read_to_string(path)?; - let mut keys = Vec::new(); - - for line in contents.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - if line.starts_with(SSH_KEY_TYPE_RSA) || line.starts_with(SSH_KEY_TYPE_ED25519) || line.starts_with(SSH_KEY_TYPE_ECDSA) { - keys.push(line.to_string()); - } else { - warn!( - "Skipping invalid authorized key line in {}: doesn't start with valid key type", - auth_keys_path - ); - } - } - - info!("Loaded {} authorized SSH public keys from {}", keys.len(), auth_keys_path); - Ok(keys) -} - -fn parse_auth_username(username: &str) -> (&str, Option<&str>) { - if let Some(idx) = username.rfind('=') { - let suffix = &username[idx..]; - if suffix == AUTH_SUFFIX_SVC || suffix == AUTH_SUFFIX_LDAP { - return (&username[..idx], Some(suffix)); - } - } - (username, None) -} - -fn validate_ssh_certificate( - russh_key: &PublicKey, - trusted_cas: &[ssh_key::PublicKey], - raw_username: &str, -) -> Result { - let (username, _suffix) = parse_auth_username(raw_username); - - let key_bytes = russh_key.public_key_bytes(); - - let cert = match Certificate::from_bytes(&key_bytes) { - Ok(c) => c, - Err(_) => { - debug!("Provided key is not a certificate. Skipping cert validation."); - return Ok(false); - } - }; - - debug!("Verifying SSH Certificate: KeyID='{}', Serial={}", cert.comment(), cert.serial()); - - let mut signature_valid = false; - let signature_key = cert.signature_key(); - - for ca in trusted_cas { - if ca.key_data() == signature_key { - signature_valid = true; - debug!("Certificate signed by trusted CA: {}", ca.fingerprint(Default::default())); - break; - } - } - - if !signature_valid { - warn!("Certificate signer not found in trusted CAs"); - return Ok(false); - } - - let now = SystemTime::now(); - let valid_after = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(cert.valid_after()); - let valid_before = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(cert.valid_before()); - - if now < valid_after { - warn!("Certificate is not yet valid (valid after {:?})", valid_after); - return Ok(false); - } - if now > valid_before { - warn!("Certificate has expired (valid until {:?})", valid_before); - return Ok(false); - } - - if !cert.valid_principals().contains(&username.to_string()) { - warn!( - "Certificate does not authorize user '{}'. Principals: {:?}", - username, - cert.valid_principals() - ); - return Ok(false); - } - - match cert.cert_type() { - CertType::User => {} - _ => { - warn!("Certificate is not a User certificate"); - return Ok(false); - } - } - - for (name, _value) in cert.critical_options().iter() { - if name.as_str() == CRITICAL_OPTION_SOURCE_ADDRESS { - } else { - warn!("Rejecting certificate due to unsupported critical option: {}", name); - return Ok(false); - } - } - - info!("SSH Certificate validation successful for user '{}'", username); - Ok(true) -} - -fn compare_keys(stored_key: &str, client_key_base64: &str) -> bool { - let stored_key_parts: Vec<&str> = stored_key.split_whitespace().collect(); - if stored_key_parts.is_empty() { - return false; - } - - let stored_key_data = stored_key_parts.get(1).unwrap_or(&stored_key); - - if *stored_key_data == client_key_base64 { - return true; - } - - if let Ok(stored_bytes) = BASE64.decode(stored_key_data) - && let Ok(client_bytes) = BASE64.decode(client_key_base64) - { - return stored_bytes == client_bytes; - } - - false -} diff --git a/scripts/run.sh b/scripts/run.sh index 0605656c7..ef2d5439a 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -196,8 +196,22 @@ export RUSTFS_HEAL_QUEUE_SIZE=10000 # rustfs trust system CA certificates export RUSTFS_TRUST_SYSTEM_CA=true -# Enable FTP server -export RUSTFS_FTPS_ENABLE=false +# FTP/FTPS Configuration +# +# Certificate directory structure: +# deploy/certs/ftps/ +# ├── rustfs_cert.pem (server certificate) +# ├── rustfs_key.pem (server private key) +# └── example1.com/ (optional: domain-specific certificates) +# ├── rustfs_cert.pem +# └── rustfs_key.pem + +# export RUSTFS_FTP_ENABLE=true +# export RUSTFS_FTP_ADDRESS="0.0.0.0:8021" + +# export RUSTFS_FTPS_ENABLE=true +# export RUSTFS_FTPS_ADDRESS="0.0.0.0:8022" +# export RUSTFS_FTPS_CERTS_DIR="${current_dir}/deploy/certs/ftps" # Use default timeout (60 seconds) # No environment variable needed @@ -233,6 +247,8 @@ fi # Start main service # To run with profiling enabled, uncomment the following line and comment the next line #cargo run --profile profiling --bin rustfs +# To run with FTP/FTPS support, use: +# cargo run --bin rustfs --features ftps # To run in release mode, use the following line #cargo run --profile release --bin rustfs # To run in debug mode, use the following line