From df279743596c51cc940b6d1fa3c68ec0b62b8cf2 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 23 Oct 2026 08:56:08 -0300 Subject: [PATCH] =?UTF-8?q?App=20desktop=20(Tauri),=20GitHub=20Actions=20d?= =?UTF-8?q?e=20CI/release,=20eleva=C3=A7=C3=A3o=20autom=C3=A1tica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move a UI de crates/sabia-cli/src/web/ para apps/gui/ui/: fonte única servida tanto pelo `sabia ui` quanto pelo app desktop. - sabia-core::engine::Engine centraliza sessões de scan/monitor/update — usado pelo servidor local da CLI e pelo app Tauri, sem duplicar lógica. - apps/gui/src-tauri: app desktop Tauri 2, mesma interface, comandos IPC espelhando a API HTTP. Pede elevação de administrador/root automaticamente ao abrir (modo monitor e captura crua exigem): manifesto requireAdministrator no Windows (UAC antes mesmo do app abrir), pkexec no Linux e osascript no macOS, com reinício automático e aviso + botão de nova tentativa na interface se for negada. - Ícone gerado a partir do logo (apps/gui/src-tauri/icons/source.png). - .github/workflows/ci.yml: fmt, clippy, testes (Linux/Windows/macOS), auditoria de dependências, compilação do app desktop. - .github/workflows/release.yml: ao empurrar uma tag vX.Y.Z, builda a CLI (Linux x86_64/aarch64, macOS x86_64/aarch64, Windows x86_64) e o app desktop pras mesmas plataformas, publica tudo numa release do GitHub com checksums SHA-256. - sabia-core::update: corrige o nome do asset esperado (casava pelo nome fixo, mas o release empacota com a versão no nome) e a extração (tar.gz no Linux/macOS, zip no Windows). - LICENSE-MIT, LICENSE-APACHE, CHANGELOG.md com a entrada 0.1.0. - cargo fmt + clippy (-D warnings) limpos em todo o workspace. --- .github/workflows/ci.yml | 71 ++++++ .github/workflows/release.yml | 146 ++++++++++++ CHANGELOG.md | 23 ++ LICENSE-APACHE | 202 +++++++++++++++++ LICENSE-MIT | 21 ++ README.md | 12 +- apps/gui/src-tauri/Cargo.toml | 28 +++ apps/gui/src-tauri/build.rs | 13 ++ apps/gui/src-tauri/icons/source.png | Bin 0 -> 94709 bytes apps/gui/src-tauri/src/main.rs | 190 ++++++++++++++++ apps/gui/src-tauri/tauri.conf.json | 39 ++++ apps/gui/src-tauri/windows-app-manifest.xml | 14 ++ .../sabia-cli/src/web => apps/gui/ui}/app.css | 0 .../sabia-cli/src/web => apps/gui/ui}/app.js | 63 ++++++ .../src/web => apps/gui/ui}/index.html | 6 + .../src/web => apps/gui/ui}/sabia-logo.svg | 0 crates/sabia-cli/src/main.rs | 60 +++-- crates/sabia-cli/src/web/mod.rs | 210 ++++++++---------- crates/sabia-core/src/audit.rs | 5 +- crates/sabia-core/src/capture.rs | 2 +- crates/sabia-core/src/dot11.rs | 37 ++- crates/sabia-core/src/engine.rs | 139 ++++++++++++ crates/sabia-core/src/iface.rs | 13 +- crates/sabia-core/src/lib.rs | 1 + crates/sabia-core/src/netlink.rs | 16 +- crates/sabia-core/src/pcapwriter.rs | 4 +- crates/sabia-core/src/radiotap.rs | 21 +- crates/sabia-core/src/scan.rs | 35 +-- crates/sabia-core/src/update.rs | 88 ++++++-- 29 files changed, 1268 insertions(+), 191 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 apps/gui/src-tauri/Cargo.toml create mode 100644 apps/gui/src-tauri/build.rs create mode 100644 apps/gui/src-tauri/icons/source.png create mode 100644 apps/gui/src-tauri/src/main.rs create mode 100644 apps/gui/src-tauri/tauri.conf.json create mode 100644 apps/gui/src-tauri/windows-app-manifest.xml rename {crates/sabia-cli/src/web => apps/gui/ui}/app.css (100%) rename {crates/sabia-cli/src/web => apps/gui/ui}/app.js (75%) rename {crates/sabia-cli/src/web => apps/gui/ui}/index.html (79%) rename {crates/sabia-cli/src/web => apps/gui/ui}/sabia-logo.svg (100%) create mode 100644 crates/sabia-core/src/engine.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e595d1b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -D warnings + +jobs: + lint: + name: Formatação e clippy + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all -- --check + - run: cargo clippy --workspace --all-targets + + test: + name: Testes (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --workspace + + audit: + name: Auditoria de dependências + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + gui: + name: Compila o app desktop (Linux) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: apps/gui/src-tauri + - uses: actions/setup-node@v7 + with: + node-version: 22 + - name: Dependências do Tauri + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + - name: Gera os ícones + run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons + - run: cargo check --manifest-path apps/gui/src-tauri/Cargo.toml + env: + RUSTFLAGS: "" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d7d2266 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,146 @@ +name: Release + +# Publica uma release pública ao empurrar uma tag vX.Y.Z (ex.: git tag v0.1.0 && git push --tags) +on: + push: + tags: ["v*.*.*"] + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + +jobs: + create-release: + name: Cria a release + runs-on: ubuntu-24.04 + outputs: + release_id: ${{ steps.rel.outputs.result }} + steps: + - uses: actions/checkout@v7 + - name: Notas da versão (do CHANGELOG) + id: notes + run: | + v="${GITHUB_REF_NAME#v}" + v="${v%%-*}" + awk -v v="$v" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ {f=0} f' CHANGELOG.md > notes.md + [ -s notes.md ] || echo "Veja o CHANGELOG." > notes.md + - uses: actions/github-script@v9 + id: rel + with: + script: | + const fs = require('fs'); + const tag = context.ref.replace('refs/tags/', ''); + const r = await github.rest.repos.createRelease({ + owner: context.repo.owner, repo: context.repo.repo, tag_name: tag, + name: `Sabiá ${tag}`, body: fs.readFileSync('notes.md', 'utf8'), + draft: true, prerelease: tag.includes('-'), + }); + return r.data.id; + + cli: + name: CLI (${{ matrix.name }}) + needs: create-release + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - { name: linux-x86_64, os: ubuntu-24.04, target: x86_64-unknown-linux-gnu } + - { name: linux-aarch64, os: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu } + - { name: macos-aarch64, os: macos-latest, target: aarch64-apple-darwin } + - { name: macos-x86_64, os: macos-latest, target: x86_64-apple-darwin } + - { name: windows-x86_64, os: windows-latest, target: x86_64-pc-windows-msvc } + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + - run: cargo build --release --locked -p sabia-cli --target ${{ matrix.target }} + - name: Empacota + shell: bash + run: | + name="sabia-${GITHUB_REF_NAME}-${{ matrix.name }}" + mkdir "$name" + if [ "$RUNNER_OS" = "Windows" ]; then + cp target/${{ matrix.target }}/release/sabia.exe "$name/" + else + cp target/${{ matrix.target }}/release/sabia "$name/" + fi + cp README.md LICENSE-MIT LICENSE-APACHE "$name/" + if [ "$RUNNER_OS" = "Windows" ]; then + 7z a "$name.zip" "$name" >/dev/null; asset="$name.zip" + else + tar czf "$name.tar.gz" "$name"; asset="$name.tar.gz" + fi + if command -v sha256sum >/dev/null; then sha256sum "$asset" > "$asset.sha256"; else shasum -a 256 "$asset" > "$asset.sha256"; fi + echo "ASSET=$asset" >> "$GITHUB_ENV" + - name: Envia para a release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "$GITHUB_REF_NAME" "$ASSET" "$ASSET.sha256" --clobber + + gui: + name: App desktop (${{ matrix.name }}) + needs: create-release + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - { name: linux-x86_64, os: ubuntu-24.04, args: "" } + - { name: macos-aarch64, os: macos-latest, args: "--target aarch64-apple-darwin" } + - { name: macos-x86_64, os: macos-latest, args: "--target x86_64-apple-darwin" } + - { name: windows-x86_64, os: windows-latest, args: "" } + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ contains(matrix.args, 'apple') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + - uses: Swatinem/rust-cache@v2 + with: + workspaces: apps/gui/src-tauri + - uses: actions/setup-node@v7 + with: + node-version: 22 + - name: Dependências do Tauri (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + - name: Gera os ícones + run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons + - uses: tauri-apps/tauri-action@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: apps/gui/src-tauri + releaseId: ${{ needs.create-release.outputs.release_id }} + args: ${{ matrix.args }} + + publish: + name: Publica a release + needs: [create-release, cli, gui] + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - name: Notas da versão (do CHANGELOG) + run: | + v="${GITHUB_REF_NAME#v}" + v="${v%%-*}" + awk -v v="$v" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ {f=0} f' CHANGELOG.md > notes.md + [ -s notes.md ] || echo "Veja o CHANGELOG." > notes.md + - uses: actions/github-script@v9 + with: + script: | + const fs = require('fs'); + await github.rest.repos.updateRelease({ + owner: context.repo.owner, repo: context.repo.repo, + release_id: ${{ needs.create-release.outputs.release_id }}, + name: `Sabiá ${context.ref.replace('refs/tags/', '')}`, + body: fs.readFileSync('notes.md', 'utf8'), + draft: false, + }); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a9b9632 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer. + +## [0.1.0] - 2026-10-23 + +Primeira versão: CLI, app desktop e núcleo compartilhado. + +### Added +- `sabia-core`: descoberta de interfaces Wi-Fi, controle de modo monitor e + canal (via `iw`/`ip`), captura crua de quadros 802.11 por AF_PACKET (sem + libpcap), parsing de radiotap e de quadros de gerenciamento (beacon, + probe, EAPOL), agregação de APs e clientes vistos, escrita de `.pcap`, + trilha de auditoria local encadeada por hash, auto-atualização por + releases do GitHub. +- `sabia`: CLI com `iface`, `scan`, `update`, `audit-verify` e `ui` + (servidor local da interface gráfica). +- App desktop (Tauri) com a mesma interface servida pelo `sabia ui`, + pedindo elevação de administrador automaticamente ao abrir — modo + monitor e captura crua exigem root/admin. +- Interface gráfica: sidebar de interfaces, painel de captura com linha + de sinal animada, tabela de redes com barras de sinal e badges de + segurança por cor. diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..9f3a103 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sabiá contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 39b0b0e..ab327db 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ crates/sabia-core biblioteca: netlink (iw), radiotap, parsing 802.11, captura crua (AF_PACKET), auditoria, auto-atualização crates/sabia-cli binário `sabia`: comandos de terminal + servidor local da interface (`sabia ui`) -apps/gui/ui mesma página HTML/JS servida pelo `sabia ui` -apps/gui/src-tauri shell desktop (Tauri) — esqueleto +apps/gui/ui mesma página HTML/JS servida pelo `sabia ui` e pelo app desktop +apps/gui/src-tauri app desktop (Tauri) — pede administrador/root ao abrir ``` ## Uso @@ -32,8 +32,12 @@ sabia update # verifica atualização sabia update --apply # baixa e aplica a atualização ``` -Capturar quadros 802.11 exige `CAP_NET_RAW`/`CAP_NET_ADMIN` — normalmente -root (`sudo sabia ...`). +Ou baixe o app desktop em [Releases](https://github.com/lucasrguerra/sabia/releases) +— ele pede elevação de administrador/root automaticamente ao abrir, já que +modo monitor e captura crua exigem esse privilégio em qualquer sistema. + +Pela CLI, capturar quadros 802.11 exige `CAP_NET_RAW`/`CAP_NET_ADMIN` — +normalmente root (`sudo sabia ...`). ## Dependências externas diff --git a/apps/gui/src-tauri/Cargo.toml b/apps/gui/src-tauri/Cargo.toml new file mode 100644 index 0000000..34ada48 --- /dev/null +++ b/apps/gui/src-tauri/Cargo.toml @@ -0,0 +1,28 @@ +# App desktop (Tauri 2). Fica fora do workspace principal porque exige +# as bibliotecas de WebView do sistema (WebKitGTK no Linux). +[package] +name = "sabia-gui" +description = "Sabiá: interface gráfica de auditoria de Wi-Fi" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/lucasrguerra/sabia" +rust-version = "1.80" + +[workspace] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +serde_json = "1" +sabia-core = { path = "../../../crates/sabia-core" } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = true diff --git a/apps/gui/src-tauri/build.rs b/apps/gui/src-tauri/build.rs new file mode 100644 index 0000000..e9b1480 --- /dev/null +++ b/apps/gui/src-tauri/build.rs @@ -0,0 +1,13 @@ +fn main() { + #[cfg(windows)] + { + // Modo monitor e captura crua exigem administrador no Windows: o + // manifesto pede elevação (UAC) assim que o app abre. + let manifest = include_str!("windows-app-manifest.xml"); + let attrs = tauri_build::Attributes::new() + .windows_attributes(tauri_build::WindowsAttributes::new().app_manifest(manifest)); + tauri_build::try_build(attrs).expect("falha no tauri-build"); + } + #[cfg(not(windows))] + tauri_build::build(); +} diff --git a/apps/gui/src-tauri/icons/source.png b/apps/gui/src-tauri/icons/source.png new file mode 100644 index 0000000000000000000000000000000000000000..0b20ffb1c053405c87120399b667c965877fbb63 GIT binary patch literal 94709 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�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 + +#[tauri::command] +fn version(state: tauri::State<'_, State>) -> String { + state.0.version().to_string() +} + +#[tauri::command] +fn list_ifaces(state: tauri::State<'_, State>) -> Result { + let list = state.0.list_ifaces().map_err(|e| e.to_string())?; + serde_json::to_value(list).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn set_monitor(state: tauri::State<'_, State>, iface: String, enable: bool) -> Result<(), String> { + state.0.set_monitor(&iface, enable).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn start_scan(state: tauri::State<'_, State>, iface: String) -> Result<(), String> { + state.0.start_scan(&iface).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn stop_scan(state: tauri::State<'_, State>, iface: String) { + state.0.stop_scan(&iface); +} + +#[tauri::command] +fn scan_results(state: tauri::State<'_, State>, iface: String) -> Result { + serde_json::to_value(state.0.scan_results(&iface)).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn update_check(state: tauri::State<'_, State>) -> Result { + let status = state.0.update_check().map_err(|e| e.to_string())?; + serde_json::to_value(status).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn update_apply(state: tauri::State<'_, State>) -> Result { + state.0.update_apply().map_err(|e| e.to_string()) +} + +/// `true` quando o processo já tem privilégio suficiente pra tudo que o +/// Sabiá faz (modo monitor, captura crua). No Windows o manifesto já +/// exige administrador antes mesmo do app abrir, então aqui é sempre `true`. +#[tauri::command] +fn is_elevated() -> bool { + #[cfg(unix)] + { + unsafe { libc::geteuid() == 0 } + } + #[cfg(windows)] + { + true + } +} + +/// Reabre o app com privilégios de administrador (Linux/macOS). O app novo +/// encerra este quando abre; se a autorização falhar ou for cancelada, a +/// janela continua aqui e a interface mostra o motivo. +#[tauri::command] +fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> { + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + do_relaunch_elevated(app) + } + #[cfg(windows)] + { + let _ = app; + Err("o Sabiá já roda como administrador no Windows".into()) + } +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> { + use tauri::Emitter; + let exe = elevated_exe()?; + let log = std::env::temp_dir().join(format!("sabia-elevate-{}.log", std::process::id())); + let _ = std::fs::write(&log, b""); + let out = std::fs::OpenOptions::new().append(true).open(&log).map_err(|e| e.to_string())?; + let err = out.try_clone().map_err(|e| e.to_string())?; + let pid = std::process::id().to_string(); + + #[cfg(target_os = "linux")] + let mut cmd = { + let mut c = std::process::Command::new("pkexec"); + c.arg("/usr/bin/env"); + for k in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "GDK_BACKEND"] { + if let Ok(v) = std::env::var(k) { + c.arg(format!("{k}={v}")); + } + } + c.arg(format!("SABIA_REPLACE_PID={pid}")); + c.arg(&exe); + c + }; + #[cfg(target_os = "macos")] + let mut cmd = { + let shell = format!("SABIA_REPLACE_PID={} '{}' >/dev/null 2>&1 &", pid, exe.display()); + let script = format!("do shell script \"{}\" with administrator privileges", shell.replace('\\', "\\\\").replace('"', "\\\"")); + let mut c = std::process::Command::new("osascript"); + c.args(["-e", &script]); + c + }; + cmd.stdout(out).stderr(err); + let mut child = cmd.spawn().map_err(|e| format!("não foi possível pedir privilégios: {e}"))?; + std::thread::spawn(move || { + let status = child.wait(); + if matches!(&status, Ok(s) if s.success()) { + let _ = std::fs::remove_file(&log); + return; + } + let detail = std::fs::read_to_string(&log).unwrap_or_default(); + let msg = match status.ok().and_then(|s| s.code()) { + Some(126) => "Autorização cancelada.".to_string(), + Some(127) => "Não foi possível autenticar como administrador.".to_string(), + _ => format!("Não foi possível reiniciar como administrador. {}", detail.lines().last().unwrap_or("").trim()), + }; + let _ = app.emit("elevate-failed", msg); + let _ = std::fs::remove_file(&log); + }); + Ok(()) +} + +/// Caminho que o root consegue executar. No AppImage, `current_exe` aponta +/// pra dentro de um ponto de montagem FUSE do usuário, inacessível ao root: +/// usa o próprio arquivo `.AppImage` (variável `APPIMAGE`). +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn elevated_exe() -> Result { + if let Some(p) = std::env::var_os("APPIMAGE").map(std::path::PathBuf::from).filter(|p| p.is_file()) { + return Ok(p); + } + std::env::current_exe().map_err(|e| e.to_string()) +} + +fn main() { + let engine = Arc::new(Engine::new()); + tauri::Builder::default() + .manage(State(engine)) + .setup(|app| { + // Quando abrimos elevados a pedido do app normal, ele fecha o app antigo. + #[cfg(unix)] + if let Some(pid) = std::env::var("SABIA_REPLACE_PID").ok().and_then(|p| p.parse::().ok()) { + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(1200)); + // SAFETY: só envia SIGTERM ao processo anterior do próprio usuário. + unsafe { + if libc::geteuid() == 0 { + libc::kill(pid, libc::SIGTERM); + } + } + }); + } + + // Captura crua e modo monitor exigem root: pede elevação assim + // que o app abre, sem esperar o usuário procurar um botão. Se + // a autorização falhar, a janela continua aberta e a interface + // mostra o aviso (com um botão pra tentar de novo). + #[cfg(any(target_os = "linux", target_os = "macos"))] + if unsafe { libc::geteuid() != 0 } && std::env::var("SABIA_REPLACE_PID").is_err() { + let handle = app.handle().clone(); + std::thread::spawn(move || { + let _ = do_relaunch_elevated(handle); + }); + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + version, + list_ifaces, + set_monitor, + start_scan, + stop_scan, + scan_results, + update_check, + update_apply, + is_elevated, + relaunch_elevated, + ]) + .run(tauri::generate_context!()) + .expect("erro ao iniciar o Sabiá"); +} diff --git a/apps/gui/src-tauri/tauri.conf.json b/apps/gui/src-tauri/tauri.conf.json new file mode 100644 index 0000000..01f6af2 --- /dev/null +++ b/apps/gui/src-tauri/tauri.conf.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Sabiá", + "version": "0.1.0", + "identifier": "dev.sabia.app", + "build": { + "frontendDist": "../ui" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Sabiá", + "width": 1180, + "height": 780, + "minWidth": 420, + "minHeight": 560, + "resizable": true + } + ], + "security": { + "csp": "default-src 'self'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self'; frame-src 'none'; object-src 'none'" + } + }, + "bundle": { + "active": true, + "targets": "all", + "category": "Utility", + "shortDescription": "Auditoria de Wi-Fi", + "longDescription": "Sabiá audita, monitora e testa a segurança de redes Wi-Fi — modo monitor, captura de quadros e handshakes, sempre com trilha de auditoria.", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/apps/gui/src-tauri/windows-app-manifest.xml b/apps/gui/src-tauri/windows-app-manifest.xml new file mode 100644 index 0000000..9597f92 --- /dev/null +++ b/apps/gui/src-tauri/windows-app-manifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/crates/sabia-cli/src/web/app.css b/apps/gui/ui/app.css similarity index 100% rename from crates/sabia-cli/src/web/app.css rename to apps/gui/ui/app.css diff --git a/crates/sabia-cli/src/web/app.js b/apps/gui/ui/app.js similarity index 75% rename from crates/sabia-cli/src/web/app.js rename to apps/gui/ui/app.js index e20fdba..5020f77 100644 --- a/crates/sabia-cli/src/web/app.js +++ b/apps/gui/ui/app.js @@ -1,5 +1,6 @@ const params = new URLSearchParams(location.search); const token = params.get('token'); +const tauri = window.__TAURI__; const els = { list: document.getElementById('ifaceList'), @@ -24,12 +25,46 @@ const ICON_WIFI = ``; async function api(path, opts) { + if (tauri) return tauriApi(path); const url = new URL(path, location.origin); url.searchParams.set('token', token); const r = await fetch(url, opts); return r.json(); } +// No app desktop não existe servidor HTTP: as mesmas rotas viram chamadas +// IPC diretas pro núcleo, pelos comandos registrados em src-tauri/src/main.rs. +async function tauriApi(path) { + const [p, qs] = path.split('?'); + const q = new URLSearchParams(qs || ''); + const invoke = tauri.core.invoke; + try { + switch (p) { + case '/api/version': + return { version: await invoke('version') }; + case '/api/ifaces': + return await invoke('list_ifaces'); + case '/api/monitor': + await invoke('set_monitor', { iface: q.get('iface'), enable: q.get('enable') === 'true' }); + return { ok: true }; + case '/api/scan/start': + await invoke('start_scan', { iface: q.get('iface') }); + return { ok: true }; + case '/api/scan/stop': + await invoke('stop_scan', { iface: q.get('iface') }); + return { ok: true }; + case '/api/scan': + return await invoke('scan_results', { iface: q.get('iface') }); + case '/api/update/check': + return await invoke('update_check'); + default: + return { error: 'rota desconhecida' }; + } + } catch (e) { + return { ok: false, error: String(e) }; + } +} + function renderEmptyResult(message, hint) { els.resultArea.innerHTML = `
${ICON_EMPTY}

${message}

${hint}

`; } @@ -185,7 +220,35 @@ async function poll() { setTimeout(poll, 1000); } +async function checkElevation() { + if (!tauri) return; // via `sabia ui` a elevação é responsabilidade do terminal (sudo) + const notice = document.getElementById('elevateNotice'); + const msg = document.getElementById('elevateMsg'); + const retry = document.getElementById('btnElevateRetry'); + const elevated = await tauri.core.invoke('is_elevated').catch(() => true); + if (elevated) return; + + notice.hidden = false; + msg.textContent = 'Pedindo privilégio de administrador — necessário pra modo monitor e captura…'; + retry.hidden = true; + + retry.addEventListener('click', async () => { + msg.textContent = 'Pedindo privilégio de administrador — necessário pra modo monitor e captura…'; + retry.hidden = true; + await tauri.core.invoke('relaunch_elevated').catch((e) => { + msg.textContent = String(e); + retry.hidden = false; + }); + }); + + tauri.event?.listen?.('elevate-failed', (e) => { + msg.textContent = String(e.payload || 'Não foi possível obter privilégio de administrador.'); + retry.hidden = false; + }); +} + (async function init() { + checkElevation(); const status = await api('/api/version'); els.verInfo.textContent = `v${status.version}`; await loadIfaces(); diff --git a/crates/sabia-cli/src/web/index.html b/apps/gui/ui/index.html similarity index 79% rename from crates/sabia-cli/src/web/index.html rename to apps/gui/ui/index.html index a6674ba..e1efa5b 100644 --- a/crates/sabia-cli/src/web/index.html +++ b/apps/gui/ui/index.html @@ -42,6 +42,12 @@ + +
Use somente em redes próprias ou com autorização explícita do responsável. Toda ativação de modo monitor e sessão de captura fica registrada na trilha de auditoria local. diff --git a/crates/sabia-cli/src/web/sabia-logo.svg b/apps/gui/ui/sabia-logo.svg similarity index 100% rename from crates/sabia-cli/src/web/sabia-logo.svg rename to apps/gui/ui/sabia-logo.svg diff --git a/crates/sabia-cli/src/main.rs b/crates/sabia-cli/src/main.rs index 68b450c..600b17d 100644 --- a/crates/sabia-cli/src/main.rs +++ b/crates/sabia-cli/src/main.rs @@ -1,14 +1,20 @@ mod web; use clap::{Parser, Subcommand}; -use sabia_core::{audit::AuditLog, capture::RawCapture, iface, netlink, pcapwriter::PcapWriter, scan::ScanTable}; +use sabia_core::{ + audit::AuditLog, capture::RawCapture, iface, netlink, pcapwriter::PcapWriter, scan::ScanTable, +}; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; #[derive(Parser)] -#[command(name = "sabia", about = "Sabiá — auditoria e monitoramento de redes Wi-Fi", version)] +#[command( + name = "sabia", + about = "Sabiá — auditoria e monitoramento de redes Wi-Fi", + version +)] struct Cli { #[command(subcommand)] cmd: Cmd, @@ -52,7 +58,11 @@ enum Cmd { enum IfaceAction { List, /// Liga/desliga modo monitor numa interface Wi-Fi. - Monitor { iface: String, #[arg(value_enum)] state: OnOff }, + Monitor { + iface: String, + #[arg(value_enum)] + state: OnOff, + }, } #[derive(Clone, Copy, clap::ValueEnum)] @@ -94,23 +104,31 @@ fn run(cli: Cli) -> sabia_core::Result<()> { iface::get(&name)?; // valida que é Wi-Fi antes de tocar nela netlink::set_monitor_mode(&name, enable)?; let mut log = AuditLog::open(audit_path())?; - log.record( - "monitor_mode", - &name, - &format!("{{\"enable\": {enable}}}"), - )?; - println!("{name}: modo monitor {}", if enable { "ativado" } else { "desativado" }); + log.record("monitor_mode", &name, &format!("{{\"enable\": {enable}}}"))?; + println!( + "{name}: modo monitor {}", + if enable { "ativado" } else { "desativado" } + ); } }, - Cmd::Scan { iface: name, channel, seconds, out } => { + Cmd::Scan { + iface: name, + channel, + seconds, + out, + } => { let info = iface::get(&name)?; if info.mode != iface::Mode::Monitor { eprintln!("aviso: {name} não está em modo monitor (sabia iface monitor {name} on)"); } let mut log = AuditLog::open(audit_path())?; - log.record("scan_start", &name, &format!("{{\"channel\": {channel:?}}}"))?; + log.record( + "scan_start", + &name, + &format!("{{\"channel\": {channel:?}}}"), + )?; let cap = RawCapture::open(&name)?; cap.set_read_timeout(Duration::from_millis(500))?; @@ -143,7 +161,10 @@ fn run(cli: Cli) -> sabia_core::Result<()> { break; } } - if channel.is_none() && !channels.is_empty() && last_hop.elapsed() > Duration::from_millis(300) { + if channel.is_none() + && !channels.is_empty() + && last_hop.elapsed() > Duration::from_millis(300) + { chan_idx = (chan_idx + 1) % channels.len(); let _ = netlink::set_channel(&name, channels[chan_idx]); last_hop = std::time::Instant::now(); @@ -167,16 +188,25 @@ fn run(cli: Cli) -> sabia_core::Result<()> { w.flush()?; } - log.record("scan_stop", &name, &format!("{{\"aps_found\": {}}}", table.access_points().len()))?; + log.record( + "scan_stop", + &name, + &format!("{{\"aps_found\": {}}}", table.access_points().len()), + )?; - println!("\n{:<18} {:<24} {:<8} {:<8} {:<6} {}", "BSSID", "SSID", "CANAL", "SINAL", "SEG", "CLIENTES"); + println!( + "\n{:<18} {:<24} {:<8} {:<8} {:<6} CLIENTES", + "BSSID", "SSID", "CANAL", "SINAL", "SEG" + ); for ap in table.access_points() { println!( "{:<18} {:<24} {:<8} {:<8} {:<6} {}", ap.bssid, ap.ssid.unwrap_or_default(), ap.channel.map(|c| c.to_string()).unwrap_or_default(), - ap.last_signal_dbm.map(|s| format!("{s}dBm")).unwrap_or_default(), + ap.last_signal_dbm + .map(|s| format!("{s}dBm")) + .unwrap_or_default(), ap.security.unwrap_or_default(), ap.clients.len() ); diff --git a/crates/sabia-cli/src/web/mod.rs b/crates/sabia-cli/src/web/mod.rs index bf26188..e042302 100644 --- a/crates/sabia-cli/src/web/mod.rs +++ b/crates/sabia-cli/src/web/mod.rs @@ -1,36 +1,24 @@ -//! Servidor local da interface: a mesma filosofia do Guará (`guara ui`) — -//! uma única página HTML/JS servida em `127.0.0.1`, protegida por um token -//! gerado na hora, falando com o núcleo por uma API JSON simples. A mesma -//! página é o que o shell desktop (Tauri, em `apps/gui`) vai carregar depois. +//! Servidor local da interface: uma única página HTML/JS servida em +//! `127.0.0.1`, protegida por um token gerado na hora, falando com o +//! núcleo por uma API JSON simples. A mesma página em `apps/gui/ui` é o +//! que o app desktop (Tauri) carrega — só muda o transporte. -use sabia_core::{audit::AuditLog, capture::RawCapture, iface, netlink, scan::ScanTable}; +use sabia_core::engine::Engine; use std::collections::HashMap; use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::thread; -use std::time::Duration; -const INDEX_HTML: &str = include_str!("index.html"); -const APP_CSS: &str = include_str!("app.css"); -const APP_JS: &str = include_str!("app.js"); -const LOGO_SVG: &str = include_str!("sabia-logo.svg"); - -struct Session { - table: Arc>, - running: Arc, -} - -#[derive(Default)] -struct AppState { - sessions: Mutex>, -} +const INDEX_HTML: &str = include_str!("../../../../apps/gui/ui/index.html"); +const APP_CSS: &str = include_str!("../../../../apps/gui/ui/app.css"); +const APP_JS: &str = include_str!("../../../../apps/gui/ui/app.js"); +const LOGO_SVG: &str = include_str!("../../../../apps/gui/ui/sabia-logo.svg"); pub fn serve(port: u16) -> sabia_core::Result<()> { let token = gen_token(); let listener = TcpListener::bind(("127.0.0.1", port))?; - let state = Arc::new(AppState::default()); + let engine = Arc::new(Engine::new()); println!("Sabiá UI em http://127.0.0.1:{port}/?token={token}"); let _ = open_browser(&format!("http://127.0.0.1:{port}/?token={token}")); @@ -40,10 +28,10 @@ pub fn serve(port: u16) -> sabia_core::Result<()> { Ok(s) => s, Err(_) => continue, }; - let state = state.clone(); + let engine = engine.clone(); let token = token.clone(); thread::spawn(move || { - let _ = handle(stream, &state, &token); + let _ = handle(stream, &engine, &token); }); } Ok(()) @@ -51,12 +39,35 @@ pub fn serve(port: u16) -> sabia_core::Result<()> { fn gen_token() -> String { use std::time::{SystemTime, UNIX_EPOCH}; - let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); format!("{nanos:x}") } fn open_browser(url: &str) -> std::io::Result<()> { - std::process::Command::new("xdg-open").arg(url).spawn().map(|_| ()) + #[cfg(target_os = "linux")] + { + std::process::Command::new("xdg-open") + .arg(url) + .spawn() + .map(|_| ()) + } + #[cfg(target_os = "macos")] + { + std::process::Command::new("open") + .arg(url) + .spawn() + .map(|_| ()) + } + #[cfg(windows)] + { + std::process::Command::new("cmd") + .args(["/C", "start", "", url]) + .spawn() + .map(|_| ()) + } } struct Request { @@ -74,7 +85,6 @@ fn parse_request(stream: &mut TcpStream) -> std::io::Result { let method = parts.next().unwrap_or("GET").to_string(); let full_path = parts.next().unwrap_or("/").to_string(); - // consome o resto dos headers loop { let mut l = String::new(); if reader.read_line(&mut l)? == 0 || l == "\r\n" { @@ -93,28 +103,54 @@ fn parse_request(stream: &mut TcpStream) -> std::io::Result { } } let token = query.get("token").cloned(); - Ok(Request { method, path, query, token }) + Ok(Request { + method, + path, + query, + token, + }) } fn urldecode(s: &str) -> String { - s.replace('+', " ").replace("%3A", ":").replace("%2F", "/").replace("%20", " ") + s.replace('+', " ") + .replace("%3A", ":") + .replace("%2F", "/") + .replace("%20", " ") } -fn handle(mut stream: TcpStream, state: &Arc, expected_token: &str) -> std::io::Result<()> { +fn handle( + mut stream: TcpStream, + engine: &Arc, + expected_token: &str, +) -> std::io::Result<()> { let req = parse_request(&mut stream)?; if req.path == "/" { if req.token.as_deref() != Some(expected_token) { return respond(&mut stream, 401, "text/plain", b"token invalido"); } - return respond(&mut stream, 200, "text/html; charset=utf-8", INDEX_HTML.as_bytes()); + return respond( + &mut stream, + 200, + "text/html; charset=utf-8", + INDEX_HTML.as_bytes(), + ); } // Assets estáticos: não carregam dado nenhum, então não exigem token. match req.path.as_str() { "/app.css" => return respond(&mut stream, 200, "text/css", APP_CSS.as_bytes()), - "/app.js" => return respond(&mut stream, 200, "application/javascript", APP_JS.as_bytes()), - "/sabia-logo.svg" => return respond(&mut stream, 200, "image/svg+xml", LOGO_SVG.as_bytes()), + "/app.js" => { + return respond( + &mut stream, + 200, + "application/javascript", + APP_JS.as_bytes(), + ) + } + "/sabia-logo.svg" => { + return respond(&mut stream, 200, "image/svg+xml", LOGO_SVG.as_bytes()) + } _ => {} } @@ -122,10 +158,15 @@ fn handle(mut stream: TcpStream, state: &Arc, expected_token: &str) -> return respond(&mut stream, 404, "text/plain", b"not found"); } if req.token.as_deref() != Some(expected_token) { - return respond(&mut stream, 401, "application/json", b"{\"error\":\"token invalido\"}"); + return respond( + &mut stream, + 401, + "application/json", + b"{\"error\":\"token invalido\"}", + ); } - let body = api(state, &req); + let body = api(engine, &req); respond(&mut stream, 200, "application/json", body.as_bytes()) } @@ -141,102 +182,45 @@ fn respond(stream: &mut TcpStream, code: u16, ctype: &str, body: &[u8]) -> std:: Ok(()) } -fn api(state: &Arc, req: &Request) -> String { +fn api(engine: &Arc, req: &Request) -> String { match (req.method.as_str(), req.path.as_str()) { - ("GET", "/api/version") => { - format!("{{\"version\":{:?}}}", env!("CARGO_PKG_VERSION")) - } - ("GET", "/api/update/check") => { - match sabia_core::update::check(env!("CARGO_PKG_VERSION")) { - Ok(status) => serde_json::to_string(&status).unwrap_or_else(|_| "{}".into()), - Err(e) => format!("{{\"error\":{:?}}}", e.to_string()), - } - } + ("GET", "/api/version") => format!("{{\"version\":{:?}}}", engine.version()), + ("GET", "/api/update/check") => match engine.update_check() { + Ok(status) => serde_json::to_string(&status).unwrap_or_else(|_| "{}".into()), + Err(e) => format!("{{\"error\":{:?}}}", e.to_string()), + }, ("GET", "/api/ifaces") => { - let list = iface::list_wireless().unwrap_or_default(); + let list = engine.list_ifaces().unwrap_or_default(); serde_json::to_string(&list).unwrap_or_else(|_| "[]".into()) } ("POST", "/api/monitor") => { let name = req.query.get("iface").cloned().unwrap_or_default(); - let enable = req.query.get("enable").map(|v| v == "true").unwrap_or(false); - match netlink::set_monitor_mode(&name, enable) { - Ok(()) => { - if let Ok(mut log) = AuditLog::open(".sabia-audit.log") { - let _ = log.record("monitor_mode", &name, &format!("{{\"enable\":{enable}}}")); - } - "{\"ok\":true}".into() - } + let enable = req + .query + .get("enable") + .map(|v| v == "true") + .unwrap_or(false); + match engine.set_monitor(&name, enable) { + Ok(()) => "{\"ok\":true}".into(), Err(e) => format!("{{\"ok\":false,\"error\":{:?}}}", e.to_string()), } } ("POST", "/api/scan/start") => { let name = req.query.get("iface").cloned().unwrap_or_default(); - start_scan(state, name) + match engine.start_scan(&name) { + Ok(()) => "{\"ok\":true}".into(), + Err(e) => format!("{{\"ok\":false,\"error\":{:?}}}", e.to_string()), + } } ("POST", "/api/scan/stop") => { let name = req.query.get("iface").cloned().unwrap_or_default(); - stop_scan(state, &name); + engine.stop_scan(&name); "{\"ok\":true}".into() } ("GET", "/api/scan") => { let name = req.query.get("iface").cloned().unwrap_or_default(); - let sessions = state.sessions.lock().unwrap(); - match sessions.get(&name) { - Some(s) => { - let table = s.table.lock().unwrap(); - serde_json::to_string(&table.access_points()).unwrap_or_else(|_| "[]".into()) - } - None => "[]".into(), - } + serde_json::to_string(&engine.scan_results(&name)).unwrap_or_else(|_| "[]".into()) } _ => "{\"error\":\"rota desconhecida\"}".into(), } } - -fn start_scan(state: &Arc, name: String) -> String { - let mut sessions = state.sessions.lock().unwrap(); - if sessions.contains_key(&name) { - return "{\"ok\":true,\"already_running\":true}".into(); - } - let table = Arc::new(Mutex::new(ScanTable::new())); - let running = Arc::new(AtomicBool::new(true)); - - let t = table.clone(); - let r = running.clone(); - let iface_name = name.clone(); - thread::spawn(move || { - let cap = match RawCapture::open(&iface_name) { - Ok(c) => c, - Err(e) => { - eprintln!("falha ao abrir captura em {iface_name}: {e}"); - return; - } - }; - let _ = cap.set_read_timeout(Duration::from_millis(500)); - let channels = netlink::supported_channels(&iface_name).unwrap_or_default(); - let mut idx = 0usize; - let mut last_hop = std::time::Instant::now(); - let mut buf = [0u8; 65535]; - while r.load(Ordering::SeqCst) { - if !channels.is_empty() && last_hop.elapsed() > Duration::from_millis(300) { - idx = (idx + 1) % channels.len(); - let _ = netlink::set_channel(&iface_name, channels[idx]); - last_hop = std::time::Instant::now(); - } - if let Ok(Some(n)) = cap.recv(&mut buf) { - let mut table = t.lock().unwrap(); - table.ingest(&buf[..n]); - } - } - }); - - sessions.insert(name, Session { table, running }); - "{\"ok\":true}".into() -} - -fn stop_scan(state: &Arc, name: &str) { - let mut sessions = state.sessions.lock().unwrap(); - if let Some(s) = sessions.remove(name) { - s.running.store(false, Ordering::SeqCst); - } -} diff --git a/crates/sabia-core/src/audit.rs b/crates/sabia-core/src/audit.rs index ecbff7f..b980a5c 100644 --- a/crates/sabia-core/src/audit.rs +++ b/crates/sabia-core/src/audit.rs @@ -75,7 +75,10 @@ impl AuditLog { hash: hash.clone(), }; - let mut file = OpenOptions::new().create(true).append(true).open(&self.path)?; + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&self.path)?; writeln!(file, "{}", serde_json::to_string(&entry)?)?; self.last_hash = hash; diff --git a/crates/sabia-core/src/capture.rs b/crates/sabia-core/src/capture.rs index 6df6684..41a0fda 100644 --- a/crates/sabia-core/src/capture.rs +++ b/crates/sabia-core/src/capture.rs @@ -38,7 +38,7 @@ impl RawCapture { let mut sll: libc::sockaddr_ll = unsafe { mem::zeroed() }; sll.sll_family = libc::AF_PACKET as u16; - sll.sll_protocol = (ETH_P_ALL as u16).to_be(); + sll.sll_protocol = ETH_P_ALL.to_be(); sll.sll_ifindex = ifindex; let ret = unsafe { diff --git a/crates/sabia-core/src/dot11.rs b/crates/sabia-core/src/dot11.rs index d8b31f5..8ced17b 100644 --- a/crates/sabia-core/src/dot11.rs +++ b/crates/sabia-core/src/dot11.rs @@ -30,7 +30,10 @@ pub struct ManagementInfo { } pub fn mac_to_string(mac: &[u8]) -> String { - format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) + format!( + "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] + ) } /// Quadro 802.11 já sem o cabeçalho radiotap. @@ -66,7 +69,14 @@ pub fn parse(buf: &[u8]) -> Option> { if buf.len() < 24 { // sem addr1/2/3 completos (comum em alguns quadros de controle) - return Some(Frame { kind, bssid: None, src: None, dst: None, body: buf, ie_offset: 0 }); + return Some(Frame { + kind, + bssid: None, + src: None, + dst: None, + body: buf, + ie_offset: 0, + }); } let addr1: [u8; 6] = buf[4..10].try_into().ok()?; @@ -77,9 +87,20 @@ pub fn parse(buf: &[u8]) -> Option> { // Para beacon/probe response, corpo fixo de 12 bytes (timestamp + // interval + capability) antecede os information elements. - let ie_offset = if matches!(kind, FrameKind::Beacon | FrameKind::ProbeResponse) { 24 + 12 } else { 24 }; + let ie_offset = if matches!(kind, FrameKind::Beacon | FrameKind::ProbeResponse) { + 24 + 12 + } else { + 24 + }; - Some(Frame { kind, bssid: Some(bssid), src: Some(src), dst: Some(dst), body: buf, ie_offset }) + Some(Frame { + kind, + bssid: Some(bssid), + src: Some(src), + dst: Some(dst), + body: buf, + ie_offset, + }) } impl<'a> Frame<'a> { @@ -137,11 +158,9 @@ impl<'a> Frame<'a> { } } 48 => rsn_present = true, // RSN IE = WPA2/WPA3 - 221 => { - // vendor-specific; OUI 00:50:F2 tipo 1 = WPA1 - if len >= 4 && val[0..3] == [0x00, 0x50, 0xF2] && val[3] == 1 { - wpa1_present = true; - } + // vendor-specific; OUI 00:50:F2 tipo 1 = WPA1 + 221 if len >= 4 && val[0..3] == [0x00, 0x50, 0xF2] && val[3] == 1 => { + wpa1_present = true; } _ => {} } diff --git a/crates/sabia-core/src/engine.rs b/crates/sabia-core/src/engine.rs new file mode 100644 index 0000000..b6cc7ec --- /dev/null +++ b/crates/sabia-core/src/engine.rs @@ -0,0 +1,139 @@ +//! Ponte única entre o núcleo e as duas interfaces (servidor local da CLI e +//! o app desktop Tauri): ambos chamam exatamente os mesmos métodos, então +//! qualquer coisa que funcione em um funciona idêntico no outro. + +use crate::audit::AuditLog; +use crate::error::Result; +use crate::iface::{self, Interface}; +use crate::netlink; +use crate::scan::{AccessPoint, ScanTable}; +use crate::update::{self, UpdateStatus}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +struct Session { + table: Arc>, + running: Arc, +} + +pub struct Engine { + sessions: Mutex>, + audit_path: PathBuf, +} + +impl Default for Engine { + fn default() -> Self { + Self::new() + } +} + +impl Engine { + pub fn new() -> Self { + Self { + sessions: Mutex::new(HashMap::new()), + audit_path: PathBuf::from(".sabia-audit.log"), + } + } + + pub fn version(&self) -> &'static str { + env!("CARGO_PKG_VERSION") + } + + pub fn list_ifaces(&self) -> Result> { + iface::list_wireless() + } + + pub fn set_monitor(&self, name: &str, enable: bool) -> Result<()> { + netlink::set_monitor_mode(name, enable)?; + if let Ok(mut log) = AuditLog::open(&self.audit_path) { + let _ = log.record("monitor_mode", name, &format!("{{\"enable\":{enable}}}")); + } + Ok(()) + } + + #[cfg(target_os = "linux")] + pub fn start_scan(&self, name: &str) -> Result<()> { + use crate::capture::RawCapture; + use std::thread; + use std::time::Duration; + + let mut sessions = self.sessions.lock().unwrap(); + if sessions.contains_key(name) { + return Ok(()); + } + let table = Arc::new(Mutex::new(ScanTable::new())); + let running = Arc::new(AtomicBool::new(true)); + + let t = table.clone(); + let r = running.clone(); + let iface_name = name.to_string(); + thread::spawn(move || { + let cap = match RawCapture::open(&iface_name) { + Ok(c) => c, + Err(e) => { + eprintln!("falha ao abrir captura em {iface_name}: {e}"); + return; + } + }; + let _ = cap.set_read_timeout(Duration::from_millis(500)); + let channels = netlink::supported_channels(&iface_name).unwrap_or_default(); + let mut idx = 0usize; + let mut last_hop = std::time::Instant::now(); + let mut buf = [0u8; 65535]; + while r.load(Ordering::SeqCst) { + if !channels.is_empty() && last_hop.elapsed() > Duration::from_millis(300) { + idx = (idx + 1) % channels.len(); + let _ = netlink::set_channel(&iface_name, channels[idx]); + last_hop = std::time::Instant::now(); + } + if let Ok(Some(n)) = cap.recv(&mut buf) { + let mut table = t.lock().unwrap(); + table.ingest(&buf[..n]); + } + } + }); + + if let Ok(mut log) = AuditLog::open(&self.audit_path) { + let _ = log.record("scan_start", name, "{}"); + } + sessions.insert(name.to_string(), Session { table, running }); + Ok(()) + } + + /// Captura crua (AF_PACKET) só existe no Linux (ver `crate::capture`). + /// Em macOS/Windows o app compila e roda, mas a captura ainda não tem + /// backend nativo — próximo passo natural do projeto. + #[cfg(not(target_os = "linux"))] + pub fn start_scan(&self, _name: &str) -> Result<()> { + Err(crate::error::SabiaError::UnsupportedPlatform) + } + + pub fn stop_scan(&self, name: &str) { + let mut sessions = self.sessions.lock().unwrap(); + if let Some(s) = sessions.remove(name) { + s.running.store(false, Ordering::SeqCst); + if let Ok(mut log) = AuditLog::open(&self.audit_path) { + let _ = log.record("scan_stop", name, "{}"); + } + } + } + + pub fn scan_results(&self, name: &str) -> Vec { + let sessions = self.sessions.lock().unwrap(); + match sessions.get(name) { + Some(s) => s.table.lock().unwrap().access_points(), + None => Vec::new(), + } + } + + pub fn update_check(&self) -> Result { + update::check(self.version()) + } + + pub fn update_apply(&self) -> Result { + let exe = std::env::current_exe()?; + update::apply(&exe) + } +} diff --git a/crates/sabia-core/src/iface.rs b/crates/sabia-core/src/iface.rs index 62a1276..e6ccc74 100644 --- a/crates/sabia-core/src/iface.rs +++ b/crates/sabia-core/src/iface.rs @@ -45,7 +45,10 @@ pub fn list_all() -> Result> { /// Lista apenas interfaces com um `phy80211` (ou seja, rádios Wi-Fi). pub fn list_wireless() -> Result> { - Ok(list_all()?.into_iter().filter(|i| i.phy.is_some()).collect()) + Ok(list_all()? + .into_iter() + .filter(|i| i.phy.is_some()) + .collect()) } pub fn get(name: &str) -> Result { @@ -94,7 +97,13 @@ fn read_interface(name: &str) -> Result { Mode::Unknown }; - Ok(Interface { name: name.to_string(), mac, phy, mode, up }) + Ok(Interface { + name: name.to_string(), + mac, + phy, + mode, + up, + }) } /// Lê o modo (`managed`/`monitor`) via `/sys/class/net//type` + `uevent` diff --git a/crates/sabia-core/src/lib.rs b/crates/sabia-core/src/lib.rs index 8f64811..179c31f 100644 --- a/crates/sabia-core/src/lib.rs +++ b/crates/sabia-core/src/lib.rs @@ -5,6 +5,7 @@ pub mod audit; pub mod dot11; +pub mod engine; pub mod error; pub mod iface; pub mod netlink; diff --git a/crates/sabia-core/src/netlink.rs b/crates/sabia-core/src/netlink.rs index cbf9235..2561ee4 100644 --- a/crates/sabia-core/src/netlink.rs +++ b/crates/sabia-core/src/netlink.rs @@ -11,10 +11,13 @@ use crate::iface::Mode; use std::process::Command; fn run(cmd: &str, args: &[&str]) -> Result { - let out = Command::new(cmd).args(args).output().map_err(|e| SabiaError::CommandFailed { - cmd: format!("{cmd} {}", args.join(" ")), - detail: e.to_string(), - })?; + let out = Command::new(cmd) + .args(args) + .output() + .map_err(|e| SabiaError::CommandFailed { + cmd: format!("{cmd} {}", args.join(" ")), + detail: e.to_string(), + })?; if !out.status.success() { let detail = String::from_utf8_lossy(&out.stderr).trim().to_string(); let detail = if detail.is_empty() { @@ -90,6 +93,9 @@ pub fn set_monitor_mode(iface: &str, enable: bool) -> Result<()> { /// Troca o canal atual da interface (precisa já estar em modo monitor). pub fn set_channel(iface: &str, channel: u32) -> Result<()> { - run("iw", &["dev", iface, "set", "channel", &channel.to_string()])?; + run( + "iw", + &["dev", iface, "set", "channel", &channel.to_string()], + )?; Ok(()) } diff --git a/crates/sabia-core/src/pcapwriter.rs b/crates/sabia-core/src/pcapwriter.rs index a40fab5..2d8c54d 100644 --- a/crates/sabia-core/src/pcapwriter.rs +++ b/crates/sabia-core/src/pcapwriter.rs @@ -31,7 +31,9 @@ impl PcapWriter { /// Grava um quadro crú (radiotap + 802.11), com timestamp de agora. pub fn write_frame(&mut self, raw: &[u8]) -> Result<()> { - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); self.out.write_all(&(now.as_secs() as u32).to_le_bytes())?; self.out.write_all(&(now.subsec_micros()).to_le_bytes())?; self.out.write_all(&(raw.len() as u32).to_le_bytes())?; // caplen diff --git a/crates/sabia-core/src/radiotap.rs b/crates/sabia-core/src/radiotap.rs index c23e310..da37b67 100644 --- a/crates/sabia-core/src/radiotap.rs +++ b/crates/sabia-core/src/radiotap.rs @@ -33,7 +33,12 @@ pub fn parse(buf: &[u8]) -> Option { if offset + 4 > buf.len() { return None; } - let word = u32::from_le_bytes([buf[offset], buf[offset + 1], buf[offset + 2], buf[offset + 3]]); + let word = u32::from_le_bytes([ + buf[offset], + buf[offset + 1], + buf[offset + 2], + buf[offset + 3], + ]); present_words.push(word); offset += 4; if word & (1 << 31) == 0 { @@ -42,7 +47,10 @@ pub fn parse(buf: &[u8]) -> Option { } let present = present_words[0]; // só o primeiro conjunto de campos padrão nos interessa - let mut info = RadiotapInfo { header_len, ..Default::default() }; + let mut info = RadiotapInfo { + header_len, + ..Default::default() + }; let mut cursor = offset; // TSFT (u64, align 8) viria antes de Flags; para o nosso subconjunto de @@ -63,11 +71,9 @@ pub fn parse(buf: &[u8]) -> Option { cursor += 8; // TSFT } if present & BIT_FLAGS != 0 { - align!(1); cursor += 1; } if present & BIT_RATE != 0 { - align!(1); cursor += 1; } if present & BIT_CHANNEL != 0 { @@ -83,11 +89,8 @@ pub fn parse(buf: &[u8]) -> Option { align!(2); cursor += 2; } - if present & BIT_ANTSIGNAL != 0 { - align!(1); - if cursor < buf.len() { - info.signal_dbm = Some(buf[cursor] as i8); - } + if present & BIT_ANTSIGNAL != 0 && cursor < buf.len() { + info.signal_dbm = Some(buf[cursor] as i8); } Some(info) diff --git a/crates/sabia-core/src/scan.rs b/crates/sabia-core/src/scan.rs index 7e71960..b15c1f6 100644 --- a/crates/sabia-core/src/scan.rs +++ b/crates/sabia-core/src/scan.rs @@ -47,24 +47,31 @@ impl ScanTable { None => return false, }; - let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); match frame.kind { FrameKind::Beacon | FrameKind::ProbeResponse => { if let Some(bssid) = frame.bssid_str() { let info = frame.management_info(); - let channel = - info.channel_ie.or_else(|| rt.freq_mhz.and_then(radiotap::freq_to_channel)); - let entry = self.aps.entry(bssid.clone()).or_insert_with(|| AccessPoint { - bssid, - ssid: None, - channel: None, - security: None, - last_signal_dbm: None, - beacons_seen: 0, - clients: Vec::new(), - last_seen_unix: now, - }); + let channel = info + .channel_ie + .or_else(|| rt.freq_mhz.and_then(radiotap::freq_to_channel)); + let entry = self + .aps + .entry(bssid.clone()) + .or_insert_with(|| AccessPoint { + bssid, + ssid: None, + channel: None, + security: None, + last_signal_dbm: None, + beacons_seen: 0, + clients: Vec::new(), + last_seen_unix: now, + }); if let Some(ssid) = info.ssid { entry.ssid = Some(ssid); } @@ -96,7 +103,7 @@ impl ScanTable { pub fn access_points(&self) -> Vec { let mut v: Vec<_> = self.aps.values().cloned().collect(); - v.sort_by(|a, b| b.beacons_seen.cmp(&a.beacons_seen)); + v.sort_by_key(|a| std::cmp::Reverse(a.beacons_seen)); v } } diff --git a/crates/sabia-core/src/update.rs b/crates/sabia-core/src/update.rs index 916d14c..06b540d 100644 --- a/crates/sabia-core/src/update.rs +++ b/crates/sabia-core/src/update.rs @@ -37,7 +37,10 @@ fn curl_text(url: &str) -> Result { let out = Command::new("curl") .args(["-fsSL", "-H", "User-Agent: sabia-updater", url]) .output() - .map_err(|e| SabiaError::CommandFailed { cmd: format!("curl {url}"), detail: e.to_string() })?; + .map_err(|e| SabiaError::CommandFailed { + cmd: format!("curl {url}"), + detail: e.to_string(), + })?; if !out.status.success() { return Err(SabiaError::CommandFailed { cmd: format!("curl {url}"), @@ -53,14 +56,23 @@ fn curl_download(url: &str, dest: &PathBuf) -> Result<()> { .arg(dest) .arg(url) .status() - .map_err(|e| SabiaError::CommandFailed { cmd: format!("curl {url}"), detail: e.to_string() })?; + .map_err(|e| SabiaError::CommandFailed { + cmd: format!("curl {url}"), + detail: e.to_string(), + })?; if !status.success() { - return Err(SabiaError::CommandFailed { cmd: format!("curl {url}"), detail: "download falhou".into() }); + return Err(SabiaError::CommandFailed { + cmd: format!("curl {url}"), + detail: "download falhou".into(), + }); } Ok(()) } -fn asset_name_for_platform() -> String { +/// Sufixo que identifica o pacote certo entre os assets da release — o +/// nome completo inclui a tag (ex.: `sabia-v0.1.0-linux-x86_64.tar.gz`), +/// então casamos pelo fim em vez do nome inteiro. +fn asset_suffix_for_platform() -> String { let os = if cfg!(target_os = "linux") { "linux" } else if cfg!(target_os = "macos") { @@ -68,8 +80,13 @@ fn asset_name_for_platform() -> String { } else { "windows" }; - let arch = if cfg!(target_arch = "x86_64") { "x86_64" } else { "aarch64" }; - format!("sabia-{os}-{arch}.tar.gz") + let arch = if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + "aarch64" + }; + let ext = if cfg!(windows) { "zip" } else { "tar.gz" }; + format!("-{os}-{arch}.{ext}") } /// Consulta a última release publicada e compara com a versão atual @@ -105,16 +122,22 @@ pub fn apply(current_exe: &PathBuf) -> Result { let body = curl_text(&url)?; let release: Release = serde_json::from_str(&body)?; - let wanted = asset_name_for_platform(); + let suffix = asset_suffix_for_platform(); let asset = release .assets .iter() - .find(|a| a.name == wanted) + .find(|a| a.name.ends_with(&suffix)) .ok_or_else(|| SabiaError::CommandFailed { cmd: "update".into(), - detail: format!("nenhum asset `{wanted}` na release {}", release.tag_name), + detail: format!( + "nenhum asset terminado em `{suffix}` na release {}", + release.tag_name + ), })?; - let checksum_asset = release.assets.iter().find(|a| a.name == format!("{}.sha256", asset.name)); + let checksum_asset = release + .assets + .iter() + .find(|a| a.name == format!("{}.sha256", asset.name)); let tmp_dir = std::env::temp_dir().join("sabia-update"); fs::create_dir_all(&tmp_dir)?; @@ -123,7 +146,11 @@ pub fn apply(current_exe: &PathBuf) -> Result { if let Some(checksum_asset) = checksum_asset { let expected = curl_text(&checksum_asset.browser_download_url)?; - let expected_hash = expected.split_whitespace().next().unwrap_or("").to_lowercase(); + let expected_hash = expected + .split_whitespace() + .next() + .unwrap_or("") + .to_lowercase(); let data = fs::read(&archive_path)?; let mut hasher = Sha256::new(); hasher.update(&data); @@ -136,13 +163,24 @@ pub fn apply(current_exe: &PathBuf) -> Result { } } - // Extrai o tar.gz e substitui o binário atual atomicamente. + // Extrai o pacote (tar.gz no Linux/macOS, zip no Windows — `tar xf` sem + // flag de compressão detecta os dois formatos nos `tar` modernos, + // incluindo o bsdtar que o Windows já traz por padrão) e substitui o + // binário atual atomicamente. let extract_dir = tmp_dir.join("extracted"); let _ = fs::remove_dir_all(&extract_dir); fs::create_dir_all(&extract_dir)?; - let status = Command::new("tar").args(["xzf"]).arg(&archive_path).arg("-C").arg(&extract_dir).status()?; + let status = Command::new("tar") + .arg("xf") + .arg(&archive_path) + .arg("-C") + .arg(&extract_dir) + .status()?; if !status.success() { - return Err(SabiaError::CommandFailed { cmd: "tar xzf".into(), detail: "extração falhou".into() }); + return Err(SabiaError::CommandFailed { + cmd: "tar xf".into(), + detail: "extração falhou".into(), + }); } let new_binary = find_binary(&extract_dir)?; @@ -161,13 +199,29 @@ pub fn apply(current_exe: &PathBuf) -> Result { Ok(release.tag_name) } +/// O binário vem dentro de uma subpasta (o CI empacota +/// `sabia-vX.Y.Z-/sabia`), então procura até um nível de +/// profundidade abaixo do diretório extraído. fn find_binary(dir: &PathBuf) -> Result { + fn is_binary(name: &std::ffi::OsStr) -> bool { + name == "sabia" || name == "sabia.exe" + } for entry in fs::read_dir(dir)? { let entry = entry?; - let name = entry.file_name(); - if name == "sabia" || name == "sabia.exe" { + if is_binary(&entry.file_name()) { return Ok(entry.path()); } + if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { + for inner in fs::read_dir(entry.path())? { + let inner = inner?; + if is_binary(&inner.file_name()) { + return Ok(inner.path()); + } + } + } } - Err(SabiaError::CommandFailed { cmd: "update".into(), detail: "binário não encontrado no pacote baixado".into() }) + Err(SabiaError::CommandFailed { + cmd: "update".into(), + detail: "binário não encontrado no pacote baixado".into(), + }) }