diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 9447a8cd2..ae177cb9b 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -9,6 +9,9 @@ inputs: cache-shared-key: required: true default: "" + cache-save-if: + required: true + default: true runs: using: "composite" @@ -39,7 +42,9 @@ runs: - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true + cache-all-crates: true shared-key: ${{ inputs.cache-shared-key }} + save-if: ${{ inputs.cache-save-if }} - uses: mlugg/setup-zig@v1 - uses: taiki-e/install-action@cargo-zigbuild diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 486bfacd8..09c54587d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,20 +33,143 @@ jobs: develop: needs: skip-check if: needs.skip-check.outputs.should_skip != 'true' - runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup + with: + cache-shared-key: "develop" + + - name: Install s3s-e2e + run: | + cargo install s3s-e2e --git https://github.com/Nugine/s3s.git + s3s-e2e --version - name: Format run: cargo fmt --all --check - name: Lint - run: cargo check - # run: cargo clippy - # run: cargo clippy -- -D warnings + run: cargo check --all-targets + # TODO: cargo clippy - - name: Test - run: cargo test --all --exclude e2e_test + - name: Build debug + run: | + touch rustfs/build.rs + cargo build -p rustfs --bins + + # - name: Build release + # run: | + # touch rustfs/build.rs + # cargo build -p rustfs --bins --release + + - run: | + mkdir -p ./target/artifacts + cp target/debug/rustfs ./target/artifacts/rustfs-debug + # cp target/release/rustfs ./target/artifacts/rustfs-release + + - uses: actions/upload-artifact@v4 + with: + name: rustfs + path: ./target/artifacts/* + + test: + name: Test + needs: + - skip-check + - develop + if: needs.skip-check.outputs.should_skip != 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + cache-shared-key: "develop" + cache-save-if: "false" + - run: cargo test --all --exclude e2e_test + + s3s-e2e: + name: E2E (s3s-e2e) + needs: + - skip-check + - develop + if: needs.skip-check.outputs.should_skip != 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + cache-shared-key: "develop" + cache-save-if: false + + - name: Install s3s-e2e + run: | + cargo install s3s-e2e --git https://github.com/Nugine/s3s.git + s3s-e2e --version + + - uses: actions/download-artifact@v4 + with: + name: rustfs + path: ./target/artifacts + + - name: Run rustfs + run: | + ./scripts/e2e-run.sh ./target/artifacts/rustfs-debug /data/rustfs + sleep 10 + + - name: Run s3s-e2e + timeout-minutes: 10 + run: | + export AWS_ACCESS_KEY_ID=rustfsadmin + export AWS_SECRET_ACCESS_KEY=rustfsadmin + export AWS_REGION=us-east-1 + export AWS_ENDPOINT_URL=http://localhost:9000 + export RUST_LOG="s3s_e2e=debug,s3s_test=info,s3s=debug" + export RUST_BACKTRACE=full + s3s-e2e + sudo killall rustfs-debug + + - uses: actions/upload-artifact@v4 + with: + name: s3s-e2e.logs + path: /tmp/rustfs.log + + # mint: + # name: E2E (mint) + # needs: + # - skip-check + # - develop + # if: needs.skip-check.outputs.should_skip != 'true' + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: ./.github/actions/setup + # with: + # cache-shared-key: "develop" + # cache-save-if: false + + # - uses: actions/download-artifact@v4 + # with: + # name: rustfs + # path: ./target/artifacts + + # - name: Run rustfs + # run: | + # ./scripts/e2e-run.sh ./target/artifacts/rustfs-release /data/rustfs + # sleep 10 + + # - name: Run mint + # timeout-minutes: 10 + # run: | + # docker run \ + # -e "SERVER_ENDPOINT=localhost:9000" \ + # -e "ACCESS_KEY=rustfsadmin" \ + # -e "SECRET_KEY=rustfsadmin" \ + # -e "ENABLE_HTTPS=0" \ + # --network host \ + # minio/mint:edge + # sudo killall rustfs-release + + # - uses: actions/upload-artifact@v4 + # with: + # name: mint.logs + # path: /tmp/rustfs.log diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index 9ec14b812..000000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: E2E - -on: - workflow_dispatch: - schedule: - - cron: '0 0 * * 0' # at midnight of each sunday - push: - branches: - - main - pull_request: - branches: - - main - -env: - CARGO_TERM_COLOR: always - -jobs: - skip-check: - permissions: - actions: write - contents: read - runs-on: ubuntu-latest - outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} - steps: - - id: skip_check - uses: fkirc/skip-duplicate-actions@v5 - with: - concurrent_skipping: 'same_content_newer' - cancel_others: true - paths_ignore: '["*.md"]' - - s3s-e2e: - needs: skip-check - if: needs.skip-check.outputs.should_skip != 'true' - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup - - - name: Build binaries - run: | - touch rustfs/build.rs - cargo build -p rustfs --bins - - - name: Install s3s-e2e - run: | - mkdir -p target/s3s-e2e - cargo install s3s-e2e \ - --git https://github.com/Nugine/s3s.git \ - --target-dir target/s3s-e2e \ - --force - s3s-e2e --version - - - name: Run tests - run: | - export SKIP_BUILD=1 - nohup ./scripts/run.sh & - sleep 3 - - export AWS_ACCESS_KEY_ID=rustfsadmin - export AWS_SECRET_ACCESS_KEY=rustfsadmin - export AWS_REGION=us-east-1 - export AWS_ENDPOINT_URL=http://localhost:9000 - export RUST_LOG="s3s_e2e=debug,s3s_test=info,s3s=debug" - export RUST_BACKTRACE=full - s3s-e2e - killall rustfs - mint: - needs: skip-check - if: needs.skip-check.outputs.should_skip != 'true' - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup - - - name: Build binaries - run: | - touch rustfs/build.rs - cargo build -p rustfs --bins --release - - - name: run this rustfs server - run: | - sudo mkdir -p /data/rustfs - sudo nohup ./target/release/rustfs /data/rustfs & - - - name: run mint test task - run: | - ps aux | grep rustfs - SERVER_ADDRESS=$(ifconfig eth0 | awk 'NR==2 { print $2 }') - docker run -e SERVER_ENDPOINT=${SERVER_ADDRESS}:9000 \ - -e ACCESS_KEY=rustfsadmin \ - -e SECRET_KEY=rustfsadmin \ - -e ENABLE_HTTPS=0 \ - minio/mint - - ### Temporarily disabled because it is not working - # e2e: - # # See this url if required matrix jobs are hanging - # # https://github.com/fkirc/skip-duplicate-actions#how-to-use-skip-check-with-required-matrix-jobs - # needs: skip-check - # if: needs.skip-check.outputs.should_skip != 'true' - - # timeout-minutes: 30 - # runs-on: ubuntu-latest - # strategy: - # matrix: - # rust: - # - stable - # # - nightly - - # steps: - # - uses: arduino/setup-protoc@v3 - # with: - # version: "27.0" - - # - uses: Nugine/setup-flatc@v1 - # with: - # version: "24.3.25" - - # - uses: actions/checkout@v4 - - # - name: toolchain - # uses: dtolnay/rust-toolchain@master - # with: - # toolchain: ${{ matrix.rust }} - # # components: rustfmt, clippy - - # - uses: Swatinem/rust-cache@v2 - # with: - # cache-on-failure: true - - # - name: run fs start - # working-directory: . - # run: | - # nohup make e2e-server & - - # - name: run fs test - # working-directory: . - # run: | - # make probe-e2e - - # - name: e2e test - # run: cargo test -p e2e_test --lib diff --git a/.gitignore b/.gitignore index 94fd091d6..81d415f64 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /test /logs .devcontainer +rustfs/static/* diff --git a/Cargo.lock b/Cargo.lock index 5f75b06b7..d15b1ba19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1829,13 +1829,34 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -1846,7 +1867,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.0", "windows-sys 0.59.0", ] @@ -3185,25 +3206,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -4047,9 +4049,9 @@ dependencies = [ [[package]] name = "numeric_cast" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6699ec124a5322dd30184abe24b2b9a92237ed0fe8e038f1f410bf7cccfe8b33" +checksum = "a3c00a0c9600379bd32f8972de90676a7672cba3bf4886986bc05902afc1e093" [[package]] name = "objc" @@ -5244,6 +5246,17 @@ dependencies = [ "bitflags 2.8.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.0" @@ -5447,6 +5460,41 @@ dependencies = [ "serde", ] +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.98", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2 0.10.8", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -5497,7 +5545,6 @@ dependencies = [ "hyper", "hyper-util", "iam", - "include_dir", "jsonwebtoken", "lazy_static", "lock", @@ -5505,6 +5552,7 @@ dependencies = [ "madmin", "matchit 0.8.6", "mime", + "mime_guess", "netif", "pin-project-lite", "prost", @@ -5513,6 +5561,7 @@ dependencies = [ "protobuf", "protos", "rmp-serde", + "rust-embed", "s3s", "serde", "serde_json", @@ -5541,7 +5590,7 @@ version = "0.0.1" dependencies = [ "chrono", "dioxus", - "dirs", + "dirs 6.0.0", "futures-util", "keyring", "reqwest", @@ -5647,7 +5696,7 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "s3s" version = "0.11.0-dev" -source = "git+https://github.com/Nugine/s3s.git?rev=529c8933a11528c506d5fbf7c4c2ab155db37dfe#529c8933a11528c506d5fbf7c4c2ab155db37dfe" +source = "git+https://github.com/Nugine/s3s.git?rev=ab139f72fe768fb9d8cecfe36269451da1ca9779#ab139f72fe768fb9d8cecfe36269451da1ca9779" dependencies = [ "arrayvec", "async-trait", @@ -5694,7 +5743,7 @@ dependencies = [ [[package]] name = "s3s-policy" version = "0.11.0-dev" -source = "git+https://github.com/Nugine/s3s.git?rev=529c8933a11528c506d5fbf7c4c2ab155db37dfe#529c8933a11528c506d5fbf7c4c2ab155db37dfe" +source = "git+https://github.com/Nugine/s3s.git?rev=ab139f72fe768fb9d8cecfe36269451da1ca9779#ab139f72fe768fb9d8cecfe36269451da1ca9779" dependencies = [ "indexmap 2.7.1", "serde", @@ -6017,6 +6066,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellexpand" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +dependencies = [ + "dirs 5.0.1", +] + [[package]] name = "shlex" version = "1.3.0" @@ -6946,7 +7004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadd75f5002e2513eaa19b2365f533090cc3e93abd38788452d9ea85cff7b48a" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda 0.15.3", "objc2 0.6.0", diff --git a/Cargo.toml b/Cargo.toml index f9a80d9d4..5bdc462a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,10 +80,10 @@ rfd = { version = "0.15.2", default-features = false, features = ["xdg-portal", rmp = "0.8.14" rmp-serde = "1.3.0" rustfs-logging = { path = "packages/logging", version = "0.0.1" } -s3s = { git = "https://github.com/Nugine/s3s.git", rev = "529c8933a11528c506d5fbf7c4c2ab155db37dfe", default-features = true, features = [ +s3s = { git = "https://github.com/Nugine/s3s.git", rev = "ab139f72fe768fb9d8cecfe36269451da1ca9779", default-features = true, features = [ "tower", ] } -s3s-policy = { git = "https://github.com/Nugine/s3s.git", rev = "529c8933a11528c506d5fbf7c4c2ab155db37dfe" } +s3s-policy = { git = "https://github.com/Nugine/s3s.git", rev = "ab139f72fe768fb9d8cecfe36269451da1ca9779" } shadow-rs = { version = "0.38.0", default-features = false } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" diff --git a/ecstore/src/disk/local.rs b/ecstore/src/disk/local.rs index cc58de3ce..8a559477e 100644 --- a/ecstore/src/disk/local.rs +++ b/ecstore/src/disk/local.rs @@ -19,7 +19,7 @@ use crate::disk::error::{ use crate::disk::os::{check_path_length, is_empty_dir}; use crate::disk::{LocalFileReader, LocalFileWriter, STORAGE_FORMAT_FILE}; use crate::error::{Error, Result}; -use crate::file_meta::read_xl_meta_no_data; +use crate::file_meta::{get_file_info, read_xl_meta_no_data, FileInfoOpts}; use crate::global::{GLOBAL_IsErasureSD, GLOBAL_RootDiskThreshold}; use crate::heal::data_scanner::{has_active_rules, scan_data_folder, ScannerItem, ShouldSleepFn, SizeSummary}; use crate::heal::data_scanner_metric::{ScannerMetric, ScannerMetrics}; @@ -1995,10 +1995,8 @@ impl DiskAPI for LocalDisk { let (data, _) = self.read_raw(volume, file_dir, file_path, read_data).await?; - let mut meta = FileMeta::default(); - meta.unmarshal_msg(&data)?; + let fi = get_file_info(&data, volume, path, version_id, FileInfoOpts { data: read_data }).await?; - let fi = meta.into_fileinfo(volume, path, version_id, read_data, true)?; Ok(fi) } async fn read_xl(&self, volume: &str, path: &str, read_data: bool) -> Result { diff --git a/ecstore/src/disk/mod.rs b/ecstore/src/disk/mod.rs index 32bc2ca5b..0b7aba4cb 100644 --- a/ecstore/src/disk/mod.rs +++ b/ecstore/src/disk/mod.rs @@ -1208,8 +1208,9 @@ pub struct VolumeInfo { pub created: Option, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Default)] pub struct ReadOptions { + pub incl_free_versions: bool, pub read_data: bool, pub healing: bool, } diff --git a/ecstore/src/endpoints.rs b/ecstore/src/endpoints.rs index 334c74d65..5a0cac5cc 100644 --- a/ecstore/src/endpoints.rs +++ b/ecstore/src/endpoints.rs @@ -504,7 +504,7 @@ impl EndpointServerPools { self.0 .first() .and_then(|v| v.endpoints.as_ref().first()) - .map_or(false, |v| v.is_local) + .is_some_and(|v| v.is_local) } /// returns a sorted list of nodes in this cluster diff --git a/ecstore/src/file_meta.rs b/ecstore/src/file_meta.rs index e130ada40..bb33f8e69 100644 --- a/ecstore/src/file_meta.rs +++ b/ecstore/src/file_meta.rs @@ -554,7 +554,7 @@ impl FileMeta { let header = &ver.header; if let Some(vid) = has_vid { - if header.version_id == Some(vid) { + if header.version_id != Some(vid) { is_latest = false; succ_mod_time = header.mod_time; continue; @@ -2045,7 +2045,7 @@ pub struct FileInfoOpts { pub data: bool, } -async fn get_file_info(buf: &[u8], volume: &str, path: &str, version_id: &str, opts: FileInfoOpts) -> Result { +pub async fn get_file_info(buf: &[u8], volume: &str, path: &str, version_id: &str, opts: FileInfoOpts) -> Result { let vid = { if version_id.is_empty() { None @@ -2068,7 +2068,6 @@ async fn get_file_info(buf: &[u8], volume: &str, path: &str, version_id: &str, o } let fi = meta.into_fileinfo(volume, path, version_id, opts.data, true)?; - Ok(fi) } diff --git a/ecstore/src/set_disk.rs b/ecstore/src/set_disk.rs index 5d457bf22..c6db0f8e1 100644 --- a/ecstore/src/set_disk.rs +++ b/ecstore/src/set_disk.rs @@ -1088,7 +1088,11 @@ impl SetDisks { let mut errors = Vec::with_capacity(disks.len()); for disk in disks.iter() { - let opts = ReadOptions { read_data, healing }; + let opts = ReadOptions { + read_data, + healing, + ..Default::default() + }; futures.push(async move { if let Some(disk) = disk { if version_id.is_empty() { @@ -3634,7 +3638,7 @@ impl ObjectIO for SetDisks { Ok(reader) } - #[tracing::instrument(level = "debug", skip(self, data))] + #[tracing::instrument(level = "debug", skip(self, data,))] async fn put_object(&self, bucket: &str, object: &str, data: &mut PutObjReader, opts: &ObjectOptions) -> Result { let disks = self.disks.read().await; @@ -5463,7 +5467,11 @@ fn get_complete_multipart_md5(parts: &[CompletePart]) -> String { for part in parts.iter() { if let Some(etag) = &part.e_tag { - buf.extend(etag.bytes()); + if let Ok(etag_bytes) = hex_simd::decode_to_vec(etag.as_bytes()) { + buf.extend(etag_bytes); + } else { + buf.extend(etag.bytes()); + } } } diff --git a/ecstore/src/store.rs b/ecstore/src/store.rs index 1aae9ab36..8a95edefc 100644 --- a/ecstore/src/store.rs +++ b/ecstore/src/store.rs @@ -58,7 +58,7 @@ use tokio::select; use tokio::sync::mpsc::Sender; use tokio::sync::{broadcast, mpsc, RwLock}; use tokio::time::{interval, sleep}; -use tracing::{debug, info, warn}; +use tracing::{debug, info}; use uuid::Uuid; const MAX_UPLOADS_LIST: usize = 10000; @@ -1235,10 +1235,8 @@ impl StorageAPI for ECStore { let mut buckets = self.peer_sys.list_bucket(opts).await?; if !opts.no_metadata { - warn!("list_bucket meta"); for bucket in buckets.iter_mut() { if let Ok(created) = metadata_sys::created_at(&bucket.name).await { - warn!("list_bucket created {:?}", &created); bucket.created = Some(created); } } diff --git a/ecstore/src/store_init.rs b/ecstore/src/store_init.rs index fb34cc8d5..3c44cc0fe 100644 --- a/ecstore/src/store_init.rs +++ b/ecstore/src/store_init.rs @@ -140,7 +140,7 @@ pub fn get_format_erasure_in_quorum(formats: &[Option]) -> Result
) { } } +pub fn get_token_signing_key() -> Option { + if let Some(s) = get_global_action_cred() { + Some(s.secret_key.clone()) + } else { + None + } +} + pub fn extract_jwt_claims(u: &UserIdentity) -> Result> { - get_claims_from_token_with_secret(&u.credentials.session_token, &u.credentials.secret_key) + let Some(sys_key) = get_token_signing_key() else { + return Err(Error::msg("global active sk not init")); + }; + + let keys = vec![&sys_key, &u.credentials.secret_key]; + + for key in keys { + if let Ok(claims) = get_claims_from_token_with_secret(&u.credentials.session_token, key) { + return Ok(claims); + } + } + Err(Error::msg("unable to extract claims")) } fn filter_policies(cache: &Cache, policy_name: &str, bucket_name: &str) -> (String, Policy) { diff --git a/iam/src/policy/function.rs b/iam/src/policy/function.rs index 27870d154..03a020dc4 100644 --- a/iam/src/policy/function.rs +++ b/iam/src/policy/function.rs @@ -131,9 +131,9 @@ impl<'de> Deserialize<'de> for Functions { } } - // if inner_data.is_empty() { - // return Err(Error::custom("has no condition element")); - // } + if inner_data.is_empty() { + return Err(Error::custom("has no condition element")); + } Ok(inner_data) } diff --git a/iam/src/store/object.rs b/iam/src/store/object.rs index e9c200646..99bf40db9 100644 --- a/iam/src/store/object.rs +++ b/iam/src/store/object.rs @@ -4,7 +4,7 @@ use crate::{ cache::{Cache, CacheEntity}, error::{is_err_no_such_policy, is_err_no_such_user}, get_global_action_cred, - manager::get_default_policyes, + manager::{extract_jwt_claims, get_default_policyes}, policy::PolicyDoc, }; use ecstore::{ @@ -420,8 +420,20 @@ impl Store for ObjectStore { u.credentials.access_key = name.to_owned(); } - if u.credentials.session_token.is_empty() { - // TODO: 解析sts + if !u.credentials.session_token.is_empty() { + match extract_jwt_claims(&u) { + Ok(claims) => { + u.credentials.claims = Some(claims); + } + Err(err) => { + if u.credentials.is_temp() { + let _ = self.delete_iam_config(get_user_identity_path(name, user_type)).await; + let _ = self.delete_iam_config(get_mapped_policy_path(name, user_type, false)).await; + } + warn!("extract_jwt_claims failed: {}", err); + return Err(Error::new(crate::error::Error::NoSuchUser(name.to_owned()))); + } + } } Ok(u) @@ -807,7 +819,7 @@ impl Store for ObjectStore { } // group policy - if let Some(item_name_list) = listed_config_items.get(SVC_ACC_LIST_KEY) { + if let Some(item_name_list) = listed_config_items.get(POLICY_DB_GROUPS_LIST_KEY) { let mut items_cache = CacheEntity::default(); for item in item_name_list.iter() { @@ -815,7 +827,9 @@ impl Store for ObjectStore { info!("load group policy: {}", name); if let Err(err) = self.load_mapped_policy(name, UserType::Reg, true, &mut items_cache).await { - return Err(Error::msg(std::format!("load group failed: {}", err))); + if !is_err_no_such_policy(&err) { + return Err(Error::msg(std::format!("load group policy failed: {}", err))); + } }; } @@ -825,7 +839,7 @@ impl Store for ObjectStore { let mut sts_policies_cache = CacheEntity::default(); // svc users - if let Some(item_name_list) = listed_config_items.get(POLICY_DB_GROUPS_LIST_KEY) { + if let Some(item_name_list) = listed_config_items.get(SVC_ACC_LIST_KEY) { let mut items_cache = HashMap::default(); for item in item_name_list.iter() { diff --git a/iam/src/sys.rs b/iam/src/sys.rs index 5188bab8d..6a0167088 100644 --- a/iam/src/sys.rs +++ b/iam/src/sys.rs @@ -229,6 +229,11 @@ impl IamSys { } } + m.insert( + "exp".to_string(), + serde_json::Value::Number(serde_json::Number::from(opts.expiration.map_or(0, |t| t.unix_timestamp()))), + ); + let (access_key, secret_key) = if !opts.access_key.is_empty() || !opts.secret_key.is_empty() { (opts.access_key, opts.secret_key) } else { diff --git a/madmin/src/service_commands.rs b/madmin/src/service_commands.rs index 1b65a5a7a..30eabdb13 100644 --- a/madmin/src/service_commands.rs +++ b/madmin/src/service_commands.rs @@ -71,29 +71,29 @@ impl ServiceTraceOpts { }) .collect(); - self.s3 = query_pairs.get("s3").map_or(false, |v| v == "true"); - self.os = query_pairs.get("os").map_or(false, |v| v == "true"); - self.scanner = query_pairs.get("scanner").map_or(false, |v| v == "true"); - self.decommission = query_pairs.get("decommission").map_or(false, |v| v == "true"); - self.healing = query_pairs.get("healing").map_or(false, |v| v == "true"); - self.batch_replication = query_pairs.get("batch-replication").map_or(false, |v| v == "true"); - self.batch_key_rotation = query_pairs.get("batch-keyrotation").map_or(false, |v| v == "true"); - self.batch_expire = query_pairs.get("batch-expire").map_or(false, |v| v == "true"); - if query_pairs.get("all").map_or(false, |v| v == "true") { + self.s3 = query_pairs.get("s3").is_some_and(|v| v == "true"); + self.os = query_pairs.get("os").is_some_and(|v| v == "true"); + self.scanner = query_pairs.get("scanner").is_some_and(|v| v == "true"); + self.decommission = query_pairs.get("decommission").is_some_and(|v| v == "true"); + self.healing = query_pairs.get("healing").is_some_and(|v| v == "true"); + self.batch_replication = query_pairs.get("batch-replication").is_some_and(|v| v == "true"); + self.batch_key_rotation = query_pairs.get("batch-keyrotation").is_some_and(|v| v == "true"); + self.batch_expire = query_pairs.get("batch-expire").is_some_and(|v| v == "true"); + if query_pairs.get("all").is_some_and(|v| v == "true") { self.s3 = true; self.internal = true; self.storage = true; self.os = true; } - self.rebalance = query_pairs.get("rebalance").map_or(false, |v| v == "true"); - self.storage = query_pairs.get("storage").map_or(false, |v| v == "true"); - self.internal = query_pairs.get("internal").map_or(false, |v| v == "true"); - self.only_errors = query_pairs.get("err").map_or(false, |v| v == "true"); - self.replication_resync = query_pairs.get("replication-resync").map_or(false, |v| v == "true"); - self.bootstrap = query_pairs.get("bootstrap").map_or(false, |v| v == "true"); - self.ftp = query_pairs.get("ftp").map_or(false, |v| v == "true"); - self.ilm = query_pairs.get("ilm").map_or(false, |v| v == "true"); + self.rebalance = query_pairs.get("rebalance").is_some_and(|v| v == "true"); + self.storage = query_pairs.get("storage").is_some_and(|v| v == "true"); + self.internal = query_pairs.get("internal").is_some_and(|v| v == "true"); + self.only_errors = query_pairs.get("err").is_some_and(|v| v == "true"); + self.replication_resync = query_pairs.get("replication-resync").is_some_and(|v| v == "true"); + self.bootstrap = query_pairs.get("bootstrap").is_some_and(|v| v == "true"); + self.ftp = query_pairs.get("ftp").is_some_and(|v| v == "true"); + self.ilm = query_pairs.get("ilm").is_some_and(|v| v == "true"); if let Some(threshold) = query_pairs.get("threshold") { let duration = parse_duration(threshold)?; diff --git a/madmin/src/user.rs b/madmin/src/user.rs index ffe402d13..7de50f5b5 100644 --- a/madmin/src/user.rs +++ b/madmin/src/user.rs @@ -104,7 +104,7 @@ pub struct ServiceAccountInfo { #[serde(rename = "description", skip_serializing_if = "Option::is_none")] pub description: Option, - #[serde(rename = "expiration", skip_serializing_if = "Option::is_none")] + #[serde(rename = "expiration", with = "time::serde::rfc3339::option")] pub expiration: Option, } @@ -134,7 +134,7 @@ pub struct AddServiceAccountReq { #[serde(rename = "description", skip_serializing_if = "Option::is_none")] pub description: Option, - #[serde(rename = "expiration", skip_serializing_if = "Option::is_none")] + #[serde(rename = "expiration", with = "time::serde::rfc3339::option")] pub expiration: Option, } diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 5b3b89f47..4097167a7 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -7,6 +7,9 @@ repository.workspace = true rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "rustfs" +path = "src/main.rs" [lints] workspace = true @@ -69,7 +72,8 @@ crypto = { path = "../crypto" } iam = { path = "../iam" } jsonwebtoken = "9.3.0" tower-http = { version = "0.6.2", features = ["cors"] } -include_dir = "0.7.4" +mime_guess = "2.0.5" +rust-embed = { version = "8.5.0", features = ["interpolate-folder-path"] } [build-dependencies] prost-build.workspace = true diff --git a/rustfs/src/admin/handlers.rs b/rustfs/src/admin/handlers.rs index 0b239bf25..dc9bfba01 100644 --- a/rustfs/src/admin/handlers.rs +++ b/rustfs/src/admin/handlers.rs @@ -18,12 +18,11 @@ use ecstore::store::is_valid_object_prefix; use ecstore::store_api::StorageAPI; use ecstore::utils::crypto::base64_encode; use ecstore::utils::path::path_join; -use ecstore::utils::xml; use ecstore::GLOBAL_Endpoints; use futures::{Stream, StreamExt}; use http::{HeaderMap, Uri}; use hyper::StatusCode; -use iam::auth::{get_claims_from_token_with_secret, get_new_credentials_with_metadata}; +use iam::auth::get_claims_from_token_with_secret; use iam::error::Error as IamError; use iam::policy::Policy; use iam::sys::SESSION_POLICY_NAME; @@ -33,10 +32,7 @@ use madmin::utils::parse_duration; use matchit::Params; use s3s::header::CONTENT_TYPE; use s3s::stream::{ByteStream, DynByteStream}; -use s3s::{ - dto::{AssumeRoleOutput, Credentials, Timestamp}, - s3_error, Body, S3Error, S3Request, S3Response, S3Result, -}; +use s3s::{s3_error, Body, S3Error, S3Request, S3Response, S3Result}; use s3s::{S3ErrorCode, StdError}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -47,7 +43,6 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration as std_Duration; -use time::{Duration, OffsetDateTime}; use tokio::sync::mpsc::{self}; use tokio::time::interval; use tokio::{select, spawn}; @@ -57,32 +52,10 @@ use tracing::{error, info, warn}; pub mod group; pub mod policy; pub mod service_account; +pub mod sts; pub mod trace; pub mod user; -const ASSUME_ROLE_ACTION: &str = "AssumeRole"; -const ASSUME_ROLE_VERSION: &str = "2011-06-15"; - -#[derive(Deserialize, Debug, Default)] -#[serde(rename_all = "PascalCase", default)] -pub struct AssumeRoleRequest { - pub action: String, - pub duration_seconds: usize, - pub version: String, - pub role_arn: String, - pub role_session_name: String, - pub policy: String, - pub external_id: String, -} - -fn get_token_signing_key() -> Option { - if let Some(s) = get_global_action_cred() { - Some(s.secret_key.clone()) - } else { - None - } -} - // check_key_valid get auth.cred pub async fn check_key_valid(security_token: Option, ak: &str) -> S3Result<(auth::Credentials, bool)> { let Some(mut cred) = get_global_action_cred() else { @@ -216,114 +189,6 @@ pub fn populate_session_policy(claims: &mut HashMap, policy: &str Ok(()) } -pub struct AssumeRoleHandle {} -#[async_trait::async_trait] -impl Operation for AssumeRoleHandle { - async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - warn!("handle AssumeRoleHandle"); - - let Some(user) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) }; - - let session_token = get_session_token(&req.headers); - if session_token.is_some() { - return Err(s3_error!(InvalidRequest, "AccessDenied1")); - } - - let (cred, _owner) = check_key_valid(session_token, &user.access_key).await?; - - // // TODO: 判断权限, 不允许sts访问 - if cred.is_temp() || cred.is_service_account() { - return Err(s3_error!(InvalidRequest, "AccessDenied")); - } - - let mut input = req.input; - - let bytes = match input.store_all_unlimited().await { - Ok(b) => b, - Err(e) => { - warn!("get body failed, e: {:?}", e); - return Err(s3_error!(InvalidRequest, "get body failed")); - } - }; - - let body: AssumeRoleRequest = from_bytes(&bytes).map_err(|_e| s3_error!(InvalidRequest, "get body failed"))?; - - if body.action.as_str() != ASSUME_ROLE_ACTION { - return Err(s3_error!(InvalidArgument, "not suport action")); - } - - if body.version.as_str() != ASSUME_ROLE_VERSION { - return Err(s3_error!(InvalidArgument, "not suport version")); - } - - let mut claims = cred.claims.unwrap_or_default(); - - populate_session_policy(&mut claims, &body.policy)?; - - let exp = { - if body.duration_seconds > 0 { - body.duration_seconds - } else { - 3600 - } - }; - - claims.insert( - "exp".to_string(), - serde_json::Value::Number(serde_json::Number::from(OffsetDateTime::now_utc().unix_timestamp() + exp as i64)), - ); - - claims.insert("parent".to_string(), serde_json::Value::String(cred.access_key.clone())); - - // warn!("AssumeRole get cred {:?}", &user); - // warn!("AssumeRole get body {:?}", &body); - - let Ok(iam_store) = iam::get() else { return Err(s3_error!(InvalidRequest, "iam not init")) }; - - if let Err(_err) = iam_store.policy_db_get(&cred.access_key, &cred.groups).await { - return Err(s3_error!(InvalidArgument, "invalid policy arg")); - } - - let Some(secret) = get_token_signing_key() else { - return Err(s3_error!(InvalidArgument, "global active sk not init")); - }; - - info!("AssumeRole get claims {:?}", &claims); - - let mut new_cred = get_new_credentials_with_metadata(&claims, &secret) - .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("get new cred failed {}", e)))?; - - new_cred.parent_user = cred.access_key.clone(); - - info!("AssumeRole get new_cred {:?}", &new_cred); - - if let Err(_err) = iam_store.set_temp_user(&new_cred.access_key, &new_cred, None).await { - return Err(s3_error!(InternalError, "set_temp_user failed")); - } - - // TODO: globalSiteReplicationSys - - let resp = AssumeRoleOutput { - credentials: Some(Credentials { - access_key_id: new_cred.access_key, - expiration: Timestamp::from( - new_cred - .expiration - .unwrap_or(OffsetDateTime::now_utc().saturating_add(Duration::seconds(3600))), - ), - secret_access_key: new_cred.secret_key, - session_token: new_cred.session_token, - }), - ..Default::default() - }; - - // getAssumeRoleCredentials - let output = xml::serialize::(&resp).unwrap(); - - Ok(S3Response::new((StatusCode::OK, Body::from(output)))) - } -} - #[derive(Debug, Serialize, Default)] #[serde(rename_all = "PascalCase", default)] pub struct AccountInfo { diff --git a/rustfs/src/admin/handlers/service_account.rs b/rustfs/src/admin/handlers/service_account.rs index be1588cc2..0af420bd6 100644 --- a/rustfs/src/admin/handlers/service_account.rs +++ b/rustfs/src/admin/handlers/service_account.rs @@ -40,9 +40,10 @@ impl Operation for AddServiceAccount { } }; - let mut create_req: AddServiceAccountReq = + let create_req: AddServiceAccountReq = serde_json::from_slice(&body[..]).map_err(|e| s3_error!(InvalidRequest, "unmarshal body failed, e: {:?}", e))?; - create_req.expiration = create_req.expiration.and_then(|expire| expire.replace_millisecond(0).ok()); + + // create_req.expiration = create_req.expiration.and_then(|expire| expire.replace_millisecond(0).ok()); if has_space_be(&create_req.access_key) { return Err(s3_error!(InvalidRequest, "access key has spaces")); @@ -71,7 +72,7 @@ impl Operation for AddServiceAccount { let req_groups = cred.groups.clone(); let mut req_is_derived_cred = false; - if cred.is_owner() || cred.is_service_account() { + if cred.is_service_account() || cred.is_temp() { req_parent_user = cred.parent_user.clone(); req_is_derived_cred = true; } @@ -128,7 +129,7 @@ impl Operation for AddServiceAccount { .await .map_err(|e| { debug!("create service account failed, e: {:?}", e); - s3_error!(InternalError, "create service account failed") + s3_error!(InternalError, "create service account failed, e: {:?}", e) })?; let resp = AddServiceAccountResp { diff --git a/rustfs/src/admin/handlers/sts.rs b/rustfs/src/admin/handlers/sts.rs new file mode 100644 index 000000000..adb31e7d6 --- /dev/null +++ b/rustfs/src/admin/handlers/sts.rs @@ -0,0 +1,139 @@ +use crate::admin::{ + handlers::{check_key_valid, get_session_token, populate_session_policy}, + router::Operation, +}; +use ecstore::utils::xml; +use http::StatusCode; +use iam::{auth::get_new_credentials_with_metadata, manager::get_token_signing_key}; +use matchit::Params; +use s3s::{ + dto::{AssumeRoleOutput, Credentials, Timestamp}, + s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, +}; +use serde::Deserialize; +use serde_urlencoded::from_bytes; +use time::{Duration, OffsetDateTime}; +use tracing::{info, warn}; + +const ASSUME_ROLE_ACTION: &str = "AssumeRole"; +const ASSUME_ROLE_VERSION: &str = "2011-06-15"; + +#[derive(Deserialize, Debug, Default)] +#[serde(rename_all = "PascalCase", default)] +pub struct AssumeRoleRequest { + pub action: String, + pub duration_seconds: usize, + pub version: String, + pub role_arn: String, + pub role_session_name: String, + pub policy: String, + pub external_id: String, +} + +pub struct AssumeRoleHandle {} +#[async_trait::async_trait] +impl Operation for AssumeRoleHandle { + async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { + warn!("handle AssumeRoleHandle"); + + let Some(user) = req.credentials else { return Err(s3_error!(InvalidRequest, "get cred failed")) }; + + let session_token = get_session_token(&req.headers); + if session_token.is_some() { + return Err(s3_error!(InvalidRequest, "AccessDenied1")); + } + + let (cred, _owner) = check_key_valid(session_token, &user.access_key).await?; + + // // TODO: 判断权限, 不允许sts访问 + if cred.is_temp() || cred.is_service_account() { + return Err(s3_error!(InvalidRequest, "AccessDenied")); + } + + let mut input = req.input; + + let bytes = match input.store_all_unlimited().await { + Ok(b) => b, + Err(e) => { + warn!("get body failed, e: {:?}", e); + return Err(s3_error!(InvalidRequest, "get body failed")); + } + }; + + let body: AssumeRoleRequest = from_bytes(&bytes).map_err(|_e| s3_error!(InvalidRequest, "get body failed"))?; + + if body.action.as_str() != ASSUME_ROLE_ACTION { + return Err(s3_error!(InvalidArgument, "not suport action")); + } + + if body.version.as_str() != ASSUME_ROLE_VERSION { + return Err(s3_error!(InvalidArgument, "not suport version")); + } + + let mut claims = cred.claims.unwrap_or_default(); + + populate_session_policy(&mut claims, &body.policy)?; + + let exp = { + if body.duration_seconds > 0 { + body.duration_seconds + } else { + 3600 + } + }; + + claims.insert( + "exp".to_string(), + serde_json::Value::Number(serde_json::Number::from(OffsetDateTime::now_utc().unix_timestamp() + exp as i64)), + ); + + claims.insert("parent".to_string(), serde_json::Value::String(cred.access_key.clone())); + + // warn!("AssumeRole get cred {:?}", &user); + // warn!("AssumeRole get body {:?}", &body); + + let Ok(iam_store) = iam::get() else { return Err(s3_error!(InvalidRequest, "iam not init")) }; + + if let Err(_err) = iam_store.policy_db_get(&cred.access_key, &cred.groups).await { + return Err(s3_error!(InvalidArgument, "invalid policy arg")); + } + + let Some(secret) = get_token_signing_key() else { + return Err(s3_error!(InvalidArgument, "global active sk not init")); + }; + + info!("AssumeRole get claims {:?}", &claims); + + let mut new_cred = get_new_credentials_with_metadata(&claims, &secret) + .map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("get new cred failed {}", e)))?; + + new_cred.parent_user = cred.access_key.clone(); + + info!("AssumeRole get new_cred {:?}", &new_cred); + + if let Err(_err) = iam_store.set_temp_user(&new_cred.access_key, &new_cred, None).await { + return Err(s3_error!(InternalError, "set_temp_user failed")); + } + + // TODO: globalSiteReplicationSys + + let resp = AssumeRoleOutput { + credentials: Some(Credentials { + access_key_id: new_cred.access_key, + expiration: Timestamp::from( + new_cred + .expiration + .unwrap_or(OffsetDateTime::now_utc().saturating_add(Duration::seconds(3600))), + ), + secret_access_key: new_cred.secret_key, + session_token: new_cred.session_token, + }), + ..Default::default() + }; + + // getAssumeRoleCredentials + let output = xml::serialize::(&resp).unwrap(); + + Ok(S3Response::new((StatusCode::OK, Body::from(output)))) + } +} diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index 119f18834..9c8f24033 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -7,7 +7,7 @@ use common::error::Result; use handlers::{ group, policy, service_account::{AddServiceAccount, DeleteServiceAccount, InfoServiceAccount, ListServiceAccount, UpdateServiceAccount}, - user, + sts, user, }; use hyper::Method; use router::{AdminOperation, S3Router}; @@ -19,7 +19,7 @@ pub fn make_admin_route() -> Result { let mut r: S3Router = S3Router::new(); // 1 - r.insert(Method::POST, "/", AdminOperation(&handlers::AssumeRoleHandle {}))?; + r.insert(Method::POST, "/", AdminOperation(&sts::AssumeRoleHandle {}))?; regist_user_route(&mut r)?; diff --git a/rustfs/src/auth.rs b/rustfs/src/auth.rs index d74081107..8106ff801 100644 --- a/rustfs/src/auth.rs +++ b/rustfs/src/auth.rs @@ -1,4 +1,3 @@ -use log::warn; use s3s::auth::S3Auth; use s3s::auth::SecretKey; use s3s::auth::SimpleAuth; @@ -20,22 +19,19 @@ impl IAMAuth { impl S3Auth for IAMAuth { async fn get_secret_key(&self, access_key: &str) -> S3Result { if access_key.is_empty() { - return Err(s3_error!(NotSignedUp, "Your account is not signed up")); + return Err(s3_error!(UnauthorizedAccess, "Your account is not signed up")); } if let Ok(key) = self.simple_auth.get_secret_key(access_key).await { return Ok(key); } - warn!("Failed to get secret key from simple auth"); - if let Ok(iam_store) = iam::get() { if let Some(id) = iam_store.get_user(access_key).await { - warn!("get cred {:?}", id.credentials); return Ok(SecretKey::from(id.credentials.secret_key.clone())); } } - Err(s3_error!(NotSignedUp, "Your account is not signed up2")) + Err(s3_error!(UnauthorizedAccess, "Your account is not signed up2")) } } diff --git a/rustfs/src/config/mod.rs b/rustfs/src/config/mod.rs index 406b211fa..663dd03e9 100644 --- a/rustfs/src/config/mod.rs +++ b/rustfs/src/config/mod.rs @@ -35,13 +35,16 @@ const LONG_VERSION: &str = concat!( #[command(version = SHORT_VERSION, long_version = LONG_VERSION)] pub struct Opt { /// DIR points to a directory on a filesystem. - #[arg(required = true)] + #[arg(required = true, env = "RUSTFS_VOLUMES")] pub volumes: Vec, /// bind to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname #[arg(long, default_value_t = format!("0.0.0.0:{}", DEFAULT_PORT), env = "RUSTFS_ADDRESS")] pub address: String, + #[arg(long, env = "RUSTFS_SERVER_DOMAINS")] + pub server_domains: Vec, + /// Access key used for authentication. #[arg(long, default_value_t = DEFAULT_ACCESS_KEY.to_string(), env = "RUSTFS_ACCESS_KEY")] pub access_key: String, diff --git a/rustfs/src/console.rs b/rustfs/src/console.rs index 07aa81199..5af9f534a 100644 --- a/rustfs/src/console.rs +++ b/rustfs/src/console.rs @@ -6,16 +6,32 @@ use axum::{ Router, }; -use include_dir::{include_dir, Dir}; +use mime_guess::from_path; +use rust_embed::RustEmbed; +use serde::Serialize; -static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static"); +#[derive(RustEmbed)] +#[folder = "$CARGO_MANIFEST_DIR/static"] +struct StaticFiles; async fn static_handler(uri: axum::http::Uri) -> impl IntoResponse { - let path = uri.path().trim_start_matches('/'); - if let Some(file) = STATIC_DIR.get_file(path) { + let mut path = uri.path().trim_start_matches('/'); + if path.is_empty() { + path = "index.html" + } + if let Some(file) = StaticFiles::get(path) { + let mime_type = from_path(path).first_or_octet_stream(); Response::builder() .status(StatusCode::OK) - .body(Body::from(file.contents())) + .header("Content-Type", mime_type.to_string()) + .body(Body::from(file.data)) + .unwrap() + } else if let Some(file) = StaticFiles::get("index.html") { + let mime_type = from_path("index.html").first_or_octet_stream(); + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", mime_type.to_string()) + .body(Body::from(file.data)) .unwrap() } else { Response::builder() @@ -25,13 +41,83 @@ async fn static_handler(uri: axum::http::Uri) -> impl IntoResponse { } } -pub async fn start_static_file_server(addrs: &str) { +#[derive(Debug, Serialize)] +struct Config { + api: Api, + s3: S3, + release: Release, + license: License, +} + +impl Config { + fn new(url: &str, version: &str, date: &str) -> Self { + Config { + api: Api { + base_url: format!("{}/rustfs/admin/v3", url), + }, + s3: S3 { + endpoint: url.to_owned(), + region: "cn-east-1".to_owned(), + }, + release: Release { + version: version.to_string(), + date: date.to_string(), + }, + license: License { + name: "Apache-2.0".to_string(), + url: "https://www.apache.org/licenses/LICENSE-2.0".to_string(), + }, + } + } + + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_default() + } +} + +#[derive(Debug, Serialize)] +struct Api { + #[serde(rename = "baseURL")] + base_url: String, +} + +#[derive(Debug, Serialize)] +struct S3 { + endpoint: String, + region: String, +} + +#[derive(Debug, Serialize)] +struct Release { + version: String, + date: String, +} + +#[derive(Debug, Serialize)] +struct License { + name: String, + url: String, +} + +async fn config_handler(axum::extract::Extension(fs_addr): axum::extract::Extension) -> impl IntoResponse { + let cfg = Config::new(&fs_addr, "v0.0.1", "2025-01-01").to_json(); + + Response::builder() + .header("content-type", "application/json") + .status(StatusCode::OK) + .body(Body::from(cfg)) + .unwrap() +} + +pub async fn start_static_file_server(addrs: &str, fs_addr: &str) { // 创建路由 - let app = Router::new().route("/*file", get(static_handler)); + let app = Router::new() + .route("/config.json", get(config_handler).layer(axum::extract::Extension(fs_addr.to_owned()))) + .nest_service("/", get(static_handler)); let listener = tokio::net::TcpListener::bind(addrs).await.unwrap(); - println!("console listening on: {}", listener.local_addr().unwrap()); + println!("console running on: http://{} with s3 api {}", listener.local_addr().unwrap(), fs_addr); axum::serve(listener, app).await.unwrap(); } diff --git a/rustfs/src/grpc.rs b/rustfs/src/grpc.rs index 9b2e88f75..0411d0957 100644 --- a/rustfs/src/grpc.rs +++ b/rustfs/src/grpc.rs @@ -67,10 +67,7 @@ fn match_for_io_error(err_status: &Status) -> Option<&std::io::Error> { } } - err = match err.source() { - Some(err) => err, - None => return None, - }; + err = err.source()?; } } diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index cfd84975a..2e6a7ab23 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -31,7 +31,7 @@ use hyper_util::{ }; use iam::init_iam_sys; use protos::proto_gen::node_service::node_service_server::NodeServiceServer; -use s3s::service::S3ServiceBuilder; +use s3s::{host::MultiDomain, service::S3ServiceBuilder}; use service::hybrid; use std::{io::IsTerminal, net::SocketAddr}; use tokio::net::TcpListener; @@ -139,6 +139,11 @@ async fn run(opt: config::Opt) -> Result<()> { b.set_route(admin::make_admin_route()?); + if !opt.server_domains.is_empty() { + info!("virtual-hosted-style requests are enabled use domain_name {:?}", &opt.server_domains); + b.set_host(MultiDomain::new(&opt.server_domains)?); + } + // // Enable parsing virtual-hosted-style requests // if let Some(dm) = opt.domain_name { // info!("virtual-hosted-style requests are enabled use domain_name {}", &dm); @@ -236,7 +241,13 @@ async fn run(opt: config::Opt) -> Result<()> { if opt.console_enable { info!("console is enabled"); tokio::spawn(async move { - console::start_static_file_server(&opt.console_address).await; + let ep = if !opt.server_domains.is_empty() { + format!("http://{}", opt.server_domains[0].clone()) + } else { + format!("http://127.0.0.1:{}", server_port) + }; + + console::start_static_file_server(&opt.console_address, &ep).await; }); } diff --git a/rustfs/src/storage/access.rs b/rustfs/src/storage/access.rs index 1581e96e0..2ff57c9af 100644 --- a/rustfs/src/storage/access.rs +++ b/rustfs/src/storage/access.rs @@ -52,7 +52,7 @@ impl S3Access for FS { ); if cx.credentials().is_none() { - return Err(s3_error!(AccessDenied, "Signature is required")); + return Err(s3_error!(UnauthorizedAccess, "Signature is required")); }; // TODO: FIXME: check auth diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index 422a1d2c8..f900883be 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -47,6 +47,7 @@ use s3s::S3; use s3s::{S3Request, S3Response}; use std::fmt::Debug; use std::str::FromStr; +use tracing::debug; use tracing::error; use tracing::info; use transform_stream::AsyncTryStream; @@ -549,6 +550,7 @@ impl S3 for FS { .map(|v| Bucket { creation_date: v.created.map(Timestamp::from), name: Some(v.name.clone()), + ..Default::default() }) .collect(); @@ -751,6 +753,7 @@ impl S3 for FS { content_length, tagging, metadata, + version_id, .. } = input; @@ -784,10 +787,12 @@ impl S3 for FS { metadata.insert(xhttp::AMZ_OBJECT_TAGGING.to_owned(), tags); } - let opts: ObjectOptions = put_opts(&bucket, &key, None, &req.headers, Some(metadata)) + let opts: ObjectOptions = put_opts(&bucket, &key, version_id, &req.headers, Some(metadata)) .await .map_err(to_s3_error)?; + debug!("put_object opts {:?}", &opts); + let obj_info = store .put_object(&bucket, &key, &mut reader, &opts) .await @@ -810,7 +815,11 @@ impl S3 for FS { req: S3Request, ) -> S3Result> { let CreateMultipartUploadInput { - bucket, key, tagging, .. + bucket, + key, + tagging, + version_id, + .. } = req.input; // mc cp step 3 @@ -827,7 +836,7 @@ impl S3 for FS { metadata.insert(xhttp::AMZ_OBJECT_TAGGING.to_owned(), tags); } - let opts: ObjectOptions = put_opts(&bucket, &key, None, &req.headers, Some(metadata)) + let opts: ObjectOptions = put_opts(&bucket, &key, version_id, &req.headers, Some(metadata)) .await .map_err(to_s3_error)?; @@ -952,7 +961,7 @@ impl S3 for FS { return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); }; - store + let oi = store .complete_multipart_upload(&bucket, &key, &upload_id, uploaded_parts, opts) .await .map_err(to_s3_error)?; @@ -960,6 +969,7 @@ impl S3 for FS { let output = CompleteMultipartUploadOutput { bucket: Some(bucket), key: Some(key), + e_tag: oi.etag, ..Default::default() }; Ok(S3Response::new(output)) diff --git a/rustfs/static/index.html b/rustfs/static/index.html deleted file mode 100644 index 612c5bbb3..000000000 --- a/rustfs/static/index.html +++ /dev/null @@ -1 +0,0 @@ -static index \ No newline at end of file diff --git a/rustfs/static/readme.md b/rustfs/static/readme.md new file mode 100644 index 000000000..325303e1a --- /dev/null +++ b/rustfs/static/readme.md @@ -0,0 +1 @@ +console static path, do not delete \ No newline at end of file diff --git a/scripts/e2e-run.sh b/scripts/e2e-run.sh new file mode 100755 index 000000000..26e3d9c69 --- /dev/null +++ b/scripts/e2e-run.sh @@ -0,0 +1,11 @@ +#!/bin/bash -ex +BIN=$1 +VOLUME=$2 + +chmod +x $BIN +sudo mkdir -p $VOLUME + +export RUST_LOG="rustfs=debug,ecstore=debug,s3s=debug,iam=debug" +export RUST_BACKTRACE=full + +sudo nohup $BIN $VOLUME > /tmp/rustfs.log 2>&1 & diff --git a/scripts/run.sh b/scripts/run.sh index b2436ac16..f85870714 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -7,10 +7,11 @@ fi current_dir=$(pwd) mkdir -p ./target/volume/test -mkdir -p ./target/volume/test{0..4} +# mkdir -p ./target/volume/test{0..4} if [ -z "$RUST_LOG" ]; then + export RUST_BACKTRACE=1 export RUST_LOG="rustfs=debug,ecstore=debug,s3s=debug,iam=debug" fi @@ -18,21 +19,16 @@ fi # export RUSTFS_STORAGE_CLASS_INLINE_BLOCK="512 KB" - -# DATA_DIR_ARG="./target/volume/test{0...4}" -DATA_DIR_ARG="./target/volume/test" +# RUSTFS_VOLUMES="./target/volume/test{0...4}" +export RUSTFS_VOLUMES="./target/volume/test" +export RUSTFS_ADDRESS="0.0.0.0:9000" +export RUSTFS_CONSOLE_ENABLE=true +export RUSTFS_CONSOLE_ADDRESS="0.0.0.0:9002" +# export RUSTFS_SERVER_DOMAINS="localhost:9000" if [ -n "$1" ]; then - DATA_DIR_ARG="$1" + export RUSTFS_VOLUMES="$1" fi -# cargo run "$DATA_DIR_ARG" - # -- --access-key AKEXAMPLERUSTFS \ - # --secret-key SKEXAMPLERUSTFS \ - # --address 0.0.0.0:9010 \ - # --domain-name 127.0.0.1:9010 \ - # "$DATA_DIR_ARG" - -./target/debug/rustfs "$DATA_DIR_ARG" -# cargo run ./target/volume/test \ No newline at end of file +cargo run --bin rustfs \ No newline at end of file