# s3req

A minimal CLI that signs arbitrary HTTP requests with [AWS Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) and sends them via `ureq`.
Aimed at one-off inspection, scripting, and debugging of S3-compatible APIs — not a replacement for the AWS CLI or any SDK.

## Features

- Arbitrary HTTP method, inline body, or body from file
- Custom headers (`-H`, repeatable)
- `x-amz-security-token` support for STS temporary credentials
- Correct double-URI-encoding for non-S3 services
- `--verbose` dumps the canonical request, string-to-sign, and all sent/received headers to stderr
- Optional `tracing` feature gate (enabled by default; controlled via `RUST_LOG`)
- AWS profile support: reads `~/.aws/credentials` and `~/.aws/config` via `--profile` / `AWS_PROFILE`

## Limitations

- **No IMDS / SSO / `AssumeRole` support.** Credentials come from CLI flags, `AWS_*` env vars, or `~/.aws` profile files only.
- **Region and service are not inferred from the hostname.** Always specify `--region` and `--service` when they differ from the defaults (`us-east-1` / `s3`).
- **Body is buffered in memory.** Not suitable for large streaming uploads.
- **30-second HTTP timeout, hard-coded.** Not configurable.
- No multipart uploads, presigning, retries, or redirect handling beyond `ureq` defaults.
- No stdin support.

## Installation

Requires **Rust 1.93+** (`fmt::from_fn`, edition 2024; no `rust-toolchain` pin is committed).

```sh
git clone <repo>
cd s3req
cargo build --release
# binary at target/release/s3req
```

Or install in-place:

```sh
cargo install --path .
```

## Quick start

```sh
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

s3req https://my-bucket.s3.us-east-1.amazonaws.com/path/to/object
```

Response body is written to **stdout**. `HTTP <status>` and verbose diagnostics go to **stderr**, so piping stdout is safe.

## CLI reference

```
Usage: s3req [OPTIONS] <URL>

Arguments:
  <URL>  Request URL (path-style or virtual-hosted)

Options:
  -m, --method <METHOD>          HTTP method [default: GET]
      --body <BODY>              Request body (string literal) [default: ""]
      --body-file <FILE>         Read body from file (conflicts with --body)
      --region <REGION>          AWS region [env: AWS_REGION]
      --service <SERVICE>        AWS service [default: s3]
      --access-key <ACCESS_KEY>  AWS access key [env: AWS_ACCESS_KEY_ID]
      --secret-key <SECRET_KEY>  AWS secret key [env: AWS_SECRET_ACCESS_KEY]
      --session-token <TOKEN>    AWS session token [env: AWS_SESSION_TOKEN]
      --profile <PROFILE>        AWS profile name [env: AWS_PROFILE] [default: default]
  -H, --header <K: V>            Extra header, repeatable
  -v, --verbose                  Print canonical request, string-to-sign and sent headers
  -h, --help                     Print help
  -V, --version                  Print version
```

| Flag | Env | Default | Notes |
|---|---|---|---|
| `-m, --method` | — | `GET` | |
| `--body` | — | `""` | Conflicts with `--body-file` |
| `--body-file FILE` | — | — | |
| `--region` | `AWS_REGION` | profile → `us-east-1` | Not inferred from hostname |
| `--service` | — | `s3` | |
| `--access-key` | `AWS_ACCESS_KEY_ID` | profile | |
| `--secret-key` | `AWS_SECRET_ACCESS_KEY` | profile | |
| `--session-token` | `AWS_SESSION_TOKEN` | profile | Adds `x-amz-security-token` |
| `--profile` | `AWS_PROFILE` | `default` | Reads `~/.aws/credentials` + `~/.aws/config` |
| `-H, --header` | — | — | `"Key: Value"`, repeatable |
| `-v, --verbose` | — | false | Writes to stderr |

### Credential precedence

For each credential field, the lookup order is:

1. CLI flag (e.g. `--access-key`)
2. AWS environment variable (e.g. `AWS_ACCESS_KEY_ID`)
3. Active profile in `~/.aws/credentials` / `~/.aws/config`
4. Built-in default (region only: `us-east-1`)

Custom file paths: `AWS_SHARED_CREDENTIALS_FILE` overrides `~/.aws/credentials`;
`AWS_CONFIG_FILE` overrides `~/.aws/config`.

## Examples

```sh
# GET an object (virtual-hosted style)
s3req https://my-bucket.s3.us-east-1.amazonaws.com/path/to/key

# List bucket (v2) — query parameters are sorted and canonicalized automatically
s3req 'https://my-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=logs/'

# PUT an object from a file
s3req -m PUT \
      --body-file ./photo.jpg \
      -H 'Content-Type: image/jpeg' \
      https://my-bucket.s3.us-east-1.amazonaws.com/photo.jpg

# PUT an inline string
s3req -m PUT --body 'hello, world' \
      https://my-bucket.s3.us-east-1.amazonaws.com/hello.txt

# DELETE
s3req -m DELETE https://my-bucket.s3.us-east-1.amazonaws.com/old.txt

# HEAD with verbose SigV4 debug output
s3req -v -m HEAD https://my-bucket.s3.us-east-1.amazonaws.com/key

# Path-style against MinIO (or any custom endpoint)
s3req --region us-east-1 \
      --access-key minioadmin --secret-key minioadmin \
      http://localhost:9000/mybucket/key

# Non-S3 AWS service (region and service must be explicit)
s3req --service iam --region us-east-1 \
      'https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08'
```

## Output and exit codes

| Stream | Content |
|---|---|
| stdout | Response body (no trailing newline added) |
| stderr | `HTTP <status>` line, and verbose blocks when `-v` is set |

Exit code is non-zero when:
- the HTTP status is not 2xx
- the URL is malformed (must start with `http://` or `https://`)
- a header is not in `Key: Value` format
- the body file cannot be read
- the HTTP request itself fails (timeout, DNS, etc.)

## Feature flags

| Feature | Default | Effect |
|---|---|---|
| `tracing` | **on** | Initialises `tracing-subscriber` with `EnvFilter` (reads `RUST_LOG`). Spans logged at TRACE level for every function in the signing pipeline. |

Build without tracing:

```sh
cargo build --release --no-default-features
```

## Building and developing

```sh
cargo build --release
cargo test
cargo clippy --all-targets -- -D warnings
cargo +nightly fmt          # rustfmt.toml uses unstable features
```

## Architecture

The signing pipeline is split across five modules, each with a single responsibility:

| Module | Role |
|---|---|
| `src/timestamp.rs` | Produces `YYYYMMDDTHHMMSSZ` and `YYYYMMDD` strings from a UTC instant |
| `src/url.rs` | Parses scheme/host/path/query, percent-encodes path segments (RFC 3986), sorts query params |
| `src/request.rs` | Builds and displays the canonical request; emits the `;`-joined signed-header list |
| `src/sign.rs` | Four-step HMAC-SHA256 key derivation and final hex signature |
| `src/main.rs` | CLI parsing, credential resolution, header merging, request dispatch, output |

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option.
