diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2d304b6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,45 @@ +# Dependabot version updates +# Docs: https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + # Rust crate dependencies (Cargo.toml + Cargo.lock) + - package-ecosystem: cargo + directory: "/" + schedule: + interval: weekly + day: saturday + time: "03:00" + timezone: "Asia/Shanghai" + open-pull-requests-limit: 10 + commit-message: + prefix: "chore: :arrow_up:" + groups: + # Tightly coupled async/HTTP stack: bump together to avoid intermediate + # incompatible Cargo.lock states between PRs. + web-stack: + patterns: + - "reqwest*" + - "hyper*" + - "tokio*" + - "tower*" + - "rustls*" + - "h2" + - "http*" + # Serialization stack + serde-stack: + patterns: + - "serde*" + - "toml*" + + # GitHub Actions used in .github/workflows + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: "chore: :arrow_up:" + ignore: + # release.yml is generated by cargo-dist, whose template pins + # actions/checkout@v6; bump it by upgrading cargo-dist instead. + - dependency-name: "actions/checkout" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1e89998 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + pull_request: + push: + branches: [develop, main] + +permissions: + contents: read + +jobs: + check: + name: Test and lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run tests + run: cargo test + - name: Run clippy + run: cargo clippy --all-targets -- -D warnings + - name: Check formatting + run: cargo fmt --all --check + - name: Install cargo-deny + uses: taiki-e/install-action@cargo-deny + - name: Check dependency licenses + run: cargo deny check licenses diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 6629433..af0ab61 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -18,7 +18,7 @@ jobs: pull-requests: read steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false @@ -59,7 +59,7 @@ jobs: cancel-in-progress: false steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false diff --git a/AGENTS.md b/AGENTS.md index 37a9117..1933085 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ src/ │ resolve_game_id / resolve_challenge_id (numeric ID, exact name, unique prefix) ├── cli.rs clap command tree + global flags (--json / --profile / --url / --token / --pager) ├── client.rs reqwest wrapper: /api/{path}, Bearer token, Set-Token refresh, streaming download, download_bytes -├── config.rs ~/.config/ret2cli/config.toml; atomic writes + file lock; [ui] section +├── config.rs user config dir per platform via dirs::config_dir() (Linux ~/.config, macOS ~/Library/Application Support, Windows %APPDATA%); atomic writes + file lock; [ui] section ├── error.rs CliError → exit codes: 1 config/serialization, 2 unauthenticated, 3 forbidden, 4 not found, 5 network/server ├── output.rs output capture + pager ($PAGER > [ui].pager > less -R > more), tabled tables, Markdown └── commands/ auth / game / challenge / team / submission / interactive / local profile management diff --git a/CHANGELOG.md b/CHANGELOG.md index ea55f55..c0f24bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,17 @@ Ret2CLI 的所有重要变更将记录在此文件中。格式基于 [Keep a Cha ## [Unreleased] +## [2.0.0] - 2026-08-05 - TearJerker + +### Added + +- 交互模式启动横幅:`figlet` 大字体渲染 `Ret2CLI` 并带 `lolcat` 式彩虹渐变(纯 Rust 实现,跨平台,不依赖系统 figlet/lolcat 或外部字体;设置 `NO_COLOR` 或 `TERM=dumb`、stdout 非终端时显示无颜色版本) +- 在 Windows 构建中加入了资源头,包含 icon、名称、公司名称、注释、版权信息等 + +### Removed + +- 移除弃用的 `game challenge start` / `stop` / `status` / `renew` 扁平子命令,请使用 `game challenge instance ` + ## [1.1.0] - 2026-08-03 - Bloom in Two ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3993f4a..295a480 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,7 @@ git push origin feat/ ### 5. 发布流程 -Ret2CLI 遵循 SemVer。`release-plz` 读取 Conventional Commits、维护版本与 CHANGELOG Release PR,并先将发布版本上传到 crates.io;`cargo-dist` 随后创建同版本的 tag、GitHub Release 和三端二进制附件。 +Ret2CLI 遵循 SemVer。`release-plz` 读取 Conventional Commits、维护版本 PR 并发布 crates.io(`release-plz.toml` 中 `changelog_update = false`,它不会改写 CHANGELOG.md);CHANGELOG 由维护者以项目风格手工维护。`cargo-dist` 随后创建同版本的 tag、GitHub Release 和三端二进制附件。 首次配置仓库时,维护者需要: @@ -74,13 +74,14 @@ Ret2CLI 遵循 SemVer。`release-plz` 读取 Conventional Commits、维护版本 日常发布流程如下: -1. 变更合入 `develop` 后,`.github/workflows/release-plz.yml` 创建或更新面向 `develop` 的版本 PR。 -2. 维护者审阅版本号和 CHANGELOG。同一 `major.minor` 发布线共用一个 codename;首次提升 minor 或 major 时,必须同时更新 `release.rs` 的映射、`dist-workspace.toml` 的展示名和 CHANGELOG 文案,否则构建会拒绝未知发布线。 -3. 版本 PR 合入 `develop` 后,从该提交切出 `release/vX.Y.Z`,完成最终验证并向 `main` 发起 PR。发布分支如有新修订,发布后也要合回或逐项 backport 到 `develop`。 +1. 变更合入 `develop` 后,`.github/workflows/release-plz.yml` 创建或更新面向 `develop` 的版本 PR。该 PR 只 bump `Cargo.toml` / `Cargo.lock`,不触碰 CHANGELOG。 +2. 首次提升 minor 或 major 时,必须先把新发布线注册到 `release.rs`(映射 + 测试)并更新 `dist-workspace.toml` 的展示名,否则 `build.rs` 会拒绝未知发布线;该改动需要先于版本 PR 合入 `develop`。 +3. 维护者从版本 PR 的 head 提交切出 `release/vX.Y.Z`(版本 PR 暂不合入 `develop`),在发布分支上补写该版本的 CHANGELOG 章节(中文、手工条目,标题格式 `## [X.Y.Z] - YYYY-MM-DD - CODENAME`)并修复版本敏感的测试(如硬编码 codename 的断言),完成最终验证(`cargo test`、`cargo fmt --all --check`、`cargo clippy --all-targets -- -D warnings`、`cargo deny check licenses`;`dist plan` / `dist generate --check` 由 CI 的 `plan` 任务验证)后向 `main` 发起 PR。 4. 发布 PR 合入 `main` 后,release-plz 将 `X.Y.Z` 发布到 crates.io,计算对应的 `vX.Y.Z`,但不自行创建 tag 或 GitHub Release;随后通过 `workflow_dispatch` 调用 cargo-dist 工作流。 5. cargo-dist 从 `main` 为 Windows x86_64、Linux x86_64/AArch64、macOS Intel/Apple Silicon 构建压缩包与校验文件,随后创建 tag 和 GitHub Release。 +6. 确认 tag 已创建后,**最后**把版本 PR 合入 `develop`,随后把 `main` 合入 `develop`,同步发布分支上的修订并保持两条长期分支的历史对齐。整个发布窗口内 `develop` 应保持冻结——任何推送都会让 release-plz rebase 版本 PR。发布完成后,release-plz 会在 `develop` 出现新提交时自动为**下一个版本**开启 PR(即使只有 docs 变更也会提议补丁版本,如 1.1.1):该 PR 是下一次发布的起点,保持开启、不要关闭;准备发布时完成发布前准备后合入即可。 -正常流程中不要手工推送版本 tag。若 crates.io 已发布但 cargo-dist 调度意外失败,维护者可从 `main` 手动 dispatch `Release` 工作流并填写同版本 tag;不得重复 bump 版本。已发布版本的紧急修复从 `main` 切出 `hotfix/*`,合入 `main` 发布后再同步到 `develop`。Winget、Scoop、Homebrew 等包管理器发布暂不属于本流程。 +正常流程中不要手工推送版本 tag。若 crates.io 已发布但 cargo-dist 调度意外失败,维护者可从 `main` 手动 dispatch `Release` 工作流并填写同版本 tag;不得重复 bump 版本。若发布中途取消,把版本 PR 照常合入 `develop` 继续开发;在 tag 创建前 release-plz 会持续更新该版本 PR,属正常现象。已发布版本的紧急修复从 `main` 切出 `hotfix/*`,合入 `main` 发布后再同步到 `develop`。Winget、Scoop、Homebrew 等包管理器发布暂不属于本流程。 正式 CI 构建会在程序报告的 SemVer 后附加 `+build...g`;Cargo.toml 和 tag 仍只保存规范版本号,不提交构建元数据。 diff --git a/Cargo.lock b/Cargo.lock index 8f607dd..f00c00c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -88,6 +94,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bitflags" version = "2.13.1" @@ -106,6 +118,12 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.12.1" @@ -161,9 +179,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.4" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" dependencies = [ "clap_builder", "clap_derive", @@ -171,9 +189,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" dependencies = [ "anstream", "anstyle", @@ -234,15 +252,14 @@ dependencies = [ [[package]] name = "console" -version = "0.15.11" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width 0.2.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -278,6 +295,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crokey" version = "1.5.0" @@ -411,14 +437,13 @@ dependencies = [ [[package]] name = "dialoguer" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" dependencies = [ "console", "shell-words", "tempfile", - "thiserror 1.0.69", "zeroize", ] @@ -497,12 +522,31 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "figlet-rs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad4cf7e378685c1af30f41b4832c29700325524c648d47752cabd5b97e7b09b" +dependencies = [ + "zip", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -625,9 +669,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -704,7 +748,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -747,12 +791,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -760,9 +805,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -773,9 +818,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -787,15 +832,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -807,15 +852,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -839,9 +884,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -859,22 +904,22 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.11" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", - "number_prefix", "portable-atomic", "unicode-width 0.2.2", + "unit-prefix", "web-time", ] [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_terminal_polyfill" @@ -930,9 +975,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] @@ -1000,13 +1045,23 @@ dependencies = [ [[package]] name = "minimad" -version = "0.13.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" +checksum = "de632ee829aec3a874d18a4192eae64a0460b3a45c54ed556b334f6fe5a1d62f" dependencies = [ "once_cell", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.2" @@ -1021,9 +1076,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.30.1" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags", "cfg-if", @@ -1040,12 +1095,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "once_cell" version = "1.21.4" @@ -1066,9 +1115,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "papergrid" -version = "0.14.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b915f831b85d984193fdc3d3611505871dc139b2534530fa01c1a6a6707b6723" +checksum = "d0984e668274d34691bc2b262ef0d115de5fa9973bcdee7ae32213f93099153e" dependencies = [ "bytecount", "fnv", @@ -1170,7 +1219,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.19", + "thiserror", "tokio", "tracing", "web-time", @@ -1192,7 +1241,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.19", + "thiserror", "tinyvec", "tracing", "web-time", @@ -1270,7 +1319,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.19", + "thiserror", ] [[package]] @@ -1287,9 +1336,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -1308,7 +1357,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -1346,15 +1395,16 @@ dependencies = [ [[package]] name = "ret2cli" -version = "1.1.0" +version = "2.0.0" dependencies = [ - "base64", + "base64 0.23.1", "chrono", "clap", "clap_complete", "colored", "dialoguer", "dirs", + "figlet-rs", "futures-util", "hex", "indicatif", @@ -1367,10 +1417,11 @@ dependencies = [ "shell-words", "tabled", "termimad", - "thiserror 2.0.19", + "thiserror", "tokio", "toml", "unicode-width 0.2.2", + "winresource", ] [[package]] @@ -1438,9 +1489,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "once_cell", "ring", @@ -1479,9 +1530,9 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rustyline" -version = "16.0.0" +version = "18.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62fd9ca5ebc709e8535e8ef7c658eb51457987e48c98ead2be482172accc408d" +checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" dependencies = [ "bitflags", "cfg-if", @@ -1493,7 +1544,7 @@ dependencies = [ "unicode-segmentation", "unicode-width 0.2.2", "utf8parse", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1559,11 +1610,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1621,6 +1672,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "slab" version = "0.4.12" @@ -1711,19 +1768,20 @@ dependencies = [ [[package]] name = "tabled" -version = "0.18.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121d8171ee5687a4978d1b244f7d99c43e7385a272185a2f1e1fa4dc0979d444" +checksum = "b5dc662e6da844ad6e428ad16b57967c9d33c82e16bb1c258326c0c078605dff" dependencies = [ "papergrid", "tabled_derive", + "testing_table", ] [[package]] name = "tabled_derive" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d9946811baad81710ec921809e2af67ad77719418673b2a3794932d57b7538" +checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846" dependencies = [ "heck", "proc-macro-error2", @@ -1747,9 +1805,9 @@ dependencies = [ [[package]] name = "termimad" -version = "0.31.3" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7301d9c2c4939c97f25376b70d3c13311f8fefdee44092fc361d2a98adc2cbb6" +checksum = "d53d4b1294b87e81925b7ae7f8f4d000376e3a5b3349d978429665428a793fcb" dependencies = [ "coolor", "crokey", @@ -1757,17 +1815,17 @@ dependencies = [ "lazy-regex", "minimad", "serde", - "thiserror 2.0.19", + "thiserror", "unicode-width 0.1.14", ] [[package]] -name = "thiserror" -version = "1.0.69" +name = "testing_table" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc" dependencies = [ - "thiserror-impl 1.0.69", + "unicode-width 0.2.2", ] [[package]] @@ -1776,18 +1834,7 @@ version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.19", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "thiserror-impl", ] [[package]] @@ -1845,13 +1892,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1879,44 +1926,42 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "toml_parser" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_writer" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -2018,6 +2063,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "untrusted" version = "0.9.0" @@ -2048,6 +2099,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -2334,11 +2391,18 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" dependencies = [ - "memchr", + "toml", + "version_check", ] [[package]] @@ -2430,6 +2494,18 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 5d606ac..876d75e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ret2cli" -version = "1.1.0" +version = "2.0.0" edition = "2024" rust-version = "1.89" authors = ["LyCecilion "] @@ -22,28 +22,41 @@ include = [ keywords = ["cli", "ret2shell", "ctf"] categories = ["command-line-utilities"] +# Windows PE resource strings (shown in Explorer → Properties → Details). +# FileVersion / ProductVersion / ProductName / FileDescription default to +# values inherited from the [package] section; only override what differs. +# Language is left unset (neutral) so i18n can be added later. +[package.metadata.winresource] +CompanyName = "Project Hazelita" +FileDescription = "CLI client for Ret2Shell CTF Platform" +Comments = "YUME∞MITA" + +[build-dependencies] +winresource = "0.1" + [dependencies] clap = { version = "4", features = ["derive"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "stream", "multipart"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -toml = "0.8" +toml = "1.1" thiserror = "2" tokio = { version = "1", features = ["full"] } -tabled = "0.18" -termimad = "0.31" +tabled = "0.21" +termimad = "0.35" colored = "3" chrono = { version = "0.4", features = ["serde"] } ring = "0.17" hex = "0.4" -base64 = "0.22" +base64 = "0.23" dirs = "6" rpassword = "7" -dialoguer = "0.11" -indicatif = "0.17" +dialoguer = "0.12" +indicatif = "0.18" futures-util = "0.3" +figlet-rs = "1.0" clap_complete = "4" -rustyline = { version = "16", default-features = false } +rustyline = { version = "18", default-features = false } shell-words = "1" unicode-width = "0.2" diff --git a/README.md b/README.md index d30dd9c..d2819ba 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ CLI client for [Ret2Shell](https://github.com/ret2shell/ret2shell) CTF platform. -[![Typing SVG](https://readme-typing-svg.demolab.com?font=Cascadia+Code&duration=2000&pause=800¢er=true&vCenter=true&width=850&lines=A+legend+from+long+ago+now+will+never+leave+my+head.;The+air+is+cool+and+night+is+coming%2C+as+the+calm+Rhine+gently+flows.;Up+high+on+a+ledge+sitting+is+a+maiden+most+marvelously+fair%2C;Combing+her+hair+with+a+golden+comb%2C+singing+as+well.;It+was+a+marvelous+and+spellbinding+melody.;The+boatman%2C+seized+by+wild+yearning+guides+his+small+raft+downstream.;His+eyes+not+at+the+rocky+ledge%2C+but+rather+high+up+into+the+sky%2C;The+waves+devour+the+boat+along+with+the+boatman+in+the+end.;And+this+by+her+song's+sheer+power;fair+Lorelei+has+done.)](https://git.io/typing-svg) +[![Typing SVG](https://readme-typing-svg.demolab.com?font=Cascadia+Code&duration=3000&pause=1000¢er=true&vCenter=true&height=40&lines=%E5%A3%B0%E9%AB%98%E3%82%89%E3%81%8B%E3%81%AB%E3%80%81%E5%AE%A3%E8%AA%93%E5%B8%83%E5%91%8A%E3%81%A0;%E5%A4%96%E9%87%8E%E3%81%AF%E3%81%84%E3%82%89%E3%81%AA%E3%81%84;%E6%8C%87%E5%92%A5%E3%81%88%E3%81%A6%E3%81%8A%E9%9D%99%E3%81%8B%E3%81%AB;%E6%84%9B%E3%82%92%E7%9F%A5%E3%82%89%E3%81%AA%E3%81%84%E6%99%82%E4%BB%A3%E3%81%AF%E3%81%84%E3%82%89%E3%81%AA%E3%81%84;%E3%81%A8%E3%81%8D%E3%82%81%E3%81%8B%E3%81%9B%E3%81%A6+NewType)](https://git.io/typing-svg) @@ -97,7 +97,15 @@ nix build .#ret2cli ## ⚙️ Configuration -配置文件位于 `~/.config/ret2cli/config.toml`。首次运行或文件不存在时,客户端使用一个空的 `default` profile,登录或添加 profile 时自动建档。 +配置文件默认位于各平台的用户配置目录: + +| 平台 | 路径 | +| --- | --- | +| Linux | `~/.config/ret2cli/config.toml`(`$XDG_CONFIG_HOME` 优先) | +| macOS | `~/Library/Application Support/ret2cli/config.toml` | +| Windows | `%APPDATA%\ret2cli\config.toml` | + +首次运行或文件不存在时,客户端使用一个空的 `default` profile,登录或添加 profile 时自动建档。 ```toml active_profile = "default" diff --git a/USAGE.md b/USAGE.md index 2014226..7804716 100644 --- a/USAGE.md +++ b/USAGE.md @@ -12,7 +12,7 @@ cargo build --release 开发时可以用 `cargo run -- <参数>` 代替已安装的 `ret2cli`。 -v1.0.x 发布线的 codename 为 **LORELEI**。正式 GitHub Actions 构建执行 `ret2cli --version` 时,还会显示 `1.0.x+build...g` 形式的构建元数据;本地构建只显示 Cargo.toml 中的规范版本。 +`ret2cli --version` 显示规范版本与发布线 codename,例如 `1.1.0 (Bloom in Two)`。正式 GitHub Actions 构建还会在版本后附加 `+build...g` 形式的构建元数据;本地构建只显示 Cargo.toml 中的规范版本。 ## 交互式命令提示符 @@ -23,9 +23,14 @@ ret2cli ret2cli interactive ``` -启动后会显示 `账号@profile:比赛ID $` 形式的彩色动态提示符。未登录时账号为 `anonymous`,未选择比赛时比赛为 `none`。直接输入 one-line 命令时省略开头的 `ret2cli` 即可,也可以原样粘贴包含 `ret2cli` 的命令: +启动时先显示 `figlet` 大字体渲染的 `Ret2CLI` 横幅,在支持颜色的终端上带 `lolcat` 式彩虹渐变(纯 Rust 实现,跨平台,不依赖系统的 figlet/lolcat;`NO_COLOR`、`TERM=dumb` 或 stdout 非终端时显示无颜色版本)。随后是 `账号@profile:比赛ID $` 形式的彩色动态提示符。未登录时账号为 `anonymous`,未选择比赛时比赛为 `none`。直接输入 one-line 命令时省略开头的 `ret2cli` 即可,也可以原样粘贴包含 `ret2cli` 的命令: ```text + ____ _ ____ ____ _ ___ +| _ \ ___| |_|___ \ / ___| | |_ _| +| |_) / _ \ __| __) | | | | | | +| _ < __/ |_ / __/| |___| |___ | | +|_| \_\___|\__|_____|\____|_____|___| Ret2CLI 1.0.0 (LORELEI) interactive shell Type "help" for commands, "context" for the active context, or "exit" to leave. limityrochen@default:22 $ game list @@ -162,8 +167,6 @@ ret2cli game challenge instance renew 'Pyjail 6' `instance start` / `stop` 在执行前先查询实例状态:实例已启动时 `start` 直接报告 already started 并返回成功(不触发 Ret2Shell 的 60 秒冷却);实例未启动时 `stop` 报告 not running 而不谎报 stopped。`instance status` 显示实例的 pod 状态、剩余时间与续期次数(剩余时间 = 创建时间 + (续期次数 + 1) 小时)。`instance renew` 为运行中的实例续期 1 小时;未启动时提示 not running,超过续期上限时后端返回错误。 -`game challenge start` / `stop` / `status` / `renew` 旧用法仍可用,但已弃用并会在下一个 major 版本移除,请迁移到 `game challenge instance `。 - 提交 flag 后,客户端会等待 Ret2Shell 的异步 checker 返回最终结果,而不是把刚创建的 pending submission 当成判题结果。 所有题目命令都可用 `--game <比赛>` 临时覆盖当前比赛: @@ -256,7 +259,15 @@ ret2cli completion zsh --output ~/.zfunc/_ret2cli --force ## 配置 -配置位于 `~/.config/ret2cli/config.toml`: +配置文件默认位于各平台的用户配置目录: + +| 平台 | 路径 | +| --- | --- | +| Linux | `~/.config/ret2cli/config.toml`(`$XDG_CONFIG_HOME` 优先) | +| macOS | `~/Library/Application Support/ret2cli/config.toml` | +| Windows | `%APPDATA%\ret2cli\config.toml` | + +完整配置示例: ```toml active_profile = "default" diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 0000000..90c6f4c Binary files /dev/null and b/assets/icon.ico differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..559402a Binary files /dev/null and b/assets/icon.png differ diff --git a/build.rs b/build.rs index 3717aea..2490af3 100644 --- a/build.rs +++ b/build.rs @@ -28,4 +28,24 @@ fn main() { println!("cargo:rustc-env=RET2CLI_VERSION={version}"); println!("cargo:rustc-env=RET2CLI_CODENAME={release_codename}"); + + embed_windows_resources(); +} + +/// Embeds the PE resource section (version info + application icon) into +/// Windows builds. +/// +/// The build script runs on the host, so the target OS has to be read from +/// `CARGO_CFG_TARGET_OS` at runtime; `cfg!(target_os)` would report the host +/// OS and silently skip resources when cross-compiling. +fn embed_windows_resources() { + if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") { + return; + } + println!("cargo:rerun-if-changed=assets/icon.ico"); + let mut resources = winresource::WindowsResource::new(); + resources + .set_icon("assets/icon.ico") + .compile() + .unwrap_or_else(|err| panic!("failed to embed Windows PE resources: {err}")); } diff --git a/dist-workspace.toml b/dist-workspace.toml index a85fdec..c4fa48d 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -5,8 +5,8 @@ members = ["cargo:."] [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) cargo-dist-version = "0.32.0" -# Human-facing name used throughout the v1.1.x release line. -display-name = "Ret2CLI - Bloom in Two" +# Human-facing name used throughout the v2.0.x release line. +display-name = "Ret2CLI - TearJerker" # CI backends to support ci = "github" # Whether CI should trigger releases with dispatches instead of tag pushes diff --git a/docs/PRIVACY-zh.md b/docs/PRIVACY-zh.md new file mode 100644 index 0000000..d9f3893 --- /dev/null +++ b/docs/PRIVACY-zh.md @@ -0,0 +1,55 @@ +# 隐私政策 + +## 概述 + +Ret2CLI 是 Ret2Shell CTF 平台的终端客户端。本文档说明该工具存储、传输以及**不**收集哪些数据。 + +## 本地存储的数据 + +Ret2CLI 仅在您的设备上存储一个配置文件: + +| 平台 | 路径 | +| ------ | ------ | +| Linux | `~/.config/ret2cli/config.toml` | +| macOS | `~/Library/Application Support/ret2cli/config.toml` | +| Windows | `%APPDATA%\ret2cli\config.toml` | + +配置文件可能包含: + +- **API 令牌** — 您的 Ret2Shell 账户 Bearer Token,以明文形式存储在文件中。 +- **电子邮箱** — 可选,与令牌一起存储用于账户标识。 +- **服务器地址** — 您所连接的 Ret2Shell 实例的 URL。 +- **界面偏好** — 分页模式、分页器程序、编辑器程序。 +- **已选比赛** — 当前选中的比赛 ID 和名称。 + +## 传输至服务器的数据 + +当您执行命令时,Ret2CLI 会向配置的 Ret2Shell 服务器发送以下内容: + +- 向配置服务器 URL 的 `/api/*` 端点发送 **HTTP 请求**。 +- 在 `Authorization` 请求头中携带 **Bearer Token**(仅在已配置令牌时)。 +- **命令数据** — 题目 ID、提交内容、战队操作等,取决于您执行的具体命令。 + +所有通信均使用 HTTPS(通过 `rustls` 实现 TLS)。不会向任何第三方服务发送数据。 + +## 不收集的数据 + +- **无遥测** — Ret2CLI 不会向任何服务器发送使用统计、崩溃报告或分析数据。 +- **无追踪** — 不使用 Cookie、设备指纹或唯一标识符。 +- **REPL 历史不写入磁盘** — 交互模式的历史记录仅保存在内存中,永不写入文件,防止意外泄露在 REPL 中输入的 flag 或令牌。 + +## 存储数据的安全措施 + +- 配置文件采用原子写入(先写临时文件再重命名),并使用文件 advisory lock 防止并发写入导致的数据损坏。 +- `--url` 命令行覆盖**不会**携带当前 profile 的令牌,防止在切换服务器实例时意外泄露凭据。 +- 编译器级别禁止 `unsafe` 代码(`#![forbid(unsafe_code)]`)。 + +## 您的控制权 + +- **查看数据**:打开上方列出的配置文件路径。 +- **删除数据**:删除配置文件。不存在其他持久化数据。 +- **撤销令牌**:使用 `ret2cli auth logout` 命令,或直接在 Ret2Shell 网页端撤销。 + +## 联系方式 + +如有隐私相关问题,请在 [GitHub Repo](https://github.com/ret2shell/ret2cli) 提交 Issue。 diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md new file mode 100644 index 0000000..83c9d4c --- /dev/null +++ b/docs/PRIVACY.md @@ -0,0 +1,55 @@ +# Privacy Policy + +## Overview + +Ret2CLI is a terminal client for the Ret2Shell CTF platform. This document describes what data the tool stores, transmits, and never collects. + +## Data Stored Locally + +Ret2CLI stores a single configuration file on your machine: + +| Platform | Path | +| ---------- | ------ | +| Linux | `~/.config/ret2cli/config.toml` | +| macOS | `~/Library/Application Support/ret2cli/config.toml` | +| Windows | `%APPDATA%\ret2cli\config.toml` | + +The configuration file may contain: + +- **API tokens** — Bearer tokens for your Ret2Shell account(s), stored in plaintext within the file. +- **Email addresses** — Optionally stored alongside tokens for account identification. +- **Server URLs** — The base URL(s) of the Ret2Shell instance(s) you connect to. +- **UI preferences** — Pager mode, pager program, and editor program. +- **Selected game** — The ID and name of your currently selected game. + +## Data Transmitted to Servers + +When you run a command, Ret2CLI sends the following to the configured Ret2Shell server: + +- **HTTP requests** to the `/api/*` endpoints of the configured server URL. +- **Bearer token** in the `Authorization` header (only if a token is configured). +- **Command payloads** — challenge IDs, submission content, team actions, etc., as required by the specific command you invoke. + +All communication uses HTTPS (TLS via `rustls`). No data is sent to any third-party service. + +## What Is NOT Collected + +- **No telemetry** — Ret2CLI does not send usage statistics, crash reports, or analytics to any server. +- **No tracking** — No cookies, no fingerprinting, no unique identifiers. +- **No REPL history on disk** — Interactive mode history is kept in memory only and is never written to a file. This prevents accidental leakage of flags or tokens typed in the REPL. + +## Security Measures for Stored Data + +- The configuration file is written atomically (write-to-temp then rename) with an advisory file lock to prevent concurrent corruption. +- The `--url` CLI override does **not** carry the profile's token, preventing accidental credential leakage when switching between server instances. +- `unsafe` code is forbidden at the compiler level (`#![forbid(unsafe_code)]`). + +## Your Control + +- **View your data**: Open the config file at the path listed above. +- **Delete your data**: Remove the config file. No other persistent data exists. +- **Revoke tokens**: Use the `ret2cli auth logout` command or revoke directly on the Ret2Shell web interface. + +## Contact + +For privacy concerns, open an issue on the [GitHub repository](https://github.com/ret2shell/ret2cli). diff --git a/docs/SECURITY-zh.md b/docs/SECURITY-zh.md new file mode 100644 index 0000000..b980ce1 --- /dev/null +++ b/docs/SECURITY-zh.md @@ -0,0 +1,70 @@ +# 安全策略 + +## 报告漏洞 + +如果您发现 Ret2CLI 的安全漏洞,请负责任地报告: + +1. **请勿**在 GitHub 上公开提交 Issue。 +2. 请通过邮件联系维护者,或使用 [GitHub 私密漏洞报告](https://github.com/LyCecilion/ret2cli/security/advisories/new)。 +3. 请包含漏洞描述、复现步骤以及建议的修复方案。 +4. 我们将尽快确认收到,并提供修复时间线。 + +## 安全架构 + +### 禁止 Unsafe 代码 + +Ret2CLI 在 crate 级别禁止 `unsafe` 代码: + +```rust +#![forbid(unsafe_code)] +``` + +该保证由编译器强制执行,并通过 CI 中的 `cargo clippy --all-targets -- -D warnings` 验证。 + +### TLS / 传输安全 + +所有网络通信仅使用 HTTPS,基于 `reqwest` 的 `rustls-tls` 特性(无系统 OpenSSL 依赖)。提供: + +- 现代 TLS(1.2/1.3),支持强密码套件。 +- 基于 Mozilla 根证书库的证书验证。 +- 不依赖宿主系统的 OpenSSL,减少供应链攻击面。 + +### 身份认证 + +- 令牌以 `Bearer` Token 形式在 `Authorization` 请求头中传输。 +- 服务器可通过 `Set-Token` 响应头刷新令牌;客户端会相应更新本地配置。 +- `--url` 命令行覆盖有意**不**携带当前 profile 的令牌,防止指向不同服务器实例时泄露凭据。 + +### 本地存储安全 + +- 配置文件存储在平台标准的用户配置目录中(路径详见 [PRIVACY-zh.md](./PRIVACY-zh.md))。 +- 写入为原子操作:内容先写入带 PID 标记的临时文件,再重命名覆盖目标文件,防止部分写入导致的数据损坏。 +- 使用 advisory 文件锁(`config.toml.lock`)序列化并发写入。 +- **注意**:令牌以明文形式存储在配置文件中。请相应保护文件权限(工具不会在操作系统默认权限之外额外设置权限)。 + +### REPL 安全性 + +- 交互模式(REPL)历史记录**永不**持久化到磁盘,防止意外存储 flag、令牌或敏感命令输出。 +- 交互提示(`confirm`、密码输入)在 `--json` 模式和非 TTY 环境下被抑制,防止在自动化管道中阻塞 stdin。 + +## 供应链安全 + +| 措施 | 工具 | +| ------ | ------ | +| 依赖许可证审计 | `cargo deny check licenses` | +| 已知漏洞检查 | `cargo audit`(advisory 数据库) | +| 严格 Lint | `cargo clippy --all-targets -- -D warnings` | +| 发布产物签名 | `cargo-dist` + GitHub Actions 签名证明 | +| 语义化版本 & 变更日志 | `release-plz` 自动化版本管理 | + +## 受支持版本 + +安全修复应用于 `main` 分支上的最新版本。旧版本不会收到回溯修复,除非漏洞严重且被明确要求回溯。 + +## 功能边界 + +Ret2CLI 永远不会实现可能破坏 CTF 比赛公平性的功能,包括但不限于: + +- 自动化 AI 解题。 +- Flag 暴力破解或枚举。 +- 绕过速率限制的批量提交。 diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..51204f0 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,70 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in Ret2CLI, please report it responsibly: + +1. **Do not** open a public GitHub issue. +2. Email the maintainer or use [GitHub's private vulnerability reporting](https://github.com/LyCecilion/ret2cli/security/advisories/new). +3. Include a description of the vulnerability, steps to reproduce, and any suggested mitigations. +4. We will acknowledge receipt ASAP and provide a timeline for a fix. + +## Security Architecture + +### No Unsafe Code + +Ret2CLI forbids `unsafe` code at the crate level: + +```rust +#![forbid(unsafe_code)] +``` + +This guarantee is enforced by the compiler and verified in CI via `cargo clippy --all-targets -- -D warnings`. + +### TLS / Transport Security + +All network communication uses HTTPS exclusively, powered by `reqwest` with the `rustls-tls` feature (no system OpenSSL dependency). This provides: + +- Modern TLS (1.2/1.3) with strong cipher suites. +- Certificate verification against the Mozilla root store. +- No dependency on the host system's OpenSSL, reducing supply-chain surface. + +### Authentication + +- Tokens are transmitted as `Bearer` tokens in the `Authorization` header. +- The server may issue token refreshes via the `Set-Token` response header; the client updates the local config accordingly. +- The `--url` CLI override intentionally does **not** carry the profile's stored token, preventing credential leakage when pointing at a different server instance. + +### Local Storage Security + +- Configuration is stored in the platform-standard user config directory (see [PRIVACY.md](./PRIVACY.md) for paths). +- Writes are atomic: content is written to a PID-tagged temp file, then renamed over the target, preventing partial-write corruption. +- An advisory file lock (`config.toml.lock`) serializes concurrent writers. +- **Note**: Tokens are stored in plaintext in the config file. Protect file permissions accordingly (the tool does not escalate permissions beyond the OS default for the config directory). + +### REPL Safety + +- Interactive mode (REPL) history is **never** persisted to disk, preventing accidental storage of flags, tokens, or sensitive command output. +- Interactive prompts (`confirm`, password input) are suppressed in `--json` mode and non-TTY environments, preventing blocking on stdin in automated pipelines. + +## Supply Chain + +| Measure | Tool | +| --------- | ------ | +| Dependency license audit | `cargo deny check licenses` | +| Known vulnerability check | `cargo audit` (advisory database) | +| Lint strictness | `cargo clippy --all-targets -- -D warnings` | +| Release artifact signing | `cargo-dist` with GitHub Actions attestations | +| SemVer & changelog | `release-plz` automated versioning | + +## Supported Versions + +Security fixes are applied to the latest release on the `main` branch. Older releases do not receive backports unless the vulnerability is critical and a backport is explicitly requested. + +## Scope Boundaries + +Ret2CLI will never implement features that could compromise CTF competition fairness, including but not limited to: + +- Automated AI-powered flag solving. +- Flag brute-forcing or enumeration. +- Batch submission bypassing rate limits. diff --git a/release.rs b/release.rs index bc29aac..c4cd813 100644 --- a/release.rs +++ b/release.rs @@ -4,6 +4,7 @@ pub fn codename_for_version(package_version: &str) -> Option<&'static str> { match (components.next(), components.next()) { (Some("1"), Some("0")) => Some("LORELEI"), (Some("1"), Some("1")) => Some("Bloom in Two"), + (Some("2"), Some("0")) => Some("TearJerker"), _ => None, } } @@ -51,10 +52,16 @@ mod tests { assert_eq!(codename_for_version("1.1.1-beta.1"), Some("Bloom in Two")); } + #[test] + fn tear_jerker_covers_the_entire_v2_0_release_line() { + assert_eq!(codename_for_version("2.0.0"), Some("TearJerker")); + assert_eq!(codename_for_version("2.0.42"), Some("TearJerker")); + assert_eq!(codename_for_version("2.0.1-beta.1"), Some("TearJerker")); + } + #[test] fn a_new_release_line_requires_an_explicit_codename() { assert_eq!(codename_for_version("1.2.0"), None); - assert_eq!(codename_for_version("2.0.0"), None); } #[test] diff --git a/src/cli.rs b/src/cli.rs index 18363ee..8ea9a71 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -135,14 +135,6 @@ pub enum ChallengeCommand { /// Manage the challenge instance #[command(subcommand)] Instance(InstanceCommand), - /// Start the challenge instance (deprecated: use `instance start`) - Start(ChallengeArgs), - /// Stop the challenge instance (deprecated: use `instance stop`) - Stop(ChallengeArgs), - /// Show the instance status (deprecated: use `instance status`) - Status(ChallengeArgs), - /// Renew the instance for another hour (deprecated: use `instance renew`) - Renew(ChallengeArgs), Files(ChallengeArgs), Download(DownloadArgs), } diff --git a/src/commands/interactive.rs b/src/commands/interactive.rs index 7406390..d5b2c6b 100644 --- a/src/commands/interactive.rs +++ b/src/commands/interactive.rs @@ -1,6 +1,7 @@ -use std::{borrow::Cow, env, io}; +use std::{borrow::Cow, env, fmt::Write as _, io, io::IsTerminal}; use clap::{CommandFactory, Parser}; +use figlet_rs::FIGlet; use rustyline::{ Editor, Helper, completion::Completer, error::ReadlineError, highlight::Highlighter, hint::Hinter, history::DefaultHistory, validate::Validator, @@ -189,6 +190,13 @@ fn parse_line(line: &str) -> Result { } fn print_banner(config: &ClientConfig) -> CliResult<()> { + if let Some(art) = banner_art() { + if colors_enabled() && io::stdout().is_terminal() { + println!("{}", rainbow(&art, rainbow_seed())); + } else { + println!("{art}"); + } + } println!("Ret2CLI {} interactive shell", crate::LONG_VERSION); println!( "Type \"help\" for commands, \"context\" for the active context, or \"exit\" to leave." @@ -201,6 +209,58 @@ fn print_banner(config: &ClientConfig) -> CliResult<()> { Ok(()) } +/// Render the `figlet Ret2CLI` banner with the embedded standard `FIGlet` font. +fn banner_art() -> Option { + let art = FIGlet::standard().ok()?.convert("Ret2CLI")?.to_string(); + Some(art.trim_end().to_owned()) +} + +/// A pseudo-random rainbow seed derived from the system clock, so every +/// session starts at a different point of the color cycle like `lolcat` does. +fn rainbow_seed() -> f64 { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |duration| duration.subsec_nanos()); + f64::from(nanos) +} + +/// Rainbow-color `text` the way `lolcat` does: each character is printed in a +/// truecolor hue that advances with the character index, and each new line +/// starts one hue step further than the line before. +fn rainbow(text: &str, seed: f64) -> String { + const SPREAD: f64 = 3.0; + const FREQUENCY: f64 = 0.1; + const HUE_STEP: f64 = std::f64::consts::TAU / 3.0; + + let mut out = String::with_capacity(text.len() + 32); + let mut line_offset = 0.0; + let mut color_seed = seed; + for character in text.chars() { + if character == '\n' { + out.push('\n'); + line_offset += 1.0; + color_seed = seed + line_offset; + continue; + } + let angle = FREQUENCY * color_seed / SPREAD; + let red = hue_channel(angle.sin()); + let green = hue_channel((angle + HUE_STEP).sin()); + let blue = hue_channel((angle + 2.0 * HUE_STEP).sin()); + let _ = write!(out, "\x1b[38;2;{red};{green};{blue}m{character}"); + color_seed += 1.0; + } + out.push_str("\x1b[39m"); + out +} + +/// Convert a lolcat hue channel in [1.0, 255.0] to a byte. The value is +/// mathematically in range (sin ∈ [-1, 1] after `mul_add`), so the truncating +/// cast never loses data. +#[allow(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] +fn hue_channel(intensity: f64) -> u8 { + intensity.mul_add(127.0, 128.0) as u8 +} + fn build_prompt(config: &ClientConfig) -> CliResult { let profile_name = config.active_profile_name(None)?; let profile = config.active_profile_resolved(None)?; @@ -453,4 +513,32 @@ mod tests { assert!(lines[1].contains("account=anonymous") && lines[1].contains("email=—")); assert_eq!(lines[2], "game=none"); } + + #[test] + fn figlet_banner_renders_five_lines_of_ascii_art() { + let art = banner_art().unwrap(); + let lines: Vec<_> = art.lines().collect(); + assert_eq!(lines.len(), 5, "Ret2CLI glyphs are five rows tall"); + assert!(lines.iter().all(|line| !line.trim().is_empty())); + assert!(art.contains('_') && art.contains('|')); + } + + #[test] + fn rainbow_matches_lolcat_hue_math_for_a_fixed_seed() { + // lolcat: i = frequency * seed / spread; channel = sin(i + phase) * 127 + 128 + assert_eq!(rainbow("A", 0.0), "\x1b[38;2;128;237;18mA\x1b[39m"); + } + + #[test] + fn rainbow_advances_the_hue_per_character_and_per_line() { + assert_eq!( + rainbow("AB\nCD", 0.0), + "\x1b[38;2;128;237;18mA\x1b[38;2;132;235;15mB\n\x1b[38;2;132;235;15mC\x1b[38;2;136;233;14mD\x1b[39m" + ); + // Consecutive blank lines each advance the line-start hue by one step. + assert_eq!( + rainbow("A\n\nB", 0.0), + "\x1b[38;2;128;237;18mA\n\n\x1b[38;2;136;233;14mB\x1b[39m" + ); + } } diff --git a/src/lib.rs b/src/lib.rs index b5b849e..5dbb6fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -193,14 +193,6 @@ async fn dispatch_network( } } -/// Warn that a flat challenge instance subcommand is deprecated in favor of `instance `. -fn warn_deprecated_instance(command: &str) { - eprintln!( - "warning: `game challenge {command}` is deprecated and will be removed in the next major \ - release; use `game challenge instance {command}` instead" - ); -} - async fn dispatch_challenge( client: &mut Client, config: &mut ClientConfig, @@ -238,22 +230,6 @@ async fn dispatch_challenge( commands::challenge::renew(client, config, args, json, profile_name).await } }, - ChallengeCommand::Start(args) => { - warn_deprecated_instance("start"); - commands::challenge::start(client, config, args, json, profile_name).await - } - ChallengeCommand::Stop(args) => { - warn_deprecated_instance("stop"); - commands::challenge::stop(client, config, args, json, profile_name).await - } - ChallengeCommand::Status(args) => { - warn_deprecated_instance("status"); - commands::challenge::status(client, config, args, json, profile_name).await - } - ChallengeCommand::Renew(args) => { - warn_deprecated_instance("renew"); - commands::challenge::renew(client, config, args, json, profile_name).await - } ChallengeCommand::Files(args) => { commands::challenge::files(client, config, args, json, profile_name).await } @@ -448,10 +424,6 @@ mod tests { "game challenge instance stop pwn", "game challenge instance status pwn", "game challenge instance renew pwn", - "game challenge start pwn", - "game challenge stop pwn", - "game challenge status pwn", - "game challenge renew pwn", "game challenge files pwn", "game challenge download pwn --file attachment.zip --output task.zip", "game team list",