reqsign-0.16.2/.cargo_vcs_info.json0000644000000001360000000000100125720ustar { "git": { "sha1": "0c960e2d658f20ce0945e5a65652f8f34a7a8811" }, "path_in_vcs": "" }reqsign-0.16.2/.env.example000064400000000000000000000015271046102023000136120ustar 00000000000000# Azure Storage REQSIGN_AZURE_STORAGE_TEST=false REQSIGN_AZURE_STORAGE_URL= REQSIGN_AZURE_STORAGE_ACCOUNT_NAME= REQSIGN_AZURE_STORAGE_ACCOUNT_KEY= # AWS V4 REQSIGN_AWS_V4_TEST=false REQSIGN_AWS_V4_SERVICE= REQSIGN_AWS_V4_URL= REQSIGN_AWS_V4_REGION= REQSIGN_AWS_V4_ACCESS_KEY= REQSIGN_AWS_V4_SECRET_KEY= REQSIGN_AWS_ROLE_ARN= REQSIGN_AWS_IDP_URL= REQSIGN_AWS_IDP_BODY= # Google Cloud Storage Test REQSIGN_GOOGLE_TEST=false REQSIGN_GOOGLE_CREDENTIAL= REQSIGN_GOOGLE_CLOUD_STORAGE_SCOPE= REQSIGN_GOOGLE_CLOUD_STORAGE_URL= ## Tencent COS Test REQSIGN_TENCENT_COS_TEST=on REQSIGN_TENCENT_COS_ACCESS_KEY= REQSIGN_TENCENT_COS_SECRET_KEY= REQSIGN_TENCENT_COS_URL=http://.urlreqsign-0.16.2/.github/FUNDING.yml000064400000000000000000000000211046102023000145300ustar 00000000000000github: [Xuanwo] reqsign-0.16.2/.github/actions/check/action.yml000064400000000000000000000006451046102023000174440ustar 00000000000000name: 'Check' description: 'Check will do all essential checks' inputs: github_token: description: "Github Token" required: true runs: using: "composite" steps: - name: Format uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - name: Clippy uses: actions-rs/cargo@v1 with: command: clippy args: --all-targets -- -D warnings reqsign-0.16.2/.github/dependabot.yml000064400000000000000000000004361046102023000155550ustar 00000000000000version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" # Maintain dependencies for rust - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" reqsign-0.16.2/.github/workflows/ci.yml000064400000000000000000000177271046102023000161130ustar 00000000000000name: CI on: push: branches: - main - v* pull_request: branches: - main - v* concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true jobs: check: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/check with: github_token: ${{ secrets.GITHUB_TOKEN }} build: runs-on: ${{ matrix.os }} strategy: matrix: os: - ubuntu-22.04 - macos-13 - windows-2022 steps: - uses: actions/checkout@v4 - name: Build uses: actions-rs/cargo@v1 with: command: build build_under_wasm: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Build run: | rustup target add wasm32-unknown-unknown cargo build --target wasm32-unknown-unknown build_single_feature: runs-on: ubuntu-22.04 strategy: matrix: feature: - services-aliyun - services-aws - services-azblob - services-google - services-huaweicloud - services-oracle - services-tencent steps: - uses: actions/checkout@v4 - name: Build uses: actions-rs/cargo@v1 with: command: build args: --no-default-features --features reqwest_request,${{ matrix.feature }} build_all_features: runs-on: ${{ matrix.os }} strategy: matrix: os: - ubuntu-22.04 - macos-13 - windows-2022 steps: - uses: actions/checkout@v4 - name: Build uses: actions-rs/cargo@v1 with: command: build args: --all-features unit: runs-on: ubuntu-22.04 permissions: id-token: write steps: - uses: actions/checkout@v4 - name: Install cargo-nextest run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin - name: Test run: cargo nextest run --no-fail-fast env: RUST_LOG: DEBUG RUST_BACKTRACE: full # Azure Storage Test REQSIGN_AZURE_STORAGE_TEST: ${{ secrets.REQSIGN_AZURE_STORAGE_TEST }} REQSIGN_AZURE_STORAGE_URL: ${{ secrets.REQSIGN_AZURE_STORAGE_URL }} REQSIGN_AZURE_STORAGE_ACCOUNT_NAME: ${{ secrets.REQSIGN_AZURE_STORAGE_ACCOUNT_NAME }} REQSIGN_AZURE_STORAGE_ACCOUNT_KEY: ${{ secrets.REQSIGN_AZURE_STORAGE_ACCOUNT_KEY }} # AWS V4 Test REQSIGN_AWS_V4_TEST: ${{ secrets.REQSIGN_AWS_V4_TEST }} REQSIGN_AWS_V4_SERVICE: ${{ secrets.REQSIGN_AWS_V4_SERVICE }} REQSIGN_AWS_V4_URL: ${{ secrets.REQSIGN_AWS_V4_URL }} REQSIGN_AWS_V4_REGION: ${{ secrets.REQSIGN_AWS_V4_REGION }} REQSIGN_AWS_V4_ACCESS_KEY: ${{ secrets.REQSIGN_AWS_V4_ACCESS_KEY }} REQSIGN_AWS_V4_SECRET_KEY: ${{ secrets.REQSIGN_AWS_V4_SECRET_KEY }} REQSIGN_AWS_ROLE_ARN: ${{ secrets.REQSIGN_AWS_ROLE_ARN }} REQSIGN_AWS_IDP_URL: ${{ secrets.REQSIGN_AWS_IDP_URL }} REQSIGN_AWS_IDP_BODY: ${{ secrets.REQSIGN_AWS_IDP_BODY }} # Google Cloud Storage Test REQSIGN_GOOGLE_TEST: ${{ secrets.REQSIGN_GOOGLE_TEST }} REQSIGN_GOOGLE_CREDENTIAL: ${{ secrets.REQSIGN_GOOGLE_CREDENTIAL }} REQSIGN_GOOGLE_CLOUD_STORAGE_SCOPE: ${{ secrets.REQSIGN_GOOGLE_CLOUD_STORAGE_SCOPE }} REQSIGN_GOOGLE_CLOUD_STORAGE_URL: ${{ secrets.REQSIGN_GOOGLE_CLOUD_STORAGE_URL }} # Aliyun OSS Test REQSIGN_ALIYUN_OSS_TEST: ${{ secrets.REQSIGN_ALIYUN_OSS_TEST }} REQSIGN_ALIYUN_OSS_BUCKET: ${{ secrets.REQSIGN_ALIYUN_OSS_BUCKET }} REQSIGN_ALIYUN_OSS_URL: ${{ secrets.REQSIGN_ALIYUN_OSS_URL }} REQSIGN_ALIYUN_OSS_ACCESS_KEY: ${{ secrets.REQSIGN_ALIYUN_OSS_ACCESS_KEY }} REQSIGN_ALIYUN_OSS_SECRET_KEY: ${{ secrets.REQSIGN_ALIYUN_OSS_SECRET_KEY }} REQSIGN_ALIYUN_PROVIDER_ARN: ${{ secrets.REQSIGN_ALIYUN_PROVIDER_ARN }} REQSIGN_ALIYUN_ROLE_ARN: ${{ secrets.REQSIGN_ALIYUN_ROLE_ARN }} REQSIGN_ALIYUN_IDP_URL: ${{ secrets.REQSIGN_ALIYUN_IDP_URL }} REQSIGN_ALIYUN_IDP_BODY: ${{ secrets.REQSIGN_ALIYUN_IDP_BODY }} # Tencent COS Test REQSIGN_TENCENT_COS_TEST: ${{ secrets.REQSIGN_TENCENT_COS_TEST }} REQSIGN_TENCENT_COS_ACCESS_KEY: ${{ secrets.REQSIGN_TENCENT_COS_ACCESS_KEY }} REQSIGN_TENCENT_COS_SECRET_KEY: ${{ secrets.REQSIGN_TENCENT_COS_SECRET_KEY }} REQSIGN_TENCENT_COS_URL: ${{ secrets.REQSIGN_TENCENT_COS_URL }} - name: Doctest run: cargo test --doc test_gcs_web_identify: runs-on: ubuntu-22.04 permissions: contents: "read" id-token: "write" if: github.event_name == 'push' || !github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 - name: Install cargo-nextest run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin - id: auth uses: google-github-actions/auth@v2.1.3 with: token_format: "access_token" create_credentials_file: true workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER_ID }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: Test run: cargo nextest run --no-fail-fast env: RUST_LOG: DEBUG RUST_BACKTRACE: full REQSIGN_GOOGLE_CREDENTIAL_PATH: ${{steps.auth.outputs.credentials_file_path}} test_tencent_cloud_web_identify: runs-on: ubuntu-22.04 permissions: contents: "read" id-token: "write" if: github.event_name == 'push' || !github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 - name: Install cargo-nextest run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin - name: Get Id Token uses: actions/github-script@v7 id: idtoken with: script: | let id_token = await core.getIDToken('sts.tencentcloudapi.com') core.exportVariable('GITHUB_ID_TOKEN', id_token) core.setSecret(id_token) - name: Test run: cargo nextest run --no-fail-fast env: RUST_LOG: DEBUG RUST_BACKTRACE: full REQSIGN_TENCENT_COS_TEST: ${{ secrets.REQSIGN_TENCENT_COS_TEST }} REQSIGN_TENCENT_COS_ACCESS_KEY: ${{ secrets.REQSIGN_TENCENT_COS_ACCESS_KEY }} REQSIGN_TENCENT_COS_SECRET_KEY: ${{ secrets.REQSIGN_TENCENT_COS_SECRET_KEY }} REQSIGN_TENCENT_COS_URL: ${{ secrets.REQSIGN_TENCENT_COS_URL }} REQSIGN_TENCENT_COS_ROLE_ARN: ${{ secrets.REQSIGN_TENCENT_COS_ROLE_ARN }} REQSIGN_TENCENT_COS_PROVIDER_ID: ${{ secrets.REQSIGN_TENCENT_COS_PROVIDER_ID }} REQSIGN_TENCENT_COS_REGION: ${{ secrets.REQSIGN_TENCENT_COS_REGION }} test_aws_web_identity: runs-on: ubuntu-22.04 permissions: contents: "read" id-token: "write" if: github.event_name == 'push' || !github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 - name: Install cargo-nextest run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin - name: Get Id Token uses: actions/github-script@v7 id: idtoken with: script: | let id_token = await core.getIDToken('sts.amazonaws.com') core.exportVariable('GITHUB_ID_TOKEN', id_token) core.setSecret(id_token) - name: Test run: cargo nextest run --no-fail-fast env: RUST_LOG: DEBUG RUST_BACKTRACE: full REQSIGN_AWS_S3_TEST: on REQSIGN_AWS_S3_REGION: ap-northeast-1 REQSIGN_AWS_ROLE_ARN: ${{ secrets.REQSIGN_AWS_ROLE_ARN }} REQSIGN_AWS_ASSUME_ROLE_ARN: ${{ secrets.REQSIGN_AWS_ASSUME_ROLE_ARN }} REQSIGN_AWS_PROVIDER_ARN: ${{ secrets.REQSIGN_AWS_PROVIDER_ARN }} reqsign-0.16.2/.gitignore000064400000000000000000000000301046102023000133430ustar 00000000000000/target Cargo.lock .env reqsign-0.16.2/.taplo.toml000064400000000000000000000026201046102023000134540ustar 00000000000000include = ["Cargo.toml", "**/*.toml"] [formatting] # Align consecutive entries vertically. align_entries = false # Append trailing commas for multi-line arrays. array_trailing_comma = true # Expand arrays to multiple lines that exceed the maximum column width. array_auto_expand = true # Collapse arrays that don't exceed the maximum column width and don't contain comments. array_auto_collapse = true # Omit white space padding from single-line arrays compact_arrays = true # Omit white space padding from the start and end of inline tables. compact_inline_tables = false # Maximum column width in characters, affects array expansion and collapse, this doesn't take whitespace into account. # Note that this is not set in stone, and works on a best-effort basis. column_width = 80 # Indent based on tables and arrays of tables and their subtables, subtables out of order are not indented. indent_tables = false # The substring that is used for indentation, should be tabs or spaces (but technically can be anything). indent_string = ' ' # Add trailing newline at the end of the file if not present. trailing_newline = true # Alphabetically reorder keys that are not separated by empty lines. reorder_keys = true # Maximum amount of allowed consecutive blank lines. This does not affect the whitespace at the end of the document, as it is always stripped. allowed_blank_lines = 2 # Use CRLF for line endings. crlf = false reqsign-0.16.2/.vscode/settings.json000064400000000000000000000001071046102023000154540ustar 00000000000000{ "rust-analyzer.linkedProjects": [ "./Cargo.toml" ] } reqsign-0.16.2/CONTRIBUTING.md000064400000000000000000000012451046102023000136150ustar 00000000000000# Contributing ## Get Started This is a Rust project, so [rustup](https://rustup.rs/) is the best place to start. This is a pure rust project, so only `cargo` is needed. - `cargo check` to analyze the current package and report errors. - `cargo build` to compile the current package. - `cargo clippy` to catch common mistakes and improve code. - `cargo test` to run unit tests. - `cargo bench` to run benchmark tests. Useful tips: - Check/Build/Test/Clippy all code: `cargo --tests --benches --examples` - Test specific function: `cargo test tests::it::services::fs` ## Test Copy `.env.example` to local, change needed test suite values. ```shell cargo test ``` reqsign-0.16.2/Cargo.lock0000644000002432640000000000100105600ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", "windows-sys 0.59.0", ] [[package]] name = "anyhow" version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "async-trait" version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-credential-types" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4471bef4c22a06d2c7a1b6492493d3fdf24a805323109d6874f9c94d5906ac14" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", "zeroize", ] [[package]] name = "aws-sigv4" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d03c3c05ff80d54ff860fe38c726f6f494c639ae975203a101335f223386db" dependencies = [ "aws-credential-types", "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", "form_urlencoded", "hex", "hmac", "http 0.2.12", "http 1.3.1", "once_cell", "percent-encoding", "sha2", "time", "tracing", ] [[package]] name = "aws-smithy-async" version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" dependencies = [ "futures-util", "pin-project-lite", "tokio", ] [[package]] name = "aws-smithy-http" version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5949124d11e538ca21142d1fba61ab0a2a2c1bc3ed323cdb3e4b878bfb83166" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "bytes", "bytes-utils", "futures-core", "http 0.2.12", "http 1.3.1", "http-body 0.4.6", "once_cell", "percent-encoding", "pin-project-lite", "pin-utils", "tracing", ] [[package]] name = "aws-smithy-runtime-api" version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3da37cf5d57011cb1753456518ec76e31691f1f474b73934a284eb2a1c76510f" dependencies = [ "aws-smithy-async", "aws-smithy-types", "bytes", "http 0.2.12", "http 1.3.1", "pin-project-lite", "tokio", "tracing", "zeroize", ] [[package]] name = "aws-smithy-types" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836155caafba616c0ff9b07944324785de2ab016141c3550bd1c07882f8cee8f" dependencies = [ "base64-simd", "bytes", "bytes-utils", "http 0.2.12", "http-body 0.4.6", "itoa", "num-integer", "pin-project-lite", "pin-utils", "ryu", "serde", "time", ] [[package]] name = "backtrace" version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64-simd" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" dependencies = [ "outref", "vsimd", ] [[package]] name = "base64ct" version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytes-utils" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" dependencies = [ "bytes", "either", ] [[package]] name = "cast" 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", ] [[package]] name = "cc" version = "1.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", ] [[package]] name = "clap" version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const-random" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ "const-random-macro", ] [[package]] name = "const-random-macro" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ "getrandom 0.2.15", "once_cell", "tiny-keccak", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "futures", "is-terminal", "itertools", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "tokio", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "der" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", "zeroize", ] [[package]] name = "deranged" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "const-oid", "crypto-common", "subtle", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dlv-list" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ "const-random", ] [[package]] name = "dotenv" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http 1.3.1", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "half" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hermit-abi" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "home" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http 0.2.12", "pin-project-lite", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.3.1", ] [[package]] name = "http-body-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", "http 1.3.1", "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", "futures-util", "h2", "http 1.3.1", "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.3.1", "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", "webpki-roots", ] [[package]] name = "hyper-tls" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", "hyper", "hyper-util", "native-tls", "tokio", "tokio-native-tls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.3.1", "http-body 1.0.1", "hyper", "libc", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_locid_transform" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_locid_transform_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "utf16_iter", "utf8_iter", "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", "icu_locid_transform", "icu_properties_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_provider_macros" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", ] [[package]] name = "inout" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", "windows-sys 0.59.0", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde", ] [[package]] name = "jiff-static" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "jsonwebtoken" version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ "base64", "js-sys", "pem", "ring", "serde", "serde_json", "simple_asn1", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ "spin", ] [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libm" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "linux-raw-sys" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "litemap" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "macro_rules_attribute" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a82271f7bc033d84bbca59a3ce3e4159938cb08a9c3aebbe54d215131518a13" dependencies = [ "macro_rules_attribute-proc_macro", "paste", ] [[package]] name = "macro_rules_attribute-proc_macro" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dd856d451cc0da70e2ef2ce95a18e39a93b7558bedf10201ad28503f918568" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "native-tls" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-bigint-dig" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ "byteorder", "lazy_static", "libm", "num-integer", "num-iter", "num-traits", "rand 0.8.5", "smallvec", "zeroize", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", "hashbrown 0.14.5", ] [[package]] name = "outref" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pbkdf2" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest", "hmac", ] [[package]] name = "pem" version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ "base64", "serde", ] [[package]] name = "pem-rfc7468" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der", "pkcs8", "spki", ] [[package]] name = "pkcs5" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" dependencies = [ "aes", "cbc", "der", "pbkdf2", "scrypt", "sha2", "spki", ] [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "pkcs5", "rand_core 0.6.4", "spki", ] [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plotters" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "portable-atomic" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro2" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" version = "0.37.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" dependencies = [ "memchr", "serde", ] [[package]] name = "quinn" version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", "socket2", "thiserror", "tokio", "tracing", "web-time", ] [[package]] name = "quinn-proto" version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" dependencies = [ "bytes", "getrandom 0.3.2", "rand 0.9.0", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", "thiserror", "tinyvec", "tracing", "web-time", ] [[package]] name = "quinn-udp" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", "windows-sys 0.59.0", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", "zerocopy", ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core 0.9.3", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.15", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.2", ] [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqsign" version = "0.16.2" dependencies = [ "anyhow", "async-trait", "aws-credential-types", "aws-sigv4", "base64", "chrono", "criterion", "dotenv", "env_logger", "form_urlencoded", "getrandom 0.2.15", "hex", "hmac", "home", "http 1.3.1", "jsonwebtoken", "log", "macro_rules_attribute", "once_cell", "percent-encoding", "pretty_assertions", "quick-xml", "rand 0.8.5", "reqwest", "rsa", "rust-ini", "serde", "serde_json", "sha1", "sha2", "temp-env", "tempfile", "test-case", "tokio", "toml", ] [[package]] name = "reqwest" version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "base64", "bytes", "encoding_rs", "futures-channel", "futures-core", "futures-util", "h2", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", "tokio-rustls", "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", "windows-registry", ] [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.15", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rsa" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", "rand_core 0.6.4", "sha2", "signature", "spki", "subtle", "zeroize", ] [[package]] name = "rust-ini" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" dependencies = [ "cfg-if", "ordered-multimap", "trim-in-place", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "rustls" version = "0.23.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" dependencies = [ "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pemfile" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ "rustls-pki-types", ] [[package]] name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ "web-time", ] [[package]] name = "rustls-webpki" version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa20" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ "cipher", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "scopeguard" 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", "salsa20", "sha2", ] [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core 0.6.4", ] [[package]] name = "simple_asn1" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", "thiserror", "time", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spki" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] [[package]] name = "synstructure" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "temp-env" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" dependencies = [ "parking_lot", ] [[package]] name = "tempfile" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "test-case" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" dependencies = [ "test-case-macros", ] [[package]] name = "test-case-core" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" dependencies = [ "cfg-if", "proc-macro2", "quote", "syn", ] [[package]] name = "test-case-macros" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", "syn", "test-case-core", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "time" version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tinyvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-util" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "toml" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "trim-in-place" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf16_iter" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vsimd" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "web-time" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-roots" version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" dependencies = [ "rustls-pki-types", ] [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "windows-core" version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings 0.4.0", ] [[package]] name = "windows-implement" version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-registry" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", "windows-strings 0.3.1", "windows-targets 0.53.0", ] [[package]] name = "windows-result" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerocopy" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", "syn", ] reqsign-0.16.2/Cargo.toml0000644000000106200000000000100105670ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "reqsign" version = "0.16.2" authors = ["Xuanwo "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Signing API requests without effort." documentation = "https://docs.rs/reqsign" readme = "README.md" categories = [ "command-line-utilities", "web-programming", ] license = "Apache-2.0" repository = "https://github.com/Xuanwo/reqsign" [package.metadata.docs.rs] all-features = true [features] default = [ "reqwest_request", "services-all", ] native-tls = ["reqwest?/default-tls"] reqwest_blocking_request = ["reqwest?/blocking"] reqwest_request = ["dep:reqwest"] rustls = ["reqwest?/rustls-tls"] services-aliyun = [ "dep:reqwest", "dep:serde", "dep:serde_json", "dep:once_cell", ] services-all = [ "services-aliyun", "services-aws", "services-azblob", "services-google", "services-huaweicloud", "services-oracle", "services-tencent", ] services-aws = [ "dep:reqwest", "dep:serde", "dep:serde_json", "dep:quick-xml", "dep:rust-ini", ] services-azblob = [ "dep:serde", "dep:serde_json", "dep:reqwest", ] services-google = [ "dep:reqwest", "dep:serde", "dep:serde_json", "dep:jsonwebtoken", "dep:rsa", ] services-huaweicloud = [ "dep:serde", "dep:serde_json", "dep:once_cell", ] services-oracle = [ "dep:reqwest", "dep:rsa", "dep:toml", "dep:serde", ] services-tencent = [ "dep:reqwest", "dep:serde", "dep:serde_json", ] [lib] name = "reqsign" path = "src/lib.rs" [[test]] name = "main" path = "tests/main.rs" [[bench]] name = "aws" path = "benches/aws.rs" harness = false [dependencies.anyhow] version = "1" [dependencies.async-trait] version = "0.1" [dependencies.base64] version = "0.22" [dependencies.chrono] version = "0.4.35" [dependencies.form_urlencoded] version = "1" [dependencies.hex] version = "0.4" [dependencies.hmac] version = "0.12" [dependencies.http] version = "1.1" [dependencies.jsonwebtoken] version = "9.2" optional = true [dependencies.log] version = "0.4" [dependencies.once_cell] version = "1" optional = true [dependencies.percent-encoding] version = "2" [dependencies.quick-xml] version = "0.37" features = ["serialize"] optional = true [dependencies.rand] version = "0.8.5" [dependencies.reqwest] version = "0.12" optional = true default-features = false [dependencies.rsa] version = "0.9.2" features = [ "pkcs5", "sha2", ] optional = true [dependencies.rust-ini] version = "0.21" optional = true [dependencies.serde] version = "1" features = ["derive"] optional = true [dependencies.serde_json] version = "1" optional = true [dependencies.sha1] version = "0.10" [dependencies.sha2] version = "0.10" features = ["oid"] [dependencies.toml] version = "0.8.9" optional = true [dev-dependencies.aws-credential-types] version = "1.1.8" [dev-dependencies.aws-sigv4] version = "1.2.0" [dev-dependencies.criterion] version = "0.5" features = [ "async_tokio", "html_reports", ] [dev-dependencies.dotenv] version = "0.15" [dev-dependencies.env_logger] version = "0.11" [dev-dependencies.macro_rules_attribute] version = "0.2.0" [dev-dependencies.once_cell] version = "1" [dev-dependencies.pretty_assertions] version = "1.3" [dev-dependencies.reqwest] version = "0.12" features = [ "blocking", "json", ] [dev-dependencies.temp-env] version = "0.3" [dev-dependencies.tempfile] version = "3.8" [dev-dependencies.test-case] version = "3.3.1" [dev-dependencies.tokio] version = "1" features = ["full"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies.home] version = "0.5" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "1" features = ["fs"] optional = true [target.'cfg(target_arch = "wasm32")'.dependencies.getrandom] version = "0.2" features = ["js"] [target.'cfg(target_arch = "wasm32")'.dependencies.tokio] version = "1" optional = true reqsign-0.16.2/Cargo.toml.orig000064400000000000000000000055761046102023000142660ustar 00000000000000[package] authors = ["Xuanwo "] categories = ["command-line-utilities", "web-programming"] description = "Signing API requests without effort." documentation = "https://docs.rs/reqsign" edition = "2021" license = "Apache-2.0" name = "reqsign" repository = "https://github.com/Xuanwo/reqsign" version = "0.16.2" [package.metadata.docs.rs] all-features = true [features] default = ["reqwest_request", "services-all"] native-tls = ["reqwest?/default-tls"] rustls = ["reqwest?/rustls-tls"] # requests that reqwest supports reqwest_blocking_request = ["reqwest?/blocking"] reqwest_request = ["dep:reqwest"] # services that reqsign supports services-all = [ "services-aliyun", "services-aws", "services-azblob", "services-google", "services-huaweicloud", "services-oracle", "services-tencent", ] services-aliyun = [ "dep:reqwest", "dep:serde", "dep:serde_json", "dep:once_cell", ] services-aws = [ "dep:reqwest", "dep:serde", "dep:serde_json", "dep:quick-xml", "dep:rust-ini", ] services-azblob = ["dep:serde", "dep:serde_json", "dep:reqwest"] services-google = [ "dep:reqwest", "dep:serde", "dep:serde_json", "dep:jsonwebtoken", "dep:rsa", ] services-huaweicloud = ["dep:serde", "dep:serde_json", "dep:once_cell"] services-oracle = ["dep:reqwest", "dep:rsa", "dep:toml", "dep:serde"] services-tencent = ["dep:reqwest", "dep:serde", "dep:serde_json"] [[bench]] harness = false name = "aws" [dependencies] anyhow = "1" async-trait = "0.1" base64 = "0.22" chrono = "0.4.35" form_urlencoded = "1" hex = "0.4" hmac = "0.12" http = "1.1" jsonwebtoken = { version = "9.2", optional = true } log = "0.4" once_cell = { version = "1", optional = true } percent-encoding = "2" quick-xml = { version = "0.37", features = ["serialize"], optional = true } rand = "0.8.5" reqwest = { version = "0.12", default-features = false, optional = true } rsa = { version = "0.9.2", features = ["pkcs5", "sha2"], optional = true } rust-ini = { version = "0.21", optional = true } serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1", optional = true } sha1 = "0.10" sha2 = { version = "0.10", features = ["oid"] } toml = { version = "0.8.9", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] home = "0.5" tokio = { version = "1", features = ["fs"], optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } tokio = { version = "1", optional = true } [dev-dependencies] aws-credential-types = "1.1.8" aws-sigv4 = "1.2.0" criterion = { version = "0.5", features = ["async_tokio", "html_reports"] } dotenv = "0.15" env_logger = "0.11" macro_rules_attribute = "0.2.0" once_cell = "1" pretty_assertions = "1.3" reqwest = { version = "0.12", features = ["blocking", "json"] } temp-env = "0.3" tempfile = "3.8" test-case = "3.3.1" tokio = { version = "1", features = ["full"] } reqsign-0.16.2/LICENSE000064400000000000000000000261161046102023000123750ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2021 Datafuse Labs 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.reqsign-0.16.2/README.md000064400000000000000000000050701046102023000126430ustar 00000000000000# reqsign   [![Build Status]][actions] [![Latest Version]][crates.io] [![Crate Downloads]][crates.io] [Build Status]: https://img.shields.io/github/actions/workflow/status/Xuanwo/reqsign/ci.yml?branch=main [actions]: https://github.com/Xuanwo/reqsign/actions?query=branch%3Amain [Latest Version]: https://img.shields.io/crates/v/reqsign.svg [crates.io]: https://crates.io/crates/reqsign [Crate Downloads]: https://img.shields.io/crates/d/reqsign.svg Signing API requests without effort. --- Most API is simple. But they could be complicated when they are hidden from complex abstraction. `reqsign` bring the simple API back: build, sign, send. ## Quick Start ```rust use anyhow::Result; use reqsign::AwsConfig; use reqsign::AwsLoader; use reqsign::AwsV4Signer; use reqwest::Client; use reqwest::Request; use reqwest::Url; #[tokio::main] async fn main() -> Result<()> { // Signer can load region and credentials from environment by default. let client = Client::new(); let config = AwsConfig::default().from_profile().from_env(); let loader = AwsLoader::new(client.clone(), config); let signer = AwsV4Signer::new("s3", "us-east-1"); // Construct request let url = Url::parse("https://s3.amazonaws.com/testbucket")?; let mut req = reqwest::Request::new(http::Method::GET, url); // Signing request with Signer let credential = loader.load().await?.unwrap(); signer.sign(&mut req, &credential)?; // Sending already signed request. let resp = client.execute(req).await?; println!("resp got status: {}", resp.status()); Ok(()) } ``` ## Features - Pure rust with minimal dependencies. - Test again official SDK and services. - Supported services - Aliyun OSS: `reqsign::AliyunOssSigner` - AWS services (SigV4): `reqsign::AwsV4Signer` - Azure Storage services: `reqsign::AzureStorageSigner` - Google services: `reqsign::GoogleSigner` - Huawei Cloud OBS: `reqsign::HuaweicloudObsSigner` ## Contributing Check out the [CONTRIBUTING.md](./CONTRIBUTING.md) guide for more details on getting started with contributing to this project. ## Getting help Submit [issues](https://github.com/Xuanwo/reqsign/issues/new/choose) for bug report or asking questions in [discussion](https://github.com/Xuanwo/reqsign/discussions/new?category=q-a). ## Acknowledge Inspired a lot from: - [aws-sigv4](https://crates.io/crates/aws-sigv4) for AWS SigV4 support. - [azure_storage_blobs](https://crates.io/crates/azure_storage_blobs) for Azure Storage support. #### License Licensed under Apache License, Version 2.0. reqsign-0.16.2/benches/aws.rs000064400000000000000000000055061046102023000141370ustar 00000000000000use std::time::SystemTime; use aws_sigv4::http_request::PayloadChecksumKind; use aws_sigv4::http_request::PercentEncodingMode; use aws_sigv4::http_request::SignableBody; use aws_sigv4::http_request::SignableRequest; use aws_sigv4::http_request::SigningSettings; use aws_sigv4::sign::v4::SigningParams; use criterion::criterion_group; use criterion::criterion_main; use criterion::Criterion; use reqsign::AwsCredential; use reqsign::AwsV4Signer; criterion_group!(benches, bench); criterion_main!(benches); pub fn bench(c: &mut Criterion) { let mut group = c.benchmark_group("aws_v4"); group.bench_function("reqsign", |b| { let cred = AwsCredential { access_key_id: "access_key_id".to_string(), secret_access_key: "secret_access_key".to_string(), ..Default::default() }; let s = AwsV4Signer::new("s3", "test"); b.iter(|| { let mut req = http::Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = "http://127.0.0.1:9000/hello" .parse() .expect("url must be valid"); s.sign(&mut req, &cred).expect("must success") }) }); group.bench_function("aws_sigv4", |b| { let mut ss = SigningSettings::default(); ss.percent_encoding_mode = PercentEncodingMode::Single; ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; let credentials = aws_credential_types::Credentials::new( "access_key_id".to_string(), "secret_access_key".to_string(), None, None, "test", ) .into(); let sp = SigningParams::builder() .identity(&credentials) .region("test") .name("s3") .time(SystemTime::now()) .settings(ss) .build() .expect("signing params must be valid") .into(); let mut req = http::Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = "http://127.0.0.1:9000/hello" .parse() .expect("url must be valid"); let method = req.method().as_str(); let uri = req.uri().to_string(); let headers = req .headers() .iter() .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())) .collect::>(); b.iter(|| { let _ = aws_sigv4::http_request::sign( SignableRequest::new( method, uri.as_str(), headers.clone().into_iter(), SignableBody::UnsignedPayload, ) .unwrap(), &sp, ) .expect("signing must succeed"); }) }); group.finish(); } reqsign-0.16.2/rust-toolchain.toml000064400000000000000000000001021046102023000152230ustar 00000000000000[toolchain] channel = "stable" components = ["rustfmt", "clippy"] reqsign-0.16.2/rustfmt.toml000064400000000000000000000003541046102023000137650ustar 00000000000000edition = "2021" reorder_imports = true # format_code_in_doc_comments = true # group_imports = "StdExternalCrate" # imports_granularity = "Item" # overflow_delimited_expr = true # trailing_comma = "Vertical" # where_single_line = true reqsign-0.16.2/src/aliyun/config.rs000064400000000000000000000057461046102023000153010ustar 00000000000000use std::collections::HashMap; use std::env; use super::constants::*; /// Config carries all the configuration for Aliyun services. #[derive(Clone)] #[cfg_attr(test, derive(Debug))] pub struct Config { /// `access_key_id` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`ALIBABA_CLOUD_ACCESS_KEY_ID`] pub access_key_id: Option, /// `access_key_secret` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`ALIBABA_CLOUD_ACCESS_KEY_SECRET`] pub access_key_secret: Option, /// `security_token` will be loaded from /// /// - this field if it's `is_some` pub security_token: Option, /// `role_arn` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`ALIBABA_CLOUD_ROLE_ARN`] pub role_arn: Option, /// `role_session_name` will be loaded from /// /// - default to `resign` pub role_session_name: String, /// `oidc_provider_arn` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`ALIBABA_CLOUD_OIDC_PROVIDER_ARN`] pub oidc_provider_arn: Option, /// `oidc_token_file` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`ALIBABA_CLOUD_OIDC_TOKEN_FILE`] pub oidc_token_file: Option, /// `sts_endpoint` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`ALIBABA_CLOUD_STS_ENDPOINT`] pub sts_endpoint: Option, } impl Default for Config { fn default() -> Self { Self { access_key_id: None, access_key_secret: None, security_token: None, role_arn: None, role_session_name: "resign".to_string(), oidc_provider_arn: None, oidc_token_file: None, sts_endpoint: None, } } } impl Config { /// Load config from env. pub fn from_env(mut self) -> Self { let envs = env::vars().collect::>(); if let Some(v) = envs.get(ALIBABA_CLOUD_ACCESS_KEY_ID) { self.access_key_id.get_or_insert(v.clone()); } if let Some(v) = envs.get(ALIBABA_CLOUD_ACCESS_KEY_SECRET) { self.access_key_secret.get_or_insert(v.clone()); } if let Some(v) = envs.get(ALIBABA_CLOUD_SECURITY_TOKEN) { self.security_token.get_or_insert(v.clone()); } if let Some(v) = envs.get(ALIBABA_CLOUD_ROLE_ARN) { self.role_arn.get_or_insert(v.clone()); } if let Some(v) = envs.get(ALIBABA_CLOUD_OIDC_PROVIDER_ARN) { self.oidc_provider_arn.get_or_insert(v.clone()); } if let Some(v) = envs.get(ALIBABA_CLOUD_OIDC_TOKEN_FILE) { self.oidc_token_file.get_or_insert(v.clone()); } if let Some(v) = envs.get(ALIBABA_CLOUD_STS_ENDPOINT) { self.sts_endpoint.get_or_insert(v.clone()); } self } } reqsign-0.16.2/src/aliyun/constants.rs000064400000000000000000000011141046102023000160310ustar 00000000000000// Env values used in aliyun services. pub const ALIBABA_CLOUD_ACCESS_KEY_ID: &str = "ALIBABA_CLOUD_ACCESS_KEY_ID"; pub const ALIBABA_CLOUD_ACCESS_KEY_SECRET: &str = "ALIBABA_CLOUD_ACCESS_KEY_SECRET"; pub const ALIBABA_CLOUD_SECURITY_TOKEN: &str = "ALIBABA_CLOUD_SECURITY_TOKEN"; pub const ALIBABA_CLOUD_ROLE_ARN: &str = "ALIBABA_CLOUD_ROLE_ARN"; pub const ALIBABA_CLOUD_OIDC_PROVIDER_ARN: &str = "ALIBABA_CLOUD_OIDC_PROVIDER_ARN"; pub const ALIBABA_CLOUD_OIDC_TOKEN_FILE: &str = "ALIBABA_CLOUD_OIDC_TOKEN_FILE"; pub const ALIBABA_CLOUD_STS_ENDPOINT: &str = "ALIBABA_CLOUD_STS_ENDPOINT"; reqsign-0.16.2/src/aliyun/credential.rs000064400000000000000000000355671046102023000161520ustar 00000000000000use std::fs; use std::sync::Arc; use std::sync::Mutex; use anyhow::anyhow; use anyhow::Result; use log::debug; use reqwest::Client; use serde::Deserialize; use super::config::Config; use crate::time::format_rfc3339; use crate::time::now; use crate::time::parse_rfc3339; use crate::time::DateTime; /// Credential that holds the access_key and secret_key. #[derive(Default, Clone)] #[cfg_attr(test, derive(Debug))] pub struct Credential { /// Access key id for credential. pub access_key_id: String, /// Access key secret for credential. pub access_key_secret: String, /// Security token for credential. pub security_token: Option, /// expires in for credential. pub expires_in: Option, } impl Credential { /// is current cred is valid? pub fn is_valid(&self) -> bool { if (self.access_key_id.is_empty() || self.access_key_secret.is_empty()) && self.security_token.is_none() { return false; } // Take 120s as buffer to avoid edge cases. if let Some(valid) = self .expires_in .map(|v| v > now() + chrono::TimeDelta::try_minutes(2).expect("in bounds")) { return valid; } true } } /// Loader will load credential from different methods. #[cfg_attr(test, derive(Debug))] pub struct Loader { client: Client, config: Config, credential: Arc>>, } impl Loader { /// Create a new loader via client and config. pub fn new(client: Client, config: Config) -> Self { Self { client, config, credential: Arc::default(), } } /// Load credential. pub async fn load(&self) -> Result> { // Return cached credential if it's valid. match self.credential.lock().expect("lock poisoned").clone() { Some(cred) if cred.is_valid() => return Ok(Some(cred)), _ => (), } let cred = if let Some(cred) = self.load_inner().await? { cred } else { return Ok(None); }; let mut lock = self.credential.lock().expect("lock poisoned"); *lock = Some(cred.clone()); Ok(Some(cred)) } async fn load_inner(&self) -> Result> { if let Ok(Some(cred)) = self .load_via_static() .map_err(|err| debug!("load credential via static failed: {err:?}")) { return Ok(Some(cred)); } if let Ok(Some(cred)) = self .load_via_assume_role_with_oidc() .await .map_err(|err| debug!("load credential load via assume_role_with_oidc: {err:?}")) { return Ok(Some(cred)); } Ok(None) } fn load_via_static(&self) -> Result> { if let (Some(ak), Some(sk)) = (&self.config.access_key_id, &self.config.access_key_secret) { Ok(Some(Credential { access_key_id: ak.clone(), access_key_secret: sk.clone(), security_token: self.config.security_token.clone(), // Set expires_in to 10 minutes to enforce re-read // from file. expires_in: Some(now() + chrono::TimeDelta::try_minutes(10).expect("in bounds")), })) } else { Ok(None) } } async fn load_via_assume_role_with_oidc(&self) -> Result> { let (token_file, role_arn, provider_arn) = match ( &self.config.oidc_token_file, &self.config.role_arn, &self.config.oidc_provider_arn, ) { (Some(token_file), Some(role_arn), Some(provider_arn)) => { (token_file, role_arn, provider_arn) } _ => return Ok(None), }; let token = fs::read_to_string(token_file)?; let role_session_name = &self.config.role_session_name; // Construct request to Aliyun STS Service. let url = format!("{}/?Action=AssumeRoleWithOIDC&OIDCProviderArn={}&RoleArn={}&RoleSessionName={}&Format=JSON&Version=2015-04-01&Timestamp={}&OIDCToken={}", self.get_sts_endpoint(), provider_arn, role_arn, role_session_name, format_rfc3339(now()), token); let req = self.client.get(&url).header( http::header::CONTENT_TYPE.as_str(), "application/x-www-form-urlencoded", ); let resp = req.send().await?; if resp.status() != http::StatusCode::OK { let content = resp.text().await?; return Err(anyhow!("request to Aliyun STS Services failed: {content}")); } let resp: AssumeRoleWithOidcResponse = serde_json::from_slice(&resp.bytes().await?)?; let resp_cred = resp.credentials; let cred = Credential { access_key_id: resp_cred.access_key_id, access_key_secret: resp_cred.access_key_secret, security_token: Some(resp_cred.security_token), expires_in: Some(parse_rfc3339(&resp_cred.expiration)?), }; Ok(Some(cred)) } fn get_sts_endpoint(&self) -> String { match &self.config.sts_endpoint { Some(defined_sts_endpoint) => format!("https://{}", defined_sts_endpoint), None => "https://sts.aliyuncs.com".to_string(), } } } #[derive(Default, Debug, Deserialize)] #[serde(default)] struct AssumeRoleWithOidcResponse { #[serde(rename = "Credentials")] credentials: AssumeRoleWithOidcCredentials, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleWithOidcCredentials { access_key_id: String, access_key_secret: String, security_token: String, expiration: String, } #[cfg(test)] mod tests { use std::env; use std::str::FromStr; use std::time::Duration; use http::Request; use http::StatusCode; use log::debug; use once_cell::sync::Lazy; use reqwest::blocking::Client; use tokio::runtime::Runtime; use super::super::constants::*; use super::super::oss::Signer; use super::*; static RUNTIME: Lazy = Lazy::new(|| { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .expect("Should create a tokio runtime") }); #[test] fn test_parse_assume_role_with_oidc_response() -> Result<()> { let content = r#"{ "RequestId": "3D57EAD2-8723-1F26-B69C-F8707D8B565D", "OIDCTokenInfo": { "Subject": "KryrkIdjylZb7agUgCEf****", "Issuer": "https://dev-xxxxxx.okta.com", "ClientIds": "496271242565057****" }, "AssumedRoleUser": { "AssumedRoleId": "33157794895460****", "Arn": "acs:ram::113511544585****:role/testoidc/TestOidcAssumedRoleSession" }, "Credentials": { "SecurityToken": "CAIShwJ1q6Ft5B2yfSjIr5bSEsj4g7BihPWGWHz****", "Expiration": "2021-10-20T04:27:09Z", "AccessKeySecret": "CVwjCkNzTMupZ8NbTCxCBRq3K16jtcWFTJAyBEv2****", "AccessKeyId": "STS.NUgYrLnoC37mZZCNnAbez****" } }"#; let resp: AssumeRoleWithOidcResponse = serde_json::from_str(content).expect("json deserialize must success"); assert_eq!( &resp.credentials.access_key_id, "STS.NUgYrLnoC37mZZCNnAbez****" ); assert_eq!( &resp.credentials.access_key_secret, "CVwjCkNzTMupZ8NbTCxCBRq3K16jtcWFTJAyBEv2****" ); assert_eq!( &resp.credentials.security_token, "CAIShwJ1q6Ft5B2yfSjIr5bSEsj4g7BihPWGWHz****" ); assert_eq!(&resp.credentials.expiration, "2021-10-20T04:27:09Z"); Ok(()) } #[test] fn test_signer_with_oidc() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_ALIYUN_OSS_TEST").is_err() || env::var("REQSIGN_ALIYUN_OSS_TEST").unwrap() != "on" { return Ok(()); } let provider_arn = env::var("REQSIGN_ALIYUN_PROVIDER_ARN").expect("REQSIGN_ALIYUN_PROVIDER_ARN not exist"); let role_arn = env::var("REQSIGN_ALIYUN_ROLE_ARN").expect("REQSIGN_ALIYUN_ROLE_ARN not exist"); let idp_url = env::var("REQSIGN_ALIYUN_IDP_URL").expect("REQSIGN_ALIYUN_IDP_URL not exist"); let idp_content = env::var("REQSIGN_ALIYUN_IDP_BODY").expect("REQSIGN_ALIYUN_IDP_BODY not exist"); let mut req = Request::new(idp_content); *req.method_mut() = http::Method::POST; *req.uri_mut() = http::Uri::from_str(&idp_url)?; req.headers_mut().insert( http::header::CONTENT_TYPE, "application/x-www-form-urlencoded".parse()?, ); #[derive(Deserialize)] struct Token { id_token: String, } let token = Client::new() .execute(req.try_into()?)? .json::()? .id_token; let file_path = format!( "{}/testdata/services/aliyun/oidc_token_file", env::current_dir() .expect("current_dir must exist") .to_string_lossy() ); fs::write(&file_path, token)?; temp_env::with_vars( vec![ (ALIBABA_CLOUD_ROLE_ARN, Some(&role_arn)), (ALIBABA_CLOUD_OIDC_PROVIDER_ARN, Some(&provider_arn)), (ALIBABA_CLOUD_OIDC_TOKEN_FILE, Some(&file_path)), ], || { RUNTIME.block_on(async { let config = Config::default().from_env(); let loader = Loader::new(reqwest::Client::new(), config); let signer = Signer::new( &env::var("REQSIGN_ALIYUN_OSS_BUCKET") .expect("env REQSIGN_ALIYUN_OSS_BUCKET must set"), ); let url = &env::var("REQSIGN_ALIYUN_OSS_URL") .expect("env REQSIGN_ALIYUN_OSS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "not_exist_file")) .expect("must valid"); let cred = loader .load() .await .expect("credential must be valid") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request url: {:?}", req.uri().to_string()); debug!("signed request: {:?}", req); let client = reqwest::Client::new(); let resp = client .execute(req.try_into().unwrap()) .await .expect("request must succeed"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await.unwrap()); assert_eq!(StatusCode::NOT_FOUND, status); }) }, ); Ok(()) } #[test] fn test_signer_with_oidc_query() -> Result<()> { let _ = env_logger::builder().try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_ALIYUN_OSS_TEST").is_err() || env::var("REQSIGN_ALIYUN_OSS_TEST").unwrap() != "on" { return Ok(()); } let provider_arn = env::var("REQSIGN_ALIYUN_PROVIDER_ARN").expect("REQSIGN_ALIYUN_PROVIDER_ARN not exist"); let role_arn = env::var("REQSIGN_ALIYUN_ROLE_ARN").expect("REQSIGN_ALIYUN_ROLE_ARN not exist"); let idp_url = env::var("REQSIGN_ALIYUN_IDP_URL").expect("REQSIGN_ALIYUN_IDP_URL not exist"); let idp_content = env::var("REQSIGN_ALIYUN_IDP_BODY").expect("REQSIGN_ALIYUN_IDP_BODY not exist"); let mut req = Request::new(idp_content); *req.method_mut() = http::Method::POST; *req.uri_mut() = http::Uri::from_str(&idp_url)?; req.headers_mut().insert( http::header::CONTENT_TYPE, "application/x-www-form-urlencoded".parse()?, ); #[derive(Deserialize)] struct Token { id_token: String, } let token = Client::new() .execute(req.try_into()?)? .json::()? .id_token; let file_path = format!( "{}/testdata/services/aliyun/oidc_token_file", env::current_dir() .expect("current_dir must exist") .to_string_lossy() ); fs::write(&file_path, token)?; temp_env::with_vars( vec![ (ALIBABA_CLOUD_ROLE_ARN, Some(&role_arn)), (ALIBABA_CLOUD_OIDC_PROVIDER_ARN, Some(&provider_arn)), (ALIBABA_CLOUD_OIDC_TOKEN_FILE, Some(&file_path)), ], || { RUNTIME.block_on(async { let config = Config::default().from_env(); let loader = Loader::new(reqwest::Client::new(), config); let signer = Signer::new( &env::var("REQSIGN_ALIYUN_OSS_BUCKET") .expect("env REQSIGN_ALIYUN_OSS_BUCKET must set"), ); let url = &env::var("REQSIGN_ALIYUN_OSS_URL") .expect("env REQSIGN_ALIYUN_OSS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "not_exist_file")) .expect("must valid"); let cred = loader .load() .await .expect("credential must be valid") .unwrap(); signer .sign_query(&mut req, Duration::from_secs(3600), &cred) .expect("sign request must success"); debug!("signed request url: {:?}", req.uri().to_string()); debug!("signed request: {:?}", req); let client = reqwest::Client::new(); let resp = client .execute(req.try_into().unwrap()) .await .expect("request must succeed"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await.unwrap()); assert_eq!(StatusCode::NOT_FOUND, status); }) }, ); Ok(()) } } reqsign-0.16.2/src/aliyun/mod.rs000064400000000000000000000004471046102023000146040ustar 00000000000000//! Aliyun service signer //! //! Only OSS has been supported. mod oss; pub use oss::Signer as AliyunOssSigner; mod config; pub use config::Config as AliyunConfig; mod credential; pub use credential::Credential as AliyunCredential; pub use credential::Loader as AliyunLoader; mod constants; reqsign-0.16.2/src/aliyun/oss.rs000064400000000000000000000202361046102023000146270ustar 00000000000000//! Aliyun OSS Singer use std::collections::HashSet; use std::fmt::Write; use std::time::Duration; use anyhow::Result; use http::header::AUTHORIZATION; use http::header::CONTENT_TYPE; use http::header::DATE; use http::HeaderValue; use log::debug; use once_cell::sync::Lazy; use percent_encoding::utf8_percent_encode; use super::credential::Credential; use crate::ctx::SigningContext; use crate::ctx::SigningMethod; use crate::hash::base64_hmac_sha1; use crate::request::SignableRequest; use crate::time; use crate::time::format_http_date; use crate::time::DateTime; const CONTENT_MD5: &str = "content-md5"; /// Singer for Aliyun OSS. pub struct Signer { bucket: String, } impl Signer { /// Create a new signer pub fn new(bucket: &str) -> Self { Self { bucket: bucket.to_owned(), } } /// Building a signing context. fn build( &self, req: &mut impl SignableRequest, method: SigningMethod, cred: &Credential, ) -> Result { let now = time::now(); let mut ctx = req.build()?; let string_to_sign = string_to_sign(&mut ctx, cred, now, method, &self.bucket)?; let signature = base64_hmac_sha1(cred.access_key_secret.as_bytes(), string_to_sign.as_bytes()); match method { SigningMethod::Header => { ctx.headers.insert(DATE, format_http_date(now).parse()?); ctx.headers.insert(AUTHORIZATION, { let mut value: HeaderValue = format!("OSS {}:{}", cred.access_key_id, signature).parse()?; value.set_sensitive(true); value }); } SigningMethod::Query(expire) => { ctx.headers.insert(DATE, format_http_date(now).parse()?); ctx.query_push("OSSAccessKeyId", &cred.access_key_id); ctx.query_push( "Expires", (now + chrono::TimeDelta::from_std(expire).unwrap()) .timestamp() .to_string(), ); ctx.query_push( "Signature", utf8_percent_encode(&signature, percent_encoding::NON_ALPHANUMERIC).to_string(), ) } } Ok(ctx) } /// Signing request with header. pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> { let ctx = self.build(req, SigningMethod::Header, cred)?; req.apply(ctx) } /// Signing request with query. pub fn sign_query( &self, req: &mut impl SignableRequest, expire: Duration, cred: &Credential, ) -> Result<()> { let ctx = self.build(req, SigningMethod::Query(expire), cred)?; req.apply(ctx) } } /// Construct string to sign. /// /// # Format /// /// ```text /// VERB + "\n" /// + Content-MD5 + "\n" /// + Content-Type + "\n" /// + Date + "\n" /// + CanonicalizedOSSHeaders /// + CanonicalizedResource /// ``` fn string_to_sign( ctx: &mut SigningContext, cred: &Credential, now: DateTime, method: SigningMethod, bucket: &str, ) -> Result { let mut s = String::new(); s.write_str(ctx.method.as_str())?; s.write_str("\n")?; s.write_str(ctx.header_get_or_default(&CONTENT_MD5.parse()?)?)?; s.write_str("\n")?; s.write_str(ctx.header_get_or_default(&CONTENT_TYPE)?)?; s.write_str("\n")?; match method { SigningMethod::Header => { writeln!(&mut s, "{}", format_http_date(now))?; } SigningMethod::Query(expires) => { writeln!( &mut s, "{}", (now + chrono::TimeDelta::from_std(expires).unwrap()).timestamp() )?; } } { let headers = canonicalize_header(ctx, method, cred)?; if !headers.is_empty() { writeln!(&mut s, "{headers}",)?; } } write!( &mut s, "{}", canonicalize_resource(ctx, bucket, method, cred) )?; debug!("string to sign: {}", &s); Ok(s) } /// Build canonicalize header /// /// # Reference /// /// [Building CanonicalizedOSSHeaders](https://help.aliyun.com/document_detail/31951.html#section-w2k-sw2-xdb) fn canonicalize_header( ctx: &mut SigningContext, method: SigningMethod, cred: &Credential, ) -> Result { if method == SigningMethod::Header { // Insert security token if let Some(token) = &cred.security_token { ctx.headers.insert("x-oss-security-token", token.parse()?); } } Ok(SigningContext::header_to_string( ctx.header_to_vec_with_prefix("x-oss-"), ":", "\n", )) } /// Build canonicalize resource /// /// # Reference /// /// [Building CanonicalizedResource](https://help.aliyun.com/document_detail/31951.html#section-w2k-sw2-xdb) fn canonicalize_resource( ctx: &mut SigningContext, bucket: &str, method: SigningMethod, cred: &Credential, ) -> String { ctx.query = ctx .query .iter() .map(|(k, v)| { ( utf8_percent_encode(k, percent_encoding::NON_ALPHANUMERIC).to_string(), utf8_percent_encode(v, percent_encoding::NON_ALPHANUMERIC).to_string(), ) }) .collect(); if let SigningMethod::Query(_) = method { // Insert security token if let Some(token) = &cred.security_token { ctx.query.push(( "security-token".to_string(), utf8_percent_encode(token, percent_encoding::NON_ALPHANUMERIC).to_string(), )); }; } let params = ctx.query_to_vec_with_filter(is_sub_resource); // OSS requires that the query string be percent-decoded. let params_str = SigningContext::query_to_percent_decoded_string(params, "=", "&"); if params_str.is_empty() { format!("/{bucket}{}", ctx.path_percent_decoded()) } else { format!("/{bucket}{}?{params_str}", ctx.path_percent_decoded()) } } fn is_sub_resource(v: &str) -> bool { SUB_RESOURCES.contains(&v) } /// This list is copied from static SUB_RESOURCES: Lazy> = Lazy::new(|| { HashSet::from([ "acl", "uploads", "location", "cors", "logging", "website", "referer", "lifecycle", "delete", "append", "tagging", "objectMeta", "uploadId", "partNumber", "security-token", "position", "img", "style", "styleName", "replication", "replicationProgress", "replicationLocation", "cname", "bucketInfo", "comp", "qos", "live", "status", "vod", "startTime", "endTime", "symlink", "x-oss-process", "response-content-type", "x-oss-traffic-limit", "response-content-language", "response-expires", "response-cache-control", "response-content-disposition", "response-content-encoding", "udf", "udfName", "udfImage", "udfId", "udfImageDesc", "udfApplication", "comp", "udfApplicationLog", "restore", "callback", "callback-var", "qosInfo", "policy", "stat", "encryption", "versions", "versioning", "versionId", "requestPayment", "x-oss-request-payer", "sequential", "inventory", "inventoryId", "continuation-token", "asyncFetch", "worm", "wormId", "wormExtend", "withHashContext", "x-oss-enable-md5", "x-oss-enable-sha1", "x-oss-enable-sha256", "x-oss-hash-ctx", "x-oss-md5-ctx", "transferAcceleration", "regionList", "cloudboxes", "x-oss-ac-source-ip", "x-oss-ac-subnet-mask", "x-oss-ac-vpc-id", "x-oss-ac-forward-allow", "metaQuery", ]) }); reqsign-0.16.2/src/aws/config.rs000064400000000000000000000341261046102023000145640ustar 00000000000000use std::collections::HashMap; use std::env; #[cfg(not(target_arch = "wasm32"))] use std::fs; #[cfg(not(target_arch = "wasm32"))] use anyhow::anyhow; #[cfg(not(target_arch = "wasm32"))] use anyhow::Result; #[cfg(not(target_arch = "wasm32"))] use ini::Ini; #[cfg(not(target_arch = "wasm32"))] use log::debug; use super::constants::*; #[cfg(not(target_arch = "wasm32"))] use crate::dirs::expand_homedir; /// Config for aws services. #[derive(Clone)] #[cfg_attr(test, derive(Debug))] pub struct Config { /// `config_file` will be load from: /// /// - env value: [`AWS_CONFIG_FILE`] /// - default to: `~/.aws/config` pub config_file: String, /// `shared_credentials_file` will be loaded from: /// /// - env value: [`AWS_SHARED_CREDENTIALS_FILE`] /// - default to: `~/.aws/credentials` pub shared_credentials_file: String, /// `profile` will be loaded from: /// /// - this field if it's `is_some` /// - env value: [`AWS_PROFILE`] /// - default to: `default` pub profile: String, /// `region` will be loaded from: /// /// - this field if it's `is_some` /// - env value: [`AWS_REGION`] /// - profile config: `region` pub region: Option, /// `sts_regional_endpoints` will be loaded from: /// /// - env value: [`AWS_STS_REGIONAL_ENDPOINTS`] /// - profile config: `sts_regional_endpoints` /// - default to `legacy` pub sts_regional_endpoints: String, /// `access_key_id` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`AWS_ACCESS_KEY_ID`] /// - profile config: `aws_access_key_id` pub access_key_id: Option, /// `secret_access_key` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`AWS_SECRET_ACCESS_KEY`] /// - profile config: `aws_secret_access_key` pub secret_access_key: Option, /// `session_token` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`AWS_SESSION_TOKEN`] /// - profile config: `aws_session_token` pub session_token: Option, /// `role_arn` value will be load from: /// /// - this field if it's `is_some`. /// - env value: [`AWS_ROLE_ARN`] /// - profile config: `role_arn` pub role_arn: Option, /// `role_session_name` value will be load from: /// /// - env value: [`AWS_ROLE_SESSION_NAME`] /// - profile config: `role_session_name` /// - default to `reqsign`. pub role_session_name: String, /// `duration_seconds` value will be load from: /// /// - this field if it's `is_some`. /// - profile config: `duration_seconds` /// - default to `3600`. pub duration_seconds: Option, /// `external_id` value will be load from: /// /// - this field if it's `is_some`. /// - profile config: `external_id` pub external_id: Option, /// `tags` value will be loaded from: /// /// - this field if it's `is_some` pub tags: Option>, /// `web_identity_token_file` value will be loaded from: /// /// - this field if it's `is_some` /// - env value: [`AWS_WEB_IDENTITY_TOKEN_FILE`] /// - profile config: `web_identity_token_file` pub web_identity_token_file: Option, /// `ec2_metadata_disabled` value will be loaded from: /// /// - this field /// - env value: [`AWS_EC2_METADATA_DISABLED`] pub ec2_metadata_disabled: bool, /// `endpoint_url` value will be loaded from: /// /// - this field /// - env value: [`AWS_ENDPOINT_URL`] pub endpoint_url: Option, } impl Default for Config { fn default() -> Self { Self { config_file: "~/.aws/config".to_string(), shared_credentials_file: "~/.aws/credentials".to_string(), profile: "default".to_string(), region: None, sts_regional_endpoints: "legacy".to_string(), access_key_id: None, secret_access_key: None, session_token: None, role_arn: None, role_session_name: "reqsign".to_string(), duration_seconds: Some(3600), external_id: None, tags: None, web_identity_token_file: None, ec2_metadata_disabled: false, endpoint_url: None, } } } impl Config { /// Load config from env. pub fn from_env(mut self) -> Self { let envs = env::vars().collect::>(); if let Some(v) = envs.get(AWS_CONFIG_FILE) { self.config_file = v.to_string(); } if let Some(v) = envs.get(AWS_SHARED_CREDENTIALS_FILE) { self.shared_credentials_file = v.to_string(); } if let Some(v) = envs.get(AWS_PROFILE) { self.profile = v.to_string(); } if let Some(v) = envs.get(AWS_REGION) { self.region = Some(v.to_string()) } if let Some(v) = envs.get(AWS_STS_REGIONAL_ENDPOINTS) { self.sts_regional_endpoints = v.to_string(); } if let Some(v) = envs.get(AWS_ACCESS_KEY_ID) { self.access_key_id = Some(v.to_string()) } if let Some(v) = envs.get(AWS_SECRET_ACCESS_KEY) { self.secret_access_key = Some(v.to_string()) } if let Some(v) = envs.get(AWS_SESSION_TOKEN) { self.session_token = Some(v.to_string()) } if let Some(v) = envs.get(AWS_ROLE_ARN) { self.role_arn = Some(v.to_string()) } if let Some(v) = envs.get(AWS_ROLE_SESSION_NAME) { self.role_session_name = v.to_string(); } if let Some(v) = envs.get(AWS_WEB_IDENTITY_TOKEN_FILE) { self.web_identity_token_file = Some(v.to_string()); } if let Some(v) = envs.get(AWS_EC2_METADATA_DISABLED) { self.ec2_metadata_disabled = v == "true"; } if let Some(v) = envs.get(AWS_ENDPOINT_URL) { self.endpoint_url = Some(v.to_string()); } self } /// Load config from profile (and shared profile). /// /// If the env var AWS_PROFILE is set, this profile will be used, /// otherwise the contents of `self.profile` will be used. #[cfg(not(target_arch = "wasm32"))] pub fn from_profile(mut self) -> Self { // self.profile is checked by the two load methods. if let Ok(profile) = env::var(AWS_PROFILE) { self.profile = profile; } // make sure we're getting profile info from the correct place. // Respecting these env vars also makes it possible to unit test // this method. if let Ok(config_file) = env::var(AWS_CONFIG_FILE) { self.config_file = config_file; } if let Ok(shared_credentials_file) = env::var(AWS_SHARED_CREDENTIALS_FILE) { self.shared_credentials_file = shared_credentials_file; } // Ignore all errors happened internally. let _ = self.load_via_profile_config_file().map_err(|err| { debug!("load_via_profile_config_file failed: {err:?}"); }); let _ = self .load_via_profile_shared_credentials_file() .map_err(|err| debug!("load_via_profile_shared_credentials_file failed: {err:?}")); self } /// Only the following fields will exist in shared_credentials_file: /// /// - `aws_access_key_id` /// - `aws_secret_access_key` /// - `aws_session_token` #[cfg(not(target_arch = "wasm32"))] fn load_via_profile_shared_credentials_file(&mut self) -> Result<()> { let path = expand_homedir(&self.shared_credentials_file) .ok_or_else(|| anyhow!("expand homedir failed"))?; let _ = fs::metadata(&path)?; let conf = Ini::load_from_file(path)?; let props = conf .section(Some(&self.profile)) .ok_or_else(|| anyhow!("section {} is not found", self.profile))?; if let Some(v) = props.get("aws_access_key_id") { self.access_key_id = Some(v.to_string()) } if let Some(v) = props.get("aws_secret_access_key") { self.secret_access_key = Some(v.to_string()) } if let Some(v) = props.get("aws_session_token") { self.session_token = Some(v.to_string()) } Ok(()) } #[cfg(not(target_arch = "wasm32"))] fn load_via_profile_config_file(&mut self) -> Result<()> { let path = expand_homedir(&self.config_file).ok_or_else(|| anyhow!("expand homedir failed"))?; let _ = fs::metadata(&path)?; let conf = Ini::load_from_file(path)?; let section = match self.profile.as_str() { "default" => "default".to_string(), x => format!("profile {x}"), }; let props = conf .section(Some(section)) .ok_or_else(|| anyhow!("section {} is not found", self.profile))?; if let Some(v) = props.get("region") { self.region = Some(v.to_string()) } if let Some(v) = props.get("sts_regional_endpoints") { self.sts_regional_endpoints = v.to_string(); } if let Some(v) = props.get("aws_access_key_id") { self.access_key_id = Some(v.to_string()) } if let Some(v) = props.get("aws_secret_access_key") { self.secret_access_key = Some(v.to_string()) } if let Some(v) = props.get("aws_session_token") { self.session_token = Some(v.to_string()) } if let Some(v) = props.get("role_arn") { self.role_arn = Some(v.to_string()) } if let Some(v) = props.get("role_session_name") { self.role_session_name = v.to_string() } if let Some(v) = props.get("duration_seconds") { self.duration_seconds = Some(v.to_string().parse::().unwrap()) } if let Some(v) = props.get("web_identity_token_file") { self.web_identity_token_file = Some(v.to_string()) } if let Some(v) = props.get("endpoint_url") { self.endpoint_url = Some(v.to_string()) } Ok(()) } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; use std::fs::File; use std::io::Write; use tempfile::tempdir; #[test] #[cfg(not(target_arch = "wasm32"))] fn test_config_from_profile_shared_credentials() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); // Create a dummy credentials file to test against let tmp_dir = tempdir()?; let file_path = tmp_dir.path().join("credentials"); let mut tmp_file = File::create(&file_path)?; writeln!(tmp_file, "[default]")?; writeln!(tmp_file, "aws_access_key_id = DEFAULTACCESSKEYID")?; writeln!(tmp_file, "aws_secret_access_key = DEFAULTSECRETACCESSKEY")?; writeln!(tmp_file, "aws_session_token = DEFAULTSESSIONTOKEN")?; writeln!(tmp_file)?; writeln!(tmp_file, "[profile1]")?; writeln!(tmp_file, "aws_access_key_id = PROFILE1ACCESSKEYID")?; writeln!(tmp_file, "aws_secret_access_key = PROFILE1SECRETACCESSKEY")?; writeln!(tmp_file, "aws_session_token = PROFILE1SESSIONTOKEN")?; temp_env::with_vars( [ (AWS_PROFILE, Some("profile1".to_owned())), (AWS_CONFIG_FILE, None::), ( AWS_SHARED_CREDENTIALS_FILE, Some(file_path.to_str().unwrap().to_owned()), ), ], || { let config = Config::default().from_profile(); assert_eq!(config.profile, "profile1".to_owned()); assert_eq!(config.access_key_id, Some("PROFILE1ACCESSKEYID".to_owned())); assert_eq!( config.secret_access_key, Some("PROFILE1SECRETACCESSKEY".to_owned()) ); assert_eq!( config.session_token, Some("PROFILE1SESSIONTOKEN".to_owned()) ); }, ); Ok(()) } #[test] #[cfg(not(target_arch = "wasm32"))] fn test_config_from_profile_config() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); // Create a dummy credentials file to test against let tmp_dir = tempdir()?; let file_path = tmp_dir.path().join("config"); let mut tmp_file = File::create(&file_path)?; writeln!(tmp_file, "[default]")?; writeln!(tmp_file, "aws_access_key_id = DEFAULTACCESSKEYID")?; writeln!(tmp_file, "aws_secret_access_key = DEFAULTSECRETACCESSKEY")?; writeln!(tmp_file, "aws_session_token = DEFAULTSESSIONTOKEN")?; writeln!(tmp_file)?; writeln!(tmp_file, "[profile profile1]")?; writeln!(tmp_file, "aws_access_key_id = PROFILE1ACCESSKEYID")?; writeln!(tmp_file, "aws_secret_access_key = PROFILE1SECRETACCESSKEY")?; writeln!(tmp_file, "aws_session_token = PROFILE1SESSIONTOKEN")?; writeln!(tmp_file, "endpoint_url = http://localhost:8080")?; temp_env::with_vars( [ (AWS_PROFILE, Some("profile1".to_owned())), ( AWS_CONFIG_FILE, Some(file_path.to_str().unwrap().to_owned()), ), (AWS_SHARED_CREDENTIALS_FILE, None::), ], || { let config = Config::default().from_profile(); assert_eq!(config.profile, "profile1".to_owned()); assert_eq!(config.access_key_id, Some("PROFILE1ACCESSKEYID".to_owned())); assert_eq!( config.secret_access_key, Some("PROFILE1SECRETACCESSKEY".to_owned()) ); assert_eq!( config.session_token, Some("PROFILE1SESSIONTOKEN".to_owned()) ); assert_eq!( config.endpoint_url, Some("http://localhost:8080".to_owned()) ); }, ); Ok(()) } } reqsign-0.16.2/src/aws/constants.rs000064400000000000000000000033331046102023000153270ustar 00000000000000use percent_encoding::AsciiSet; use percent_encoding::NON_ALPHANUMERIC; // Headers used in aws services. pub const X_AMZ_CONTENT_SHA_256: &str = "x-amz-content-sha256"; pub const X_AMZ_DATE: &str = "x-amz-date"; pub const X_AMZ_SECURITY_TOKEN: &str = "x-amz-security-token"; // Env values used in aws services. pub const AWS_ACCESS_KEY_ID: &str = "AWS_ACCESS_KEY_ID"; pub const AWS_SECRET_ACCESS_KEY: &str = "AWS_SECRET_ACCESS_KEY"; pub const AWS_SESSION_TOKEN: &str = "AWS_SESSION_TOKEN"; pub const AWS_REGION: &str = "AWS_REGION"; pub const AWS_PROFILE: &str = "AWS_PROFILE"; pub const AWS_CONFIG_FILE: &str = "AWS_CONFIG_FILE"; pub const AWS_SHARED_CREDENTIALS_FILE: &str = "AWS_SHARED_CREDENTIALS_FILE"; pub const AWS_WEB_IDENTITY_TOKEN_FILE: &str = "AWS_WEB_IDENTITY_TOKEN_FILE"; pub const AWS_ROLE_ARN: &str = "AWS_ROLE_ARN"; pub const AWS_ROLE_SESSION_NAME: &str = "AWS_ROLE_SESSION_NAME"; pub const AWS_STS_REGIONAL_ENDPOINTS: &str = "AWS_STS_REGIONAL_ENDPOINTS"; pub const AWS_EC2_METADATA_DISABLED: &str = "AWS_EC2_METADATA_DISABLED"; pub const AWS_ENDPOINT_URL: &str = "AWS_ENDPOINT_URL"; /// AsciiSet for [AWS UriEncode](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html) /// /// - URI encode every byte except the unreserved characters: 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'. pub static AWS_URI_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC .remove(b'/') .remove(b'-') .remove(b'.') .remove(b'_') .remove(b'~'); /// AsciiSet for [AWS UriEncode](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html) /// /// But used in query. pub static AWS_QUERY_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC .remove(b'-') .remove(b'.') .remove(b'_') .remove(b'~'); reqsign-0.16.2/src/aws/credential.rs000064400000000000000000001051071046102023000154270ustar 00000000000000use std::fmt::Debug; use std::fmt::Write; use std::fs; use std::sync::Arc; use std::sync::Mutex; use anyhow::anyhow; use anyhow::Result; use async_trait::async_trait; use http::header::CONTENT_LENGTH; use log::debug; use quick_xml::de; use reqwest::Client; use serde::Deserialize; use super::config::Config; use super::constants::X_AMZ_CONTENT_SHA_256; use super::v4::Signer; use crate::time::now; use crate::time::parse_rfc3339; use crate::time::DateTime; pub const EMPTY_STRING_SHA256: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; /// Credential that holds the access_key and secret_key. #[derive(Default, Clone)] #[cfg_attr(test, derive(Debug))] pub struct Credential { /// Access key id for aws services. pub access_key_id: String, /// Secret access key for aws services. pub secret_access_key: String, /// Session token for aws services. pub session_token: Option, /// Expiration time for this credential. pub expires_in: Option, } impl Credential { /// is current cred is valid? pub fn is_valid(&self) -> bool { if (self.access_key_id.is_empty() || self.secret_access_key.is_empty()) && self.session_token.is_none() { return false; } // Take 120s as buffer to avoid edge cases. if let Some(valid) = self .expires_in .map(|v| v > now() + chrono::TimeDelta::try_minutes(2).expect("in bounds")) { return valid; } true } } /// Loader trait will try to load credential from different sources. #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] pub trait CredentialLoad: 'static + Send + Sync { /// Load credential from sources. /// /// - If succeed, return `Ok(Some(cred))` /// - If not found, return `Ok(None)` /// - If unexpected errors happened, return `Err(err)` async fn load_credential(&self, client: Client) -> Result>; } /// CredentialLoader will load credential from different methods. pub struct DefaultLoader { client: Client, config: Config, credential: Arc>>, imds_v2_loader: Option, } impl DefaultLoader { /// Create a new CredentialLoader pub fn new(client: Client, config: Config) -> Self { let imds_v2_loader = if config.ec2_metadata_disabled { None } else { Some(IMDSv2Loader::new(client.clone())) }; Self { client, config, credential: Arc::default(), imds_v2_loader, } } /// Disable load from ec2 metadata. pub fn with_disable_ec2_metadata(mut self) -> Self { self.imds_v2_loader = None; self } /// Load credential. /// /// Resolution order: /// 1. Environment variables /// 2. Shared config (`~/.aws/config`, `~/.aws/credentials`) /// 3. Web Identity Tokens /// 4. ECS (IAM Roles for Tasks) & General HTTP credentials: /// 5. EC2 IMDSv2 pub async fn load(&self) -> Result> { // Return cached credential if it has been loaded at least once. match self.credential.lock().expect("lock poisoned").clone() { Some(cred) if cred.is_valid() => return Ok(Some(cred)), _ => (), } let cred = self.load_inner().await?; let mut lock = self.credential.lock().expect("lock poisoned"); lock.clone_from(&cred); Ok(cred) } async fn load_inner(&self) -> Result> { if let Some(cred) = self.load_via_config().map_err(|err| { debug!("load credential via config failed: {err:?}"); err })? { return Ok(Some(cred)); } if let Some(cred) = self .load_via_assume_role_with_web_identity() .await .map_err(|err| { debug!("load credential via assume_role_with_web_identity failed: {err:?}"); err })? { return Ok(Some(cred)); } if let Some(cred) = self.load_via_imds_v2().await.map_err(|err| { debug!("load credential via imds_v2 failed: {err:?}"); err })? { return Ok(Some(cred)); } Ok(None) } fn load_via_config(&self) -> Result> { if let (Some(ak), Some(sk)) = (&self.config.access_key_id, &self.config.secret_access_key) { Ok(Some(Credential { access_key_id: ak.clone(), secret_access_key: sk.clone(), session_token: self.config.session_token.clone(), // Set expires_in to 10 minutes to enforce re-read // from file. expires_in: Some(now() + chrono::TimeDelta::try_minutes(10).expect("in bounds")), })) } else { Ok(None) } } async fn load_via_imds_v2(&self) -> Result> { let loader = match &self.imds_v2_loader { Some(loader) => loader, None => return Ok(None), }; loader.load().await } async fn load_via_assume_role_with_web_identity(&self) -> Result> { let (token_file, role_arn) = match (&self.config.web_identity_token_file, &self.config.role_arn) { (Some(token_file), Some(role_arn)) => (token_file, role_arn), _ => return Ok(None), }; let token = fs::read_to_string(token_file)?; let role_session_name = &self.config.role_session_name; let endpoint = self.sts_endpoint()?; // Construct request to AWS STS Service. let url = format!("https://{endpoint}/?Action=AssumeRoleWithWebIdentity&RoleArn={role_arn}&WebIdentityToken={token}&Version=2011-06-15&RoleSessionName={role_session_name}"); let req = self.client.get(&url).header( http::header::CONTENT_TYPE.as_str(), "application/x-www-form-urlencoded", ); let resp = req.send().await?; if resp.status() != http::StatusCode::OK { let content = resp.text().await?; return Err(anyhow!("request to AWS STS Services failed: {content}")); } let resp: AssumeRoleWithWebIdentityResponse = de::from_str(&resp.text().await?)?; let resp_cred = resp.result.credentials; let cred = Credential { access_key_id: resp_cred.access_key_id, secret_access_key: resp_cred.secret_access_key, session_token: Some(resp_cred.session_token), expires_in: Some(parse_rfc3339(&resp_cred.expiration)?), }; Ok(Some(cred)) } /// Get the sts endpoint. /// /// The returning format may look like `sts.{region}.amazonaws.com` /// /// # Notes /// /// AWS could have different sts endpoint based on it's region. /// We can check them by region name. /// /// ref: https://github.com/awslabs/aws-sdk-rust/blob/31cfae2cf23be0c68a47357070dea1aee9227e3a/sdk/sts/src/aws_endpoint.rs fn sts_endpoint(&self) -> Result { // use regional sts if sts_regional_endpoints has been set. if self.config.sts_regional_endpoints == "regional" { let region = self.config.region.clone().ok_or_else(|| { anyhow!("sts_regional_endpoints set to reginal, but region is not set") })?; if region.starts_with("cn-") { Ok(format!("sts.{region}.amazonaws.com.cn")) } else { Ok(format!("sts.{region}.amazonaws.com")) } } else { let region = self.config.region.clone().unwrap_or_default(); if region.starts_with("cn") { // TODO: seems aws china doesn't support global sts? Ok("sts.amazonaws.com.cn".to_string()) } else { Ok("sts.amazonaws.com".to_string()) } } } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl CredentialLoad for DefaultLoader { async fn load_credential(&self, _: Client) -> Result> { self.load().await } } pub struct IMDSv2Loader { client: Client, token: Arc>, } impl IMDSv2Loader { /// Create a new IMDSv2Loader. pub fn new(client: Client) -> Self { Self { client, token: Arc::new(Mutex::new(("".to_string(), DateTime::MIN_UTC))), } } pub async fn load(&self) -> Result> { let token = self.load_ec2_metadata_token().await?; // List all credentials that node has. let url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"; let req = self .client .get(url) .header("x-aws-ec2-metadata-token", &token); let resp = req.send().await?; if resp.status() != http::StatusCode::OK { let content = resp.text().await?; return Err(anyhow!( "request to AWS EC2 Metadata Services failed: {content}" )); } let profile_name = resp.text().await?; // Get the credentials via role_name. let url = format!( "http://169.254.169.254/latest/meta-data/iam/security-credentials/{profile_name}" ); let req = self .client .get(&url) .header("x-aws-ec2-metadata-token", &token); let resp = req.send().await?; if resp.status() != http::StatusCode::OK { let content = resp.text().await?; return Err(anyhow!( "request to AWS EC2 Metadata Services failed: {content}" )); } let content = resp.text().await?; let resp: Ec2MetadataIamSecurityCredentials = serde_json::from_str(&content)?; if resp.code != "Success" { return Err(anyhow!( "request to AWS EC2 Metadata Services failed: {content}" )); } let cred = Credential { access_key_id: resp.access_key_id, secret_access_key: resp.secret_access_key, session_token: Some(resp.token), expires_in: Some(parse_rfc3339(&resp.expiration)?), }; Ok(Some(cred)) } /// load_ec2_metadata_token will load ec2 metadata token from IMDS. /// /// Return value is (token, expires_in). async fn load_ec2_metadata_token(&self) -> Result { { let (token, expires_in) = self.token.lock().expect("lock poisoned").clone(); if expires_in > now() { return Ok(token); } } let url = "http://169.254.169.254/latest/api/token"; #[allow(unused_mut)] let mut req = self .client .put(url) .header(CONTENT_LENGTH, "0") // 21600s (6h) is recommended by AWS. .header("x-aws-ec2-metadata-token-ttl-seconds", "21600"); // Set timeout to 1s to avoid hanging on non-s3 env. #[cfg(not(target_arch = "wasm32"))] { req = req.timeout(std::time::Duration::from_secs(1)); } let resp = req.send().await?; if resp.status() != http::StatusCode::OK { let content = resp.text().await?; return Err(anyhow!( "request to AWS EC2 Metadata Services failed: {content}" )); } let ec2_token = resp.text().await?; // Set expires_in to 10 minutes to enforce re-read. let expires_in = now() + chrono::TimeDelta::try_seconds(21600).expect("in bounds") - chrono::TimeDelta::try_seconds(600).expect("in bounds"); { *self.token.lock().expect("lock poisoned") = (ec2_token.clone(), expires_in); } Ok(ec2_token) } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl CredentialLoad for IMDSv2Loader { async fn load_credential(&self, _: Client) -> Result> { self.load().await } } /// AssumeRoleLoader will load credential via assume role. pub struct AssumeRoleLoader { client: Client, config: Config, source_credential: Box, sts_signer: Signer, } impl AssumeRoleLoader { /// Create a new assume role loader. pub fn new( client: Client, config: Config, source_credential: Box, ) -> Result { let region = config.region.clone().ok_or_else(|| { anyhow!("assume role loader requires region, but not found, please check your configuration") })?; Ok(Self { client, config, source_credential, sts_signer: Signer::new("sts", ®ion), }) } /// Load credential via assume role. pub async fn load(&self) -> Result> { let role_arn =self.config.role_arn.clone().ok_or_else(|| { anyhow!("assume role loader requires role_arn, but not found, please check your configuration") })?; let role_session_name = &self.config.role_session_name; let endpoint = self.sts_endpoint()?; // Construct request to AWS STS Service. let mut url = format!("https://{endpoint}/?Action=AssumeRole&RoleArn={role_arn}&Version=2011-06-15&RoleSessionName={role_session_name}"); if let Some(external_id) = &self.config.external_id { write!(url, "&ExternalId={external_id}")?; } if let Some(duration_seconds) = &self.config.duration_seconds { write!(url, "&DurationSeconds={duration_seconds}")?; } if let Some(tags) = &self.config.tags { for (idx, (key, value)) in tags.iter().enumerate() { let tag_index = idx + 1; write!( url, "&Tags.member.{tag_index}.Key={key}&Tags.member.{tag_index}.Value={value}" )?; } } let mut req = self .client .get(&url) .header( http::header::CONTENT_TYPE.as_str(), "application/x-www-form-urlencoded", ) // Set content sha to empty string. .header(X_AMZ_CONTENT_SHA_256, EMPTY_STRING_SHA256) .build()?; let source_cred = self .source_credential .load_credential(self.client.clone()) .await? .ok_or_else(|| { anyhow!("source credential is required for AssumeRole, but not found, please check your configuration") })?; self.sts_signer.sign(&mut req, &source_cred)?; let resp = self.client.execute(req).await?; if resp.status() != http::StatusCode::OK { let content = resp.text().await?; return Err(anyhow!("request to AWS STS Services failed: {content}")); } let resp: AssumeRoleResponse = de::from_str(&resp.text().await?)?; let resp_cred = resp.result.credentials; let cred = Credential { access_key_id: resp_cred.access_key_id, secret_access_key: resp_cred.secret_access_key, session_token: Some(resp_cred.session_token), expires_in: Some(parse_rfc3339(&resp_cred.expiration)?), }; Ok(Some(cred)) } /// Get the sts endpoint. /// /// The returning format may look like `sts.{region}.amazonaws.com` /// /// # Notes /// /// AWS could have different sts endpoint based on it's region. /// We can check them by region name. /// /// ref: https://github.com/awslabs/aws-sdk-rust/blob/31cfae2cf23be0c68a47357070dea1aee9227e3a/sdk/sts/src/aws_endpoint.rs fn sts_endpoint(&self) -> Result { // use regional sts if sts_regional_endpoints has been set. if self.config.sts_regional_endpoints == "regional" { let region = self.config.region.clone().ok_or_else(|| { anyhow!("sts_regional_endpoints set to reginal, but region is not set") })?; if region.starts_with("cn-") { Ok(format!("sts.{region}.amazonaws.com.cn")) } else { Ok(format!("sts.{region}.amazonaws.com")) } } else { let region = self.config.region.clone().unwrap_or_default(); if region.starts_with("cn") { // TODO: seems aws china doesn't support global sts? Ok("sts.amazonaws.com.cn".to_string()) } else { Ok("sts.amazonaws.com".to_string()) } } } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl CredentialLoad for AssumeRoleLoader { async fn load_credential(&self, _: Client) -> Result> { self.load().await } } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleWithWebIdentityResponse { #[serde(rename = "AssumeRoleWithWebIdentityResult")] result: AssumeRoleWithWebIdentityResult, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleWithWebIdentityResult { credentials: AssumeRoleWithWebIdentityCredentials, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleWithWebIdentityCredentials { access_key_id: String, secret_access_key: String, session_token: String, expiration: String, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleResponse { #[serde(rename = "AssumeRoleResult")] result: AssumeRoleResult, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleResult { credentials: AssumeRoleCredentials, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleCredentials { access_key_id: String, secret_access_key: String, session_token: String, expiration: String, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct Ec2MetadataIamSecurityCredentials { access_key_id: String, secret_access_key: String, token: String, expiration: String, code: String, } #[cfg(test)] mod tests { use std::env; use std::str::FromStr; use std::vec; use anyhow::Result; use http::Request; use http::StatusCode; use once_cell::sync::Lazy; use quick_xml::de; use reqwest::Client; use tokio::runtime::Runtime; use super::*; use crate::aws::constants::*; use crate::aws::v4::Signer; static RUNTIME: Lazy = Lazy::new(|| { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .expect("Should create a tokio runtime") }); #[test] fn test_credential_env_loader_without_env() { let _ = env_logger::builder().is_test(true).try_init(); temp_env::with_vars_unset(vec![AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY], || { RUNTIME.block_on(async { let l = DefaultLoader::new(reqwest::Client::new(), Config::default()) .with_disable_ec2_metadata(); let x = l.load().await.expect("load must succeed"); assert!(x.is_none()); }) }); } #[test] fn test_credential_env_loader_with_env() { let _ = env_logger::builder().is_test(true).try_init(); temp_env::with_vars( vec![ (AWS_ACCESS_KEY_ID, Some("access_key_id")), (AWS_SECRET_ACCESS_KEY, Some("secret_access_key")), ], || { RUNTIME.block_on(async { let l = DefaultLoader::new(Client::new(), Config::default().from_env()); let x = l.load().await.expect("load must succeed"); let x = x.expect("must load succeed"); assert_eq!("access_key_id", x.access_key_id); assert_eq!("secret_access_key", x.secret_access_key); }) }, ); } #[test] fn test_credential_profile_loader_from_config() { let _ = env_logger::builder().is_test(true).try_init(); temp_env::with_vars( vec![ (AWS_ACCESS_KEY_ID, None), (AWS_SECRET_ACCESS_KEY, None), ( AWS_CONFIG_FILE, Some(format!( "{}/testdata/services/aws/default_config", env::current_dir() .expect("current_dir must exist") .to_string_lossy() )), ), ( AWS_SHARED_CREDENTIALS_FILE, Some(format!( "{}/testdata/services/aws/not_exist", env::current_dir() .expect("current_dir must exist") .to_string_lossy() )), ), ], || { RUNTIME.block_on(async { let l = DefaultLoader::new( Client::new(), Config::default().from_env().from_profile(), ); let x = l.load().await.unwrap().unwrap(); assert_eq!("config_access_key_id", x.access_key_id); assert_eq!("config_secret_access_key", x.secret_access_key); }) }, ); } #[test] fn test_credential_profile_loader_from_shared() { let _ = env_logger::builder().is_test(true).try_init(); temp_env::with_vars( vec![ (AWS_ACCESS_KEY_ID, None), (AWS_SECRET_ACCESS_KEY, None), ( AWS_CONFIG_FILE, Some(format!( "{}/testdata/services/aws/not_exist", env::current_dir() .expect("load must exist") .to_string_lossy() )), ), ( AWS_SHARED_CREDENTIALS_FILE, Some(format!( "{}/testdata/services/aws/default_credential", env::current_dir() .expect("load must exist") .to_string_lossy() )), ), ], || { RUNTIME.block_on(async { let l = DefaultLoader::new( Client::new(), Config::default().from_env().from_profile(), ); let x = l.load().await.unwrap().unwrap(); assert_eq!("shared_access_key_id", x.access_key_id); assert_eq!("shared_secret_access_key", x.secret_access_key); }) }, ); } /// AWS_SHARED_CREDENTIALS_FILE should be taken first. #[test] fn test_credential_profile_loader_from_both() { let _ = env_logger::builder().is_test(true).try_init(); temp_env::with_vars( vec![ (AWS_ACCESS_KEY_ID, None), (AWS_SECRET_ACCESS_KEY, None), ( AWS_CONFIG_FILE, Some(format!( "{}/testdata/services/aws/default_config", env::current_dir() .expect("current_dir must exist") .to_string_lossy() )), ), ( AWS_SHARED_CREDENTIALS_FILE, Some(format!( "{}/testdata/services/aws/default_credential", env::current_dir() .expect("current_dir must exist") .to_string_lossy() )), ), ], || { RUNTIME.block_on(async { let l = DefaultLoader::new( Client::new(), Config::default().from_env().from_profile(), ); let x = l.load().await.expect("load must success").unwrap(); assert_eq!("shared_access_key_id", x.access_key_id); assert_eq!("shared_secret_access_key", x.secret_access_key); }) }, ); } #[test] fn test_signer_with_web_loader() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_AWS_S3_TEST").is_err() || env::var("REQSIGN_AWS_S3_TEST").unwrap() != "on" { return Ok(()); } // Ignore test if role_arn not set let role_arn = if let Ok(v) = env::var("REQSIGN_AWS_ASSUME_ROLE_ARN") { v } else { return Ok(()); }; // let provider_arn = env::var("REQSIGN_AWS_PROVIDER_ARN").expect("REQSIGN_AWS_PROVIDER_ARN not exist"); let region = env::var("REQSIGN_AWS_S3_REGION").expect("REQSIGN_AWS_S3_REGION not exist"); let github_token = env::var("GITHUB_ID_TOKEN").expect("GITHUB_ID_TOKEN not exist"); let file_path = format!( "{}/testdata/services/aws/web_identity_token_file", env::current_dir() .expect("current_dir must exist") .to_string_lossy() ); fs::write(&file_path, github_token)?; temp_env::with_vars( vec![ (AWS_REGION, Some(®ion)), (AWS_ROLE_ARN, Some(&role_arn)), (AWS_WEB_IDENTITY_TOKEN_FILE, Some(&file_path)), ], || { RUNTIME.block_on(async { let config = Config::default().from_env(); let loader = DefaultLoader::new(reqwest::Client::new(), config); let signer = Signer::new("s3", ®ion); let endpoint = format!("https://s3.{}.amazonaws.com/opendal-testing", region); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", endpoint, "not_exist_file")).unwrap(); let cred = loader .load() .await .expect("credential must be valid") .unwrap(); signer.sign(&mut req, &cred).expect("sign must success"); debug!("signed request url: {:?}", req.uri().to_string()); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client.execute(req.try_into().unwrap()).await.unwrap(); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {:?}", resp.text().await.unwrap()); assert_eq!(status, StatusCode::NOT_FOUND); }) }, ); Ok(()) } #[test] fn test_signer_with_web_loader_assume_role() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_AWS_S3_TEST").is_err() || env::var("REQSIGN_AWS_S3_TEST").unwrap() != "on" { return Ok(()); } // Ignore test if role_arn not set let role_arn = if let Ok(v) = env::var("REQSIGN_AWS_ROLE_ARN") { v } else { return Ok(()); }; // Ignore test if assume_role_arn not set let assume_role_arn = if let Ok(v) = env::var("REQSIGN_AWS_ASSUME_ROLE_ARN") { v } else { return Ok(()); }; let region = env::var("REQSIGN_AWS_S3_REGION").expect("REQSIGN_AWS_S3_REGION not exist"); let github_token = env::var("GITHUB_ID_TOKEN").expect("GITHUB_ID_TOKEN not exist"); let file_path = format!( "{}/testdata/services/aws/web_identity_token_file", env::current_dir() .expect("current_dir must exist") .to_string_lossy() ); fs::write(&file_path, github_token)?; temp_env::with_vars( vec![ (AWS_REGION, Some(®ion)), (AWS_ROLE_ARN, Some(&role_arn)), (AWS_WEB_IDENTITY_TOKEN_FILE, Some(&file_path)), ], || { RUNTIME.block_on(async { let client = reqwest::Client::new(); let default_loader = DefaultLoader::new(client.clone(), Config::default().from_env()) .with_disable_ec2_metadata(); let cfg = Config { role_arn: Some(assume_role_arn.clone()), region: Some(region.clone()), sts_regional_endpoints: "regional".to_string(), ..Default::default() }; let loader = AssumeRoleLoader::new(client.clone(), cfg, Box::new(default_loader)) .expect("AssumeRoleLoader must be valid"); let signer = Signer::new("s3", ®ion); let endpoint = format!("https://s3.{}.amazonaws.com/opendal-testing", region); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", endpoint, "not_exist_file")).unwrap(); let cred = loader .load() .await .expect("credential must be valid") .unwrap(); signer.sign(&mut req, &cred).expect("sign must success"); debug!("signed request url: {:?}", req.uri().to_string()); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client.execute(req.try_into().unwrap()).await.unwrap(); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {:?}", resp.text().await.unwrap()); assert_eq!(status, StatusCode::NOT_FOUND); }) }, ); Ok(()) } #[test] fn test_parse_assume_role_with_web_identity_response() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); let content = r#" test_audience role_id:reqsign arn:aws:sts::123:assumed-role/reqsign/reqsign arn:aws:iam::123:oidc-provider/example.com/ access_key_id secret_access_key session_token 2022-05-25T11:45:17Z subject b1663ad1-23ab-45e9-b465-9af30b202eba "#; let resp: AssumeRoleWithWebIdentityResponse = de::from_str(content).expect("xml deserialize must success"); assert_eq!(&resp.result.credentials.access_key_id, "access_key_id"); assert_eq!( &resp.result.credentials.secret_access_key, "secret_access_key" ); assert_eq!(&resp.result.credentials.session_token, "session_token"); assert_eq!(&resp.result.credentials.expiration, "2022-05-25T11:45:17Z"); Ok(()) } #[test] fn test_parse_assume_role_response() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); let content = r#" Alice arn:aws:sts::123456789012:assumed-role/demo/TestAR ARO123EXAMPLE123:TestAR ASIAIOSFODNN7EXAMPLE wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== 2019-11-09T13:34:41Z 6 c6104cbe-af31-11e0-8154-cbc7ccf896c7 "#; let resp: AssumeRoleResponse = de::from_str(content).expect("xml deserialize must success"); assert_eq!( &resp.result.credentials.access_key_id, "ASIAIOSFODNN7EXAMPLE" ); assert_eq!( &resp.result.credentials.secret_access_key, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY" ); assert_eq!( &resp.result.credentials.session_token, "AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==" ); assert_eq!(&resp.result.credentials.expiration, "2019-11-09T13:34:41Z"); Ok(()) } } reqsign-0.16.2/src/aws/mod.rs000064400000000000000000000006321046102023000140710ustar 00000000000000//! AWS service signer //! //! Only sigv4 has been supported. mod config; pub use config::Config as AwsConfig; mod credential; pub use credential::AssumeRoleLoader as AwsAssumeRoleLoader; pub use credential::Credential as AwsCredential; pub use credential::CredentialLoad as AwsCredentialLoad; pub use credential::DefaultLoader as AwsDefaultLoader; mod v4; pub use v4::Signer as AwsV4Signer; mod constants; reqsign-0.16.2/src/aws/v4.rs000064400000000000000000000700441046102023000136470ustar 00000000000000//! AWS service sigv4 signer use std::fmt::Debug; use std::fmt::Write; use std::time::Duration; use anyhow::Result; use http::header; use http::HeaderValue; use log::debug; use percent_encoding::percent_decode_str; use percent_encoding::utf8_percent_encode; use super::constants::AWS_QUERY_ENCODE_SET; use super::constants::X_AMZ_CONTENT_SHA_256; use super::constants::X_AMZ_DATE; use super::constants::X_AMZ_SECURITY_TOKEN; use super::credential::Credential; use crate::ctx::SigningContext; use crate::ctx::SigningMethod; use crate::hash::hex_hmac_sha256; use crate::hash::hex_sha256; use crate::hash::hmac_sha256; use crate::request::SignableRequest; use crate::time::format_date; use crate::time::format_iso8601; use crate::time::now; use crate::time::DateTime; /// Singer that implement AWS SigV4. /// /// - [Signature Version 4 signing process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) #[derive(Debug)] pub struct Signer { service: String, region: String, time: Option, } impl Signer { /// Create a builder. pub fn new(service: &str, region: &str) -> Self { Self { service: service.to_string(), region: region.to_string(), time: None, } } /// Specify the signing time. /// /// # Note /// /// We should always take current time to sign requests. /// Only use this function for testing. #[cfg(test)] pub fn time(mut self, time: DateTime) -> Self { self.time = Some(time); self } fn build( &self, req: &mut impl SignableRequest, method: SigningMethod, cred: &Credential, ) -> Result { let now = self.time.unwrap_or_else(now); let mut ctx = req.build()?; // canonicalize context canonicalize_header(&mut ctx, method, cred, now)?; canonicalize_query(&mut ctx, method, cred, now, &self.service, &self.region)?; // build canonical request and string to sign. let creq = canonical_request_string(&mut ctx)?; let encoded_req = hex_sha256(creq.as_bytes()); // Scope: "20220313///aws4_request" let scope = format!( "{}/{}/{}/aws4_request", format_date(now), self.region, self.service ); debug!("calculated scope: {scope}"); // StringToSign: // // AWS4-HMAC-SHA256 // 20220313T072004Z // 20220313///aws4_request // let string_to_sign = { let mut f = String::new(); writeln!(f, "AWS4-HMAC-SHA256")?; writeln!(f, "{}", format_iso8601(now))?; writeln!(f, "{}", &scope)?; write!(f, "{}", &encoded_req)?; f }; debug!("calculated string to sign: {string_to_sign}"); let signing_key = generate_signing_key(&cred.secret_access_key, now, &self.region, &self.service); let signature = hex_hmac_sha256(&signing_key, string_to_sign.as_bytes()); match method { SigningMethod::Header => { let mut authorization = HeaderValue::from_str(&format!( "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}", cred.access_key_id, scope, ctx.header_name_to_vec_sorted().join(";"), signature ))?; authorization.set_sensitive(true); ctx.headers .insert(http::header::AUTHORIZATION, authorization); } SigningMethod::Query(_) => { ctx.query.push(("X-Amz-Signature".into(), signature)); } } Ok(ctx) } /// Get the region of this signer. pub fn region(&self) -> &str { &self.region } /// Signing request with header. /// /// # Example /// /// ```rust,no_run /// use anyhow::Result; /// use reqsign::AwsConfig; /// use reqsign::AwsDefaultLoader; /// use reqsign::AwsV4Signer; /// use reqwest::Client; /// use reqwest::Request; /// use reqwest::Url; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// let client = Client::new(); /// let config = AwsConfig::default().from_profile().from_env(); /// let loader = AwsDefaultLoader::new(client.clone(), config); /// let signer = AwsV4Signer::new("s3", "us-east-1"); /// // Construct request /// let url = Url::parse("https://s3.amazonaws.com/testbucket")?; /// let mut req = reqwest::Request::new(http::Method::GET, url); /// // Signing request with Signer /// let credential = loader.load().await?.unwrap(); /// signer.sign(&mut req, &credential)?; /// // Sending already signed request. /// let resp = client.execute(req).await?; /// println!("resp got status: {}", resp.status()); /// Ok(()) /// } /// ``` pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> { let ctx = self.build(req, SigningMethod::Header, cred)?; req.apply(ctx) } /// Signing request with query. /// /// # Example /// /// ```rust,no_run /// use std::time::Duration; /// /// use anyhow::Result; /// use reqsign::AwsConfig; /// use reqsign::AwsDefaultLoader; /// use reqsign::AwsV4Signer; /// use reqwest::Client; /// use reqwest::Request; /// use reqwest::Url; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// let client = Client::new(); /// let config = AwsConfig::default().from_profile().from_env(); /// let loader = AwsDefaultLoader::new(client.clone(), config); /// let signer = AwsV4Signer::new("s3", "us-east-1"); /// // Construct request /// let url = Url::parse("https://s3.amazonaws.com/testbucket")?; /// let mut req = reqwest::Request::new(http::Method::GET, url); /// // Signing request with Signer /// let credential = loader.load().await?.unwrap(); /// signer.sign_query(&mut req, Duration::from_secs(3600), &credential)?; /// // Sending already signed request. /// let resp = client.execute(req).await?; /// println!("resp got status: {}", resp.status()); /// Ok(()) /// } /// ``` pub fn sign_query( &self, req: &mut impl SignableRequest, expire: Duration, cred: &Credential, ) -> Result<()> { let ctx = self.build(req, SigningMethod::Query(expire), cred)?; req.apply(ctx) } } fn canonical_request_string(ctx: &mut SigningContext) -> Result { // 256 is specially chosen to avoid reallocation for most requests. let mut f = String::with_capacity(256); // Insert method writeln!(f, "{}", ctx.method)?; // Insert encoded path let path = percent_decode_str(&ctx.path).decode_utf8()?; writeln!( f, "{}", utf8_percent_encode(&path, &super::constants::AWS_URI_ENCODE_SET) )?; // Insert query writeln!( f, "{}", ctx.query .iter() .map(|(k, v)| { format!("{k}={v}") }) .collect::>() .join("&") )?; // Insert signed headers let signed_headers = ctx.header_name_to_vec_sorted(); for header in signed_headers.iter() { let value = &ctx.headers[*header]; writeln!( f, "{}:{}", header, value.to_str().expect("header value must be valid") )?; } writeln!(f)?; writeln!(f, "{}", signed_headers.join(";"))?; if ctx.headers.get(X_AMZ_CONTENT_SHA_256).is_none() { write!(f, "UNSIGNED-PAYLOAD")?; } else { write!(f, "{}", ctx.headers[X_AMZ_CONTENT_SHA_256].to_str()?)?; } Ok(f) } fn canonicalize_header( ctx: &mut SigningContext, method: SigningMethod, cred: &Credential, now: DateTime, ) -> Result<()> { // Header names and values need to be normalized according to Step 4 of https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html for (_, value) in ctx.headers.iter_mut() { SigningContext::header_value_normalize(value) } // Insert HOST header if not present. if ctx.headers.get(header::HOST).is_none() { ctx.headers .insert(header::HOST, ctx.authority.as_str().parse()?); } if method == SigningMethod::Header { // Insert DATE header if not present. if ctx.headers.get(X_AMZ_DATE).is_none() { let date_header = HeaderValue::try_from(format_iso8601(now))?; ctx.headers.insert(X_AMZ_DATE, date_header); } // Insert X_AMZ_CONTENT_SHA_256 header if not present. if ctx.headers.get(X_AMZ_CONTENT_SHA_256).is_none() { ctx.headers.insert( X_AMZ_CONTENT_SHA_256, HeaderValue::from_static("UNSIGNED-PAYLOAD"), ); } // Insert X_AMZ_SECURITY_TOKEN header if security token exists. if let Some(token) = &cred.session_token { let mut value = HeaderValue::from_str(token)?; // Set token value sensitive to valid leaking. value.set_sensitive(true); ctx.headers.insert(X_AMZ_SECURITY_TOKEN, value); } } Ok(()) } fn canonicalize_query( ctx: &mut SigningContext, method: SigningMethod, cred: &Credential, now: DateTime, service: &str, region: &str, ) -> Result<()> { if let SigningMethod::Query(expire) = method { ctx.query .push(("X-Amz-Algorithm".into(), "AWS4-HMAC-SHA256".into())); ctx.query.push(( "X-Amz-Credential".into(), format!( "{}/{}/{}/{}/aws4_request", cred.access_key_id, format_date(now), region, service ), )); ctx.query.push(("X-Amz-Date".into(), format_iso8601(now))); ctx.query .push(("X-Amz-Expires".into(), expire.as_secs().to_string())); ctx.query.push(( "X-Amz-SignedHeaders".into(), ctx.header_name_to_vec_sorted().join(";"), )); if let Some(token) = &cred.session_token { ctx.query .push(("X-Amz-Security-Token".into(), token.into())); } } // Return if query is empty. if ctx.query.is_empty() { return Ok(()); } // Sort by param name ctx.query.sort(); ctx.query = ctx .query .iter() .map(|(k, v)| { ( utf8_percent_encode(k, &AWS_QUERY_ENCODE_SET).to_string(), utf8_percent_encode(v, &AWS_QUERY_ENCODE_SET).to_string(), ) }) .collect(); Ok(()) } fn generate_signing_key(secret: &str, time: DateTime, region: &str, service: &str) -> Vec { // Sign secret let secret = format!("AWS4{secret}"); // Sign date let sign_date = hmac_sha256(secret.as_bytes(), format_date(time).as_bytes()); // Sign region let sign_region = hmac_sha256(sign_date.as_slice(), region.as_bytes()); // Sign service let sign_service = hmac_sha256(sign_region.as_slice(), service.as_bytes()); // Sign request let sign_request = hmac_sha256(sign_service.as_slice(), "aws4_request".as_bytes()); sign_request } #[cfg(test)] mod tests { use std::time::SystemTime; use anyhow::Result; use aws_credential_types::Credentials; use aws_sigv4::http_request::PayloadChecksumKind; use aws_sigv4::http_request::PercentEncodingMode; use aws_sigv4::http_request::SignableBody; use aws_sigv4::http_request::SignableRequest; use aws_sigv4::http_request::SignatureLocation; use aws_sigv4::http_request::SigningSettings; use aws_sigv4::sign::v4; use http::header; use macro_rules_attribute::apply; use reqwest::Client; use super::super::AwsDefaultLoader; use super::*; use crate::aws::AwsConfig; fn test_get_request() -> http::Request<&'static str> { let mut req = http::Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = "http://127.0.0.1:9000/hello" .parse() .expect("url must be valid"); req } fn test_get_request_with_sse() -> http::Request<&'static str> { let mut req = http::Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = "http://127.0.0.1:9000/hello" .parse() .expect("url must be valid"); req.headers_mut().insert( "x-amz-server-side-encryption", "a".parse().expect("must be valid"), ); req.headers_mut().insert( "x-amz-server-side-encryption-customer-algorithm", "b".parse().expect("must be valid"), ); req.headers_mut().insert( "x-amz-server-side-encryption-customer-key", "c".parse().expect("must be valid"), ); req.headers_mut().insert( "x-amz-server-side-encryption-customer-key-md5", "d".parse().expect("must be valid"), ); req.headers_mut().insert( "x-amz-server-side-encryption-aws-kms-key-id", "e".parse().expect("must be valid"), ); req } fn test_get_request_with_query() -> http::Request<&'static str> { let mut req = http::Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = "http://127.0.0.1:9000/hello?list-type=2&max-keys=3&prefix=CI/&start-after=ExampleGuide.pdf" .parse() .expect("url must be valid"); req } fn test_get_request_virtual_host() -> http::Request<&'static str> { let mut req = http::Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = "http://hello.s3.test.example.com" .parse() .expect("url must be valid"); req } fn test_get_request_with_query_virtual_host() -> http::Request<&'static str> { let mut req = http::Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = "http://hello.s3.test.example.com?list-type=2&max-keys=3&prefix=CI/&start-after=ExampleGuide.pdf" .parse() .expect("url must be valid"); req } fn test_put_request() -> http::Request<&'static str> { let content = "Hello,World!"; let mut req = http::Request::new(content); *req.method_mut() = http::Method::PUT; *req.uri_mut() = "http://127.0.0.1:9000/hello" .parse() .expect("url must be valid"); req.headers_mut().insert( http::header::CONTENT_LENGTH, HeaderValue::from_str(&content.len().to_string()).expect("must be valid"), ); req } fn test_put_request_with_body_digest() -> http::Request<&'static str> { let content = "Hello,World!"; let mut req = http::Request::new(content); *req.method_mut() = http::Method::PUT; *req.uri_mut() = "http://127.0.0.1:9000/hello" .parse() .expect("url must be valid"); req.headers_mut().insert( header::CONTENT_LENGTH, HeaderValue::from_str(&content.len().to_string()).expect("must be valid"), ); let body = hex_sha256(content.as_bytes()); req.headers_mut().insert( "x-amz-content-sha256", HeaderValue::from_str(&body).expect("must be valid"), ); req } fn test_put_request_virtual_host() -> http::Request<&'static str> { let content = "Hello,World!"; let mut req = http::Request::new(content); *req.method_mut() = http::Method::PUT; *req.uri_mut() = "http://hello.s3.test.example.com" .parse() .expect("url must be valid"); req.headers_mut().insert( header::CONTENT_LENGTH, HeaderValue::from_str(&content.len().to_string()).expect("must be valid"), ); req } macro_rules! test_cases { ($($tt:tt)*) => { #[test_case::test_case(test_get_request)] #[test_case::test_case(test_get_request_with_sse)] #[test_case::test_case(test_get_request_with_query)] #[test_case::test_case(test_get_request_virtual_host)] #[test_case::test_case(test_get_request_with_query_virtual_host)] #[test_case::test_case(test_put_request)] #[test_case::test_case(test_put_request_virtual_host)] #[test_case::test_case(test_put_request_with_body_digest)] $($tt)* }; } fn compare_request(name: &str, l: &http::Request<&str>, r: &http::Request<&str>) { fn format_headers(req: &http::Request<&str>) -> Vec { let mut hs = req .headers() .iter() .map(|(k, v)| format!("{}:{}", k, v.to_str().expect("must be valid"))) .collect::>(); // Insert host if original request doesn't have it. if !hs.contains(&format!("host:{}", req.uri().authority().unwrap())) { hs.push(format!("host:{}", req.uri().authority().unwrap())) } hs.sort(); hs } assert_eq!( format_headers(l), format_headers(r), "{name} header mismatch" ); fn format_query(req: &http::Request<&str>) -> Vec { let query = req.uri().query().unwrap_or_default(); let mut query = form_urlencoded::parse(query.as_bytes()) .map(|(k, v)| format!("{}={}", &k, &v)) .collect::>(); query.sort(); query } assert_eq!(format_query(l), format_query(r), "{name} query mismatch"); } #[apply(test_cases)] #[tokio::test] async fn test_calculate(req_fn: fn() -> http::Request<&'static str>) -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); let mut req = req_fn(); let name = format!( "{} {} {:?}", req.method(), req.uri().path(), req.uri().query(), ); let now = now(); let mut ss = SigningSettings::default(); ss.percent_encoding_mode = PercentEncodingMode::Double; ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; let id = Credentials::new( "access_key_id", "secret_access_key", None, None, "hardcoded-credentials", ) .into(); let sp = v4::SigningParams::builder() .identity(&id) .region("test") .name("s3") .time(SystemTime::from(now)) .settings(ss) .build() .expect("signing params must be valid"); let mut body = SignableBody::UnsignedPayload; if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() { body = SignableBody::Bytes(req.body().as_bytes()); } let output = aws_sigv4::http_request::sign( SignableRequest::new( req.method().as_str(), req.uri().to_string(), req.headers() .iter() .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())), body, ) .unwrap(), &sp.into(), ) .expect("signing must succeed"); let (aws_sig, _) = output.into_parts(); aws_sig.apply_to_request_http1x(&mut req); let expected_req = req; let mut req = req_fn(); let loader = AwsDefaultLoader::new( Client::new(), AwsConfig { access_key_id: Some("access_key_id".to_string()), secret_access_key: Some("secret_access_key".to_string()), ..Default::default() }, ); let cred = loader.load().await?.unwrap(); let signer = Signer::new("s3", "test").time(now); signer.sign(&mut req, &cred).expect("must apply success"); let actual_req = req; compare_request(&name, &expected_req, &actual_req); Ok(()) } #[apply(test_cases)] #[tokio::test] async fn test_calculate_in_query(req_fn: fn() -> http::Request<&'static str>) -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); let mut req = req_fn(); let name = format!( "{} {} {:?}", req.method(), req.uri().path(), req.uri().query(), ); let now = now(); let mut ss = SigningSettings::default(); ss.percent_encoding_mode = PercentEncodingMode::Double; ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; ss.signature_location = SignatureLocation::QueryParams; ss.expires_in = Some(std::time::Duration::from_secs(3600)); let id = Credentials::new( "access_key_id", "secret_access_key", None, None, "hardcoded-credentials", ) .into(); let sp = v4::SigningParams::builder() .identity(&id) .region("test") .name("s3") .time(SystemTime::from(now)) .settings(ss) .build() .expect("signing params must be valid"); let mut body = SignableBody::UnsignedPayload; if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() { body = SignableBody::Bytes(req.body().as_bytes()); } let output = aws_sigv4::http_request::sign( SignableRequest::new( req.method().as_str(), req.uri().to_string(), req.headers() .iter() .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())), body, ) .unwrap(), &sp.into(), ) .expect("signing must succeed"); let (aws_sig, _) = output.into_parts(); aws_sig.apply_to_request_http1x(&mut req); let expected_req = req; let mut req = req_fn(); let loader = AwsDefaultLoader::new( Client::new(), AwsConfig { access_key_id: Some("access_key_id".to_string()), secret_access_key: Some("secret_access_key".to_string()), ..Default::default() }, ); let cred = loader.load().await?.unwrap(); let signer = Signer::new("s3", "test").time(now); signer.sign_query(&mut req, Duration::from_secs(3600), &cred)?; let actual_req = req; compare_request(&name, &expected_req, &actual_req); Ok(()) } #[apply(test_cases)] #[tokio::test] async fn test_calculate_with_token(req_fn: fn() -> http::Request<&'static str>) -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); let mut req = req_fn(); let name = format!( "{} {} {:?}", req.method(), req.uri().path(), req.uri().query(), ); let now = now(); let mut ss = SigningSettings::default(); ss.percent_encoding_mode = PercentEncodingMode::Double; ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; let id = Credentials::new( "access_key_id", "secret_access_key", Some("security_token".to_string()), None, "hardcoded-credentials", ) .into(); let sp = v4::SigningParams::builder() .identity(&id) .region("test") .name("s3") .time(SystemTime::from(now)) .settings(ss) .build() .expect("signing params must be valid"); let mut body = SignableBody::UnsignedPayload; if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() { body = SignableBody::Bytes(req.body().as_bytes()); } let output = aws_sigv4::http_request::sign( SignableRequest::new( req.method().as_str(), req.uri().to_string(), req.headers() .iter() .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())), body, ) .unwrap(), &sp.into(), ) .expect("signing must succeed"); let (aws_sig, _) = output.into_parts(); aws_sig.apply_to_request_http1x(&mut req); let expected_req = req; let mut req = req_fn(); let loader = AwsDefaultLoader::new( Client::new(), AwsConfig { access_key_id: Some("access_key_id".to_string()), secret_access_key: Some("secret_access_key".to_string()), session_token: Some("security_token".to_string()), ..Default::default() }, ); let cred = loader.load().await?.unwrap(); let signer = Signer::new("s3", "test").time(now); signer.sign(&mut req, &cred).expect("must apply success"); let actual_req = req; compare_request(&name, &expected_req, &actual_req); Ok(()) } #[apply(test_cases)] #[tokio::test] async fn test_calculate_with_token_in_query( req_fn: fn() -> http::Request<&'static str>, ) -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); let mut req = req_fn(); let name = format!( "{} {} {:?}", req.method(), req.uri().path(), req.uri().query(), ); let now = now(); let mut ss = SigningSettings::default(); ss.percent_encoding_mode = PercentEncodingMode::Double; ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; ss.signature_location = SignatureLocation::QueryParams; ss.expires_in = Some(std::time::Duration::from_secs(3600)); let id = Credentials::new( "access_key_id", "secret_access_key", Some("security_token".to_string()), None, "hardcoded-credentials", ) .into(); let sp = v4::SigningParams::builder() .identity(&id) .region("test") // .security_token("security_token") .name("s3") .time(SystemTime::from(now)) .settings(ss) .build() .expect("signing params must be valid"); let mut body = SignableBody::UnsignedPayload; if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() { body = SignableBody::Bytes(req.body().as_bytes()); } let output = aws_sigv4::http_request::sign( SignableRequest::new( req.method().as_str(), req.uri().to_string(), req.headers() .iter() .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())), body, ) .unwrap(), &sp.into(), ) .expect("signing must succeed"); let (aws_sig, _) = output.into_parts(); aws_sig.apply_to_request_http1x(&mut req); let expected_req = req; let mut req = req_fn(); let loader = AwsDefaultLoader::new( Client::new(), AwsConfig { access_key_id: Some("access_key_id".to_string()), secret_access_key: Some("secret_access_key".to_string()), session_token: Some("security_token".to_string()), ..Default::default() }, ); let cred = loader.load().await?.unwrap(); let signer = Signer::new("s3", "test").time(now); signer .sign_query(&mut req, Duration::from_secs(3600), &cred) .expect("must apply success"); let actual_req = req; compare_request(&name, &expected_req, &actual_req); Ok(()) } } reqsign-0.16.2/src/azure/constants.rs000064400000000000000000000005111046102023000156560ustar 00000000000000use percent_encoding::{AsciiSet, NON_ALPHANUMERIC}; // Headers used in azure services. pub const X_MS_DATE: &str = "x-ms-date"; pub const CONTENT_MD5: &str = "content-md5"; pub static AZURE_QUERY_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC .remove(b'-') .remove(b'.') .remove(b'_') .remove(b'/') .remove(b'~'); reqsign-0.16.2/src/azure/mod.rs000064400000000000000000000002331046102023000144220ustar 00000000000000//! Azure Storage SharedKey support //! //! Use [`azure::storage::Signer`][crate::azure::storage::Signer] mod storage; pub use storage::*; mod constants; reqsign-0.16.2/src/azure/storage/client_secret_credential.rs000064400000000000000000000052471046102023000223360ustar 00000000000000use crate::azure::storage::config::Config; use http::HeaderValue; use http::Method; use http::Request; use reqwest::Client; use serde::Deserialize; use std::str; pub async fn get_client_secret_token(config: &Config) -> anyhow::Result> { let (secret, tenant_id, client_id, authority_host) = match ( &config.client_secret, &config.tenant_id, &config.client_id, &config.authority_host, ) { (Some(client_secret), Some(tenant_id), Some(client_id), Some(authority_host)) => { (client_secret, tenant_id, client_id, authority_host) } _ => return Ok(None), }; let url = &format!("{authority_host}/{tenant_id}/oauth2/v2.0/token"); let scopes: &[&str] = &[STORAGE_TOKEN_SCOPE]; let encoded_body: String = form_urlencoded::Serializer::new(String::new()) .append_pair("client_id", client_id) .append_pair("scope", &scopes.join(" ")) .append_pair("client_secret", secret) .append_pair("grant_type", "client_credentials") .finish(); let mut req = Request::builder() .method(Method::POST) .uri(url.to_string()) .body(encoded_body)?; req.headers_mut().insert( http::header::CONTENT_TYPE.as_str(), HeaderValue::from_static("application/x-www-form-urlencoded"), ); req.headers_mut() .insert(API_VERSION, HeaderValue::from_static("2019-06-01")); let res = Client::new().execute(req.try_into()?).await?; let rsp_status = res.status(); let rsp_body = res.text().await?; if !rsp_status.is_success() { return Err(anyhow::anyhow!( "Failed to get token from client_credentials, rsp_status = {}, rsp_body = {}", rsp_status, rsp_body )); } let resp: LoginResponse = serde_json::from_str(&rsp_body)?; Ok(Some(resp)) } pub const API_VERSION: &str = "api-version"; const STORAGE_TOKEN_SCOPE: &str = "https://storage.azure.com/.default"; /// Gets an access token for the specified resource and configuration. /// /// See #[derive(Debug, Clone, Deserialize)] pub struct LoginResponse { pub expires_in: i64, pub access_token: String, } impl From for super::credential::Credential { fn from(response: LoginResponse) -> Self { super::credential::Credential::BearerToken( response.access_token, chrono::Utc::now() + chrono::TimeDelta::seconds( response.expires_in.saturating_sub(10).clamp(0, i64::MAX), ), ) } } reqsign-0.16.2/src/azure/storage/config.rs000064400000000000000000000114161046102023000165610ustar 00000000000000use std::collections::HashMap; use std::env; /// Config carries all the configuration for Azure Storage services. #[derive(Clone, Default)] #[cfg_attr(test, derive(Debug))] pub struct Config { /// `account_name` will be loaded from /// /// - this field if it's `is_some` pub account_name: Option, /// `account_key` will be loaded from /// /// - this field if it's `is_some` pub account_key: Option, /// `sas_token` will be loaded from /// /// - this field if it's `is_some` pub sas_token: Option, /// Specifies the object id associated with a user assigned managed service identity resource /// /// The values of client_id and msi_res_id are discarded /// /// This is part of use AAD(Azure Active Directory) authenticate on Azure VM pub object_id: Option, /// Specifies the application id (client id) associated with a user assigned managed service identity resource /// /// The values of object_id and msi_res_id are discarded /// - cnv value: [`AZURE_CLIENT_ID`] /// /// This is part of use AAD(Azure Active Directory) authenticate on Azure VM pub client_id: Option, /// Specifies the ARM resource id of the user assigned managed service identity resource /// /// The values of object_id and client_id are discarded /// /// This is part of use AAD(Azure Active Directory) authenticate on Azure VM pub msi_res_id: Option, /// Specifies the header that should be used to retrieve the access token. /// /// This header mitigates server-side request forgery (SSRF) attacks. /// /// This is part of use AAD(Azure Active Directory) authenticate on Azure VM pub msi_secret: Option, /// Specifies the endpoint from which the identity should be retrieved. /// /// If not specified, the default endpoint of `http://169.254.169.254/metadata/identity/oauth2/token` will be used. /// /// This is part of use AAD(Azure Active Directory) authenticate on Azure VM pub endpoint: Option, /// `federated_token_file` value will be loaded from: /// /// - this field if it's `is_some` /// - env value: [`AZURE_FEDERATED_TOKEN_FILE`] /// - profile config: `federated_token_file` pub federated_token_file: Option, /// `tenant_id` value will be loaded from: /// /// - this field if it's `is_some` /// - env value: [`AZURE_TENANT_ID`] /// - profile config: `tenant_id` pub tenant_id: Option, /// `authority_host` value will be loaded from: /// /// - this field if it's `is_some` /// - env value: [`AZURE_AUTHORITY_HOST`] /// - profile config: `authority_host` pub authority_host: Option, /// `client_secret` value will be loaded from: /// - this field if it's `is_some` /// - profile config: `client_secret` /// - env value: `AZURE_CLIENT_SECRET` pub client_secret: Option, } pub const AZURE_FEDERATED_TOKEN_FILE: &str = "AZURE_FEDERATED_TOKEN_FILE"; pub const AZURE_TENANT_ID: &str = "AZURE_TENANT_ID"; pub const AZURE_CLIENT_ID: &str = "AZURE_CLIENT_ID"; pub const AZURE_CLIENT_SECRET: &str = "AZURE_CLIENT_SECRET"; pub const AZURE_AUTHORITY_HOST: &str = "AZURE_AUTHORITY_HOST"; const AZBLOB_ENDPOINT: &str = "AZBLOB_ENDPOINT"; const AZBLOB_ACCOUNT_KEY: &str = "AZBLOB_ACCOUNT_KEY"; const AZBLOB_ACCOUNT_NAME: &str = "AZBLOB_ACCOUNT_NAME"; const AZURE_PUBLIC_CLOUD: &str = "https://login.microsoftonline.com"; impl Config { /// Load config from env. pub fn from_env(mut self) -> Self { let envs = env::vars().collect::>(); // federated_token can be loaded from both `AZURE_FEDERATED_TOKEN` and `AZURE_FEDERATED_TOKEN_FILE`. if let Some(v) = envs.get(AZURE_FEDERATED_TOKEN_FILE) { self.federated_token_file = Some(v.to_string()); } if let Some(v) = envs.get(AZURE_TENANT_ID) { self.tenant_id = Some(v.to_string()); } if let Some(v) = envs.get(AZURE_CLIENT_ID) { self.client_id = Some(v.to_string()); } if let Some(v) = envs.get(AZBLOB_ENDPOINT) { self.endpoint = Some(v.to_string()); } if let Some(v) = envs.get(AZBLOB_ACCOUNT_KEY) { self.account_key = Some(v.to_string()); } if let Some(v) = envs.get(AZBLOB_ACCOUNT_NAME) { self.account_name = Some(v.to_string()); } if let Some(v) = envs.get(AZURE_AUTHORITY_HOST) { self.authority_host = Some(v.to_string()); } else { self.authority_host = Some(AZURE_PUBLIC_CLOUD.to_string()); } if let Some(v) = envs.get(AZURE_CLIENT_SECRET) { self.client_secret = Some(v.to_string()); } self } } reqsign-0.16.2/src/azure/storage/credential.rs000064400000000000000000000032261046102023000174260ustar 00000000000000use crate::time::DateTime; /// Credential that holds the access_key and secret_key. #[derive(Clone)] #[cfg_attr(test, derive(Debug))] pub enum Credential { /// Credential via account key /// /// Refer to SharedKey(String, String), /// Credential via SAS token /// /// Refer to SharedAccessSignature(String), /// Create an Bearer Token based credential /// /// Azure Storage accepts OAuth 2.0 access tokens from the Azure AD tenant /// associated with the subscription that contains the storage account. /// /// ref: BearerToken(String, DateTime), } impl Credential { /// is current cred is valid? pub fn is_valid(&self) -> bool { if self.is_empty() { return false; } if let Credential::BearerToken(_, expires_on) = self { let buffer = chrono::TimeDelta::try_seconds(20).expect("in bounds"); if expires_on < &(chrono::Utc::now() + buffer) { return false; } }; true } fn is_empty(&self) -> bool { match self { Credential::SharedKey(account_name, account_key) => { account_name.is_empty() || account_key.is_empty() } Credential::SharedAccessSignature(sas_token) => sas_token.is_empty(), Credential::BearerToken(bearer_token, _) => bearer_token.is_empty(), } } } reqsign-0.16.2/src/azure/storage/imds_credential.rs000064400000000000000000000045731046102023000204500ustar 00000000000000use std::str; use http::HeaderValue; use http::Method; use http::Request; use reqwest::Client; use reqwest::Url; use serde::Deserialize; use super::config::Config; const MSI_API_VERSION: &str = "2019-08-01"; const MSI_ENDPOINT: &str = "http://169.254.169.254/metadata/identity/oauth2/token"; /// Gets an access token for the specified resource and configuration. /// /// See pub async fn get_access_token(resource: &str, config: &Config) -> anyhow::Result { let endpoint = config.endpoint.as_deref().unwrap_or(MSI_ENDPOINT); let mut query_items = vec![("api-version", MSI_API_VERSION), ("resource", resource)]; match ( config.object_id.as_ref(), config.client_id.as_ref(), config.msi_res_id.as_ref(), ) { (Some(object_id), None, None) => query_items.push(("object_id", object_id)), (None, Some(client_id), None) => query_items.push(("client_id", client_id)), (None, None, Some(msi_res_id)) => query_items.push(("msi_res_id", msi_res_id)), // Only one of the object_id, client_id, or msi_res_id can be specified, if you specify both, will ignore all. _ => (), }; let url = Url::parse_with_params(endpoint, &query_items)?; let mut req = Request::builder() .method(Method::GET) .uri(url.to_string()) .body("")?; req.headers_mut() .insert("metadata", HeaderValue::from_static("true")); if let Some(secret) = &config.msi_secret { req.headers_mut() .insert("x-identity-header", HeaderValue::from_str(secret)?); }; let res = Client::new().execute(req.try_into()?).await?; let rsp_status = res.status(); let rsp_body = res.text().await?; if !rsp_status.is_success() { return Err(anyhow::anyhow!("Failed to get token from IMDS endpoint")); } let token: AccessToken = serde_json::from_str(&rsp_body)?; Ok(token) } // NOTE: expires_on is a String version of unix epoch time, not an integer. // https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=dotnet#rest-protocol-examples #[derive(Debug, Clone, Deserialize)] #[allow(unused)] pub struct AccessToken { pub access_token: String, pub expires_on: String, pub token_type: String, pub resource: String, } reqsign-0.16.2/src/azure/storage/loader.rs000064400000000000000000000072351046102023000165660ustar 00000000000000use std::sync::Arc; use std::sync::Mutex; use anyhow::Result; use crate::time::{now, parse_rfc3339}; use super::credential::Credential; use super::imds_credential; use super::{config::Config, workload_identity_credential}; /// Loader will load credential from different methods. #[cfg_attr(test, derive(Debug))] pub struct Loader { config: Config, credential: Arc>>, } impl Loader { /// Create a new loader via config. pub fn new(config: Config) -> Self { Self { config, credential: Arc::default(), } } /// Load credential. pub async fn load(&self) -> Result> { // Return cached credential if it's valid. match self.credential.lock().expect("lock poisoned").clone() { Some(cred) if cred.is_valid() => return Ok(Some(cred)), _ => (), } let cred = self.load_inner().await?; let mut lock = self.credential.lock().expect("lock poisoned"); lock.clone_from(&cred); Ok(cred) } async fn load_inner(&self) -> Result> { if let Some(cred) = self.load_via_config().await? { return Ok(Some(cred)); } if let Some(cred) = self.load_via_client_secret().await? { return Ok(Some(cred)); } if let Some(cred) = self.load_via_workload_identity().await? { return Ok(Some(cred)); } // try to load credential using AAD(Azure Active Directory) authenticate on Azure VM // we may get an error if not running on Azure VM // see https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal,http#using-the-rest-protocol self.load_via_imds().await } async fn load_via_config(&self) -> Result> { if let Some(token) = &self.config.sas_token { let cred = Credential::SharedAccessSignature(token.clone()); return Ok(Some(cred)); } if let (Some(ak), Some(sk)) = (&self.config.account_name, &self.config.account_key) { let cred = Credential::SharedKey(ak.clone(), sk.clone()); return Ok(Some(cred)); } Ok(None) } async fn load_via_imds(&self) -> Result> { let token = imds_credential::get_access_token("https://storage.azure.com/", &self.config).await?; let expires_on = if token.expires_on.is_empty() { now() + chrono::TimeDelta::try_minutes(10).expect("in bounds") } else { parse_rfc3339(&token.expires_on)? }; let cred = Some(Credential::BearerToken(token.access_token, expires_on)); Ok(cred) } async fn load_via_workload_identity(&self) -> Result> { let workload_identity_token = workload_identity_credential::get_workload_identity_token(&self.config).await?; match workload_identity_token { Some(token) => { let expires_on_duration = match token.expires_on { None => now() + chrono::TimeDelta::try_minutes(10).expect("in bounds"), Some(expires_on) => parse_rfc3339(&expires_on)?, }; Ok(Some(Credential::BearerToken( token.access_token, expires_on_duration, ))) } None => Ok(None), } } async fn load_via_client_secret(&self) -> Result> { super::client_secret_credential::get_client_secret_token(&self.config) .await .map(|token| token.map(Into::into)) } } reqsign-0.16.2/src/azure/storage/mod.rs000064400000000000000000000005721046102023000160740ustar 00000000000000//! Azure Storage Singer mod signer; pub use signer::Signer as AzureStorageSigner; mod config; pub use config::Config as AzureStorageConfig; mod credential; pub use credential::Credential as AzureStorageCredential; mod imds_credential; mod workload_identity_credential; mod loader; pub use loader::Loader as AzureStorageLoader; mod client_secret_credential; mod sas; reqsign-0.16.2/src/azure/storage/sas/account_sas.rs000064400000000000000000000104451046102023000204050ustar 00000000000000use anyhow::Result; use crate::hash; use crate::time; use crate::time::DateTime; /// The default parameters that make up a SAS token /// https://learn.microsoft.com/en-us/rest/api/storageservices/create-account-sas#specify-the-account-sas-parameters const ACCOUNT_SAS_VERSION: &str = "2018-11-09"; const ACCOUNT_SAS_RESOURCE: &str = "bqtf"; const ACCOUNT_SAS_RESOURCE_TYPE: &str = "sco"; const ACCOUNT_SAS_PERMISSIONS: &str = "rwdlacu"; pub struct AccountSharedAccessSignature { account: String, key: String, version: String, resource: String, resource_type: String, permissions: String, expiry: DateTime, start: Option, ip: Option, protocol: Option, } impl AccountSharedAccessSignature { /// Create a SAS token signer with default parameters pub fn new(account: String, key: String, expiry: DateTime) -> Self { Self { account, key, expiry, start: None, ip: None, protocol: None, version: ACCOUNT_SAS_VERSION.to_string(), resource: ACCOUNT_SAS_RESOURCE.to_string(), resource_type: ACCOUNT_SAS_RESOURCE_TYPE.to_string(), permissions: ACCOUNT_SAS_PERMISSIONS.to_string(), } } // Azure documentation: https://learn.microsoft.com/en-us/rest/api/storageservices/create-account-sas#construct-the-signature-string fn signature(&self) -> Result { let string_to_sign = format!( "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", self.account, self.permissions, self.resource, self.resource_type, self.start .as_ref() .map_or("".to_string(), |v| urlencoded(time::format_rfc3339(*v))), time::format_rfc3339(self.expiry), self.ip.clone().unwrap_or_default(), self.protocol .as_ref() .map_or("".to_string(), |v| v.to_string()), self.version, ); let decode_content = hash::base64_decode(self.key.clone().as_str())?; Ok(hash::base64_hmac_sha256( &decode_content, string_to_sign.as_bytes(), )) } /// [Example](https://docs.microsoft.com/rest/api/storageservices/create-service-sas#service-sas-example) from Azure documentation. pub fn token(&self) -> Result> { let mut elements: Vec<(String, String)> = vec![ ("sv".to_string(), self.version.to_string()), ("ss".to_string(), self.resource.to_string()), ("srt".to_string(), self.resource_type.to_string()), ( "se".to_string(), urlencoded(time::format_rfc3339(self.expiry)), ), ("sp".to_string(), self.permissions.to_string()), ]; if let Some(start) = &self.start { elements.push(("st".to_string(), urlencoded(time::format_rfc3339(*start)))) } if let Some(ip) = &self.ip { elements.push(("sip".to_string(), ip.to_string())) } if let Some(protocol) = &self.protocol { elements.push(("spr".to_string(), protocol.to_string())) } let sig = AccountSharedAccessSignature::signature(self)?; elements.push(("sig".to_string(), urlencoded(sig))); Ok(elements) } } fn urlencoded(s: String) -> String { form_urlencoded::byte_serialize(s.as_bytes()).collect() } #[cfg(test)] mod tests { use std::str::FromStr; use super::*; fn test_time() -> DateTime { DateTime::from_str("2022-03-01T08:12:34Z").unwrap() } #[test] fn test_can_generate_sas_token() { let key = hash::base64_encode("key".as_bytes()); let expiry = test_time() + chrono::TimeDelta::try_minutes(5).expect("in bounds"); let sign = AccountSharedAccessSignature::new("account".to_string(), key, expiry); let token_content = sign.token().expect("token decode failed"); let token = token_content .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join("&"); assert_eq!(token, "sv=2018-11-09&ss=bqtf&srt=sco&se=2022-03-01T08%3A17%3A34Z&sp=rwdlacu&sig=jgK9nDUT0ntH%2Fp28LPs0jzwxsk91W6hePLPlfrElv4k%3D"); } } reqsign-0.16.2/src/azure/storage/sas/mod.rs000064400000000000000000000000251046102023000166530ustar 00000000000000pub mod account_sas; reqsign-0.16.2/src/azure/storage/signer.rs000064400000000000000000000254041046102023000166050ustar 00000000000000//! Azure Storage Singer use std::fmt::Debug; use std::fmt::Write; use std::time::Duration; use anyhow::anyhow; use anyhow::Result; use http::header::*; use log::debug; use percent_encoding::percent_encode; use super::super::constants::*; use super::credential::Credential; use crate::azure::storage::sas::account_sas; use crate::ctx::SigningContext; use crate::ctx::SigningMethod; use crate::hash::base64_decode; use crate::hash::base64_hmac_sha256; use crate::request::SignableRequest; use crate::time; use crate::time::format_http_date; use crate::time::DateTime; /// Singer that implement Azure Storage Shared Key Authorization. /// /// - [Authorize with Shared Key](https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key) #[derive(Debug, Default)] pub struct Signer { time: Option, } impl Signer { /// Create a signer. pub fn new() -> Self { Self::default() } /// Specify the signing time. /// /// # Note /// /// We should always take current time to sign requests. /// Only use this function for testing. #[cfg(test)] pub fn time(&mut self, time: DateTime) -> &mut Self { self.time = Some(time); self } fn build( &self, req: &mut impl SignableRequest, method: SigningMethod, cred: &Credential, ) -> Result { let mut ctx = req.build()?; match cred { Credential::SharedAccessSignature(token) => { ctx.query_append(token); return Ok(ctx); } Credential::BearerToken(token, _) => match method { SigningMethod::Query(_) => { return Err(anyhow!("BearerToken can't be used in query string")); } SigningMethod::Header => { ctx.headers .insert(X_MS_DATE, format_http_date(time::now()).parse()?); ctx.headers.insert(AUTHORIZATION, { let mut value: HeaderValue = format!("Bearer {}", token).parse()?; value.set_sensitive(true); value }); } }, Credential::SharedKey(ak, sk) => match method { SigningMethod::Query(d) => { // try sign request use account_sas token let signer = account_sas::AccountSharedAccessSignature::new( ak.to_string(), sk.to_string(), time::now() + chrono::TimeDelta::from_std(d)?, ); let signer_token = signer.token()?; signer_token.iter().for_each(|(k, v)| { ctx.query_push(k, v); }); } SigningMethod::Header => { let now = self.time.unwrap_or_else(time::now); let string_to_sign = string_to_sign(&mut ctx, ak, now)?; let decode_content = base64_decode(sk)?; let signature = base64_hmac_sha256(&decode_content, string_to_sign.as_bytes()); ctx.headers.insert(AUTHORIZATION, { let mut value: HeaderValue = format!("SharedKey {ak}:{signature}").parse()?; value.set_sensitive(true); value }); } }, } Ok(ctx) } /// Signing request. /// /// # Example /// /// ```rust,no_run /// use anyhow::Result; /// use reqsign::AzureStorageConfig; /// use reqsign::AzureStorageLoader; /// use reqsign::AzureStorageSigner; /// use reqwest::Client; /// use reqwest::Request; /// use reqwest::Url; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// let config = AzureStorageConfig { /// account_name: Some("account_name".to_string()), /// account_key: Some("YWNjb3VudF9rZXkK".to_string()), /// ..Default::default() /// }; /// let loader = AzureStorageLoader::new(config); /// let signer = AzureStorageSigner::new(); /// // Construct request /// let url = Url::parse("https://test.blob.core.windows.net/testbucket/testblob")?; /// let mut req = reqwest::Request::new(http::Method::GET, url); /// // Signing request with Signer /// let credential = loader.load().await?.unwrap(); /// signer.sign(&mut req, &credential)?; /// // Sending already signed request. /// let resp = Client::new().execute(req).await?; /// println!("resp got status: {}", resp.status()); /// Ok(()) /// } /// ``` pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> { let mut ctx = self.build(req, SigningMethod::Header, cred)?; for (_, v) in ctx.query.iter_mut() { *v = percent_encode(v.as_bytes(), &AZURE_QUERY_ENCODE_SET).to_string(); } req.apply(ctx) } /// Signing request with query. pub fn sign_query( &self, req: &mut impl SignableRequest, expire: Duration, cred: &Credential, ) -> Result<()> { let ctx = self.build(req, SigningMethod::Query(expire), cred)?; req.apply(ctx) } } /// Construct string to sign /// /// ## Format /// /// ```text /// VERB + "\n" + /// Content-Encoding + "\n" + /// Content-Language + "\n" + /// Content-Length + "\n" + /// Content-MD5 + "\n" + /// Content-Type + "\n" + /// Date + "\n" + /// If-Modified-Since + "\n" + /// If-Match + "\n" + /// If-None-Match + "\n" + /// If-Unmodified-Since + "\n" + /// Range + "\n" + /// CanonicalizedHeaders + /// CanonicalizedResource; /// ``` /// ## Note /// For sub-requests of batch API, requests should be signed without `x-ms-version` header. /// Set the `omit_service_version` to `ture` for such. /// /// ## Reference /// /// - [Blob, Queue, and File Services (Shared Key authorization)](https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key) fn string_to_sign(ctx: &mut SigningContext, ak: &str, now: DateTime) -> Result { let mut s = String::with_capacity(128); writeln!(&mut s, "{}", ctx.method.as_str())?; writeln!(&mut s, "{}", ctx.header_get_or_default(&CONTENT_ENCODING)?)?; writeln!(&mut s, "{}", ctx.header_get_or_default(&CONTENT_LANGUAGE)?)?; writeln!( &mut s, "{}", ctx.header_get_or_default(&CONTENT_LENGTH) .map(|v| if v == "0" { "" } else { v })? )?; writeln!( &mut s, "{}", ctx.header_get_or_default(&CONTENT_MD5.parse()?)? )?; writeln!(&mut s, "{}", ctx.header_get_or_default(&CONTENT_TYPE)?)?; writeln!(&mut s, "{}", ctx.header_get_or_default(&DATE)?)?; writeln!(&mut s, "{}", ctx.header_get_or_default(&IF_MODIFIED_SINCE)?)?; writeln!(&mut s, "{}", ctx.header_get_or_default(&IF_MATCH)?)?; writeln!(&mut s, "{}", ctx.header_get_or_default(&IF_NONE_MATCH)?)?; writeln!( &mut s, "{}", ctx.header_get_or_default(&IF_UNMODIFIED_SINCE)? )?; writeln!(&mut s, "{}", ctx.header_get_or_default(&RANGE)?)?; writeln!(&mut s, "{}", canonicalize_header(ctx, now)?)?; write!(&mut s, "{}", canonicalize_resource(ctx, ak))?; debug!("string to sign: {}", &s); Ok(s) } /// ## Reference /// /// - [Constructing the canonicalized headers string](https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#constructing-the-canonicalized-headers-string) fn canonicalize_header(ctx: &mut SigningContext, now: DateTime) -> Result { ctx.headers .insert(X_MS_DATE, format_http_date(now).parse()?); Ok(SigningContext::header_to_string( ctx.header_to_vec_with_prefix("x-ms-"), ":", "\n", )) } /// ## Reference /// /// - [Constructing the canonicalized resource string](https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#constructing-the-canonicalized-resource-string) fn canonicalize_resource(ctx: &mut SigningContext, ak: &str) -> String { if ctx.query.is_empty() { return format!("/{}{}", ak, ctx.path); } let query = ctx .query .iter() .map(|(k, v)| (k.to_lowercase(), v.clone())) .collect(); format!( "/{}{}\n{}", ak, ctx.path, SigningContext::query_to_percent_decoded_string(query, ":", "\n") ) } #[cfg(test)] mod tests { use std::time::Duration; use http::Request; use super::super::config::Config; use crate::AzureStorageCredential; use crate::AzureStorageSigner; use crate::{azure::storage::loader::Loader, time::now}; #[tokio::test] async fn test_sas_url() { let _ = env_logger::builder().is_test(true).try_init(); let config = Config { sas_token: Some("sv=2021-01-01&ss=b&srt=c&sp=rwdlaciytfx&se=2022-01-01T11:00:14Z&st=2022-01-02T03:00:14Z&spr=https&sig=KEllk4N8f7rJfLjQCmikL2fRVt%2B%2Bl73UBkbgH%2FK3VGE%3D".to_string()), ..Default::default() }; let loader = Loader::new(config); let cred = loader.load().await.unwrap().unwrap(); let signer = AzureStorageSigner::new(); // Construct request let mut req = Request::builder() .uri("https://test.blob.core.windows.net/testbucket/testblob") .body(()) .unwrap(); // Signing request with Signer assert!(signer .sign_query(&mut req, Duration::from_secs(1), &cred) .is_ok()); assert_eq!(req.uri(), "https://test.blob.core.windows.net/testbucket/testblob?sv=2021-01-01&ss=b&srt=c&sp=rwdlaciytfx&se=2022-01-01T11:00:14Z&st=2022-01-02T03:00:14Z&spr=https&sig=KEllk4N8f7rJfLjQCmikL2fRVt%2B%2Bl73UBkbgH%2FK3VGE%3D") } #[tokio::test] async fn test_can_sign_request_use_bearer_token() { let signer = AzureStorageSigner::new(); let mut req = Request::builder() .uri("https://test.blob.core.windows.net/testbucket/testblob") .body(()) .unwrap(); let cred = AzureStorageCredential::BearerToken("token".to_string(), now()); // Can effectively sign request with SigningMethod::Header assert!(signer.sign(&mut req, &cred).is_ok()); let authorization = req .headers() .get("Authorization") .unwrap() .to_str() .unwrap(); assert_eq!("Bearer token", authorization); // Will not sign request with SigningMethod::Query *req.headers_mut() = http::header::HeaderMap::new(); assert!(signer .sign_query(&mut req, Duration::from_secs(1), &cred) .is_err()); } } reqsign-0.16.2/src/azure/storage/workload_identity_credential.rs000064400000000000000000000050331046102023000232370ustar 00000000000000use std::{fs, str}; use http::HeaderValue; use http::Method; use http::Request; use reqwest::Client; use reqwest::Url; use serde::Deserialize; use super::config::Config; pub const API_VERSION: &str = "api-version"; const STORAGE_TOKEN_SCOPE: &str = "https://storage.azure.com/.default"; /// Gets an access token for the specified resource and configuration. /// /// See pub async fn get_workload_identity_token(config: &Config) -> anyhow::Result> { let (token_file, tenant_id, client_id, authority_host) = match ( &config.federated_token_file, &config.tenant_id, &config.client_id, &config.authority_host, ) { (Some(token_file), Some(tenant_id), Some(client_id), Some(authority_host)) => { (token_file, tenant_id, client_id, authority_host) } _ => return Ok(None), }; let token = fs::read_to_string(token_file)?; let url = Url::parse(authority_host)?.join(&format!("/{tenant_id}/oauth2/v2.0/token"))?; let scopes: &[&str] = &[STORAGE_TOKEN_SCOPE]; let encoded_body: String = form_urlencoded::Serializer::new(String::new()) .append_pair("client_id", client_id) .append_pair("scope", &scopes.join(" ")) .append_pair( "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", ) .append_pair("client_assertion", &token) .append_pair("grant_type", "client_credentials") .finish(); let mut req = Request::builder() .method(Method::POST) .uri(url.to_string()) .body(encoded_body)?; req.headers_mut().insert( http::header::CONTENT_TYPE.as_str(), HeaderValue::from_static("application/x-www-form-urlencoded"), ); req.headers_mut() .insert(API_VERSION, HeaderValue::from_static("2019-06-01")); let res = Client::new().execute(req.try_into()?).await?; let rsp_status = res.status(); let rsp_body = res.text().await?; if !rsp_status.is_success() { return Err(anyhow::anyhow!( "Failed to get token from workload identity credential, rsp_status = {}, rsp_body = {}", rsp_status, rsp_body )); } let resp: LoginResponse = serde_json::from_str(&rsp_body)?; Ok(Some(resp)) } #[derive(Debug, Clone, Deserialize)] pub struct LoginResponse { pub expires_on: Option, pub access_token: String, } reqsign-0.16.2/src/ctx.rs000064400000000000000000000117641046102023000133260ustar 00000000000000use std::borrow::Cow; use std::time::Duration; use anyhow::Result; use http::header::HeaderName; use http::uri::Authority; use http::uri::Scheme; use http::HeaderMap; use http::HeaderValue; use http::Method; pub struct SigningContext { pub method: Method, pub scheme: Scheme, pub authority: Authority, pub path: String, pub query: Vec<(String, String)>, pub headers: HeaderMap, } impl SigningContext { pub fn path_percent_decoded(&self) -> Cow { percent_encoding::percent_decode_str(&self.path).decode_utf8_lossy() } #[inline] pub fn query_size(&self) -> usize { self.query .iter() .map(|(k, v)| k.len() + v.len()) .sum::() } /// Push a new query pair into query list. #[inline] pub fn query_push(&mut self, key: impl Into, value: impl Into) { self.query.push((key.into(), value.into())); } /// Push a query string into query list. #[inline] pub fn query_append(&mut self, query: &str) { self.query.push((query.to_string(), "".to_string())); } pub fn query_to_vec_with_filter(&self, filter: impl Fn(&str) -> bool) -> Vec<(String, String)> { self.query .iter() // Filter all queries .filter(|(k, _)| filter(k)) // Clone all queries .map(|(k, v)| (k.to_string(), v.to_string())) .collect() } /// Convert sorted query to string. /// /// ```shell /// [(a, b), (c, d)] => "a:b\nc:d" /// ``` pub fn query_to_string(mut query: Vec<(String, String)>, sep: &str, join: &str) -> String { let mut s = String::with_capacity(16); // Sort via header name. query.sort(); for (idx, (k, v)) in query.into_iter().enumerate() { if idx != 0 { s.push_str(join); } s.push_str(&k); if !v.is_empty() { s.push_str(sep); s.push_str(&v); } } s } /// Convert sorted query to percent decoded string. /// /// ```shell /// [(a, b), (c, d)] => "a:b\nc:d" /// ``` pub fn query_to_percent_decoded_string( mut query: Vec<(String, String)>, sep: &str, join: &str, ) -> String { let mut s = String::with_capacity(16); // Sort via header name. query.sort(); for (idx, (k, v)) in query.into_iter().enumerate() { if idx != 0 { s.push_str(join); } s.push_str(&k); if !v.is_empty() { s.push_str(sep); s.push_str(&percent_encoding::percent_decode_str(&v).decode_utf8_lossy()); } } s } #[inline] pub fn header_get_or_default(&self, key: &HeaderName) -> Result<&str> { match self.headers.get(key) { Some(v) => Ok(v.to_str()?), None => Ok(""), } } pub fn header_value_normalize(v: &mut HeaderValue) { let bs = v.as_bytes(); let starting_index = bs.iter().position(|b| *b != b' ').unwrap_or(0); let ending_offset = bs.iter().rev().position(|b| *b != b' ').unwrap_or(0); let ending_index = bs.len() - ending_offset; // This can't fail because we started with a valid HeaderValue and then only trimmed spaces *v = HeaderValue::from_bytes(&bs[starting_index..ending_index]) .expect("invalid header value") } pub fn header_name_to_vec_sorted(&self) -> Vec<&str> { let mut h = self .headers .keys() .map(|k| k.as_str()) .collect::>(); h.sort_unstable(); h } pub fn header_to_vec_with_prefix(&self, prefix: &str) -> Vec<(String, String)> { self.headers .iter() // Filter all header that starts with prefix .filter(|(k, _)| k.as_str().starts_with(prefix)) // Convert all header name to lowercase .map(|(k, v)| { ( k.as_str().to_lowercase(), v.to_str().expect("must be valid header").to_string(), ) }) .collect() } /// Convert sorted headers to string. /// /// ```shell /// [(a, b), (c, d)] => "a:b\nc:d" /// ``` pub fn header_to_string(mut headers: Vec<(String, String)>, sep: &str, join: &str) -> String { let mut s = String::with_capacity(16); // Sort via header name. headers.sort(); for (idx, (k, v)) in headers.into_iter().enumerate() { if idx != 0 { s.push_str(join); } s.push_str(&k); s.push_str(sep); s.push_str(&v); } s } } /// SigningMethod is the method that used in signing. #[derive(Copy, Clone, PartialEq, Eq)] pub enum SigningMethod { /// Signing with header. Header, /// Signing with query. Query(Duration), } reqsign-0.16.2/src/dirs.rs000064400000000000000000000010131046102023000134530ustar 00000000000000//! Directory related utils. /// Expand `~` in input path. /// /// - If path not starts with `~/` or `~\\`, returns `Some(path)` directly. /// - Otherwise, replace `~` with home dir instead. /// - If home_dir is not found, returns `None`. #[cfg(not(target_arch = "wasm32"))] pub fn expand_homedir(path: &str) -> Option { if !path.starts_with("~/") && !path.starts_with("~\\") { Some(path.to_string()) } else { home::home_dir().map(|home| path.replace('~', &home.to_string_lossy())) } } reqsign-0.16.2/src/google/constants.rs000064400000000000000000000015231046102023000160100ustar 00000000000000use percent_encoding::AsciiSet; use percent_encoding::NON_ALPHANUMERIC; // Env values used in google services. pub const GOOGLE_APPLICATION_CREDENTIALS: &str = "GOOGLE_APPLICATION_CREDENTIALS"; /// AsciiSet for [Google UriEncode](https://cloud.google.com/storage/docs/authentication/canonical-requests) /// /// - URI encode every byte except the unreserved characters: 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'. pub static GOOG_URI_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC .remove(b'/') .remove(b'-') .remove(b'.') .remove(b'_') .remove(b'~'); /// AsciiSet for [Google UriEncode](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html) /// /// But used in query. pub static GOOG_QUERY_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC .remove(b'-') .remove(b'.') .remove(b'_') .remove(b'~'); reqsign-0.16.2/src/google/credential/external_account.rs000064400000000000000000000106621046102023000214500ustar 00000000000000//! An external account. use anyhow::bail; use anyhow::Result; pub use credential_source::CredentialSource; pub use credential_source::FileSourcedCredentials; pub use credential_source::UrlSourcedCredentials; use serde::Deserialize; use serde_json::Value; /// Credential is the file which stores service account's client_id and private key. /// /// Reference: https://google.aip.dev/auth/4117#expected-behavior. #[derive(Clone, Deserialize)] #[cfg_attr(test, derive(Debug))] #[serde(rename_all = "snake_case")] pub struct ExternalAccount { /// This is the STS audience containing the resource name for the workload /// identity pool and provider identifier. pub audience: String, /// This is the STS subject token type. pub subject_token_type: String, /// This is the URL for the service account impersonation request. /// If not present the STS access token should be used without impersonation. pub service_account_impersonation_url: Option, /// This object defines additional service account impersonation options. pub service_account_impersonation: Option, /// This is the STS token exchange endpoint. pub token_url: String, /// This object defines the mechanism used to retrieve the external credential /// from the local environment so that it can be exchanged for a GCP access /// token via the STS endpoint. pub credential_source: CredentialSource, } /// A source format type. #[derive(Clone, Debug, Default, Deserialize)] #[serde(rename_all = "snake_case", tag = "type")] pub enum FormatType { /// A raw token. #[default] Text, /// A JSON payload containing the token. Json { /// The field containing the token. subject_token_field_name: String, }, } impl FormatType { /// Parse a slice of bytes as the expected format. pub fn parse(&self, slice: &[u8]) -> Result { match &self { Self::Text => Ok(String::from_utf8(slice.to_vec())?), Self::Json { subject_token_field_name, } => { let Value::Object(mut obj) = serde_json::from_slice(slice)? else { bail!("failed to decode token JSON"); }; match obj.remove(subject_token_field_name) { Some(Value::String(access_token)) => Ok(access_token), _ => bail!("JSON missing token field {subject_token_field_name}"), } } } } } /// Extra information about the impersonation exchange. #[derive(Clone, Deserialize)] #[cfg_attr(test, derive(Debug))] #[serde(rename_all = "snake_case")] pub struct ServiceAccountImpersonation { /// The lifetime in seconds to be used when exchanging the STS token. pub token_lifetime_seconds: Option, } /// This module describes the types of credential sources an external account /// might use to generate an ID token. /// /// For reference, see . mod credential_source { use super::FormatType; use serde::Deserialize; use std::collections::HashMap; /// An instruction on how to load a token for the local environment. /// /// **NOTE:** environment and executable sources are not yet supported. #[derive(Clone, Deserialize)] #[cfg_attr(test, derive(Debug))] #[serde(untagged)] pub enum CredentialSource { /// An OIDC token provided via file. FileSourced(FileSourcedCredentials), /// An OIDC token provided via a URL. UrlSourced(UrlSourcedCredentials), } /// A file sourced OIDC token. #[derive(Clone, Deserialize)] #[cfg_attr(test, derive(Debug))] #[serde(rename_all = "snake_case")] pub struct FileSourcedCredentials { /// The file containing the token. pub file: String, /// The format of the file. #[serde(default)] pub format: FormatType, } /// A URL sourced OIDC token. Used by Azure and other OIDC providers. #[derive(Clone, Deserialize)] #[cfg_attr(test, derive(Debug))] #[serde(rename_all = "snake_case")] pub struct UrlSourcedCredentials { /// The URL to where the POST request is made. pub url: String, /// The headers to be injected in the request. #[serde(default)] pub headers: HashMap, /// The format of the response payload. #[serde(default)] pub format: FormatType, } } reqsign-0.16.2/src/google/credential/impersonated_service_account.rs000064400000000000000000000007751046102023000240440ustar 00000000000000//! An impersonated service account. #[derive(Clone, serde::Deserialize)] #[cfg_attr(test, derive(Debug))] #[serde(rename_all = "snake_case")] pub struct ImpersonatedServiceAccount { pub delegates: Vec, pub service_account_impersonation_url: String, pub source_credentials: SourceCredentials, } #[derive(Clone, serde::Deserialize)] #[cfg_attr(test, derive(Debug))] pub struct SourceCredentials { pub client_id: String, pub client_secret: String, pub refresh_token: String, } reqsign-0.16.2/src/google/credential/service_account.rs000064400000000000000000000005711046102023000212640ustar 00000000000000//! A service account. /// Credential is the file which stores service account's client_id and private key. #[derive(Clone, serde::Deserialize)] #[cfg_attr(test, derive(Debug))] #[serde(rename_all = "snake_case")] pub struct ServiceAccount { /// Private key of credential pub private_key: String, /// The client email of credential pub client_email: String, } reqsign-0.16.2/src/google/credential.rs000064400000000000000000000306371046102023000161160ustar 00000000000000pub mod external_account; pub mod impersonated_service_account; pub mod service_account; #[cfg(not(target_arch = "wasm32"))] use std::env; use std::sync::Arc; use std::sync::Mutex; use anyhow::anyhow; use anyhow::Result; use log::debug; pub use self::external_account::ExternalAccount; use self::impersonated_service_account::ImpersonatedServiceAccount; pub use self::service_account::ServiceAccount; use super::constants::GOOGLE_APPLICATION_CREDENTIALS; use crate::hash::base64_decode; #[derive(Clone, serde::Deserialize)] #[cfg_attr(test, derive(Debug))] #[serde(rename_all = "snake_case")] #[allow(clippy::enum_variant_names)] pub enum CredentialType { ImpersonatedServiceAccount, ExternalAccount, ServiceAccount, } /// A Google API credential file. #[derive(Clone, Default)] #[cfg_attr(test, derive(Debug))] pub struct Credential { pub(crate) service_account: Option, pub(crate) impersonated_service_account: Option, pub(crate) external_account: Option, } impl Credential { /// Deserialize credential file pub fn from_slice(v: &[u8]) -> Result { let service_account = serde_json::from_slice(v).ok(); let impersonated_service_account = serde_json::from_slice(v).ok(); let external_account = serde_json::from_slice(v).ok(); let cred = Credential { service_account, impersonated_service_account, external_account, }; if cred.service_account.is_none() && cred.impersonated_service_account.is_none() && cred.external_account.is_none() { return Err(anyhow!("Couldn't deserialize credential file")); } Ok(cred) } } /// CredentialLoader will load credential from different methods. #[derive(Default)] #[cfg_attr(test, derive(Debug))] pub struct CredentialLoader { path: Option, content: Option, disable_env: bool, disable_well_known_location: bool, credential: Arc>>, } impl CredentialLoader { /// Disable load from env. pub fn with_disable_env(mut self) -> Self { self.disable_env = true; self } /// Disable load from well known location. pub fn with_disable_well_known_location(mut self) -> Self { self.disable_well_known_location = true; self } /// Set credential path. pub fn with_path(mut self, path: &str) -> Self { self.path = Some(path.to_string()); self } /// Set credential content. pub fn with_content(mut self, content: &str) -> Self { self.content = Some(content.to_string()); self } /// Load credential from pre-configured methods. pub fn load(&self) -> Result> { // Return cached credential if it has been loaded at least once. if let Some(cred) = self.credential.lock().expect("lock poisoned").clone() { return Ok(Some(cred)); } let cred = if let Some(cred) = self.load_inner()? { cred } else { return Ok(None); }; let mut lock = self.credential.lock().expect("lock poisoned"); *lock = Some(cred.clone()); Ok(Some(cred)) } fn load_inner(&self) -> Result> { if let Ok(Some(cred)) = self.load_via_content() { return Ok(Some(cred)); } #[cfg(not(target_arch = "wasm32"))] if let Ok(Some(cred)) = self.load_via_path() { return Ok(Some(cred)); } #[cfg(not(target_arch = "wasm32"))] if let Ok(Some(cred)) = self.load_via_env() { return Ok(Some(cred)); } #[cfg(not(target_arch = "wasm32"))] if let Ok(Some(cred)) = self.load_via_well_known_location() { return Ok(Some(cred)); } Ok(None) } #[cfg(not(target_arch = "wasm32"))] fn load_via_path(&self) -> Result> { let path = if let Some(path) = &self.path { path } else { return Ok(None); }; Ok(Some(Self::load_file(path)?)) } /// Build credential loader from given base64 content. fn load_via_content(&self) -> Result> { let content = if let Some(content) = &self.content { content } else { return Ok(None); }; let decode_content = base64_decode(content)?; let cred = Credential::from_slice(&decode_content).map_err(|err| { debug!("load credential from content failed: {err:?}"); err })?; Ok(Some(cred)) } /// Load from env GOOGLE_APPLICATION_CREDENTIALS. #[cfg(not(target_arch = "wasm32"))] fn load_via_env(&self) -> Result> { if self.disable_env { return Ok(None); } if let Ok(cred_path) = env::var(GOOGLE_APPLICATION_CREDENTIALS) { let cred = Self::load_file(&cred_path)?; Ok(Some(cred)) } else { Ok(None) } } /// Load from well known locations: /// /// - `$HOME/.config/gcloud/application_default_credentials.json` /// - `%APPDATA%\gcloud\application_default_credentials.json` #[cfg(not(target_arch = "wasm32"))] fn load_via_well_known_location(&self) -> Result> { if self.disable_well_known_location { return Ok(None); } let config_dir = if let Ok(v) = env::var("APPDATA") { v } else if let Ok(v) = env::var("XDG_CONFIG_HOME") { v } else if let Ok(v) = env::var("HOME") { format!("{v}/.config") } else { // User's env doesn't have a config dir. return Ok(None); }; let cred = Self::load_file(&format!( "{config_dir}/gcloud/application_default_credentials.json" ))?; Ok(Some(cred)) } /// Build credential loader from given path. fn load_file(path: &str) -> Result { let content = std::fs::read(path).map_err(|err| { debug!("load credential failed at reading file: {err:?}"); err })?; let account = Credential::from_slice(&content).map_err(|err| { debug!("load credential failed at serde_json: {err:?}"); err })?; Ok(account) } } #[cfg(test)] mod tests { use log::warn; use super::external_account::CredentialSource; use super::external_account::FormatType; use super::*; #[test] fn loader_returns_service_account() { temp_env::with_vars( vec![( GOOGLE_APPLICATION_CREDENTIALS, Some(format!( "{}/testdata/services/google/test_credential.json", env::current_dir() .expect("current_dir must exist") .to_string_lossy() )), )], || { let cred_loader = CredentialLoader::default(); let cred = cred_loader .load() .expect("credential must exist") .unwrap() .service_account .expect("couldn't deserialize service account"); assert_eq!("test-234@test.iam.gserviceaccount.com", &cred.client_email); assert_eq!( "-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDOy4jaJIcVlffi5ENtlNhJ0tsI1zt21BI3DMGtPq7n3Ymow24w BV2Z73l4dsqwRo2QVSwnCQ2bVtM2DgckMNDShfWfKe3LRcl96nnn51AtAYIfRnc+ ogstzxZi4J64f7IR3KIAFxJnzo+a6FS6MmsYMAs8/Oj68fRmCD0AbAs5ZwIDAQAB AoGAVpPkMeBFJgZph/alPEWq4A2FYogp/y/+iEmw9IVf2PdpYNyhTz2P2JjoNEUX ywFe12SxXY5uwfBx8RmiZ8aARkIBWs7q9Sz6f/4fdCHAuu3GAv5hmMO4dLQsGcKl XAQW4QxZM5/x5IXlDh4KdcUP65P0ZNS3deqDlsq/vVfY9EECQQD9I/6KNmlSrbnf Fa/5ybF+IV8mOkEfkslQT4a9pWbA1FF53Vk4e7B+Faow3uUGHYs/HUwrd3vIVP84 S+4Jeuc3AkEA0SGF5l3BrWWTok1Wr/UE+oPOUp2L4AV6kH8co11ZyxSQkRloLdMd bNzNXShuhwgvNjvgkseNSeQPJKxFRn73UQJACacMtrJ6c6eiNcp66lhxhzC4kxmX kB+lw4U0yxh6gZHXBYGWPFwjD7u9wJ1POFt6Cs8QL3wf4TS0gq4KhpwEIwJACIA8 WSjmfo3qemZ6Z5ymHyjMcj9FOE4AtW71Uw6wX7juR3eo7HPwdkRjdK34EDUc9i9o 6Y6DB8Xld7ApALyYgQJBAPTMFpKpCRNvYH5VrdObid5+T7OwDrJFHGWdbDGiT++O V08rl535r74rMilnQ37X1/zaKBYyxpfhnd2XXgoCgTM= -----END RSA PRIVATE KEY----- ", &cred.private_key ); }, ); } #[test] fn loader_returns_impersonated_service_account() { temp_env::with_vars( vec![( GOOGLE_APPLICATION_CREDENTIALS, Some(format!( "{}/testdata/services/google/test_impersonated_service_account.json", env::current_dir() .expect("current_dir must exist") .to_string_lossy() )), )], || { let cred_loader = CredentialLoader::default(); let cred = cred_loader .load() .expect("credential must exist") .unwrap() .impersonated_service_account .expect("couldn't deserialize impersonated service account"); assert_eq!("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/example-01-iam@example-01.iam.gserviceaccount.com:generateAccessToken", &cred.service_account_impersonation_url); assert_eq!("placeholder_client_id", &cred.source_credentials.client_id); assert_eq!( "placeholder_client_secret", &cred.source_credentials.client_secret ); assert_eq!( "placeholder_refresh_token", &cred.source_credentials.refresh_token ); }, ); } #[test] fn loader_returns_external_account() { temp_env::with_vars( vec![( GOOGLE_APPLICATION_CREDENTIALS, Some(format!( "{}/testdata/services/google/test_external_account.json", env::current_dir() .expect("current_dir must exist") .to_string_lossy() )), )], || { let cred_loader = CredentialLoader::default(); let cred = cred_loader .load() .expect("credential must exist") .unwrap() .external_account .expect("couldn't deserialize external account"); assert_eq!( "//iam.googleapis.com/projects/000000000000/locations/global/workloadIdentityPools/reqsign/providers/reqsign-provider", &cred.audience ); assert_eq!( "urn:ietf:params:oauth:token-type:jwt", &cred.subject_token_type ); assert_eq!( "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-234@test.iam.gserviceaccount.com:generateAccessToken", &cred.service_account_impersonation_url.unwrap() ); assert_eq!("https://sts.googleapis.com/v1/token", &cred.token_url); let CredentialSource::UrlSourced(source) = cred.credential_source else { panic!("expected URL credential source"); }; assert_eq!("http://localhost:5000/token", &source.url); assert!(matches!(&source.format, FormatType::Json { .. })); }, ); } #[test] fn loader_returns_external_account_from_github_oidc() { let path = if let Ok(path) = env::var("REQSIGN_GOOGLE_CREDENTIAL_PATH") { path } else { warn!("REQSIGN_GOOGLE_CREDENTIAL_PATH is not set, ignore"); return; }; let cred_loader = CredentialLoader::default().with_path(&path); let cred: ExternalAccount = cred_loader .load() .expect("credential must exist") .unwrap() .external_account .expect("couldn't deserialize external account from Github OIDC"); assert_eq!( "urn:ietf:params:oauth:token-type:jwt", &cred.subject_token_type ); assert_eq!("https://sts.googleapis.com/v1/token", &cred.token_url); } } reqsign-0.16.2/src/google/mod.rs000064400000000000000000000006401046102023000145520ustar 00000000000000//! Google Service Signer mod constants; mod credential; pub(crate) use credential::external_account; pub use credential::Credential as GoogleCredential; pub use credential::CredentialLoader as GoogleCredentialLoader; mod token; pub use token::Token as GoogleToken; pub use token::TokenLoad as GoogleTokenLoad; pub use token::TokenLoader as GoogleTokenLoader; mod signer; pub use signer::Signer as GoogleSigner; reqsign-0.16.2/src/google/signer.rs000064400000000000000000000320761046102023000152720ustar 00000000000000use std::borrow::Cow; use std::time::Duration; use anyhow::Result; use http::header; use log::debug; use percent_encoding::percent_decode_str; use percent_encoding::utf8_percent_encode; use rsa::pkcs1v15::SigningKey; use rsa::pkcs8::DecodePrivateKey; use rsa::signature::RandomizedSigner; use super::constants::GOOG_QUERY_ENCODE_SET; use super::credential::Credential; use super::credential::ServiceAccount; use super::token::Token; use crate::ctx::SigningContext; use crate::ctx::SigningMethod; use crate::hash::hex_sha256; use crate::request::SignableRequest; use crate::time; use crate::time::format_date; use crate::time::format_iso8601; use crate::time::DateTime; /// Singer that implement Google OAuth2 Authentication. /// /// ## Reference /// /// - [Authenticating as a service account](https://cloud.google.com/docs/authentication/production) pub struct Signer { service: String, region: String, time: Option, } impl Signer { /// Create a builder of Signer. pub fn new(service: &str) -> Self { Self { service: service.to_string(), region: "auto".to_string(), time: None, } } /// Set the region name that used for google v4 signing. /// /// Default to `auto` pub fn region(&mut self, region: &str) -> &mut Self { self.region = region.to_string(); self } /// Specify the signing time. /// /// # Note /// /// We should always take current time to sign requests. /// Only use this function for testing. #[cfg(test)] pub fn time(mut self, time: DateTime) -> Self { self.time = Some(time); self } fn build_header( &self, req: &mut impl SignableRequest, token: &Token, ) -> Result { let mut ctx = req.build()?; ctx.headers.insert(header::AUTHORIZATION, { let mut value: http::HeaderValue = format!("Bearer {}", token.access_token()).parse()?; value.set_sensitive(true); value }); Ok(ctx) } fn build_query( &self, req: &mut impl SignableRequest, expire: Duration, cred: &ServiceAccount, ) -> Result { let mut ctx = req.build()?; let now = self.time.unwrap_or_else(time::now); // canonicalize context canonicalize_header(&mut ctx)?; canonicalize_query( &mut ctx, SigningMethod::Query(expire), cred, now, &self.service, &self.region, )?; // build canonical request and string to sign. let creq = canonical_request_string(&mut ctx)?; let encoded_req = hex_sha256(creq.as_bytes()); // Scope: "20220313///goog4_request" let scope = format!( "{}/{}/{}/goog4_request", format_date(now), self.region, self.service ); debug!("calculated scope: {scope}"); // StringToSign: // // GOOG4-RSA-SHA256 // 20220313T072004Z // 20220313///goog4_request // let string_to_sign = { let mut f = String::new(); f.push_str("GOOG4-RSA-SHA256"); f.push('\n'); f.push_str(&format_iso8601(now)); f.push('\n'); f.push_str(&scope); f.push('\n'); f.push_str(&encoded_req); f }; debug!("calculated string to sign: {string_to_sign}"); let mut rng = rand::thread_rng(); let private_key = rsa::RsaPrivateKey::from_pkcs8_pem(&cred.private_key)?; let signing_key = SigningKey::::new(private_key); let signature = signing_key.sign_with_rng(&mut rng, string_to_sign.as_bytes()); ctx.query .push(("X-Goog-Signature".to_string(), signature.to_string())); Ok(ctx) } /// Signing request. /// /// # Example /// /// ```rust,no_run /// use anyhow::Result; /// use reqsign::GoogleSigner; /// use reqsign::GoogleTokenLoader; /// use reqwest::Client; /// use reqwest::Request; /// use reqwest::Url; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// // Signer will load region and credentials from environment by default. /// let token_loader = GoogleTokenLoader::new( /// "https://www.googleapis.com/auth/devstorage.read_only", /// Client::new(), /// ); /// let signer = GoogleSigner::new("storage"); /// /// // Construct request /// let url = Url::parse("https://storage.googleapis.com/storage/v1/b/test")?; /// let mut req = reqwest::Request::new(http::Method::GET, url); /// /// // Signing request with Signer /// let token = token_loader.load().await?.unwrap(); /// signer.sign(&mut req, &token)?; /// /// // Sending already signed request. /// let resp = Client::new().execute(req).await?; /// println!("resp got status: {}", resp.status()); /// Ok(()) /// } /// ``` /// /// # TODO /// /// we can also send API via signed JWT: [Addendum: Service account authorization without OAuth](https://developers.google.com/identity/protocols/oauth2/service-account#jwt-auth) pub fn sign(&self, req: &mut impl SignableRequest, token: &Token) -> Result<()> { let ctx = self.build_header(req, token)?; req.apply(ctx) } /// Sign the query with a duration. /// /// # Example /// ```rust,no_run /// use std::time::Duration; /// /// use anyhow::Result; /// use reqsign::GoogleCredentialLoader; /// use reqsign::GoogleSigner; /// use reqwest::Client; /// use reqwest::Url; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// // Signer will load region and credentials from environment by default. /// let credential_loader = GoogleCredentialLoader::default(); /// let signer = GoogleSigner::new("stroage"); /// /// // Construct request /// let url = Url::parse("https://storage.googleapis.com/testbucket-reqsign/CONTRIBUTING.md")?; /// let mut req = reqwest::Request::new(http::Method::GET, url); /// /// // Signing request with Signer /// let credential = credential_loader.load()?.unwrap(); /// signer.sign_query(&mut req, Duration::from_secs(3600), &credential)?; /// /// println!("signed request: {:?}", req); /// // Sending already signed request. /// let resp = Client::new().execute(req).await?; /// println!("resp got status: {}", resp.status()); /// println!("resp got body: {}", resp.text().await?); /// Ok(()) /// } /// ``` pub fn sign_query( &self, req: &mut impl SignableRequest, duration: Duration, cred: &Credential, ) -> Result<()> { let Some(cred) = &cred.service_account else { anyhow::bail!("expected service account credential, got external account"); }; let ctx = self.build_query(req, duration, cred)?; req.apply(ctx) } } fn canonical_request_string(ctx: &mut SigningContext) -> Result { // 256 is specially chosen to avoid reallocation for most requests. let mut f = String::with_capacity(256); // Insert method f.push_str(ctx.method.as_str()); f.push('\n'); // Insert encoded path let path = percent_decode_str(&ctx.path).decode_utf8()?; f.push_str(&Cow::from(utf8_percent_encode( &path, &super::constants::GOOG_URI_ENCODE_SET, ))); f.push('\n'); // Insert query f.push_str(&SigningContext::query_to_string( ctx.query.clone(), "=", "&", )); f.push('\n'); // Insert signed headers let signed_headers = ctx.header_name_to_vec_sorted(); for header in signed_headers.iter() { let value = &ctx.headers[*header]; f.push_str(header); f.push(':'); f.push_str(value.to_str().expect("header value must be valid")); f.push('\n'); } f.push('\n'); f.push_str(&signed_headers.join(";")); f.push('\n'); f.push_str("UNSIGNED-PAYLOAD"); debug!("string to sign: {}", f); Ok(f) } fn canonicalize_header(ctx: &mut SigningContext) -> Result<()> { for (_, value) in ctx.headers.iter_mut() { SigningContext::header_value_normalize(value) } // Insert HOST header if not present. if ctx.headers.get(header::HOST).is_none() { ctx.headers .insert(header::HOST, ctx.authority.as_str().parse()?); } Ok(()) } fn canonicalize_query( ctx: &mut SigningContext, method: SigningMethod, cred: &ServiceAccount, now: DateTime, service: &str, region: &str, ) -> Result<()> { if let SigningMethod::Query(expire) = method { ctx.query .push(("X-Goog-Algorithm".into(), "GOOG4-RSA-SHA256".into())); ctx.query.push(( "X-Goog-Credential".into(), format!( "{}/{}/{}/{}/goog4_request", &cred.client_email, format_date(now), region, service ), )); ctx.query.push(("X-Goog-Date".into(), format_iso8601(now))); ctx.query .push(("X-Goog-Expires".into(), expire.as_secs().to_string())); ctx.query.push(( "X-Goog-SignedHeaders".into(), ctx.header_name_to_vec_sorted().join(";"), )); } // Return if query is empty. if ctx.query.is_empty() { return Ok(()); } // Sort by param name ctx.query.sort(); ctx.query = ctx .query .iter() .map(|(k, v)| { ( utf8_percent_encode(k, &GOOG_QUERY_ENCODE_SET).to_string(), utf8_percent_encode(v, &GOOG_QUERY_ENCODE_SET).to_string(), ) }) .collect(); Ok(()) } #[cfg(test)] mod tests { use chrono::Utc; use pretty_assertions::assert_eq; use super::super::credential::CredentialLoader; use super::*; #[tokio::test] async fn test_sign_query() -> Result<()> { let credential_path = format!( "{}/testdata/services/google/testbucket_credential.json", std::env::current_dir() .expect("current_dir must exist") .to_string_lossy() ); let loader = CredentialLoader::default().with_path(&credential_path); let cred = loader.load()?.unwrap(); let signer = Signer::new("storage"); let mut req = http::Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = "https://storage.googleapis.com/testbucket-reqsign/CONTRIBUTING.md" .parse() .expect("url must be valid"); signer.sign_query(&mut req, Duration::from_secs(3600), &cred)?; let query = req.uri().query().unwrap(); assert!(query.contains("X-Goog-Algorithm=GOOG4-RSA-SHA256")); assert!(query.contains("X-Goog-Credential")); Ok(()) } #[tokio::test] async fn test_sign_query_deterministic() -> Result<()> { let credential_path = format!( "{}/testdata/services/google/testbucket_credential.json", std::env::current_dir() .expect("current_dir must exist") .to_string_lossy() ); let loader = CredentialLoader::default().with_path(&credential_path); let cred = loader.load()?.unwrap(); let mut req = http::Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = "https://storage.googleapis.com/testbucket-reqsign/CONTRIBUTING.md" .parse() .expect("url must be valid"); let time_offset = chrono::DateTime::parse_from_rfc2822("Mon, 15 Aug 2022 16:50:12 GMT") .unwrap() .with_timezone(&Utc); let signer = Signer::new("storage").time(time_offset); signer.sign_query(&mut req, Duration::from_secs(3600), &cred)?; let query = req.uri().query().unwrap(); assert!(query.contains("X-Goog-Algorithm=GOOG4-RSA-SHA256")); assert!(query.contains("X-Goog-Credential")); assert_eq!(query, "X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=testbucket-reqsign-account%40iam-testbucket-reqsign-project.iam.gserviceaccount.com%2F20220815%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220815T165012Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&X-Goog-Signature=9F423139DB223D818F2D4D6BCA4916DD1EE5AEB8E72D99EC60E8B903DC3CF0586C27A0F821C8CB20C6BB76C776E63134DAFF5957E7862BB89926F18E0D3618E4EE40EF8DBEC64D87F5AD4CAF6FE4C2BC3239E1076A33BE3113D6E0D1AF263C16FA5E1C9590C8F8E4E2CA2FED11533607B5AFE84B53E2E00CB320E0BC853C138EBBDCFEC3E9219C73551478EE12AABBD2576686F887738A21DC5AE00DFF3D481BD08F642342C8CCB476E74C8FEA0C02BA6FEFD61300218D6E216EAD4B59F3351E456601DF38D1CC1B4CE639D2748739933672A08B5FEBBED01B5BC0785E81A865EE0252A0C5AE239061F3F5DB4AFD8CC676646750C762A277FBFDE70A85DFDF33"); Ok(()) } } reqsign-0.16.2/src/google/token/external_account.rs000064400000000000000000000126011046102023000204510ustar 00000000000000use std::time::Duration; use anyhow::bail; use anyhow::Result; use http::header::ACCEPT; use http::header::CONTENT_TYPE; use log::error; use serde::Deserialize; use super::Token; use super::TokenLoader; use crate::google::credential::external_account::CredentialSource; use crate::google::credential::ExternalAccount; /// The maximum impersonated token lifetime allowed, 1 hour. const MAX_LIFETIME: Duration = Duration::from_secs(3600); #[derive(Clone, Deserialize, Default)] #[cfg_attr(test, derive(Debug))] #[serde(default, rename_all = "camelCase")] struct ImpersonatedToken { access_token: String, expire_time: String, } // As documented in https://google.aip.dev/auth/4117 async fn load_security_token( cred: &ExternalAccount, oidc_token: &str, client: &reqwest::Client, ) -> Result { // As documented in https://cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token. let req = serde_json::json!({ "grantType": "urn:ietf:params:oauth:grant-type:token-exchange", "requestedTokenType": "urn:ietf:params:oauth:token-type:access_token", "audience": &cred.audience, "scope": "https://www.googleapis.com/auth/cloud-platform", "subjectToken": oidc_token, "subjectTokenType": &cred.subject_token_type, }); let req = serde_json::to_vec(&req)?; let resp = client .post(&cred.token_url) .header(ACCEPT, "application/json") .header(CONTENT_TYPE, "application/json") .body(req) .send() .await?; if !resp.status().is_success() { error!("exchange token got unexpected response: {:?}", resp); bail!("exchange token failed: {}", resp.text().await?); } let token = serde_json::from_slice(&resp.bytes().await?)?; Ok(token) } async fn load_impersonated_token( cred: &ExternalAccount, access_token: &str, scope: &str, client: &reqwest::Client, ) -> Result> { let Some(url) = &cred.service_account_impersonation_url else { return Ok(None); }; let lifetime = cred .service_account_impersonation .as_ref() .and_then(|s| s.token_lifetime_seconds) .unwrap_or(MAX_LIFETIME.as_secs() as usize); let req = serde_json::json!({ "scope": [scope], "lifetime": format!("{lifetime}s"), }); let req = serde_json::to_vec(&req)?; let resp = client .post(url) .header(ACCEPT, "application/json") .header(CONTENT_TYPE, "application/json") .bearer_auth(access_token) .body(req) .send() .await?; if !resp.status().is_success() { error!("impersonated token got unexpected response: {:?}", resp); bail!("exchange impersonated token failed: {}", resp.text().await?); } let token: ImpersonatedToken = serde_json::from_slice(&resp.bytes().await?)?; Ok(Some(Token::new(&token.access_token, lifetime, scope))) } impl TokenLoader { /// Exchange token via Google's External Account Credentials. /// /// Reference: [External Account Credentials (Workload Identity Federation)](https://google.aip.dev/auth/4117) pub(super) async fn load_via_external_account(&self) -> Result> { let Some(cred) = self .credential .as_ref() .and_then(|cred| cred.external_account.as_ref()) else { return Ok(None); }; let oidc_token = credential_source::load_oidc_token(&cred.credential_source, &self.client).await?; let sts = load_security_token(cred, &oidc_token, &self.client).await?; let token = load_impersonated_token(cred, sts.access_token(), &self.scope, &self.client) .await? .unwrap_or(sts); Ok(Some(token)) } } mod credential_source { use std::io::Read; use http::header::HeaderName; use http::HeaderMap; use http::HeaderValue; use super::*; use crate::external_account::FileSourcedCredentials; use crate::external_account::UrlSourcedCredentials; pub(super) async fn load_oidc_token( source: &CredentialSource, client: &reqwest::Client, ) -> Result { match source { CredentialSource::FileSourced(source) => load_file_sourced_oidc_token(source), CredentialSource::UrlSourced(source) => { load_url_sourced_oidc_token(source, client).await } } } async fn load_url_sourced_oidc_token( source: &UrlSourcedCredentials, client: &reqwest::Client, ) -> Result { let headers: HeaderMap = source .headers .iter() .map(|(key, value)| Ok((HeaderName::try_from(key)?, HeaderValue::try_from(value)?))) .collect::>()?; let resp = client.get(&source.url).headers(headers).send().await?; if !resp.status().is_success() { error!("exchange token got unexpected response: {:?}", resp); bail!("exchange OIDC token failed: {}", resp.text().await?); } let body = resp.bytes().await?; source.format.parse(&body) } fn load_file_sourced_oidc_token(source: &FileSourcedCredentials) -> Result { let mut file = std::fs::OpenOptions::new().read(true).open(&source.file)?; let mut buf = Vec::new(); file.read_to_end(&mut buf)?; source.format.parse(&buf) } } reqsign-0.16.2/src/google/token/impersonated_service_account.rs000064400000000000000000000066651046102023000230560ustar 00000000000000use std::time::Duration; use anyhow::bail; use anyhow::Result; use http::header::CONTENT_TYPE; use log::error; use serde::Deserialize; use crate::google::credential::impersonated_service_account::ImpersonatedServiceAccount; use super::Token; use super::TokenLoader; #[derive(Clone, Deserialize, Default)] #[cfg_attr(test, derive(Debug))] #[serde(default, rename_all = "camelCase")] struct ImpersonatedToken { access_token: String, expire_time: String, } /// The maximum impersonated token lifetime allowed, 1 hour. const MAX_LIFETIME: Duration = Duration::from_secs(3600); impl TokenLoader { pub(super) async fn load_via_impersonated_service_account(&self) -> Result> { let Some(cred) = self .credential .as_ref() .and_then(|cred| cred.impersonated_service_account.as_ref()) else { return Ok(None); }; let bearer_auth_token = self.generate_bearer_auth_token(cred).await?; self.generate_access_token(cred, bearer_auth_token) .await .map(Some) } async fn generate_bearer_auth_token(&self, cred: &ImpersonatedServiceAccount) -> Result { let req = serde_json::json!({ "grant_type": "refresh_token", "refresh_token": &cred.source_credentials.refresh_token, "client_id": &cred.source_credentials.client_id, "client_secret": &cred.source_credentials.client_secret, }); let req = serde_json::to_vec(&req)?; let resp = self .client .post("https://oauth2.googleapis.com/token") .header(CONTENT_TYPE, "application/json") .body(req) .send() .await?; if !resp.status().is_success() { error!("bearer token loader for impersonated service account got unexpected response: {:?}", resp); bail!( "bearer token loader for impersonated service account failed: {}", resp.text().await? ); } let token: Option = serde_json::from_slice(&resp.bytes().await?)?; let token = token.expect("couldn't parse bearer token response"); Ok(token) } async fn generate_access_token( &self, cred: &ImpersonatedServiceAccount, temp_token: Token, ) -> Result { let req = serde_json::json!({ "lifetime": format!("{}s", MAX_LIFETIME.as_secs()), "scope": &temp_token.scope.split(' ').collect::>(), "delegates": &cred.delegates, }); let req = serde_json::to_vec(&req)?; let resp = self .client .post(&cred.service_account_impersonation_url) .header(CONTENT_TYPE, "application/json") .bearer_auth(temp_token.access_token) .body(req) .send() .await?; if !resp.status().is_success() { error!("access token loader for impersonated service account got unexpected response: {:?}", resp); bail!( "access token loader for impersonated service account failed: {}", resp.text().await? ); } let token: Option = serde_json::from_slice(&resp.bytes().await?)?; let token = token.expect("couldn't parse access token response"); Ok(Token::new(&token.access_token, 3600, &temp_token.scope)) } } reqsign-0.16.2/src/google/token/service_account.rs000064400000000000000000000031361046102023000202720ustar 00000000000000use anyhow::bail; use anyhow::Result; use http::header; use jsonwebtoken::Algorithm; use jsonwebtoken::EncodingKey; use jsonwebtoken::Header; use log::error; use super::Claims; use super::Token; use super::TokenLoader; impl TokenLoader { /// Exchange token via Google OAuth2 Service. /// /// Reference: [Using OAuth 2.0 for Server to Server Applications](https://developers.google.com/identity/protocols/oauth2/service-account#authorizingrequests) pub(super) async fn load_via_service_account(&self) -> Result> { let Some(cred) = self .credential .as_ref() .and_then(|cred| cred.service_account.as_ref()) else { return Ok(None); }; let jwt = jsonwebtoken::encode( &Header::new(Algorithm::RS256), &Claims::new(&cred.client_email, &self.scope), &EncodingKey::from_rsa_pem(cred.private_key.as_bytes())?, )?; let resp = self .client .post("https://oauth2.googleapis.com/token") .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") .form(&[ ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"), ("assertion", &jwt), ]) .send() .await?; if !resp.status().is_success() { error!("exchange token got unexpected response: {:?}", resp); bail!("exchange token failed: {}", resp.text().await?); } let token = serde_json::from_slice(&resp.bytes().await?)?; Ok(Some(token)) } } reqsign-0.16.2/src/google/token.rs000064400000000000000000000166241046102023000151240ustar 00000000000000mod external_account; mod impersonated_service_account; mod service_account; use std::fmt::Debug; use std::fmt::Formatter; use std::sync::Arc; use std::sync::Mutex; use anyhow::Result; use async_trait::async_trait; use reqwest::Client; use serde::Deserialize; use serde::Serialize; use super::credential::Credential; use crate::time::now; use crate::time::DateTime; /// Token is the authentication methods used by google services. /// /// Most of the time, they will be exchanged via application credentials. #[derive(Clone, Deserialize, Default)] #[serde(default)] pub struct Token { access_token: String, scope: String, token_type: String, expires_in: usize, } impl Token { /// Create a new token. /// /// scope will looks like: `https://www.googleapis.com/auth/devstorage.read_only`. pub fn new(access_token: &str, expires_in: usize, scope: &str) -> Self { Self { access_token: access_token.to_string(), scope: scope.to_string(), expires_in, token_type: "Bearer".to_string(), } } /// Notes: don't allow get token from reqsign. pub(crate) fn access_token(&self) -> &str { &self.access_token } /// Notes: don't allow get expires_in from reqsign. pub(crate) fn expires_in(&self) -> usize { self.expires_in } } /// Make sure `access_token` is redacted for Token impl Debug for Token { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Token") .field("access_token", &"") .field("scope", &self.scope) .field("token_type", &self.token_type) .field("expires_in", &self.expires_in) .finish() } } /// Claims is used to build JWT for google cloud. /// /// ```json /// { /// "iss": "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com", /// "scope": "https://www.googleapis.com/auth/devstorage.read_only", /// "aud": "https://oauth2.googleapis.com/token", /// "exp": 1328554385, /// "iat": 1328550785 /// } /// ``` #[derive(Debug, Serialize)] pub struct Claims { iss: String, scope: String, aud: String, exp: u64, iat: u64, } impl Claims { pub fn new(client_email: &str, scope: &str) -> Claims { let current = now().timestamp() as u64; Claims { iss: client_email.to_string(), scope: scope.to_string(), aud: "https://oauth2.googleapis.com/token".to_string(), exp: current + 3600, iat: current, } } } /// Loader trait will try to load credential from different sources. #[async_trait] pub trait TokenLoad: 'static + Send + Sync + Debug { /// Load credential from sources. /// /// - If succeed, return `Ok(Some(cred))` /// - If not found, return `Ok(None)` /// - If unexpected errors happened, return `Err(err)` async fn load(&self, client: Client) -> Result>; } /// TokenLoader will load token from different methods. #[cfg_attr(test, derive(Debug))] pub struct TokenLoader { scope: String, client: Client, credential: Option, disable_vm_metadata: bool, service_account: Option, customized_token_loader: Option>, token: Arc>>, } impl TokenLoader { /// Create a new token loader. /// /// ## Scope /// /// For example, valid scopes for google cloud services should be /// /// - read-only: `https://www.googleapis.com/auth/devstorage.read_only` /// - read-write: `https://www.googleapis.com/auth/devstorage.read_write` /// - full-control: `https://www.googleapis.com/auth/devstorage.full_control` /// /// Reference: [Cloud Storage authentication](https://cloud.google.com/storage/docs/authentication) pub fn new(scope: &str, client: Client) -> Self { Self { scope: scope.to_string(), client, credential: None, disable_vm_metadata: false, service_account: None, customized_token_loader: None, token: Arc::default(), } } /// Set the credential for token loader. pub fn with_credentials(mut self, credentials: Credential) -> Self { self.credential = Some(credentials); self } /// Disable vm metadata. pub fn with_disable_vm_metadata(mut self, disable_vm_metadata: bool) -> Self { self.disable_vm_metadata = disable_vm_metadata; self } /// Set the service account for token loader. pub fn with_service_account(mut self, service_account: &str) -> Self { self.service_account = Some(service_account.to_string()); self } /// Set the customized token loader for token loader. pub fn with_customized_token_loader( mut self, customized_token_loader: Box, ) -> Self { self.customized_token_loader = Some(customized_token_loader); self } /// Load token from different sources. pub async fn load(&self) -> Result> { match self.token.lock().expect("lock poisoned").clone() { Some((token, expire_in)) if now() < expire_in - chrono::TimeDelta::try_seconds(2 * 60).expect("in bounds") => { return Ok(Some(token)) } _ => (), } let token = if let Some(token) = self.load_inner().await? { token } else { return Ok(None); }; let expire_in = now() + chrono::TimeDelta::try_seconds(token.expires_in() as i64).expect("in bounds"); let mut lock = self.token.lock().expect("lock poisoned"); *lock = Some((token.clone(), expire_in)); Ok(Some(token)) } async fn load_inner(&self) -> Result> { if let Some(token) = self.load_via_customized_token_loader().await? { return Ok(Some(token)); } if let Some(token) = self.load_via_service_account().await? { return Ok(Some(token)); } if let Some(token) = self.load_via_impersonated_service_account().await? { return Ok(Some(token)); } if let Some(token) = self.load_via_external_account().await? { return Ok(Some(token)); } if let Some(token) = self.load_via_vm_metadata().await? { return Ok(Some(token)); } Ok(None) } async fn load_via_customized_token_loader(&self) -> Result> { match &self.customized_token_loader { Some(f) => f.load(self.client.clone()).await, None => Ok(None), } } /// Exchange token via vm metadata async fn load_via_vm_metadata(&self) -> Result> { if self.disable_vm_metadata { return Ok(None); } // Use `default` if service account not set by user. let service_account = self.service_account.as_deref().unwrap_or("default"); let url = format!("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/{service_account}/token?scopes={}", self.scope); let resp = self .client .get(&url) .header("Metadata-Flavor", "Google") .send() .await?; let token: Token = serde_json::from_slice(&resp.bytes().await?)?; Ok(Some(token)) } } reqsign-0.16.2/src/hash.rs000064400000000000000000000047501046102023000134500ustar 00000000000000//! Hash related utils. use anyhow::anyhow; use anyhow::Result; use base64::prelude::BASE64_STANDARD; use base64::Engine; use hmac::Hmac; use hmac::Mac; use sha1::Sha1; use sha2::Digest; use sha2::Sha256; /// Base64 encode pub fn base64_encode(content: &[u8]) -> String { BASE64_STANDARD.encode(content) } // Base64 decode pub fn base64_decode(content: &str) -> Result> { BASE64_STANDARD .decode(content) .map_err(|e| anyhow!("base64 decode failed for {e:?}")) } /// SHA256 hash. #[allow(dead_code)] pub fn sha256(content: &[u8]) -> Vec { Sha256::digest(content).as_slice().to_vec() } /// Hex encoded SHA1 hash. /// /// Use this function instead of `hex::encode(sha1(content))` can reduce /// extra copy. pub fn hex_sha1(content: &[u8]) -> String { hex::encode(Sha1::digest(content).as_slice()) } /// Hex encoded SHA256 hash. /// /// Use this function instead of `hex::encode(sha256(content))` can reduce /// extra copy. pub fn hex_sha256(content: &[u8]) -> String { hex::encode(Sha256::digest(content).as_slice()) } /// HMAC with SHA256 hash. pub fn hmac_sha256(key: &[u8], content: &[u8]) -> Vec { let mut h = Hmac::::new_from_slice(key).expect("invalid key length"); h.update(content); h.finalize().into_bytes().to_vec() } /// Base64 encoded HMAC with SHA256 hash. pub fn base64_hmac_sha256(key: &[u8], content: &[u8]) -> String { let mut h = Hmac::::new_from_slice(key).expect("invalid key length"); h.update(content); base64_encode(&h.finalize().into_bytes()) } /// Hex encoded HMAC with SHA1 hash. /// /// Use this function instead of `hex::encode(hmac_sha1(key, content))` can /// reduce extra copy. pub fn hex_hmac_sha1(key: &[u8], content: &[u8]) -> String { let mut h = Hmac::::new_from_slice(key).expect("invalid key length"); h.update(content); hex::encode(h.finalize().into_bytes()) } /// Hex encoded HMAC with SHA256 hash. /// /// Use this function instead of `hex::encode(hmac_sha256(key, content))` can /// reduce extra copy. pub fn hex_hmac_sha256(key: &[u8], content: &[u8]) -> String { let mut h = Hmac::::new_from_slice(key).expect("invalid key length"); h.update(content); hex::encode(h.finalize().into_bytes()) } /// Base64 encoded HMAC with SHA1 hash. pub fn base64_hmac_sha1(key: &[u8], content: &[u8]) -> String { let mut h = Hmac::::new_from_slice(key).expect("invalid key length"); h.update(content); base64_encode(&h.finalize().into_bytes()) } reqsign-0.16.2/src/huaweicloud/constants.rs000064400000000000000000000006101046102023000170410ustar 00000000000000// Headers used in huawei cloud services. pub const CONTENT_MD5: &str = "Content-MD5"; // different from azure // Env values used in huawei cloud services. pub const HUAWEI_CLOUD_ACCESS_KEY_ID: &str = "HUAWEI_CLOUD_ACCESS_KEY_ID"; pub const HUAWEI_CLOUD_SECRET_ACCESS_KEY: &str = "HUAWEI_CLOUD_SECRET_ACCESS_KEY"; pub const HUAWEI_CLOUD_SECURITY_TOKEN: &str = "HUAWEI_CLOUD_SECURITY_TOKEN"; reqsign-0.16.2/src/huaweicloud/mod.rs000064400000000000000000000003141046102023000156050ustar 00000000000000//! Huawei Cloud signers //! Currently only support Object Storage Service (OBS) singer. //! //! Use [`huaweicloud::obs::Signer`][crate::huaweicloud::obs::Signer] mod obs; pub use obs::*; mod constants; reqsign-0.16.2/src/huaweicloud/obs/config.rs000064400000000000000000000025231046102023000170620ustar 00000000000000use std::collections::HashMap; use std::env; use super::super::constants::*; /// Config carries all the configuration for Huawei Cloud OBS services. #[derive(Clone, Default)] #[cfg_attr(test, derive(Debug))] pub struct Config { /// `access_key_id` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`HUAWEI_CLOUD_ACCESS_KEY_ID`] pub access_key_id: Option, /// `secret_access_key` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`HUAWEI_CLOUD_SECRET_ACCESS_KEY`] pub secret_access_key: Option, /// `security_token` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`HUAWEI_CLOUD_SECURITY_TOKEN`] pub security_token: Option, } impl Config { /// Load config from env. pub fn from_env(mut self) -> Self { let envs = env::vars().collect::>(); if let Some(v) = envs.get(HUAWEI_CLOUD_ACCESS_KEY_ID) { self.access_key_id.get_or_insert(v.clone()); } if let Some(v) = envs.get(HUAWEI_CLOUD_SECRET_ACCESS_KEY) { self.secret_access_key.get_or_insert(v.clone()); } if let Some(v) = envs.get(HUAWEI_CLOUD_SECURITY_TOKEN) { self.security_token.get_or_insert(v.clone()); } self } } reqsign-0.16.2/src/huaweicloud/obs/credential.rs000064400000000000000000000057451046102023000177400ustar 00000000000000use std::sync::Arc; use std::sync::Mutex; use anyhow::Result; use super::config::Config; /// Credential for obs. #[derive(Clone)] #[cfg_attr(test, derive(Debug))] pub struct Credential { /// Access key id for obs pub access_key_id: String, /// Secret access key for obs pub secret_access_key: String, /// security_token for obs. pub security_token: Option, } /// CredentialLoader will load credential from different methods. #[derive(Default)] #[cfg_attr(test, derive(Debug))] pub struct CredentialLoader { config: Config, credential: Arc>>, } impl CredentialLoader { /// Create a new loader via config. pub fn new(config: Config) -> Self { Self { config, credential: Arc::default(), } } /// Load credential pub async fn load(&self) -> Result> { // Return cached credential if it's valid. if let Some(cred) = self.credential.lock().expect("lock poisoned").clone() { return Ok(Some(cred)); } let cred = self.load_inner().await?; let mut lock = self.credential.lock().expect("lock poisoned"); lock.clone_from(&cred); Ok(cred) } async fn load_inner(&self) -> Result> { if let Some(cred) = self.load_via_config()? { return Ok(Some(cred)); } Ok(None) } fn load_via_config(&self) -> Result> { if let (Some(ak), Some(sk)) = (&self.config.access_key_id, &self.config.secret_access_key) { let cred = Credential { access_key_id: ak.clone(), secret_access_key: sk.clone(), security_token: self.config.security_token.clone(), }; return Ok(Some(cred)); } Ok(None) } } #[cfg(test)] mod tests { use once_cell::sync::Lazy; use tokio::runtime::Runtime; use super::super::super::constants::*; use super::*; static RUNTIME: Lazy = Lazy::new(|| { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .expect("Should create a tokio runtime") }); #[test] fn test_credential_env_loader_with_env() { let _ = env_logger::builder().is_test(true).try_init(); temp_env::with_vars( vec![ (HUAWEI_CLOUD_ACCESS_KEY_ID, Some("access_key_id")), (HUAWEI_CLOUD_SECRET_ACCESS_KEY, Some("secret_access_key")), ], || { RUNTIME.block_on(async { let l = CredentialLoader::new(Config::default().from_env()); let x = l.load().await.expect("load must succeed"); let x = x.expect("must load succeed"); assert_eq!("access_key_id", x.access_key_id); assert_eq!("secret_access_key", x.secret_access_key); }) }, ); } } reqsign-0.16.2/src/huaweicloud/obs/mod.rs000064400000000000000000000004701046102023000163730ustar 00000000000000//! Signers for huaweicloud obs service. mod signer; pub use signer::Signer as HuaweicloudObsSigner; mod config; pub use config::Config as HuaweicloudObsConfig; mod credential; pub use credential::Credential as HuaweicloudObsCredential; pub use credential::CredentialLoader as HuaweicloudObsCredentialLoader; reqsign-0.16.2/src/huaweicloud/obs/signer.rs000064400000000000000000000325241046102023000171100ustar 00000000000000//! Huawei Cloud Object Storage Service (OBS) signer use std::collections::HashSet; use std::fmt::Debug; use std::fmt::Write; use std::time::Duration; use anyhow::Result; use http::header::AUTHORIZATION; use http::header::CONTENT_TYPE; use http::header::DATE; use http::HeaderValue; use log::debug; use once_cell::sync::Lazy; use percent_encoding::utf8_percent_encode; use super::super::constants::*; use super::credential::Credential; use crate::ctx::SigningContext; use crate::ctx::SigningMethod; use crate::hash::base64_hmac_sha1; use crate::request::SignableRequest; use crate::time::format_http_date; use crate::time::now; use crate::time::DateTime; /// Singer that implement Huawei Cloud Object Storage Service Authorization. /// /// - [User Signature Authentication](https://support.huaweicloud.com/intl/en-us/api-obs/obs_04_0009.html) #[derive(Debug)] pub struct Signer { bucket: String, time: Option, } impl Signer { /// Create a builder. pub fn new(bucket: &str) -> Self { Self { bucket: bucket.to_string(), time: None, } } /// Specify the signing time. /// /// # Note /// /// We should always take current time to sign requests. /// Only use this function for testing. #[cfg(test)] pub fn with_time(mut self, time: DateTime) -> Self { self.time = Some(time); self } fn build( &self, req: &mut impl SignableRequest, method: SigningMethod, cred: &Credential, ) -> Result { let now = self.time.unwrap_or_else(now); let mut ctx = req.build()?; let string_to_sign = string_to_sign(&mut ctx, cred, now, method, &self.bucket)?; let signature = base64_hmac_sha1(cred.secret_access_key.as_bytes(), string_to_sign.as_bytes()); match method { SigningMethod::Header => { ctx.headers.insert(DATE, format_http_date(now).parse()?); ctx.headers.insert(AUTHORIZATION, { let mut value: HeaderValue = format!("OBS {}:{}", cred.access_key_id, signature).parse()?; value.set_sensitive(true); value }); } SigningMethod::Query(expire) => { ctx.headers.insert(DATE, format_http_date(now).parse()?); ctx.query_push("AccessKeyId", &cred.access_key_id); ctx.query_push( "Expires", (now + chrono::TimeDelta::from_std(expire).unwrap()) .timestamp() .to_string(), ); ctx.query_push( "Signature", utf8_percent_encode(&signature, percent_encoding::NON_ALPHANUMERIC).to_string(), ) } } Ok(ctx) } /// Signing request. /// /// # Example /// /// ```no_run /// use anyhow::Result; /// use reqsign::HuaweicloudObsConfig; /// use reqsign::HuaweicloudObsCredentialLoader; /// use reqsign::HuaweicloudObsSigner; /// use reqwest::Client; /// use reqwest::Request; /// use reqwest::Url; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// let loader = HuaweicloudObsCredentialLoader::new(HuaweicloudObsConfig::default()); /// let signer = HuaweicloudObsSigner::new("bucket"); /// /// // Construct request /// let url = Url::parse("https://bucket.obs.cn-north-4.myhuaweicloud.com/object.txt")?; /// let mut req = Request::new(http::Method::GET, url); /// // Signing request with Signer /// let credential = loader.load().await?.unwrap(); /// signer.sign(&mut req, &credential)?; /// // Sending already signed request. /// let resp = Client::new().execute(req).await?; /// println!("resp got status: {}", resp.status()); /// Ok(()) /// } /// ``` pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> { let ctx = self.build(req, SigningMethod::Header, cred)?; req.apply(ctx) } /// Signing request with query. pub fn sign_query( &self, req: &mut impl SignableRequest, expire: Duration, cred: &Credential, ) -> Result<()> { let ctx = self.build(req, SigningMethod::Query(expire), cred)?; req.apply(ctx) } } /// Construct string to sign /// /// ## Format /// /// ```text /// VERB + "\n" + /// Content-MD5 + "\n" + /// Content-Type + "\n" + /// Date + "\n" + /// CanonicalizedHeaders + /// CanonicalizedResource; /// ``` /// /// ## Reference /// /// - [User Signature Authentication (OBS)](https://support.huaweicloud.com/intl/en-us/api-obs/obs_04_0009.html) fn string_to_sign( ctx: &mut SigningContext, cred: &Credential, now: DateTime, method: SigningMethod, bucket: &str, ) -> Result { let mut s = String::new(); s.write_str(ctx.method.as_str())?; s.write_str("\n")?; s.write_str(ctx.header_get_or_default(&CONTENT_MD5.parse()?)?)?; s.write_str("\n")?; s.write_str(ctx.header_get_or_default(&CONTENT_TYPE)?)?; s.write_str("\n")?; match method { SigningMethod::Header => { writeln!(&mut s, "{}", format_http_date(now))?; } SigningMethod::Query(expires) => { writeln!( &mut s, "{}", (now + chrono::TimeDelta::from_std(expires).unwrap()).timestamp() )?; } } { let headers = canonicalize_header(ctx, method, cred)?; if !headers.is_empty() { writeln!(&mut s, "{headers}",)?; } } write!( &mut s, "{}", canonicalize_resource(ctx, bucket, method, cred) )?; debug!("string to sign: {}", &s); Ok(s) } /// ## Reference /// /// - [Authentication of Signature in a Header](https://support.huaweicloud.com/intl/en-us/api-obs/obs_04_0010.html) fn canonicalize_header( ctx: &mut SigningContext, method: SigningMethod, cred: &Credential, ) -> Result { if method == SigningMethod::Header { // Insert security token if let Some(token) = &cred.security_token { ctx.headers.insert("x-obs-security-token", token.parse()?); } } Ok(SigningContext::header_to_string( ctx.header_to_vec_with_prefix("x-obs-"), ":", "\n", )) } /// ## Reference /// /// - [Authentication of Signature in a Header](https://support.huaweicloud.com/intl/en-us/api-obs/obs_04_0010.html) fn canonicalize_resource( ctx: &mut SigningContext, bucket: &str, method: SigningMethod, cred: &Credential, ) -> String { if let SigningMethod::Query(_) = method { // Insert security token if let Some(token) = &cred.security_token { ctx.query .push(("security-token".to_string(), token.to_string())); }; } let params = ctx.query_to_vec_with_filter(is_sub_resource); let params_str = SigningContext::query_to_string(params, "=", "&"); if params_str.is_empty() { format!("/{bucket}{}", ctx.path) } else { format!("/{bucket}{}?{params_str}", ctx.path) } } fn is_sub_resource(param: &str) -> bool { SUBRESOURCES.contains(param) } // Please attention: the subsources are case sensitive. static SUBRESOURCES: Lazy> = Lazy::new(|| { HashSet::from([ "CDNNotifyConfiguration", "acl", "append", "attname", "backtosource", "cors", "customdomain", "delete", "deletebucket", "directcoldaccess", "encryption", "inventory", "length", "lifecycle", "location", "logging", "metadata", "modify", "name", "notification", "partNumber", "policy", "position", "quota", "rename", "replication", "response-cache-control", "response-content-disposition", "response-content-encoding", "response-content-language", "response-content-type", "response-expires", "restore", "storageClass", "storagePolicy", "storageinfo", "tagging", "torrent", "truncate", "uploadId", "uploads", "versionId", "versioning", "versions", "website", "x-image-process", "x-image-save-bucket", "x-image-save-object", "x-obs-security-token", ]) }); #[cfg(test)] mod tests { use std::str::FromStr; use anyhow::Result; use chrono::Utc; use http::header::HeaderName; use http::Uri; use super::super::config::Config; use super::super::credential::CredentialLoader; use super::*; #[tokio::test] async fn test_sign() -> Result<()> { let config = Config { access_key_id: Some("access_key".to_string()), secret_access_key: Some("123456".to_string()), ..Default::default() }; let loader = CredentialLoader::new(config); let cred = loader.load().await?.unwrap(); let signer = Signer::new("bucket").with_time( chrono::DateTime::parse_from_rfc2822("Mon, 15 Aug 2022 16:50:12 GMT") .unwrap() .with_timezone(&Utc), ); let get_req = "http://bucket.obs.cn-north-4.myhuaweicloud.com/object.txt"; let mut req = http::Request::get(Uri::from_str(get_req)?).body(())?; req.headers_mut().insert( HeaderName::from_str("Content-MD5")?, HeaderValue::from_str("abc")?, ); req.headers_mut().insert( HeaderName::from_str("Content-Type")?, HeaderValue::from_str("text/plain")?, ); // Signing request with Signer signer.sign(&mut req, &cred)?; let headers = req.headers(); let auth = headers.get("Authorization").unwrap(); // calculated from Huaweicloud OBS Signature tool // https://obs-community.obs.cn-north-1.myhuaweicloud.com/sign/header_signature.html assert_eq!( "OBS access_key:9gUZ4ol2W19LyYcc92Bu3U0V09E=", auth.to_str()?, ); Ok(()) } #[tokio::test] async fn test_sign_with_subresource() -> Result<()> { let config = Config { access_key_id: Some("access_key".to_string()), secret_access_key: Some("123456".to_string()), ..Default::default() }; let loader = CredentialLoader::new(config); let cred = loader.load().await?.unwrap(); let signer = Signer::new("bucket").with_time( chrono::DateTime::parse_from_rfc2822("Mon, 15 Aug 2022 16:50:12 GMT") .unwrap() .with_timezone(&Utc), ); let get_req = "http://bucket.obs.cn-north-4.myhuaweicloud.com/object.txt?name=hello&abc=def"; let mut req = http::Request::get(Uri::from_str(get_req)?).body(())?; req.headers_mut().insert( HeaderName::from_str("Content-MD5")?, HeaderValue::from_str("abc")?, ); req.headers_mut().insert( HeaderName::from_str("Content-Type")?, HeaderValue::from_str("text/plain")?, ); // Signing request with Signer signer.sign(&mut req, &cred)?; let headers = req.headers(); let auth = headers.get("Authorization").unwrap(); // calculated from Huaweicloud OBS Signature tool // https://obs-community.obs.cn-north-1.myhuaweicloud.com/sign/header_signature.html // CanonicalizedResource: /bucket/object.txt?name=hello assert_eq!( "OBS access_key:EaTKiO1Qh5KFUvWAVvbCNGktJUY=", auth.to_str()?, ); Ok(()) } #[tokio::test] async fn test_sign_list_objects() -> Result<()> { let config = Config { access_key_id: Some("access_key".to_string()), secret_access_key: Some("123456".to_string()), ..Default::default() }; let loader = CredentialLoader::new(config); let cred = loader.load().await?.unwrap(); let signer = Signer::new("bucket").with_time( chrono::DateTime::parse_from_rfc2822("Mon, 15 Aug 2022 16:50:12 GMT") .unwrap() .with_timezone(&Utc), ); let get_req = "http://bucket.obs.cn-north-4.myhuaweicloud.com?name=hello&abc=def"; let mut req = http::Request::get(Uri::from_str(get_req)?).body(())?; req.headers_mut().insert( HeaderName::from_str("Content-MD5")?, HeaderValue::from_str("abc")?, ); req.headers_mut().insert( HeaderName::from_str("Content-Type")?, HeaderValue::from_str("text/plain")?, ); // Signing request with Signer signer.sign(&mut req, &cred)?; let headers = req.headers(); let auth = headers.get("Authorization").unwrap(); // calculated from Huaweicloud OBS Signature tool // https://obs-community.obs.cn-north-1.myhuaweicloud.com/sign/header_signature.html // CanonicalizedResource: /bucket/?name=hello assert_eq!( "OBS access_key:9OdOsf8PRdhGhpkp7IIbKE0kRvA=", auth.to_str()?, ); Ok(()) } } reqsign-0.16.2/src/lib.rs000064400000000000000000000053201046102023000132650ustar 00000000000000//! Signing API requests without effort. //! //! # Example //! //! ```rust,no_run //! use anyhow::Result; //! use reqsign::AwsConfig; //! use reqsign::AwsDefaultLoader; //! use reqsign::AwsV4Signer; //! use reqwest::Client; //! use reqwest::Request; //! use reqwest::Url; //! //! #[tokio::main] //! async fn main() -> Result<()> { //! // Signer can load region and credentials from environment by default. //! let client = Client::new(); //! let config = AwsConfig::default().from_profile().from_env(); //! let loader = AwsDefaultLoader::new(client.clone(), config); //! let signer = AwsV4Signer::new("s3", "us-east-1"); //! // Construct request //! let url = Url::parse("https://s3.amazonaws.com/testbucket")?; //! let mut req = reqwest::Request::new(http::Method::GET, url); //! // Signing request with Signer //! let credential = loader.load().await?.unwrap(); //! signer.sign(&mut req, &credential)?; //! // Sending already signed request. //! let resp = client.execute(req).await?; //! println!("resp got status: {}", resp.status()); //! Ok(()) //! } //! ``` //! //! # Available Services //! //! - [Aliyun OSS][crate::AliyunOssSigner] for Aliyun OSS. //! - [AWS SigV4][crate::AwsV4Signer] for AWS services like S3. //! - [Azure Storage][crate::AzureStorageSigner] for Azure Storage services like Azure Blob Service. //! - [Google][crate::GoogleSigner] for All google cloud services like Google Cloud Storage Service. //! - [Huawei Cloud OBS][crate::HuaweicloudObsSigner] for Huawei Cloud Object Storage Service (OBS). //! //! # Features //! //! reqsign support [`http::Request`] by default. Other request types support are hided //! under feature gates to reduce dependencies. //! //! - `reqwest_request`: Enable to support signing [`reqwest::Request`] //! - `reqwest_blocking_request`: Enable to support signing [`reqwest::blocking::Request`] // Make sure all our public APIs have docs. #![warn(missing_docs)] #[cfg(feature = "services-aliyun")] mod aliyun; #[cfg(feature = "services-aliyun")] pub use aliyun::*; #[cfg(feature = "services-aws")] mod aws; #[cfg(feature = "services-aws")] pub use aws::*; #[cfg(feature = "services-azblob")] mod azure; #[cfg(feature = "services-azblob")] pub use azure::*; #[cfg(feature = "services-google")] mod google; #[cfg(feature = "services-google")] pub use google::*; #[cfg(feature = "services-huaweicloud")] mod huaweicloud; #[cfg(feature = "services-huaweicloud")] pub use huaweicloud::*; #[cfg(feature = "services-oracle")] mod oracle; #[cfg(feature = "services-oracle")] pub use oracle::*; #[cfg(feature = "services-tencent")] mod tencent; #[cfg(feature = "services-tencent")] pub use tencent::*; mod ctx; mod dirs; mod hash; mod request; mod time; reqsign-0.16.2/src/oracle/config.rs000064400000000000000000000016121046102023000152310ustar 00000000000000use anyhow::Result; use serde::Deserialize; use std::fs::read_to_string; use toml::from_str; /// Config carries all the configuration for Oracle services. /// will be loaded from default config file ~/.oci/config #[derive(Clone, Default, Deserialize)] #[cfg_attr(test, derive(Debug))] pub struct Config { /// userID for Oracle Cloud Infrastructure. pub user: String, /// tenancyID for Oracle Cloud Infrastructure. pub tenancy: String, /// region for Oracle Cloud Infrastructure. pub region: String, /// private key file for Oracle Cloud Infrastructure. pub key_file: Option, /// fingerprint for the key_file. pub fingerprint: Option, } impl Config { /// Load config from env. pub fn from_config(path: &str) -> Result { let content = read_to_string(path)?; let config = from_str(&content)?; Ok(config) } } reqsign-0.16.2/src/oracle/constants.rs000064400000000000000000000001621046102023000157770ustar 00000000000000// Env values used in oracle cloud infrastructure services. pub const ORACLE_CONFIG_PATH: &str = "~/.oci/config"; reqsign-0.16.2/src/oracle/credential.rs000064400000000000000000000050421046102023000160770ustar 00000000000000use std::sync::Arc; use std::sync::Mutex; use anyhow::Result; use log::debug; use super::config::Config; use super::constants::ORACLE_CONFIG_PATH; use crate::time::now; use crate::time::DateTime; /// Credential that holds the API private key. /// private_key_path is optional, because some other credential will be added later #[derive(Default, Clone)] #[cfg_attr(test, derive(Debug))] pub struct Credential { /// TenantID for Oracle Cloud Infrastructure. pub tenancy: String, /// UserID for Oracle Cloud Infrastructure. pub user: String, /// API Private Key for credential. pub key_file: Option, /// Fingerprint of the API Key. pub fingerprint: Option, /// expires in for credential. pub expires_in: Option, } impl Credential { /// is current cred is valid? pub fn is_valid(&self) -> bool { self.key_file.is_some() && self.fingerprint.is_some() && self.expires_in.unwrap_or_default() > now() } } /// Loader will load credential from different methods. #[derive(Default)] #[cfg_attr(test, derive(Debug))] pub struct Loader { credential: Arc>>, } impl Loader { /// Load credential. pub async fn load(&self) -> Result> { // Return cached credential if it's valid. match self.credential.lock().expect("lock poisoned").clone() { Some(cred) if cred.is_valid() => return Ok(Some(cred)), _ => (), } let cred = if let Some(cred) = self.load_inner().await? { cred } else { return Ok(None); }; let mut lock = self.credential.lock().expect("lock poisoned"); *lock = Some(cred.clone()); Ok(Some(cred)) } async fn load_inner(&self) -> Result> { if let Ok(Some(cred)) = self .load_via_config() .map_err(|err| debug!("load credential via static failed: {err:?}")) { return Ok(Some(cred)); } Ok(None) } fn load_via_config(&self) -> Result> { let config = Config::from_config(ORACLE_CONFIG_PATH)?; Ok(Some(Credential { tenancy: config.tenancy, user: config.user, key_file: config.key_file, fingerprint: config.fingerprint, // Set expires_in to 10 minutes to enforce re-read // from file. expires_in: Some(now() + chrono::TimeDelta::try_minutes(10).expect("in bounds")), })) } } reqsign-0.16.2/src/oracle/mod.rs000064400000000000000000000004301046102023000145400ustar 00000000000000//! Oracle Cloud Infrastructure service signer //! mod oci; pub use oci::APIKeySigner as OCIAPIKeySigner; mod config; pub use config::Config as OCIConfig; mod credential; pub use credential::Credential as OCICredential; pub use credential::Loader as OCILoader; mod constants; reqsign-0.16.2/src/oracle/oci.rs000064400000000000000000000057301046102023000145430ustar 00000000000000//! Oracle Cloud Infrastructure Singer use anyhow::{Error, Result}; use base64::{engine::general_purpose, Engine as _}; use http::{ header::{AUTHORIZATION, DATE}, HeaderValue, }; use log::debug; use rsa::pkcs1v15::SigningKey; use rsa::sha2::Sha256; use rsa::signature::{SignatureEncoding, Signer}; use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; use std::fmt::Write; use super::credential::Credential; use crate::ctx::SigningContext; use crate::request::SignableRequest; use crate::time; use crate::time::DateTime; /// Singer for Oracle Cloud Infrastructure using API Key. #[derive(Default)] pub struct APIKeySigner {} impl APIKeySigner { /// Building a signing context. fn build(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result { let now = time::now(); let mut ctx = req.build()?; let string_to_sign = string_to_sign(&mut ctx, now)?; let private_key = if let Some(path) = &cred.key_file { RsaPrivateKey::read_pkcs8_pem_file(path)? } else { return Err(Error::msg("no private key")); }; let signing_key = SigningKey::::new(private_key); let signature = signing_key.try_sign(string_to_sign.as_bytes())?; let encoded_signature = general_purpose::STANDARD.encode(signature.to_bytes()); ctx.headers .insert(DATE, HeaderValue::from_str(&time::format_http_date(now))?); if let Some(fp) = &cred.fingerprint { let mut auth_value = String::new(); write!(auth_value, "Signature version=\"1\",")?; write!(auth_value, "headers=\"date (request-target) host\",")?; write!( auth_value, "keyId=\"{}/{}/{}\",", cred.tenancy, cred.user, &fp )?; write!(auth_value, "algorithm=\"rsa-sha256\",")?; write!(auth_value, "signature=\"{}\"", encoded_signature)?; ctx.headers .insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?); } else { return Err(Error::msg("no fingerprint")); } Ok(ctx) } /// Signing request with header. pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> { let ctx = self.build(req, cred)?; req.apply(ctx) } } /// Construct string to sign. /// /// # Format /// /// ```text /// "date: {Date}" + "\n" /// + "(request-target): {verb} {uri}" + "\n" /// + "host: {Host}" /// ``` fn string_to_sign(ctx: &mut SigningContext, now: DateTime) -> Result { let string_to_sign = { let mut f = String::new(); writeln!(f, "date: {}", time::format_http_date(now))?; writeln!( f, "(request-target): {} {}", ctx.method.as_str().to_lowercase(), ctx.path )?; write!(f, "host: {}", ctx.authority)?; f }; debug!("string to sign: {}", &string_to_sign); Ok(string_to_sign) } reqsign-0.16.2/src/request.rs000064400000000000000000000145621046102023000142170ustar 00000000000000//! Provide common request trait for signing. use std::mem; use std::str::FromStr; use anyhow::anyhow; use anyhow::Result; use http::uri::PathAndQuery; use http::uri::Scheme; use http::Uri; use crate::ctx::SigningContext; /// Trait for all signable request. /// /// Any request type that implement this trait can be used by signers as input. /// Different requests may have different uri implementations, so we return detailed /// uri components instead of a complete struct. pub trait SignableRequest { fn build(&mut self) -> Result; fn apply(&mut self, _ctx: SigningContext) -> Result<()>; } /// Implement `SignableRequest` for [`http::Request`] impl SignableRequest for http::Request { fn build(&mut self) -> Result { let this = self as &mut http::Request; let uri = mem::take(this.uri_mut()).into_parts(); let paq = uri .path_and_query .unwrap_or_else(|| PathAndQuery::from_static("/")); Ok(SigningContext { method: this.method().clone(), scheme: uri.scheme.unwrap_or(Scheme::HTTP), authority: uri .authority .ok_or_else(|| anyhow!("request without authority is invalid for signing"))?, path: paq.path().to_string(), query: paq .query() .map(|v| { form_urlencoded::parse(v.as_bytes()) .map(|(k, v)| (k.into_owned(), v.into_owned())) .collect() }) .unwrap_or_default(), // Take the headers out of the request to avoid copy. // We will return it back when apply the context. headers: mem::take(this.headers_mut()), }) } fn apply(&mut self, mut ctx: SigningContext) -> Result<()> { let this = self as &mut http::Request; let query_size = ctx.query_size(); // Return headers back. mem::swap(this.headers_mut(), &mut ctx.headers); let mut parts = mem::take(this.uri_mut()).into_parts(); // Return scheme bakc. parts.scheme = Some(ctx.scheme); // Return authority back. parts.authority = Some(ctx.authority); // Build path and query. parts.path_and_query = { let paq = if query_size == 0 { ctx.path } else { let mut s = ctx.path; s.reserve(query_size + 1); s.push('?'); for (i, (k, v)) in ctx.query.iter().enumerate() { if i > 0 { s.push('&'); } s.push_str(k); if !v.is_empty() { s.push('='); s.push_str(v); } } s }; Some(PathAndQuery::from_str(&paq)?) }; *this.uri_mut() = Uri::from_parts(parts)?; Ok(()) } } /// Implement `SignableRequest` for [`reqwest::Request`] impl SignableRequest for reqwest::Request { fn build(&mut self) -> Result { let this = self as &mut reqwest::Request; let uri = Uri::from_str(this.url().as_str()) .expect("input request must contains valid uri") .into_parts(); let paq = uri .path_and_query .unwrap_or_else(|| PathAndQuery::from_static("/")); Ok(SigningContext { method: this.method().clone(), scheme: uri.scheme.unwrap_or(Scheme::HTTP), authority: uri .authority .ok_or_else(|| anyhow!("request without authority is invalid for signing"))?, path: paq.path().to_string(), query: paq .query() .map(|v| { form_urlencoded::parse(v.as_bytes()) .map(|(k, v)| (k.into_owned(), v.into_owned())) .collect() }) .unwrap_or_default(), // Take the headers out of the request to avoid copy. // We will return it back when apply the context. headers: mem::take(this.headers_mut()), }) } fn apply(&mut self, mut ctx: SigningContext) -> Result<()> { let this = self as &mut reqwest::Request; // Return headers back. mem::swap(this.headers_mut(), &mut ctx.headers); if ctx.query.is_empty() { return Ok(()); } this.url_mut() .set_query(Some(&SigningContext::query_to_string(ctx.query, "=", "&"))); Ok(()) } } /// Implement `SignableRequest` for [`reqwest::blocking::Request`] #[cfg(feature = "reqwest_blocking_request")] impl SignableRequest for reqwest::blocking::Request { fn build(&mut self) -> Result { let this = self as &mut reqwest::blocking::Request; let uri = Uri::from_str(this.url().as_str()) .expect("input request must contains valid uri") .into_parts(); let paq = uri .path_and_query .unwrap_or_else(|| PathAndQuery::from_static("/")); Ok(SigningContext { method: this.method().clone(), scheme: uri.scheme.unwrap_or(Scheme::HTTP), authority: uri .authority .ok_or_else(|| anyhow!("request without authority is invalid for signing"))?, path: paq.path().to_string(), query: paq .query() .map(|v| { form_urlencoded::parse(v.as_bytes()) .map(|(k, v)| (k.into_owned(), v.into_owned())) .collect() }) .unwrap_or_default(), // Take the headers out of the request to avoid copy. // We will return it back when apply the context. headers: mem::take(this.headers_mut()), }) } fn apply(&mut self, mut ctx: SigningContext) -> Result<()> { let this = self as &mut reqwest::blocking::Request; // Return headers back. mem::swap(this.headers_mut(), &mut ctx.headers); if ctx.query.is_empty() { return Ok(()); } this.url_mut() .set_query(Some(&SigningContext::query_to_string(ctx.query, "=", "&"))); Ok(()) } } reqsign-0.16.2/src/tencent/config.rs000064400000000000000000000076071046102023000154360ustar 00000000000000use std::collections::HashMap; use std::env; use super::constants::*; /// Config carries all the configuration for Tencent COS services. #[derive(Clone)] #[cfg_attr(test, derive(Debug))] pub struct Config { /// `region` will be loaded from: /// /// - this field if it's `is_some` /// - env value: [`TENCENTCLOUD_REGION`] or [`TKE_REGION`] pub region: Option, /// `access_key_id` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`TENCENTCLOUD_SECRET_ID`] or [`TKE_SECRET_ID`] pub secret_id: Option, /// `secret_access_key` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`TENCENTCLOUD_SECRET_KEY`] or [`TKE_SECRET_KEY`] pub secret_key: Option, /// `security_token` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`TENCENTCLOUD_TOKEN`] or [`TENCENTCLOUD_SECURITY_TOKEN`] pub security_token: Option, /// `role_arn` value will be load from: /// /// - this field if it's `is_some`. /// - env value: [`TENCENTCLOUD_ROLE_ARN`] or [`TKE_ROLE_ARN`] pub role_arn: Option, /// `role_session_name` value will be load from: /// /// - env value: [`TENCENTCLOUD_ROLE_SESSSION_NAME`] or [`TKE_ROLE_SESSSION_NAME`] /// - default to `reqsign`. pub role_session_name: String, /// `provider_id` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`TENCENTCLOUD_PROVIDER_ID`] or [`TKE_PROVIDER_ID`] pub provider_id: Option, /// `web_identity_token_file` will be loaded from /// /// - this field if it's `is_some` /// - env value: [`TENCENTCLOUD_WEB_IDENTITY_TOKEN_FILE`] or [`TKE_IDENTITY_TOKEN_FILE`] pub web_identity_token_file: Option, } impl Default for Config { fn default() -> Self { Self { region: None, secret_id: None, secret_key: None, security_token: None, role_arn: None, role_session_name: "reqsign".to_string(), provider_id: None, web_identity_token_file: None, } } } impl Config { /// Load config from env. pub fn from_env(mut self) -> Self { let envs = env::vars().collect::>(); if let Some(v) = envs .get(TENCENTCLOUD_REGION) .or_else(|| envs.get(TKE_REGION)) { self.region = Some(v.to_string()); } if let Some(v) = envs .get(TENCENTCLOUD_SECRET_ID) .or_else(|| envs.get(TKE_SECRET_ID)) { self.secret_id = Some(v.to_string()); } if let Some(v) = envs .get(TENCENTCLOUD_SECRET_KEY) .or_else(|| envs.get(TKE_SECRET_KEY)) { self.secret_key = Some(v.to_string()); } if let Some(v) = envs .get(TENCENTCLOUD_TOKEN) .or_else(|| envs.get(TENCENTCLOUD_SECURITY_TOKEN)) { self.security_token = Some(v.to_string()); } if let Some(v) = envs .get(TENCENTCLOUD_ROLE_ARN) .or_else(|| envs.get(TKE_ROLE_ARN)) { self.role_arn = Some(v.to_string()); } if let Some(v) = envs .get(TENCENTCLOUD_ROLE_SESSSION_NAME) .or_else(|| envs.get(TKE_ROLE_SESSSION_NAME)) { self.role_session_name = v.to_string(); } if let Some(v) = envs .get(TENCENTCLOUD_PROVIDER_ID) .or_else(|| envs.get(TKE_PROVIDER_ID)) { self.provider_id = Some(v.to_string()); } if let Some(v) = envs .get(TENCENTCLOUD_WEB_IDENTITY_TOKEN_FILE) .or_else(|| envs.get(TKE_IDENTITY_TOKEN_FILE)) { self.web_identity_token_file = Some(v.to_string()); } self } } reqsign-0.16.2/src/tencent/constants.rs000064400000000000000000000024601046102023000161750ustar 00000000000000use percent_encoding::AsciiSet; use percent_encoding::NON_ALPHANUMERIC; pub const TENCENTCLOUD_REGION: &str = "TENCENTCLOUD_REGION"; pub const TKE_REGION: &str = "TKE_REGION"; pub const TENCENTCLOUD_SECRET_ID: &str = "TENCENTCLOUD_SECRET_ID"; pub const TKE_SECRET_ID: &str = "TKE_SECRET_ID"; pub const TENCENTCLOUD_SECRET_KEY: &str = "TENCENTCLOUD_SECRET_KEY"; pub const TKE_SECRET_KEY: &str = "TKE_SECRET_KEY"; pub const TENCENTCLOUD_TOKEN: &str = "TENCENTCLOUD_TOKEN"; pub const TENCENTCLOUD_SECURITY_TOKEN: &str = "TENCENTCLOUD_SECURITY_TOKEN"; pub const TENCENTCLOUD_ROLE_ARN: &str = "TENCENTCLOUD_ROLE_ARN"; pub const TKE_ROLE_ARN: &str = "TKE_ROLE_ARN"; pub const TENCENTCLOUD_ROLE_SESSSION_NAME: &str = "TENCENTCLOUD_ROLE_SESSSION_NAME"; pub const TKE_ROLE_SESSSION_NAME: &str = "TKE_ROLE_SESSSION_NAME"; pub const TENCENTCLOUD_PROVIDER_ID: &str = "TENCENTCLOUD_PROVIDER_ID"; pub const TKE_PROVIDER_ID: &str = "TKE_PROVIDER_ID"; pub const TENCENTCLOUD_WEB_IDENTITY_TOKEN_FILE: &str = "TENCENTCLOUD_WEB_IDENTITY_TOKEN_FILE"; pub const TKE_IDENTITY_TOKEN_FILE: &str = "TKE_IDENTITY_TOKEN_FILE"; /// AsciiSet for [Tencent UriEncode](https://cloud.tencent.com/document/product/436/7778) pub static TENCENT_URI_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC .remove(b'-') .remove(b'_') .remove(b'.') .remove(b'~'); reqsign-0.16.2/src/tencent/cos.rs000064400000000000000000000137571046102023000147600ustar 00000000000000//! Tencent COS Singer use std::time::Duration; use anyhow::Result; use http::header::AUTHORIZATION; use http::header::DATE; use http::HeaderValue; use log::debug; use percent_encoding::percent_decode_str; use percent_encoding::utf8_percent_encode; use super::constants::*; use super::credential::Credential; use crate::ctx::SigningContext; use crate::ctx::SigningMethod; use crate::hash::hex_hmac_sha1; use crate::hash::hex_sha1; use crate::request::SignableRequest; use crate::time; use crate::time::format_http_date; use crate::time::DateTime; /// Singer for Tencent COS. #[derive(Default)] pub struct Signer { time: Option, } impl Signer { /// Load credential via credential load chain specified while building. /// /// # Note /// /// This function should never be exported to avoid credential leaking by /// mistake. pub fn new() -> Self { Self::default() } /// Specify the signing time. /// /// # Note /// /// We should always take current time to sign requests. /// Only use this function for testing. #[cfg(test)] pub fn with_time(mut self, time: DateTime) -> Self { self.time = Some(time); self } fn build( &self, req: &mut impl SignableRequest, method: SigningMethod, cred: &Credential, ) -> Result { let now = self.time.unwrap_or_else(time::now); let mut ctx = req.build()?; match method { SigningMethod::Header => { let signature = build_signature(&mut ctx, cred, now, Duration::from_secs(3600)); ctx.headers.insert(DATE, format_http_date(now).parse()?); ctx.headers.insert(AUTHORIZATION, { let mut value: HeaderValue = signature.parse()?; value.set_sensitive(true); value }); if let Some(token) = &cred.security_token { ctx.headers.insert("x-cos-security-token", { let mut value: HeaderValue = token.parse()?; value.set_sensitive(true); value }); } } SigningMethod::Query(expire) => { let signature = build_signature(&mut ctx, cred, now, expire); ctx.headers.insert(DATE, format_http_date(now).parse()?); ctx.query_append(&signature); if let Some(token) = &cred.security_token { ctx.query_push( "x-cos-security-token".to_string(), utf8_percent_encode(token, percent_encoding::NON_ALPHANUMERIC).to_string(), ); } } } Ok(ctx) } /// Signing request with header. pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> { let ctx = self.build(req, SigningMethod::Header, cred)?; req.apply(ctx) } /// Signing request with query. pub fn sign_query( &self, req: &mut impl SignableRequest, expire: Duration, cred: &Credential, ) -> Result<()> { let ctx = self.build(req, SigningMethod::Query(expire), cred)?; req.apply(ctx) } } fn build_signature( ctx: &mut SigningContext, cred: &Credential, now: DateTime, expires: Duration, ) -> String { let key_time = format!( "{};{}", now.timestamp(), (now + chrono::TimeDelta::from_std(expires).unwrap()).timestamp() ); let sign_key = hex_hmac_sha1(cred.secret_key.as_bytes(), key_time.as_bytes()); let mut params = ctx .query .iter() .map(|(k, v)| { ( utf8_percent_encode(&k.to_lowercase(), &TENCENT_URI_ENCODE_SET).to_string(), utf8_percent_encode(v, &TENCENT_URI_ENCODE_SET).to_string(), ) }) .collect::>(); params.sort(); let param_list = params .iter() .map(|(k, _)| k.to_string()) .collect::>() .join(";"); debug!("param list: {param_list}"); let param_string = params .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join("&"); debug!("param string: {param_string}"); let mut headers = ctx .header_to_vec_with_prefix("") .iter() .map(|(k, v)| { ( k.to_lowercase(), utf8_percent_encode(v, &TENCENT_URI_ENCODE_SET).to_string(), ) }) .collect::>(); headers.sort(); let header_list = headers .iter() .map(|(k, _)| k.to_string()) .collect::>() .join(";"); debug!("header list: {header_list}"); let header_string = headers .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join("&"); debug!("header string: {header_string}"); let mut http_string = String::new(); http_string.push_str(&ctx.method.as_str().to_ascii_lowercase()); http_string.push('\n'); http_string.push_str(&percent_decode_str(&ctx.path).decode_utf8_lossy()); http_string.push('\n'); http_string.push_str(¶m_string); http_string.push('\n'); http_string.push_str(&header_string); http_string.push('\n'); debug!("http string: {http_string}"); let mut string_to_sign = String::new(); string_to_sign.push_str("sha1"); string_to_sign.push('\n'); string_to_sign.push_str(&key_time); string_to_sign.push('\n'); string_to_sign.push_str(&hex_sha1(http_string.as_bytes())); string_to_sign.push('\n'); debug!("string_to_sign: {string_to_sign}"); let signature = hex_hmac_sha1(sign_key.as_bytes(), string_to_sign.as_bytes()); format!("q-sign-algorithm=sha1&q-ak={}&q-sign-time={}&q-key-time={}&q-header-list={}&q-url-param-list={}&q-signature={}", cred.secret_id, key_time, key_time, header_list, param_list, signature) } reqsign-0.16.2/src/tencent/credential.rs000064400000000000000000000273031046102023000162760ustar 00000000000000use std::fs; use std::sync::Arc; use std::sync::Mutex; use anyhow::anyhow; use anyhow::Result; use http::header::AUTHORIZATION; use http::header::CONTENT_LENGTH; use http::header::CONTENT_TYPE; use log::debug; use reqwest::Client; use serde::Deserialize; use serde::Serialize; use super::config::Config; use crate::time::now; use crate::time::parse_rfc3339; use crate::time::DateTime; /// Credential for cos. #[derive(Clone)] #[cfg_attr(test, derive(Debug))] pub struct Credential { /// Secret ID pub secret_id: String, /// Secret Key pub secret_key: String, /// security_token pub security_token: Option, /// expires in for credential. pub expires_in: Option, } /// CredentialLoader will load credential from different methods. #[derive(Default)] #[cfg_attr(test, derive(Debug))] pub struct CredentialLoader { client: Client, config: Config, credential: Arc>>, } impl CredentialLoader { /// Create a new loader via config. pub fn new(client: Client, config: Config) -> Self { Self { client, config, credential: Arc::default(), } } /// Load credential pub async fn load(&self) -> Result> { // Return cached credential if it's valid. if let Some(cred) = self.credential.lock().expect("lock poisoned").clone() { return Ok(Some(cred)); } let cred = self.load_inner().await?; let mut lock = self.credential.lock().expect("lock poisoned"); lock.clone_from(&cred); Ok(cred) } async fn load_inner(&self) -> Result> { if let Ok(Some(cred)) = self .load_via_config() .map_err(|err| debug!("load credential via config failed: {err:?}")) { return Ok(Some(cred)); } if let Ok(Some(cred)) = self .load_via_assume_role_with_web_identity() .await .map_err(|err| { debug!("load credential via assume_role_with_web_identity failed: {err:?}") }) { return Ok(Some(cred)); } Ok(None) } fn load_via_config(&self) -> Result> { if let (Some(ak), Some(sk)) = (&self.config.secret_id, &self.config.secret_key) { let cred = Credential { secret_id: ak.clone(), secret_key: sk.clone(), security_token: self.config.security_token.clone(), // Set expires_in to 10 minutes to enforce re-read // from file. expires_in: Some(now() + chrono::TimeDelta::try_minutes(10).expect("in bounds")), }; return Ok(Some(cred)); } Ok(None) } async fn load_via_assume_role_with_web_identity(&self) -> Result> { let (region, token_file, role_arn, provider_id) = match ( &self.config.region, &self.config.web_identity_token_file, &self.config.role_arn, &self.config.provider_id, ) { (Some(region), Some(token_file), Some(role_arn), Some(provider_id)) => { (region, token_file, role_arn, provider_id) } _ => { let missing = [ ("region", self.config.region.is_none()), ( "web_identity_token_file", self.config.web_identity_token_file.is_none(), ), ("role_arn", self.config.role_arn.is_none()), ("provider_id", self.config.provider_id.is_none()), ] .iter() .filter_map(|&(k, v)| if v { Some(k) } else { None }) .collect::>() .join(", "); debug!( "assume_role_with_web_identity is not configured fully: [{}] is missing", missing ); return Ok(None); } }; let token = fs::read_to_string(token_file)?; let role_session_name = &self.config.role_session_name; // Construct request to Tencent Cloud STS Service. let url = "https://sts.tencentcloudapi.com".to_string(); let bs = serde_json::to_vec(&AssumeRoleWithWebIdentityRequest { role_session_name: role_session_name.clone(), web_identity_token: token, role_arn: role_arn.clone(), provider_id: provider_id.clone(), })?; let req = self .client .post(&url) .header(AUTHORIZATION.as_str(), "SKIP") .header(CONTENT_TYPE.as_str(), "application/json") .header(CONTENT_LENGTH, bs.len()) .header("X-TC-Action", "AssumeRoleWithWebIdentity") .header("X-TC-Region", region) .header("X-TC-Timestamp", now().timestamp()) .header("X-TC-Version", "2018-08-13") .body(bs); let resp = req.send().await?; if resp.status() != http::StatusCode::OK { let content = resp.text().await?; return Err(anyhow!( "request to Tencent Cloud STS Services failed: {content}" )); } let resp: AssumeRoleWithWebIdentityResult = serde_json::from_str(&resp.text().await?)?; if let Some(error) = resp.response.error { return Err(anyhow!( "request to Tencent Cloud STS Services failed: {error:?}" )); } let resp_expiration = resp.response.expiration; let resp_cred = resp.response.credentials; let cred = Credential { secret_id: resp_cred.tmp_secret_id, secret_key: resp_cred.tmp_secret_key, security_token: Some(resp_cred.token), expires_in: Some(parse_rfc3339(&resp_expiration)?), }; Ok(Some(cred)) } } #[derive(Default, Debug, Serialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleWithWebIdentityRequest { role_session_name: String, web_identity_token: String, role_arn: String, provider_id: String, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleWithWebIdentityResult { response: AssumeRoleWithWebIdentityResponse, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleWithWebIdentityResponse { error: Option, expiration: String, credentials: AssumeRoleWithWebIdentityCredentials, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleWithWebIdentityCredentials { token: String, tmp_secret_id: String, tmp_secret_key: String, } #[derive(Default, Debug, Deserialize)] #[serde(default, rename_all = "PascalCase")] struct AssumeRoleWithWebIdentityError { code: String, message: String, } #[cfg(test)] mod tests { use std::env; use std::str::FromStr; use http::Request; use http::StatusCode; use log::debug; use once_cell::sync::Lazy; use tokio::runtime::Runtime; use super::super::constants::*; use super::super::cos::Signer; use super::*; static RUNTIME: Lazy = Lazy::new(|| { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .expect("Should create a tokio runtime") }); #[test] fn test_parse_assume_role_with_web_identity() -> Result<()> { let content = r#"{ "Response": { "ExpiredTime": 1543914376, "Expiration": "2018-12-04T09:06:16Z", "Credentials": { "Token": "1siMD5r0tPAq9xpR******6a1ad76f09a0069002923def8aFw7tUMd2nH", "TmpSecretId": "AKID65zyIP0mp****qt2SlWIQVMn1umNH58", "TmpSecretKey": "q95K84wrzuE****y39zg52boxvp71yoh" }, "RequestId": "f6e7cbcb-add1-47bd-9097-d08cf8f3a919" } }"#; let resp: AssumeRoleWithWebIdentityResult = serde_json::from_str(content).expect("json deserialize must success"); assert_eq!( &resp.response.credentials.tmp_secret_id, "AKID65zyIP0mp****qt2SlWIQVMn1umNH58" ); assert_eq!( &resp.response.credentials.tmp_secret_key, "q95K84wrzuE****y39zg52boxvp71yoh" ); assert_eq!( &resp.response.credentials.token, "1siMD5r0tPAq9xpR******6a1ad76f09a0069002923def8aFw7tUMd2nH" ); assert_eq!(&resp.response.expiration, "2018-12-04T09:06:16Z"); Ok(()) } #[test] fn test_signer_with_web_identidy_token() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_TENCENT_COS_TEST").is_err() || env::var("REQSIGN_TENCENT_COS_TEST").unwrap() != "on" { return Ok(()); } // Ignore test if role_arn not set let role_arn = if let Ok(v) = env::var("REQSIGN_TENCENT_COS_ROLE_ARN") { v } else { return Ok(()); }; let provider_id = env::var("REQSIGN_TENCENT_COS_PROVIDER_ID") .expect("REQSIGN_TENCENT_COS_PROVIDER_ID not exist"); let region = env::var("REQSIGN_TENCENT_COS_REGION").expect("REQSIGN_TENCENT_COS_REGION not exist"); let github_token = env::var("GITHUB_ID_TOKEN").expect("GITHUB_ID_TOKEN not exist"); let file_path = format!( "{}/testdata/services/tencent/web_identity_token_file", env::current_dir() .expect("current_dir must exist") .to_string_lossy() ); fs::write(&file_path, github_token)?; temp_env::with_vars( vec![ (TENCENTCLOUD_REGION, Some(®ion)), (TENCENTCLOUD_ROLE_ARN, Some(&role_arn)), (TENCENTCLOUD_PROVIDER_ID, Some(&provider_id)), (TENCENTCLOUD_WEB_IDENTITY_TOKEN_FILE, Some(&file_path)), ], || { RUNTIME.block_on(async { let config = Config::default().from_env(); let loader = CredentialLoader::new(reqwest::Client::new(), config); let signer = Signer::new(); let url = &env::var("REQSIGN_TENCENT_COS_URL") .expect("env REQSIGN_TENCENT_COS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "not_exist_file")) .expect("must valid"); let cred = loader .load() .await .expect("credential must be valid") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request url: {:?}", req.uri().to_string()); debug!("signed request: {:?}", req); let client = reqwest::Client::new(); let resp = client .execute(req.try_into().unwrap()) .await .expect("request must succeed"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await.unwrap()); assert_eq!(StatusCode::NOT_FOUND, status); }) }, ); Ok(()) } } reqsign-0.16.2/src/tencent/mod.rs000064400000000000000000000005171046102023000147410ustar 00000000000000//! Tencent Cloud service signer //! //! Only Cos has been supported. mod cos; pub use cos::Signer as TencentCosSigner; mod credential; pub use credential::Credential as TencentCosCredential; pub use credential::CredentialLoader as TencentCosCredentialLoader; mod config; pub use config::Config as TencentCosConfig; mod constants; reqsign-0.16.2/src/time.rs000064400000000000000000000072121046102023000134570ustar 00000000000000//! Time related utils. use anyhow::anyhow; use anyhow::Result; use chrono::format::Fixed; use chrono::format::Item; use chrono::format::Numeric; use chrono::format::Pad; use chrono::SecondsFormat; use chrono::Utc; pub type DateTime = chrono::DateTime; /// Create datetime of now. pub fn now() -> DateTime { Utc::now() } /// DATE is a time format like `20220301` const DATE: &[Item<'static>] = &[ Item::Numeric(Numeric::Year, Pad::Zero), Item::Numeric(Numeric::Month, Pad::Zero), Item::Numeric(Numeric::Day, Pad::Zero), ]; /// Format time into date: `20220301` pub fn format_date(t: DateTime) -> String { t.format_with_items(DATE.iter()).to_string() } /// ISO8601 is a time format like `20220313T072004Z`. const ISO8601: &[Item<'static>] = &[ Item::Numeric(Numeric::Year, Pad::Zero), Item::Numeric(Numeric::Month, Pad::Zero), Item::Numeric(Numeric::Day, Pad::Zero), Item::Literal("T"), Item::Numeric(Numeric::Hour, Pad::Zero), Item::Numeric(Numeric::Minute, Pad::Zero), Item::Numeric(Numeric::Second, Pad::Zero), Item::Literal("Z"), ]; /// Format time into ISO8601: `20220313T072004Z` pub fn format_iso8601(t: DateTime) -> String { t.format_with_items(ISO8601.iter()).to_string() } /// HTTP_DATE is a time format like `Sun, 06 Nov 1994 08:49:37 GMT`. const HTTP_DATE: &[Item<'static>] = &[ Item::Fixed(Fixed::ShortWeekdayName), Item::Literal(", "), Item::Numeric(Numeric::Day, Pad::Zero), Item::Literal(" "), Item::Fixed(Fixed::ShortMonthName), Item::Literal(" "), Item::Numeric(Numeric::Year, Pad::Zero), Item::Literal(" "), Item::Numeric(Numeric::Hour, Pad::Zero), Item::Literal(":"), Item::Numeric(Numeric::Minute, Pad::Zero), Item::Literal(":"), Item::Numeric(Numeric::Second, Pad::Zero), Item::Literal(" GMT"), ]; /// Format time into http date: `Sun, 06 Nov 1994 08:49:37 GMT` /// /// ## Note /// /// HTTP date is slightly different from RFC2822. /// /// - Timezone is fixed to GMT. /// - Day must be 2 digit. pub fn format_http_date(t: DateTime) -> String { t.format_with_items(HTTP_DATE.iter()).to_string() } /// Format time into RFC3339: `2022-03-13T07:20:04Z` pub fn format_rfc3339(t: DateTime) -> String { t.to_rfc3339_opts(SecondsFormat::Secs, true) } /// Parse time from RFC3339. /// /// All of them are valid time: /// /// - `2022-03-13T07:20:04Z` /// - `2022-03-01T08:12:34+00:00` /// - `2022-03-01T08:12:34.00+00:00` pub fn parse_rfc3339(s: &str) -> Result { Ok(chrono::DateTime::parse_from_rfc3339(s) .map_err(|err| anyhow!("parse {s} into rfc3339 failed for {err:?}"))? .with_timezone(&Utc)) } #[cfg(test)] mod tests { use chrono::TimeZone; use super::*; fn test_time() -> DateTime { Utc.with_ymd_and_hms(2022, 3, 1, 8, 12, 34).unwrap() } #[test] fn test_format_date() { let t = test_time(); assert_eq!("20220301", format_date(t)) } #[test] fn test_format_ios8601() { let t = test_time(); assert_eq!("20220301T081234Z", format_iso8601(t)) } #[test] fn test_format_http_date() { let t = test_time(); assert_eq!("Tue, 01 Mar 2022 08:12:34 GMT", format_http_date(t)) } #[test] fn test_format_rfc3339() { let t = test_time(); assert_eq!("2022-03-01T08:12:34Z", format_rfc3339(t)) } #[test] fn test_parse_rfc3339() { let t = test_time(); for v in [ "2022-03-01T08:12:34Z", "2022-03-01T08:12:34+00:00", "2022-03-01T08:12:34.00+00:00", ] { assert_eq!(t, parse_rfc3339(v).expect("must be valid time")); } } } reqsign-0.16.2/testdata/services/aliyun/.gitignore000064400000000000000000000000201046102023000202770ustar 00000000000000oidc_token_file reqsign-0.16.2/testdata/services/aws/.gitignore000064400000000000000000000000301046102023000175710ustar 00000000000000web_identity_token_file reqsign-0.16.2/testdata/services/aws/default_config000064400000000000000000000001621046102023000205030ustar 00000000000000[default] region = test aws_access_key_id = config_access_key_id aws_secret_access_key = config_secret_access_key reqsign-0.16.2/testdata/services/aws/default_credential000064400000000000000000000001441046102023000213500ustar 00000000000000[default] aws_access_key_id = shared_access_key_id aws_secret_access_key = shared_secret_access_key reqsign-0.16.2/testdata/services/google/test_credential.json000064400000000000000000000026301046102023000223370ustar 00000000000000{ "type": "service_account", "project_id": "test", "private_key_id": "test_private_key_id", "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDOy4jaJIcVlffi5ENtlNhJ0tsI1zt21BI3DMGtPq7n3Ymow24w\nBV2Z73l4dsqwRo2QVSwnCQ2bVtM2DgckMNDShfWfKe3LRcl96nnn51AtAYIfRnc+\nogstzxZi4J64f7IR3KIAFxJnzo+a6FS6MmsYMAs8/Oj68fRmCD0AbAs5ZwIDAQAB\nAoGAVpPkMeBFJgZph/alPEWq4A2FYogp/y/+iEmw9IVf2PdpYNyhTz2P2JjoNEUX\nywFe12SxXY5uwfBx8RmiZ8aARkIBWs7q9Sz6f/4fdCHAuu3GAv5hmMO4dLQsGcKl\nXAQW4QxZM5/x5IXlDh4KdcUP65P0ZNS3deqDlsq/vVfY9EECQQD9I/6KNmlSrbnf\nFa/5ybF+IV8mOkEfkslQT4a9pWbA1FF53Vk4e7B+Faow3uUGHYs/HUwrd3vIVP84\nS+4Jeuc3AkEA0SGF5l3BrWWTok1Wr/UE+oPOUp2L4AV6kH8co11ZyxSQkRloLdMd\nbNzNXShuhwgvNjvgkseNSeQPJKxFRn73UQJACacMtrJ6c6eiNcp66lhxhzC4kxmX\nkB+lw4U0yxh6gZHXBYGWPFwjD7u9wJ1POFt6Cs8QL3wf4TS0gq4KhpwEIwJACIA8\nWSjmfo3qemZ6Z5ymHyjMcj9FOE4AtW71Uw6wX7juR3eo7HPwdkRjdK34EDUc9i9o\n6Y6DB8Xld7ApALyYgQJBAPTMFpKpCRNvYH5VrdObid5+T7OwDrJFHGWdbDGiT++O\nV08rl535r74rMilnQ37X1/zaKBYyxpfhnd2XXgoCgTM=\n-----END RSA PRIVATE KEY-----\n", "client_email": "test-234@test.iam.gserviceaccount.com", "client_id": "111942493510252143344", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-234%40test.iam.gserviceaccount.com" } reqsign-0.16.2/testdata/services/google/test_external_account.json000064400000000000000000000011461046102023000235640ustar 00000000000000{ "type": "external_account", "audience": "//iam.googleapis.com/projects/000000000000/locations/global/workloadIdentityPools/reqsign/providers/reqsign-provider", "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-234@test.iam.gserviceaccount.com:generateAccessToken", "token_url": "https://sts.googleapis.com/v1/token", "credential_source": { "url": "http://localhost:5000/token", "format": { "type": "json", "subject_token_field_name": "id_token" } } } reqsign-0.16.2/testdata/services/google/test_impersonated_service_account.json000064400000000000000000000007311046102023000261530ustar 00000000000000{ "delegates": [], "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/example-01-iam@example-01.iam.gserviceaccount.com:generateAccessToken", "source_credentials": { "client_id": "placeholder_client_id", "client_secret": "placeholder_client_secret", "refresh_token": "placeholder_refresh_token", "type": "authorized_user" }, "type": "impersonated_service_account" } reqsign-0.16.2/testdata/services/google/testbucket_credential.json000064400000000000000000000045571046102023000235470ustar 00000000000000{ "type": "service_account", "project_id": "iam-testbucket-reqsign-project", "private_key_id": "abcdefabcdef4f96c0c0ed6cffe88a6413e91cec", "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDMXQUceY0V5Id3\nhT4Xu/eUOkj7vYMxmcM7u03g/R5d1Jj+5WdpeLu/Kjh2EhXuCfodCSawEoU1O66h\nfL9A2EMefHRE3tPMYDCYJgUqes/6of4QcSJKev29blbryKrNfQEp4xzRX9oY68bs\nRNVoDN/tIKovZle7VvOPjK83HeTv/R9l93dbKS1lTIYrNI86aRhvvnfbDZGTq/iI\nMHreTD8ICc06qsH774uVMBhTH3bxRQ8NTrZP70PS8RfCCTVNwiwSPE146aU6XUWU\nh+29rqbMNHHQsWLCLeLTY565kD+a2DtNIU0H89wPxnirDouZBYwCjtivvlE0mN5y\nj1qkPrwfAgMBAAECggEBAMO6k5qiEC5XoicmxkGVFZox+JSi/XQUAJjE2+IQi3Ty\nmVYIAPNTXv3IQitTRw2lIJeOnC8mjc5eSvL/t20zs5UPPYx4ngGwXtpaD7iPx4IU\nhHDa6izLfxpfA4DvwCbvAp5Llt4xH4Gez/aaNophSlaiYlzjeENFFCD4bRgs2Ye+\n/pyF0akNf7RO2PXC0u0KbP3rf/tqY+tGnNg7Ykx3+4yEetwFW2RgL3IlLU4PM2Xg\nRrjdiLHNOjS1VXCBMaEYuoP1HFCR+JzyBdidD++/kTSHVCab9Dz4HemyKxr8gpYK\nZUREirN8GFZwVt+SysWzIh788adhhy7Tscyy6SoQ2AECgYEA+76McDcxlEGTkRuO\nXh/dEeFDnSGEqOC3HfYJbzlo+K5bD9ged3DtwSN+bxDizHtcQnhqMPOhrD0tgiCn\nmNsEPXAhj3eoRd050R6THEXwE5Az84SR9gvm27Q8JoAUWXWIkrpVKPwDDjq7XoIF\nY1lTld84UGwg8Hgux05KcMZROd8CgYEAz9FszBaX1OgmRcOklxmDgFsWsdiepFpL\nrTj6oNpbHiJYYDJ0DQgvty8dI+1aWfqgHh1TczjYRx3Cf77qUGpYzY/Vkc2lC+fU\naxXz3qmaKIYZH/IA5+EZshXjMSwTTaGzpmzoqXZ57J9uwmQeUsLeXnlZd1habk7O\nD3CZhuK3RcECgYBGWUdRjHr0XSbpo/Oy5eCXQIXugRFbSACkBL86L6bf54lW8iQB\naLNoB40raGKYldiAUroKF+sUALyY4pszIfEbYhxexSdm7p1bjNm7Suf974w0/tTz\nFvxaZRFyCNSm8ytJJXzqyRHphgwaKudqjenHtes8vhquWEdqNryiqyjDrQKBgCV7\nNBArEv9HT3/NpWXLKDiCNTmmRBaIYpW/bRSNzVlGAIJ5Fw0yqMh1KuBL8ru/xBkq\nWN6zJe7No0K/ACu4woNwqag+WsIm8dzOfMlv9WnRpb5pO1iW9Ld10yAPPvwFag1e\nHyhRQfQ3XRaaUA3FL64CXOx1dvnmJKwMNuRpB30BAoGBAJ+DhBf9CM6ndMBJaOZa\nD8Let93+NYu/iJEn2whzN1cGBcnsOw9uAWiOkFUOuNqYVzSih11vLvUIw9Q9LKnI\n8MLsFLjvierQ1h+V1mxhNO8bEv9tTO+O8h1GMNmCy4S0wZjfsbrJzPL+pKrpgEJS\nqjDDLD9IP7jvoKHSODDsecNq\n-----END PRIVATE KEY-----\n", "client_email": "testbucket-reqsign-account@iam-testbucket-reqsign-project.iam.gserviceaccount.com", "client_id": "101000000000000000000", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/testbucket-reqsign-account%iam-testbucket-reqsign-project.iam.gserviceaccount.iam.gserviceaccount.com" } reqsign-0.16.2/testdata/services/tencent/.gitignore000064400000000000000000000000301046102023000204370ustar 00000000000000web_identity_token_file reqsign-0.16.2/tests/aliyun/mod.rs000064400000000000000000000000111046102023000151420ustar 00000000000000mod oss; reqsign-0.16.2/tests/aliyun/oss.rs000064400000000000000000000227171046102023000152100ustar 00000000000000use std::env; use std::str::FromStr; use std::time::Duration; use anyhow::Result; use http::header::CONTENT_LENGTH; use http::Request; use http::StatusCode; use log::debug; use log::warn; use percent_encoding::utf8_percent_encode; use percent_encoding::NON_ALPHANUMERIC; use reqsign::AliyunConfig; use reqsign::AliyunLoader; use reqsign::AliyunOssSigner; use reqwest::Client; fn init_signer() -> Option<(AliyunLoader, AliyunOssSigner)> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_ALIYUN_OSS_TEST").is_err() || env::var("REQSIGN_ALIYUN_OSS_TEST").unwrap() != "on" { return None; } let config = AliyunConfig { access_key_id: Some( env::var("REQSIGN_ALIYUN_OSS_ACCESS_KEY") .expect("env REQSIGN_ALIYUN_OSS_ACCESS_KEY must set"), ), access_key_secret: Some( env::var("REQSIGN_ALIYUN_OSS_SECRET_KEY") .expect("env REQSIGN_ALIYUN_OSS_SECRET_KEY must set"), ), ..Default::default() }; let loader = AliyunLoader::new(Client::new(), config); let signer = AliyunOssSigner::new( &env::var("REQSIGN_ALIYUN_OSS_BUCKET").expect("env REQSIGN_ALIYUN_OSS_BUCKET must set"), ); Some((loader, signer)) } #[tokio::test] async fn test_get_object() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_ALIYUN_OSS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_ALIYUN_OSS_URL").expect("env REQSIGN_ALIYUN_OSS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "not_exist_file"))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::NOT_FOUND, status); Ok(()) } #[tokio::test] async fn test_delete_objects() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_ALIYUN_OSS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_ALIYUN_OSS_URL").expect("env REQSIGN_ALIYUN_OSS_URL must set"); let mut req = Request::new( r#" sample1.txt sample2.txt "#, ); *req.method_mut() = http::Method::POST; *req.uri_mut() = http::Uri::from_str(&format!("{}/?delete", url))?; req.headers_mut() .insert("CONTENT-MD5", "WOctCY1SS662e7ziElh4cw==".parse().unwrap()); let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::OK, status); Ok(()) } #[tokio::test] async fn test_get_object_with_query_sign() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_ALIYUN_OSS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_ALIYUN_OSS_URL").expect("env REQSIGN_ALIYUN_OSS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "not_exist_file"))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign_query(&mut req, Duration::from_secs(3600), &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::NOT_FOUND, status); Ok(()) } #[tokio::test] async fn test_head_object_with_special_characters() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_ALIYUN_OSS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_ALIYUN_OSS_URL").expect("env REQSIGN_ALIYUN_OSS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::HEAD; *req.uri_mut() = http::Uri::from_str(&format!( "{}/{}", url, utf8_percent_encode("not-exist-!@#$%^&*()_+-=;:'><,/?.txt", NON_ALPHANUMERIC) ))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_put_object_with_special_characters() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_ALIYUN_OSS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_ALIYUN_OSS_URL").expect("env REQSIGN_ALIYUN_OSS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::PUT; *req.uri_mut() = http::Uri::from_str(&format!( "{}/{}", url, utf8_percent_encode("put-!@#$%^&*()_+-=;:'><,/?.txt", NON_ALPHANUMERIC) ))?; req.headers_mut() .insert(CONTENT_LENGTH, 0.to_string().parse()?); let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {:?}", resp.text().await?); assert_eq!(StatusCode::OK, status); Ok(()) } #[tokio::test] async fn test_list_bucket() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_ALIYUN_OSS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_ALIYUN_OSS_URL").expect("env REQSIGN_ALIYUN_OSS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{url}?list-type=2&delimiter=/&encoding-type=url"))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::OK, status); Ok(()) } #[tokio::test] async fn test_list_bucket_with_invalid_token() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_ALIYUN_OSS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_ALIYUN_OSS_URL").expect("env REQSIGN_ALIYUN_OSS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!( "{}?list-type=2&delimiter=/&encoding-type=url&continuation-token={}", url, utf8_percent_encode("hello.txt", NON_ALPHANUMERIC) ))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::BAD_REQUEST, status); Ok(()) } reqsign-0.16.2/tests/aws/mod.rs000064400000000000000000000000101046102023000144320ustar 00000000000000mod v4; reqsign-0.16.2/tests/aws/v4.rs000064400000000000000000000177371046102023000142340ustar 00000000000000use std::env; use std::str::FromStr; use std::time::Duration; use anyhow::Result; use http::Request; use http::StatusCode; use log::debug; use log::warn; use percent_encoding::utf8_percent_encode; use percent_encoding::NON_ALPHANUMERIC; use reqsign::AwsConfig; use reqsign::AwsDefaultLoader; use reqsign::AwsV4Signer; use reqwest::Client; use sha2::Digest; use sha2::Sha256; fn init_signer() -> Option<(AwsDefaultLoader, AwsV4Signer)> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_AWS_V4_TEST").is_err() || env::var("REQSIGN_AWS_V4_TEST").unwrap() != "on" { return None; } let config = AwsConfig { region: Some( env::var("REQSIGN_AWS_V4_REGION").expect("env REQSIGN_AWS_V4_REGION must set"), ), access_key_id: Some( env::var("REQSIGN_AWS_V4_ACCESS_KEY").expect("env REQSIGN_AWS_V4_ACCESS_KEY must set"), ), secret_access_key: Some( env::var("REQSIGN_AWS_V4_SECRET_KEY").expect("env REQSIGN_AWS_V4_SECRET_KEY must set"), ), ..Default::default() } .from_env() .from_profile(); let region = config.region.as_deref().unwrap().to_string(); let loader = AwsDefaultLoader::new(Client::new(), config); let signer = AwsV4Signer::new( &env::var("REQSIGN_AWS_V4_SERVICE").expect("env REQSIGN_AWS_V4_SERVICE must set"), ®ion, ); Some((loader, signer)) } #[tokio::test] async fn test_head_object() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AWS_V4_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AWS_V4_URL").expect("env REQSIGN_AWS_V4_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::HEAD; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "not_exist_file"))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_put_object_with_query() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AWS_V4_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AWS_V4_URL").expect("env REQSIGN_AWS_V4_URL must set"); let body = "Hello, World!"; let body_digest = hex::encode(Sha256::digest(body).as_slice()); let mut req = Request::new(body); req.headers_mut().insert( "x-amz-content-sha256", body_digest.parse().expect("parse digest failed"), ); *req.method_mut() = http::Method::PUT; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "put_object_test"))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign_query(&mut req, Duration::from_secs(3600), &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); let status = resp.status(); debug!( "got response: {:?}", String::from_utf8(resp.bytes().await?.to_vec())? ); assert_eq!(StatusCode::OK, status); Ok(()) } #[tokio::test] async fn test_get_object_with_query() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AWS_V4_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AWS_V4_URL").expect("env REQSIGN_AWS_V4_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "not_exist_file"))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign_query(&mut req, Duration::from_secs(3600), &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_head_object_with_special_characters() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AWS_V4_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AWS_V4_URL").expect("env REQSIGN_AWS_V4_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::HEAD; *req.uri_mut() = http::Uri::from_str(&format!( "{}/{}", url, utf8_percent_encode("!@#$%^&*()_+-=;:'><,/?.txt", NON_ALPHANUMERIC) ))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_head_object_with_encoded_characters() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AWS_V4_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AWS_V4_URL").expect("env REQSIGN_AWS_V4_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::HEAD; *req.uri_mut() = http::Uri::from_str(&format!( "{}/{}", url, utf8_percent_encode("!@#$%^&*()_+-=;:'><,/?.txt", NON_ALPHANUMERIC) ))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_list_bucket() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AWS_V4_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AWS_V4_URL").expect("env REQSIGN_AWS_V4_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{url}?list-type=2&delimiter=/&encoding-type=url"))?; let cred = loader .load() .await .expect("load request must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::OK, resp.status()); Ok(()) } reqsign-0.16.2/tests/azure/mod.rs000064400000000000000000000000151046102023000147730ustar 00000000000000mod storage; reqsign-0.16.2/tests/azure/storage.rs000064400000000000000000000355131046102023000156730ustar 00000000000000use std::env; use std::str::FromStr; use std::time::Duration; use anyhow::Result; use http::StatusCode; use log::debug; use log::warn; use percent_encoding::utf8_percent_encode; use percent_encoding::NON_ALPHANUMERIC; use reqsign::AzureStorageConfig; use reqsign::AzureStorageLoader; use reqsign::AzureStorageSigner; use reqwest::Client; fn init_signer() -> Option<(AzureStorageLoader, AzureStorageSigner)> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_AZURE_STORAGE_TEST").is_err() || env::var("REQSIGN_AZURE_STORAGE_TEST").unwrap() != "on" { return None; } let config = AzureStorageConfig { account_name: Some( env::var("REQSIGN_AZURE_STORAGE_ACCOUNT_NAME") .expect("env REQSIGN_AZURE_STORAGE_ACCOUNT_NAME must set"), ), account_key: Some( env::var("REQSIGN_AZURE_STORAGE_ACCOUNT_KEY") .expect("env REQSIGN_AZURE_STORAGE_ACCOUNT_KEY must set"), ), ..Default::default() }; let loader = AzureStorageLoader::new(config); Some((loader, AzureStorageSigner::new())) } #[tokio::test] async fn test_head_blob() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set"); let mut builder = http::Request::builder(); builder = builder.method(http::Method::HEAD); builder = builder.header("x-ms-version", "2023-01-03"); builder = builder.uri(format!("{}/{}", url, "not_exist_file")); let mut req = builder.body("")?; let cred = loader .load() .await .expect("load credential must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_head_object_with_encoded_characters() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set"); let mut req = http::Request::new(""); *req.method_mut() = http::Method::HEAD; req.headers_mut() .insert("x-ms-version", "2023-01-03".parse().unwrap()); *req.uri_mut() = http::Uri::from_str(&format!( "{}/{}", url, utf8_percent_encode("!@#$%^&*()_+-=;:'><,/?.txt", NON_ALPHANUMERIC) ))?; let cred = loader .load() .await .expect("load credential must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_list_container_blobs() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set"); for query in [ // Without prefix "restype=container&comp=list", // With not encoded prefix "restype=container&comp=list&prefix=test/path/to/dir", // With encoded prefix "restype=container&comp=list&prefix=test%2Fpath%2Fto%2Fdir", ] { let mut builder = http::Request::builder(); builder = builder.method(http::Method::GET); builder = builder.uri(format!("{url}?{query}")); builder = builder.header("x-ms-version", "2023-01-03"); let mut req = builder.body("")?; let cred = loader .load() .await .expect("load credential must success") .unwrap(); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::OK, resp.status()); } Ok(()) } #[tokio::test] async fn test_can_head_blob_with_sas() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set"); let mut builder = http::Request::builder(); builder = builder.method(http::Method::HEAD); builder = builder.header("x-ms-version", "2023-01-03"); builder = builder.uri(format!("{}/{}", url, "not_exist_file")); let mut req = builder.body("")?; let cred = loader .load() .await .expect("load credential must success") .unwrap(); signer .sign_query(&mut req, Duration::from_secs(60), &cred) .expect("sign request must success"); println!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); println!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_can_list_container_blobs() -> Result<()> { // API https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs?tabs=azure-ad let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set"); for query in [ // Without prefix "restype=container&comp=list", // With not encoded prefix "restype=container&comp=list&prefix=test/path/to/dir", // With encoded prefix "restype=container&comp=list&prefix=test%2Fpath%2Fto%2Fdir", ] { let mut builder = http::Request::builder(); builder = builder.method(http::Method::GET); builder = builder.header("x-ms-version", "2023-01-03"); builder = builder.uri(format!("{url}?{query}")); let mut req = builder.body("")?; let cred = loader .load() .await .expect("load credential must success") .unwrap(); signer .sign_query(&mut req, Duration::from_secs(60), &cred) .expect("sign request must success"); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::OK, resp.status()); } Ok(()) } /// This test must run on azure vm with imds enabled, #[tokio::test] async fn test_head_blob_with_ldms() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_AZURE_STORAGE_TEST").is_err() || env::var("REQSIGN_AZURE_STORAGE_TEST").unwrap() != "on" || env::var("REQSIGN_AZURE_STORAGE_CRED").is_err() || env::var("REQSIGN_AZURE_STORAGE_CRED").unwrap() != "imds" { return Ok(()); } let config = AzureStorageConfig { ..Default::default() }; let loader = AzureStorageLoader::new(config); let cred = loader .load() .await .expect("load credential must success") .unwrap(); let url = &env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set"); let mut req = http::Request::builder() .method(http::Method::HEAD) .header("x-ms-version", "2023-01-03") .uri(format!("{}/{}", url, "not_exist_file")) .body("")?; AzureStorageSigner::new() .sign(&mut req, &cred) .expect("sign request must success"); println!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } /// This test must run on azure vm with imds enabled #[tokio::test] async fn test_can_list_container_blobs_with_ldms() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_AZURE_STORAGE_TEST").is_err() || env::var("REQSIGN_AZURE_STORAGE_TEST").unwrap() != "on" || env::var("REQSIGN_AZURE_STORAGE_CRED").is_err() || env::var("REQSIGN_AZURE_STORAGE_CRED").unwrap() != "imds" { return Ok(()); } let config = AzureStorageConfig { ..Default::default() }; let loader = AzureStorageLoader::new(config); let cred = loader .load() .await .expect("load credential must success") .unwrap(); let url = &env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set"); for query in [ // Without prefix "restype=container&comp=list", // With not encoded prefix "restype=container&comp=list&prefix=test/path/to/dir", // With encoded prefix "restype=container&comp=list&prefix=test%2Fpath%2Fto%2Fdir", ] { let mut builder = http::Request::builder(); builder = builder.method(http::Method::GET); builder = builder.header("x-ms-version", "2023-01-03"); builder = builder.uri(format!("{url}?{query}")); let mut req = builder.body("")?; AzureStorageSigner::new() .sign(&mut req, &cred) .expect("sign request must success"); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::OK, resp.status()); } Ok(()) } /// This test must run on azure vm with imds enabled, #[tokio::test] async fn test_head_blob_with_client_secret() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_AZURE_STORAGE_TEST").is_err() || env::var("REQSIGN_AZURE_STORAGE_TEST").unwrap() != "on" { warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped"); return Ok(()); } if env::var("REQSIGN_AZURE_STORAGE_CLIENT_SECRET") .unwrap_or_default() .is_empty() { warn!("REQSIGN_AZURE_STORAGE_CLIENT_SECRET is not set, skipped"); return Ok(()); } let config = AzureStorageConfig::default().from_env(); assert!(config.client_secret.is_some()); assert!(config.tenant_id.is_some()); assert!(config.client_id.is_some()); assert!(config.authority_host.is_some()); assert!(config.account_key.is_none()); let loader = AzureStorageLoader::new(config); let cred = loader .load() .await .expect("load credential must success") .unwrap(); let url = &env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set"); let mut req = http::Request::builder() .method(http::Method::HEAD) .header("x-ms-version", "2023-01-03") .uri(format!("{}/{}", url, "not_exist_file")) .body("")?; AzureStorageSigner::new() .sign(&mut req, &cred) .expect("sign request must success"); println!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } /// This test must run on azure vm with imds enabled #[tokio::test] async fn test_can_list_container_blobs_client_secret() -> Result<()> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_AZURE_STORAGE_TEST").is_err() || env::var("REQSIGN_AZURE_STORAGE_TEST").unwrap() != "on" { warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped"); return Ok(()); } if env::var("REQSIGN_AZURE_STORAGE_CLIENT_SECRET") .unwrap_or_default() .is_empty() { warn!("REQSIGN_AZURE_STORAGE_CLIENT_SECRET is not set, skipped"); return Ok(()); } let config = AzureStorageConfig::default().from_env(); assert!(config.client_secret.is_some()); assert!(config.tenant_id.is_some()); assert!(config.client_id.is_some()); assert!(config.authority_host.is_some()); assert!(config.account_key.is_none()); let loader = AzureStorageLoader::new(config); let cred = loader .load() .await .expect("load credential must success") .unwrap(); let url = &env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set"); for query in [ // Without prefix "restype=container&comp=list", // With not encoded prefix "restype=container&comp=list&prefix=test/path/to/dir", // With encoded prefix "restype=container&comp=list&prefix=test%2Fpath%2Fto%2Fdir", ] { let mut builder = http::Request::builder(); builder = builder.method(http::Method::GET); builder = builder.header("x-ms-version", "2023-01-03"); builder = builder.uri(format!("{url}?{query}")); let mut req = builder.body("")?; AzureStorageSigner::new() .sign(&mut req, &cred) .expect("sign request must success"); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); let stat = resp.status(); debug!("got response: {:?}", resp); debug!("{}", resp.text().await?); assert_eq!(StatusCode::OK, stat); } Ok(()) } reqsign-0.16.2/tests/google/mod.rs000064400000000000000000000000151046102023000151210ustar 00000000000000mod storage; reqsign-0.16.2/tests/google/storage.rs000064400000000000000000000103511046102023000160120ustar 00000000000000use std::env; use std::time::Duration; use anyhow::Result; use http::StatusCode; use log::debug; use log::warn; use reqsign::GoogleCredentialLoader; use reqsign::GoogleSigner; use reqsign::GoogleTokenLoader; use reqwest::Client; async fn init_signer() -> Option<(GoogleCredentialLoader, GoogleTokenLoader, GoogleSigner)> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_GOOGLE_TEST").is_err() || env::var("REQSIGN_GOOGLE_TEST").unwrap() != "on" { return None; } let cred_loader = GoogleCredentialLoader::default().with_content( &env::var("REQSIGN_GOOGLE_CREDENTIAL").expect("env REQSIGN_GOOGLE_CREDENTIAL must set"), ); let token_loader = GoogleTokenLoader::new( &env::var("REQSIGN_GOOGLE_CLOUD_STORAGE_SCOPE") .expect("env REQSIGN_GOOGLE_CLOUD_STORAGE_SCOPE must set"), Client::new(), ) .with_credentials(cred_loader.load().unwrap().unwrap()); let signer = GoogleSigner::new("storage"); Some((cred_loader, token_loader, signer)) } #[tokio::test] async fn test_get_object() -> Result<()> { let signer = init_signer().await; if signer.is_none() { warn!("REQSIGN_GOOGLE_TEST is not set, skipped"); return Ok(()); } let (_, token_loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_GOOGLE_CLOUD_STORAGE_URL") .expect("env REQSIGN_GOOGLE_CLOUD_STORAGE_URL must set"); let mut builder = http::Request::builder(); builder = builder.method(http::Method::GET); builder = builder.uri(format!("{}/o/{}", url, "not_exist_file")); let mut req = builder.body("")?; let token = token_loader.load().await?.unwrap(); signer .sign(&mut req, &token) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_list_objects() -> Result<()> { let signer = init_signer().await; if signer.is_none() { warn!("REQSIGN_GOOGLE_TEST is not set, skipped"); return Ok(()); } let (_, token_loader, signer) = signer.unwrap(); let url = &env::var("REQSIGN_GOOGLE_CLOUD_STORAGE_URL") .expect("env REQSIGN_GOOGLE_CLOUD_STORAGE_URL must set"); let mut builder = http::Request::builder(); builder = builder.method(http::Method::GET); builder = builder.uri(format!("{url}/o")); let mut req = builder.body("")?; let token = token_loader.load().await?.unwrap(); signer .sign(&mut req, &token) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::OK, resp.status()); Ok(()) } #[tokio::test] async fn test_get_object_with_query() -> Result<()> { let signer = init_signer().await; if signer.is_none() { warn!("REQSIGN_GOOGLE_TEST is not set, skipped"); return Ok(()); } let (cred_loader, _, signer) = signer.unwrap(); let url = &env::var("REQSIGN_GOOGLE_CLOUD_STORAGE_URL") .expect("env REQSIGN_GOOGLE_CLOUD_STORAGE_URL must set"); let mut builder = http::Request::builder(); builder = builder.method(http::Method::GET); builder = builder.uri(format!( "{}/{}", url.replace("storage/v1/b/", ""), "not_exist_file" )); let mut req = builder.body("")?; let cred = cred_loader.load()?.unwrap(); signer .sign_query(&mut req, Duration::from_secs(3600), &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); let code = resp.status(); debug!("got response: {:?}", resp); debug!("got body: {}", resp.text().await?); assert_eq!(StatusCode::NOT_FOUND, code); Ok(()) } reqsign-0.16.2/tests/main.rs000064400000000000000000000000711046102023000140140ustar 00000000000000mod aliyun; mod aws; mod azure; mod google; mod tencent; reqsign-0.16.2/tests/tencent/cos.rs000064400000000000000000000217271046102023000153270ustar 00000000000000use std::env; use std::str::FromStr; use std::time::Duration; use anyhow::Result; use http::header::AUTHORIZATION; use http::header::CONTENT_LENGTH; use http::Request; use http::StatusCode; use log::debug; use log::warn; use percent_encoding::utf8_percent_encode; use percent_encoding::NON_ALPHANUMERIC; use reqsign::TencentCosConfig; use reqsign::TencentCosCredentialLoader; use reqsign::TencentCosSigner; use reqwest::Client; fn init_signer() -> Option<(TencentCosCredentialLoader, TencentCosSigner)> { let _ = env_logger::builder().is_test(true).try_init(); dotenv::from_filename(".env").ok(); if env::var("REQSIGN_TENCENT_COS_TEST").is_err() || env::var("REQSIGN_TENCENT_COS_TEST").unwrap() != "on" { return None; } let config = TencentCosConfig { secret_id: Some( env::var("REQSIGN_TENCENT_COS_ACCESS_KEY") .expect("env REQSIGN_TENCENT_COS_ACCESS_KEY must set"), ), secret_key: Some( env::var("REQSIGN_TENCENT_COS_SECRET_KEY") .expect("env REQSIGN_TENCENT_COS_SECRET_KEY must set"), ), ..Default::default() }; let loader = TencentCosCredentialLoader::new(reqwest::Client::new(), config); Some((loader, TencentCosSigner::new())) } #[tokio::test] async fn test_get_object() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_TENCENT_COS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let cred = loader.load().await?.unwrap(); let url = &env::var("REQSIGN_TENCENT_COS_URL").expect("env REQSIGN_TENCENT_COS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "not_exist_file"))?; signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req.headers().get(AUTHORIZATION)); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::NOT_FOUND, status); Ok(()) } #[tokio::test] async fn test_delete_objects() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_TENCENT_COS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let cred = loader.load().await?.unwrap(); let url = &env::var("REQSIGN_TENCENT_COS_URL").expect("env REQSIGN_TENCENT_COS_URL must set"); let content = r#" sample1.txt sample2.txt "#; let mut req = Request::new(content); *req.method_mut() = http::Method::POST; *req.uri_mut() = http::Uri::from_str(&format!("{}/?delete", url))?; req.headers_mut() .insert(CONTENT_LENGTH, content.len().to_string().parse().unwrap()); req.headers_mut() .insert("CONTENT-MD5", "WOctCY1SS662e7ziElh4cw==".parse().unwrap()); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::OK, status); Ok(()) } #[tokio::test] async fn test_get_object_with_query_sign() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_TENCENT_COS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let cred = loader.load().await?.unwrap(); let url = &env::var("REQSIGN_TENCENT_COS_URL").expect("env REQSIGN_TENCENT_COS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{}/{}", url, "not_exist_file"))?; signer .sign_query(&mut req, Duration::from_secs(3600), &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must succeed"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::NOT_FOUND, status); Ok(()) } #[tokio::test] async fn test_head_object_with_special_characters() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_TENCENT_COS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let cred = loader.load().await?.unwrap(); let url = &env::var("REQSIGN_TENCENT_COS_URL").expect("env REQSIGN_TENCENT_COS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::HEAD; *req.uri_mut() = http::Uri::from_str(&format!( "{}/{}", url, utf8_percent_encode("not-exist-!@#$%^&*()_+-=;:'><,/?.txt", NON_ALPHANUMERIC) ))?; signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); debug!("got response: {:?}", resp); assert_eq!(StatusCode::NOT_FOUND, resp.status()); Ok(()) } #[tokio::test] async fn test_put_object_with_special_characters() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_TENCENT_COS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let cred = loader.load().await?.unwrap(); let url = &env::var("REQSIGN_TENCENT_COS_URL").expect("env REQSIGN_TENCENT_COS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::PUT; *req.uri_mut() = http::Uri::from_str(&format!( "{}/{}", url, utf8_percent_encode("put-!@#$%^&*()_+-=;:'><,/?.txt", NON_ALPHANUMERIC) ))?; req.headers_mut() .insert(CONTENT_LENGTH, "0".parse().unwrap()); signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {:?}", resp.text().await?); assert_eq!(StatusCode::OK, status); Ok(()) } #[tokio::test] async fn test_list_bucket() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_TENCENT_COS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let cred = loader.load().await?.unwrap(); let url = &env::var("REQSIGN_TENCENT_COS_URL").expect("env REQSIGN_TENCENT_COS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{url}?list-type=2&delimiter=/&encoding-type=url"))?; signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::OK, status); Ok(()) } #[tokio::test] async fn test_list_bucket_with_upper_cases() -> Result<()> { let signer = init_signer(); if signer.is_none() { warn!("REQSIGN_TENCENT_COS_TEST is not set, skipped"); return Ok(()); } let (loader, signer) = signer.unwrap(); let cred = loader.load().await?.unwrap(); let url = &env::var("REQSIGN_TENCENT_COS_URL").expect("env REQSIGN_TENCENT_COS_URL must set"); let mut req = Request::new(""); *req.method_mut() = http::Method::GET; *req.uri_mut() = http::Uri::from_str(&format!("{url}?prefix=stage/1712557668-ZgPY8Ql4"))?; signer .sign(&mut req, &cred) .expect("sign request must success"); debug!("signed request: {:?}", req); let client = Client::new(); let resp = client .execute(req.try_into()?) .await .expect("request must success"); let status = resp.status(); debug!("got response: {:?}", resp); debug!("got response content: {}", resp.text().await?); assert_eq!(StatusCode::OK, status); Ok(()) } reqsign-0.16.2/tests/tencent/mod.rs000064400000000000000000000000111046102023000153010ustar 00000000000000mod cos; reqsign-0.16.2/vercel.json000064400000000000000000000001331046102023000135320ustar 00000000000000{ "redirects": [ { "source": "/", "destination": "/reqsign/" } ] }