From e7ceabbbea296a616053f44ec8b17c3a5fb6d7c2 Mon Sep 17 00:00:00 2001 From: Antruly Date: Sun, 25 Oct 2026 03:27:18 +0800 Subject: [PATCH 1/7] chore(release): 1.0.0 Ship the first stable release: complete the 1.0 documentation set, add the documentation site, verify HTTP/2 at runtime on Windows and macOS, add a repeatable proxy benchmark, and publish a signed, checksummed release with an SBOM. - rewrite README around the 1.0 product surface, support matrix and limitations; update API/ARCHITECTURE/BUILDING/SECURITY/CONTRIBUTING/ AGENTS/help for 1.0 - add the MkDocs Material documentation site and GitHub Pages workflow, plus EVENTS (1.x event contract), EXTENDING, OPERATIONS and PERFORMANCE guides - add scripts/h2-smoke.sh and wire it into Windows/macOS CI and release; add scripts/bench.sh + bench.py and a documented baseline - release workflow: SPDX SBOM, flat-name SHA256SUMS, Sigstore keyless signature, and a workflow_dispatch dry-run that skips publishing - bump daemon, frontend, embedded UI and Windows installer versions to 1.0.0 Verified: build.sh, --version reports 1.0.0, ctest 2/2, self-check 0 failures, full smoke suite, H2 runtime smoke, i18n parity (1295 keys), benchmark baseline. Documentation site build is validated by CI. --- .github/workflows/ci.yml | 16 ++ .github/workflows/docs.yml | 53 +++++++ .github/workflows/release.yml | 45 +++++- .gitignore | 4 + AGENTS.md | 22 ++- CHANGELOG.md | 39 ++++- CMakeLists.txt | 2 +- CONTRIBUTING.md | 25 ++- README.md | 270 ++++++++++++++++++++------------- ROADMAP.md | 178 ++++++++++------------ SECURITY.md | 33 +++- docs/API.md | 4 + docs/ARCHITECTURE.md | 33 +++- docs/BUILDING.md | 42 ++++- docs/DEVELOPMENT_PLAN.zh-CN.md | 66 ++++++-- docs/EVENTS.md | 178 ++++++++++++++++++++++ docs/EXTENDING.md | 133 ++++++++++++++++ docs/OPERATIONS.md | 121 +++++++++++++++ docs/PERFORMANCE.md | 66 ++++++++ docs/index.md | 77 ++++++++++ mkdocs.yml | 69 +++++++++ packaging/windows/uvscope.iss | 2 +- scripts/bench.py | 245 ++++++++++++++++++++++++++++++ scripts/bench.sh | 17 +++ scripts/h2-smoke.sh | 147 ++++++++++++++++++ web/package-lock.json | 4 +- web/package.json | 2 +- web/src/i18n/help.ts | 28 ++-- web/src/stores/auth.ts | 2 +- web/src/views/HelpView.vue | 1 + 30 files changed, 1654 insertions(+), 270 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/EVENTS.md create mode 100644 docs/EXTENDING.md create mode 100644 docs/OPERATIONS.md create mode 100644 docs/PERFORMANCE.md create mode 100644 docs/index.md create mode 100644 mkdocs.yml create mode 100644 scripts/bench.py create mode 100644 scripts/bench.sh create mode 100644 scripts/h2-smoke.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35dff1c..01ba156 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,9 @@ jobs: mingw-w64-x86_64-libuv mingw-w64-x86_64-sqlite3 mingw-w64-x86_64-zlib + curl + openssl + nodejs python - name: Build libuvcpp and uvscope shell: msys2 {0} @@ -244,6 +247,15 @@ jobs: print("Windows ConPTY terminal smoke OK") PY + - name: HTTP/2 runtime smoke + shell: msys2 {0} + run: | + set -e + # Prefer the MSYS toolchain here: its curl/openssl/node understand + # POSIX paths, unlike the native MinGW binaries earlier on PATH. + export PATH="/usr/bin:$PWD/.deps/libuvcpp/bin:$MINGW_PREFIX/bin:$PATH" + bash ./scripts/h2-smoke.sh ./build-mingw/uvscope.exe 18820 18821 + macos: name: macOS arm64 runs-on: macos-14 @@ -272,3 +284,7 @@ jobs: cmake --build build -j"$(sysctl -n hw.ncpu)" ./build/uvscope --version ./build/uvscope --self-check + - name: HTTP/2 runtime smoke + run: | + OPENSSL_BIN="$(brew --prefix openssl@3)/bin/openssl" \ + bash ./scripts/h2-smoke.sh ./build/uvscope 18820 18821 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f910381 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,53 @@ +name: Docs + +on: + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + - ".github/workflows/docs.yml" + pull_request: + paths: + - "docs/**" + - "mkdocs.yml" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Build site + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install MkDocs Material + run: pip install mkdocs-material + - name: Build + run: mkdocs build + - uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + name: Deploy + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b223f92..c1a89de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,6 +105,9 @@ jobs: mingw-w64-x86_64-sqlite3 mingw-w64-x86_64-zlib mingw-w64-x86_64-curl + curl + openssl + nodejs - name: Build and stage shell: msys2 {0} run: | @@ -122,6 +125,9 @@ jobs: cp -f "$MINGW_PREFIX/bin/"*.dll build-mingw/ 2>/dev/null || true find .deps/libuvcpp-src/build-uvscope -name 'libllhttp.dll' -type f -exec cp -f {} build-mingw/ \; 2>/dev/null || true timeout 90 ./build-mingw/uvscope.exe --self-check + # Same HTTP/2 runtime check as CI, using the MSYS tools. + PATH="/usr/bin:$PWD/.deps/libuvcpp/bin:$MINGW_PREFIX/bin:$PATH" \ + bash ./scripts/h2-smoke.sh ./build-mingw/uvscope.exe 18820 18821 cmake --install build-mingw --prefix "$(cygpath -m "$PWD/stage")" mkdir -p stage/bin cp -f build-mingw/libllhttp.dll stage/bin/ 2>/dev/null || true @@ -180,11 +186,13 @@ jobs: -DCMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH" cmake --build build -j"$(sysctl -n hw.ncpu)" ./build/uvscope --self-check + OPENSSL_BIN="$(brew --prefix openssl@3)/bin/openssl" \ + bash ./scripts/h2-smoke.sh ./build/uvscope 18820 18821 - name: Bundle and package run: | mkdir -p packages stage cmake --install build --prefix "$PWD/stage" - # macOS 0.11.0 is a source-oriented build: libuvcpp and llhttp are + # macOS 1.0.0 is a source-oriented build: libuvcpp and llhttp are # bundled; Homebrew supplies libuv and OpenSSL. Record the linkage for # diagnostics and ad-hoc sign the executable. otool -L stage/bin/uvscope || true @@ -203,10 +211,16 @@ jobs: name: Publish release runs-on: ubuntu-latest needs: [linux, windows, macos] + permissions: + contents: write + id-token: write # Publish only when every platform job succeeded. `always()` used to publish - # a partial release even when one platform failed. - if: startsWith(github.ref, 'refs/tags/') + # a partial release even when one platform failed. A manual dispatch runs + # the SBOM/signature steps without creating a release, which lets us test + # the supply-chain pipeline before tagging. + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' steps: + - uses: actions/checkout@v4 # Download only the platform packages. `web-dist` is a build input for the # platform jobs, not a release asset; downloading every artifact used to # leak its JS/CSS into the release. @@ -226,9 +240,34 @@ jobs: with: name: macos-arm64 path: artifacts/macos-arm64 + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + path: . + format: spdx-json + output-file: artifacts/uvscope-${{ github.ref_name }}-sbom.spdx.json + upload-artifact: false + upload-release-assets: false + - uses: sigstore/cosign-installer@v3 + - name: Checksums and signature + run: | + cd artifacts + # Flat basenames: GitHub releases download all assets into one + # directory, so `sha256sum -c SHA256SUMS` must work without the + # per-platform staging folders used while building. + find . -type f \( ! -name SHA256SUMS ! -name '*.sigstore.json' \) -print0 \ + | sort -z | while IFS= read -r -d '' file; do + (cd "$(dirname "$file")" && sha256sum "$(basename "$file")") + done > SHA256SUMS + cosign sign-blob --yes --bundle SHA256SUMS.sigstore.json SHA256SUMS + echo "--- SHA256SUMS ---" + cat SHA256SUMS + echo "--- signature bundle ---" + ls -l SHA256SUMS.sigstore.json - name: Show assets run: find artifacts -type f -maxdepth 3 -printf '%p %k KB\n' - uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') with: files: artifacts/**/* generate_release_notes: true diff --git a/.gitignore b/.gitignore index bdfc211..e67231f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,10 @@ dist/ web/node_modules/ web/dist/ web/.vite/ + +# Documentation site +site/ +.cache/ npm-debug.log* yarn-error.log* diff --git a/AGENTS.md b/AGENTS.md index b8e5e65..bd99565 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,9 +10,13 @@ This repository is designed for incremental, verifiable agent work. Read - `./scripts/build.sh` - `./build/uvscope --self-check` - `bash ./scripts/smoke-test.sh ./build/uvscope 18787 18788` + - `bash ./scripts/h2-smoke.sh ./build/uvscope 18820 18821` - `npm --prefix web run build` + - `mkdocs build` - No second networking stack. All target I/O goes through libuvcpp. - Never fabricate protocol fields. Missing data is omitted or explicitly marked unknown. +- Keep `README.md`, `docs/API.md`, `docs/EVENTS.md` and the help page in sync with behavior. + Event fields may be added in 1.x but not renamed or retyped. - Do not include credentials, certificates, API keys or traffic captures in commits. ## Module template @@ -39,10 +43,18 @@ patches retired with libuvcpp 1.8.2 in `docs/LIBUVCPP_UPSTREAM.zh-CN.md` are the ## Release checklist -1. `npm --prefix web run build` +1. `npm --prefix web ci && npm --prefix web run check:i18n && npm --prefix web run build` 2. `./scripts/build.sh` 3. `./build/uvscope --self-check` -4. `bash ./scripts/smoke-test.sh ./build/uvscope` -5. CI green on Linux, Windows MinGW and macOS -6. Update `CHANGELOG.md` and tag `v` -7. Verify release assets: Linux tar/deb/rpm, Windows zip/setup.exe, macOS tar +4. `bash ./scripts/smoke-test.sh ./build/uvscope 18787 18788` +5. `bash ./scripts/h2-smoke.sh ./build/uvscope 18820 18821` +6. `mkdocs build` and `./scripts/bench.sh ./build/uvscope` (record the reference run) +7. CI green on Linux x64, Windows x64 MinGW and macOS arm64 +8. Update the version in `CMakeLists.txt`, `web/package.json`, `web/package-lock.json`, + `web/src/stores/auth.ts`, `packaging/windows/uvscope.iss` and the docs +9. Update `CHANGELOG.md` and tag `v` +10. Verify release assets: Linux x64/arm64 tar/deb/rpm, Windows zip/setup.exe, macOS tar.gz, + `SHA256SUMS`, `SHA256SUMS.sigstore.json` and the SPDX SBOM +11. Verify the published Linux artifact with `--version` and `--self-check` + +Windows arm64, HTTP/3 and native plugin loading are not part of the 1.x release matrix. diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c2dc6..7ef4519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ All notable changes to uvscope are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/). +## [1.0.0] - 2026-10-25 + +First stable release. No protocol behavior changed since 0.11.0; 1.0 adds the release +engineering, documentation and verification work required to treat uvscope as a product. + +### Added + +- Documentation site (MkDocs Material on GitHub Pages, built in CI) with guides for building, + operations and performance, plus reference pages for the API, event schema, extension boundary + and architecture. +- Release supply chain: `SHA256SUMS`, a Sigstore keyless signature over the checksums + (`SHA256SUMS.sigstore.json`) and a generated SPDX SBOM. +- HTTP/2 runtime verification on Windows MinGW and macOS arm64 CI (`scripts/h2-smoke.sh`). +- `scripts/bench.sh` / `scripts/bench.py` and a documented reverse-proxy baseline in + `docs/PERFORMANCE.md`. +- `docs/EVENTS.md` fixes the 1.x event compatibility contract; `docs/EXTENDING.md` documents + variables, request scripts, the command library and the internal child-process IPC boundary. +- `docs/OPERATIONS.md` covers the data layout, backup/restore, upgrades, isolation and hardening. + +### Changed + +- README and all project documentation reorganized for the 1.0 product surface, supported + platform matrix and known limitations. +- Help-page texts updated for backend WebSocket sessions, SOCKS5 byte counters, proxy supervision + and the isolation note. + +### Compatibility + +- The 1.x API and event schema are additive-only: existing field names, types and meanings stay + stable, and consumers ignore unknown fields and event types. +- Windows arm64, HTTP/3 stabilization and a native plugin ABI are explicitly outside the 1.x + matrix and tracked in the roadmap. + ## [0.11.0] - 2026-10-25 ### Added @@ -461,9 +494,9 @@ The libuvcpp fixes are submitted upstream as ### Fixed -- Added TLS SNI support to libuvcpp HTTP/1.1 clients via - [`patches/upstream/0001-tls-sni.patch`](patches/upstream/0001-tls-sni.patch). Without it, HTTPS - requests to modern virtual hosts fail or receive an unrelated certificate. +- Added TLS SNI support to libuvcpp HTTP/1.1 clients via the (now retired) patch + `patches/upstream/0001-tls-sni.patch`. Without it, HTTPS requests to modern virtual hosts fail + or receive an unrelated certificate. ### Known limitations diff --git a/CMakeLists.txt b/CMakeLists.txt index 4aa6eba..c3ca6a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.20) project(uvscope - VERSION 0.11.0 + VERSION 1.0.0 DESCRIPTION "A libuvcpp-powered multi-protocol web debugging workbench and gateway" HOMEPAGE_URL "https://github.com/Antruly/uvscope" LANGUAGES CXX) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91f66e1..986ac4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,9 +18,15 @@ rewrites. ./build/uvscope --self-check npm --prefix web run typecheck npm --prefix web run build +npm --prefix web run check:i18n +bash ./scripts/smoke-test.sh ./build/uvscope 18787 18788 +bash ./scripts/h2-smoke.sh ./build/uvscope 18820 18821 +python3 -m pip install mkdocs-material && mkdocs build ``` -For UI changes, include a screenshot at 1600×1000 (and a narrow viewport if relevant). +`smoke-test.sh` needs `curl`, `node`, `openssl` and `python3` for the full matrix. For UI changes, +include a screenshot at 1600×1000 (and a narrow viewport if relevant). Follow the +[Code of Conduct](CODE_OF_CONDUCT.md). ## Code guidelines @@ -40,20 +46,29 @@ For UI changes, include a screenshot at 1600×1000 (and a narrow viewport if rel ## libuvcpp changes -Changes to libuvcpp live in a patch under `patches/` and must include: +uvscope builds against the libuvcpp **v1.8.2** release SDK. One patch is currently carried on top +of that tag: `patches/upstream/0005-ws-client-handshake-headers.patch`, which adds +`uvcpp_ws_client::set_handshake_header()` so the backend WebSocket debugger can send +`Authorization`, `Origin`, `Cookie` and `Sec-WebSocket-Protocol` during the Upgrade request. The +earlier SNI, POSIX timeout, install-completeness and SOCKS5 stage patches were retired in +1.8.1/1.8.2; see [docs/LIBUVCPP_UPSTREAM.zh-CN.md](docs/LIBUVCPP_UPSTREAM.zh-CN.md). + +If a new patch is needed, it must include: - a focused explanation of the failure; - the smallest possible code change; - a reproducible test or a precise manual verification; - a note for the upstream project. -The current SNI patch exists because the HTTP client did not set `SSL_set_tlsext_host_name()`, so -modern virtual hosts either abort or serve a default certificate. +Patch files are applied in filename order by `scripts/build-libuvcpp.sh`; a patch that no longer +applies must be updated or dropped when the SDK tag moves. ## Security Do not include real credentials, private keys or customer data in issues, tests or screenshots. -See [SECURITY.md](SECURITY.md) for private reporting. +See [SECURITY.md](SECURITY.md) for private reporting. Event schemas and the public API are +documented in [docs/EVENTS.md](docs/EVENTS.md) and [docs/API.md](docs/API.md); additive changes +are expected within 1.x. ## License diff --git a/README.md b/README.md index a868ee6..eb66896 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,103 @@ # uvscope -**A libuvcpp-powered, local-first multi-protocol web debugging workbench and gateway.** +**A local-first, multi-protocol web debugging workbench and gateway.** [![CI](https://github.com/Antruly/uvscope/actions/workflows/ci.yml/badge.svg)](https://github.com/Antruly/uvscope/actions/workflows/ci.yml) +[![Docs](https://github.com/Antruly/uvscope/actions/workflows/docs.yml/badge.svg)](https://antruly.github.io/uvscope/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) uvscope puts HTTP, WebSocket, TCP, UDP, TLS, SOAP/WSDL and managed reverse proxies behind one -Vue 3 control plane. A single C++ daemon owns the event loop, SQLite store, authentication and -all outbound network traffic; long-lived services can optionally move to supervised child -processes, and the browser only renders state and sends commands. +Vue 3 control plane. A single C++ daemon owns the event loop, SQLite store, authentication and all +outbound network traffic; long-lived services can move to supervised child processes, and the +browser only renders state and sends commands. ![uvscope dashboard](docs/screenshots/dashboard.png) -## Why another web debugger? +## Why uvscope -Most tools stop at request/response. uvscope is designed around the whole handshake: +Most tools stop at request/response. uvscope is built around the whole handshake: -- **Protocol workbench** — HTTP methods and headers, WebSocket frames, raw TCP/UDP payloads, - X.509 certificate decoding and WSDL-driven SOAP calls. -- **Gateway in the same process** — launch HTTP reverse proxies, watch request traces, and stop - them without editing nginx config or restarting the daemon. -- **Local-first security** — SQLite, server-side sessions, PBKDF2 password hashing, RBAC and a - first-run administrator flow. No telemetry and no cloud account. -- **One network stack** — every outbound connection is driven by - [libuvcpp](https://github.com/Antruly/libuvcpp) 1.8.2. One event-driven C++ stack from socket - to UI. -- **A UI that keeps context** — collapsible navigation, cached multi-tab workspaces and live - telemetry built with Vue 3, Pinia and Vite. +- **One workbench for every protocol** — HTTP/1.1 and HTTP/2, WebSocket, raw TCP/UDP, X.509 and + WSDL-driven SOAP, with the same history, telemetry and export model. +- **A real gateway, not a mock** — run reverse proxies with routing rules, weighted upstreams, + retries, health checks, circuit breaking and WebSocket pass-through, then watch the traffic live. +- **Long-lived services without a second stack** — TCP/UDP listeners, SOCKS5 servers, reverse + proxies and terminal PTYs can run in child processes, all under one desired-state supervisor. +- **Local-first** — SQLite, server-side sessions, PBKDF2 password hashing, RBAC and a first-run + administrator flow. No telemetry, no cloud account, no external database. +- **One network stack** — every target connection is driven by + [libuvcpp](https://github.com/Antruly/libuvcpp) 1.8.2, from socket to UI. +- **A UI that keeps context** — collapsible navigation, cached multi-tab workspaces, live + telemetry and an offline command library built with Vue 3, Pinia and Vite. -![HTTP Studio](docs/screenshots/http-studio.png) +## Feature overview -## What is in 0.11.0 +### HTTP and APIs -| Area | Status | +| Capability | Status | | --- | --- | -| HTTP/1.1 request debugger with headers, body, timing and history | ✅ Stable | -| Request body modes: raw, form-urlencoded, multipart with files | ✅ New in 0.5.1 | -| cURL / HAR import and export | ✅ New in 0.5.2 | -| Environments with {{variables}}, saved request collections, OpenAPI/Postman import | ✅ New in 0.6.0 | -| Pre-request / post-response scripts with assertions | ✅ New in 0.6.1 | -| Service supervisor with desired-state restarts | ✅ New in 0.7.0 | -| SQLite backup download and staged restore | ✅ New in 0.5.2 | -| gzip / deflate / brotli decoding with a raw-body toggle | ✅ New in 0.4.2 | -| Large-response streaming download with progress, cancel and Range resume | ✅ New in 0.4.3; resume in 0.8.0 | -| HTTP/2 over TLS with ALPN, stream IDs, GOAWAY and frame summary | ✅ New in 0.4.0 | -| Response preview: sandboxed HTML, images, audio, video and PDF | ✅ Stable (opt-in scripts) | -| Chinese / English UI with browser-language detection and manual switch | ✅ Stable | -| Dark / light / follow-system theme | ✅ Stable | -| HTTPS with system CA bundle / explicit insecure mode | ✅ Stable | -| WebSocket console (browser-native, text + binary) | ✅ Stable | -| Backend WebSocket sessions: headers, frames, ping/pong, close codes | ✅ New in 0.4.3 | -| TCP and UDP one-shot probes (text / hex) | ✅ Stable | -| Persistent TCP sessions with repeated sends and event logs | ✅ Stable | -| TCP / UDP server listeners with sessions, datagrams and replies | ✅ Stable | -| SOCKS5 server (CONNECT / BIND / UDP ASSOCIATE) and outbound proxy | ✅ Stable | -| SOCKS5 per-session bytes, datagrams and duration | ✅ New in 0.4.0 | -| TLS certificate decoder (PEM / DER) and live peer-chain probe | ✅ New in 0.4.0 | -| TLS certificate vault: generate, import, inspect, download, delete | ✅ New in 0.4.0 | -| Local root CA, CA-signed server certificates and in-place renewal | ✅ New in 0.8.0 | -| Virtual terminal (Unix PTY / Windows ConPTY) with command library | ✅ New in 0.4.0, off by default | -| SOAP 1.1 + WSDL operation discovery | ✅ Stable | -| Managed HTTP reverse proxy with live logs and routing rules | ✅ Stable (method/host/header conditions in 0.4.4) | -| Async proxy upstream pool (slow targets do not block other requests) | ✅ New in 0.9.0 | -| Configurable upstream timeouts, worker count and hot reload | ✅ New in 0.9.0 | -| Per-upstream circuit breaker with half-open recovery | ✅ New in 0.9.0 | -| Length-prefixed JSON IPC and child-process runtime foundation | ✅ New in 0.10.0 | -| Optional per-proxy child-process isolation with supervisor restart | ✅ New in 0.10.0 | -| Optional SOCKS5 server child-process isolation | ✅ New in 0.11.0 | -| Optional TCP/UDP listener child-process isolation | ✅ New in 0.11.0 | -| Optional virtual-terminal child-process isolation | ✅ New in 0.11.0 | -| Unified desired-state supervision for proxies, listeners, SOCKS5 and terminals | ✅ New in 0.11.0 | -| Weighted upstreams with failover and retries | ✅ New in 0.4.4 | -| Per-proxy QPS, p50/p95 latency and status counts | ✅ New in 0.4.5 | -| Active upstream health checks (TCP/TLS, 10 s) | ✅ New in 0.4.5 | -| WebSocket pass-through in the reverse proxy | ✅ New in 0.4.5 | -| WebSocket upgrades honor routing rules and healthy weighted upstreams | ✅ New in 0.8.0 | -| Audit log and active session revocation | ✅ New in 0.5.0 | -| SQLite persistence, local auth, roles, first-run setup | ✅ Stable | -| Module toggles, factory reset, update check, about/help | ✅ Stable | -| HTTP/3 and QUIC transports | 🧪 Experimental — needs an OpenSSL 3.6+ SDK build | -| HTTP/3 ALPN, stream ID and peer GOAWAY metadata | ✅ New in 0.8.1 (experimental builds) | -| QUIC/HTTP3 end-to-end CI with a local HTTP/3 origin | ✅ New in 0.8.1 | -| QUIC client probe and echo server | 🧪 Experimental in the OpenSSL 3.6+/QUIC-enabled SDK | - -HTTP/2 is the supported alternate transport: selecting H2 negotiates ALPN `h2` and returns the -stream ID, GOAWAY state and the first 200 received frames. HTTP/3 and raw QUIC remain closed in -release builds until the cross-platform matrix exists. The virtual terminal module is disabled by -default and limited to admin/operator roles because it grants shell access on the daemon host. - -SOCKS5 outbound proxying is mandatory when configured: a failed handshake fails the request rather -than falling back to a direct connection. - -## Screens - -| Gateway | TLS Inspector | +| HTTP/1.1 + HTTPS debugger with headers, body, timing and history | Stable | +| Body modes: raw, form-urlencoded, multipart with files | Stable | +| cURL / HAR import and export | Stable | +| Environments with `{{variables}}`, saved collections, OpenAPI/Postman import | Stable | +| Pre-request / post-response scripts with assertions | Stable (browser-side) | +| HTTP/2 over TLS with ALPN, stream IDs, GOAWAY and frame summary | Stable | +| gzip / deflate / brotli decoding with a raw-body toggle | Stable | +| Large-response streaming download with progress, cancel and Range resume | Stable | +| Response preview: sandboxed HTML, images, audio, video and PDF | Stable (scripts opt-in) | +| SOAP 1.1 + WSDL operation discovery | Stable | +| HTTP/3 and raw QUIC probes | Experimental — not in release builds | + +### Realtime and transports + +| Capability | Status | | --- | --- | -| ![reverse proxy](docs/screenshots/reverse-proxy.png) | ![login](docs/screenshots/login.png) | +| WebSocket console: daemon-side backend sessions and browser-native mode | Stable | +| Backend WebSocket headers, subprotocols, frames, ping/pong, close codes | Stable | +| One-shot TCP and UDP probes (text / hex) | Stable | +| Persistent TCP sessions with repeated sends and event logs | Stable | +| TCP / UDP server listeners with sessions, datagrams and replies | Stable | +| SOCKS5 client for HTTP/TCP/UDP with authentication and error classification | Stable | +| SOCKS5 server (CONNECT / BIND / UDP ASSOCIATE) with authentication and session logs | Stable | +| Virtual terminal (Unix PTY / Windows ConPTY) with an offline command library | Stable — off by default | + +### Gateway and long-lived services + +| Capability | Status | +| --- | --- | +| Reverse proxy: exact / prefix / regex rules, redirects and rewrites | Stable | +| Header / method / host conditions, weighted upstreams, retries | Stable | +| Active upstream health checks, per-upstream circuit breaker, QPS/latency metrics | Stable | +| Proxy config hot reload with runtime config versions | Stable | +| WebSocket pass-through in the reverse proxy | Stable | +| Child-process isolation for proxies, listeners, SOCKS5 servers and terminals | Stable | +| Unified desired-state supervisor: restart, backoff, `restarts`/events in `/api/supervisor` | Stable | +| Managed TLS certificates, local CA, CA-signed certs and in-place renewal | Stable | + +### Operations and security + +| Capability | Status | +| --- | --- | +| SQLite persistence, first-run setup, local auth, RBAC (admin / operator / viewer) | Stable | +| Server-side sessions, active-session revocation, audit log | Stable | +| Backup download and staged restore | Stable | +| Module toggles, factory reset, update check, about/help | Stable | +| Chinese / English UI with browser-language detection and manual switch | Stable | +| Dark / light / follow-system theme | Stable | +| Linux tar/deb/rpm, Windows zip + installer, macOS tar.gz | Stable | ## Download -Prebuilt **v0.11.0** assets are on the [Releases page](https://github.com/Antruly/uvscope/releases/tag/v0.11.0): +Prebuilt **v1.0.0** assets are on the +[Releases page](https://github.com/Antruly/uvscope/releases/tag/v1.0.0). The release ships with +`SHA256SUMS`, a Sigstore keyless signature covering the checksums and a generated SPDX SBOM. - Linux x64 / arm64: `.tar.gz`, `.deb`, `.rpm` -- Windows x64 (MinGW): `.zip` and `uvscope-0.11.0-setup.exe` +- Windows x64 (MinGW): `.zip` and `uvscope-1.0.0-setup.exe` - macOS arm64: `.tar.gz` (Homebrew `libuv` and `openssl@3` are required) -The Windows executable and installer share the uvscope icon; Linux packages install the hicolor -icon set and a desktop entry with `Icon=uvscope`. +Verify a download with `sha256sum -c SHA256SUMS` and +`cosign verify-blob --bundle SHA256SUMS.sigstore.json --certificate-identity-regexp 'https://github.com/Antruly/uvscope/.*' --certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS`. ## Quick start @@ -115,7 +106,7 @@ icon set and a desktop entry with `Icon=uvscope`. - C++17 compiler, CMake ≥ 3.20, Ninja - Node.js 20+ and npm - libuv, zlib, SQLite and OpenSSL development headers -- Linux, macOS, or Windows with MinGW-w64 +- Linux, macOS, or Windows with MSYS2 MinGW-w64 On Debian/Ubuntu: @@ -133,8 +124,9 @@ cd uvscope ./build/uvscope --port 8787 --open ``` -`build.sh` clones the libuvcpp v1.8.2 release into `.deps/`, applies the minimal WebSocket -handshake-header patch from `patches/upstream/`, builds the frontend, then builds uvscope. +`build.sh` clones the libuvcpp v1.8.2 release into `.deps/`, applies the one minimal upstream +patch shipped in `patches/upstream/` (WebSocket handshake headers), builds the frontend, then +builds uvscope and runs `--self-check`. If you already have a libuvcpp 1.8.2 SDK: @@ -148,7 +140,7 @@ cmake --build build -j Open . The first visit creates the administrator account. All runtime state lives in the data directory printed at startup (`~/.local/share/uvscope` on Linux by -default). +default; override it with `--data-dir`). ### CLI @@ -156,7 +148,7 @@ default). uvscope [options] --host Listen address (default 127.0.0.1) - --port Listen port (default 8787) + --port Listen port (default 8787, 0 picks a free port) --data-dir SQLite and runtime data --web-root Built frontend directory --tls-cert Serve the workbench UI over HTTPS @@ -166,46 +158,108 @@ uvscope [options] --version Print version and exit ``` +Hidden child modes (`--proxy-child`, `--listener-child`, `--socks5-child`, `--terminal-child`, +`--ipc-child`) are spawned by the daemon and are not meant to be invoked by hand. + +## Isolation and supervision + +Proxies, TCP/UDP listeners, SOCKS5 servers and virtual terminals accept `isolated: true`. The +parent process keeps the API state, scrollback and ownership metadata; the service runs in a +child process and talks to the parent over a length-prefixed JSON IPC channel. + +All four service kinds are registered with one desired-state supervisor: + +- a crashed child is restarted with exponential backoff (2–60 s) and a new PID; +- restart counts and events are visible in `/api/supervisor` and `/api/supervisor/events`; +- stopping a service clears its desired state, so the watchdog leaves it down; +- deleting a service unregisters it before teardown; +- a normal terminal `exit` is **not** a crash — the child stays alive so scrollback and the exit + code remain readable. A crashed terminal is respawned with a fresh shell, and attached + WebSocket clients receive a `reset` frame before the new output. + +See [Service supervisor](docs/API.md#service-supervisor) for the API and +[Architecture](docs/ARCHITECTURE.md#process-model) for the process model. + ## Architecture ```text Vue 3 SPA (multi-tab, cached workspaces) - │ same-origin HTTP + cookies + │ same-origin HTTP + WebSocket + cookies ▼ uvscope daemon (C++17 / libuvcpp) - ├─ auth + RBAC + SQLite (single writer) + ├─ auth + RBAC + SQLite + audit + backup ├─ HTTP API and static asset server - ├─ protocol probes (HTTP/TCP/UDP/TLS) - └─ reverse proxy runtimes (one libuv loop each) - │ - └─ libuvcpp → libuv → sockets + ├─ probes, debug sessions and terminal sessions + ├─ desired-state supervisor + └─ optional child processes (isolated services) + ├─ reverse proxy runtime + ├─ TCP/UDP listener runtime + ├─ SOCKS5 server runtime + └─ terminal PTY / ConPTY + │ + └─ libuvcpp → libuv → sockets ``` -The daemon is the only process that opens outbound sockets in the default Daemon mode. The -WebSocket view also offers a browser-native mode, because origin, mixed-content and certificate -prompts are part of what is being debugged. +The daemon is the only process that serves the UI and owns SQLite. In the default in-process +mode it also owns every outbound socket; with isolation enabled, a service opens its own sockets +inside its child process. The WebSocket view additionally offers a browser-native mode because +origin, mixed-content and certificate prompts are part of what is being debugged. + +See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the data model and request flows. + +## Supported platforms -See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the data model and request flow. +| Platform | Release | Notes | +| --- | --- | --- | +| Linux x64 | tar.gz, deb, rpm | Primary CI and smoke matrix | +| Linux arm64 | tar.gz, deb, rpm | Release build and self-check | +| Windows x64 (MinGW) | zip, setup.exe | CI build, self-check and ConPTY smoke | +| macOS arm64 | tar.gz | CI build, self-check and HTTP/2 runtime check | +| Windows arm64 | — | Post-1.0 (MSYS2 CLANGARM64 is still preview) | +| HTTP/3 / QUIC | Experimental | Requires an OpenSSL 3.2+ SDK build; closed in release builds | -## Security notes +## Security model -- Default bind is `127.0.0.1`. Do not expose uvscope to an untrusted network. +- Default bind is `127.0.0.1`. Do not expose uvscope to an untrusted network without an + authenticating proxy. - Passwords are PBKDF2-HMAC-SHA256 (210,000 iterations); sessions are server-side and stored as SHA-256 token hashes. Cookies are `HttpOnly` + `SameSite=Lax`. -- The reverse proxy and HTTP Studio can reach internal services. Treat access to uvscope as - equivalent to code execution on the host network. -- Use the TLS Inspector and `Allow insecure TLS` only on systems you control. -- Report vulnerabilities privately as described in [`SECURITY.md`](SECURITY.md). +- Anyone who can authenticate can reach the host network, start listeners/proxies, and — when the + terminal module is enabled — obtain a shell as the daemon user. Treat access as equivalent to + code execution on that host. +- Isolated services run as the same OS user; isolation is a fault boundary, not a security + sandbox. +- Use the TLS Inspector and “Allow insecure TLS” only against systems you control. +- Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md). ## Documentation -- [Development plan (中文)](docs/DEVELOPMENT_PLAN.zh-CN.md) +- **Documentation site**: - [Architecture](docs/ARCHITECTURE.md) - [HTTP API](docs/API.md) +- [Event schema](docs/EVENTS.md) +- [Extending uvscope](docs/EXTENDING.md) +- [Performance baselines](docs/PERFORMANCE.md) +- [Operations](docs/OPERATIONS.md) - [Building and packaging](docs/BUILDING.md) - [Roadmap](ROADMAP.md) -- [Agent guide](AGENTS.md) - [Changelog](CHANGELOG.md) +- [Development plan (中文)](docs/DEVELOPMENT_PLAN.zh-CN.md) +- [Agent guide](AGENTS.md) + +## Development + +```sh +npm --prefix web ci +npm --prefix web run build # typecheck + bundle +./scripts/build.sh # SDK, frontend, daemon, self-check +bash ./scripts/smoke-test.sh ./build/uvscope 18787 18788 +./scripts/h2-smoke.sh ./build/uvscope 18820 18821 +./scripts/bench.sh ./build/uvscope +mkdocs serve # documentation site (pip install mkdocs-material) +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow and verification rules. ## License diff --git a/ROADMAP.md b/ROADMAP.md index a8382dd..d134ab6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,102 +1,82 @@ # Roadmap -uvscope is developed in small, verifiable slices. The detailed plan is in -[`docs/DEVELOPMENT_PLAN.zh-CN.md`](docs/DEVELOPMENT_PLAN.zh-CN.md). - -## 0.1.0 — first usable workbench - -- [x] Local daemon, SQLite, first-run admin, sessions, RBAC -- [x] Vue 3 shell with collapsible menu and cached tabs -- [x] HTTP/1.1 + HTTPS request debugger with history -- [x] WebSocket console -- [x] TCP/UDP one-shot probes -- [x] TLS certificate decoder -- [x] SOAP 1.1 / WSDL workbench -- [x] Managed HTTP reverse proxies with live logs -- [x] Module switches, reset, update check, about/help -- [x] CI, Linux packages, Windows installer scaffold -- [x] libuvcpp SNI, POSIX sync timeouts and install-completeness patches - ([PR #38](https://github.com/Antruly/libuvcpp/pull/38), - [#39](https://github.com/Antruly/libuvcpp/pull/39), - [#40](https://github.com/Antruly/libuvcpp/pull/40)) - -## 0.2 — debug experience - -- [x] Response preview for HTML (sandboxed), images, audio, video and PDF -- [x] Preview safety: no scripts, no remote resources by default, clear limits -- [x] Chinese/English shell, navigation, login and setup; browser-language detection and manual switch -- [x] Light / dark / system theme switching +uvscope 1.0.0 is the first stable release: the functional surface is complete, and the release +matrix, documentation and supply-chain artifacts are in place. Work after 1.0 is additive. + +The detailed history and per-slice acceptance notes are in +[`docs/DEVELOPMENT_PLAN.zh-CN.md`](docs/DEVELOPMENT_PLAN.zh-CN.md); user-facing changes are in +[`CHANGELOG.md`](CHANGELOG.md). + +## 1.0 — stable (released 2026-10-25) + +### Product surface + +- [x] HTTP/1.1 and HTTPS debugging with history, body modes, cURL/HAR import/export +- [x] HTTP/2 over TLS with ALPN, stream IDs, GOAWAY and frame summaries +- [x] WebSocket console: daemon-side backend sessions and browser-native mode +- [x] TCP/UDP probes, persistent TCP sessions and TCP/UDP listeners +- [x] TLS decoding/probing, certificate vault, local CA and CA-signed certificates +- [x] SOAP 1.1 + WSDL workbench +- [x] Managed reverse proxy: routing rules, weighted upstreams, retries, health checks, + circuit breaker, metrics, hot reload and WebSocket pass-through +- [x] SOCKS5 client and server (CONNECT / BIND / UDP ASSOCIATE) +- [x] Virtual terminal (Unix PTY / Windows ConPTY) with an offline command library +- [x] Workspaces, collections, environments, pre/post scripts and assertions +- [x] Users/RBAC, server-side sessions, audit log, backup/restore and module toggles +- [x] Chinese/English UI and light/dark/system themes + +### Isolation and supervision + +- [x] Child-process isolation for reverse proxies, TCP/UDP listeners, SOCKS5 servers and terminals +- [x] Length-prefixed JSON IPC, per-service telemetry and pid/snapshot proxies +- [x] One desired-state supervisor with backoff, restart counters and an event log +- [x] Stop/delete/module-disable/reset paths unregister before teardown +- [x] Terminal crash recovery with a fresh shell and WebSocket `reset` continuity + +### Release engineering + +- [x] Matrix: Linux x64/arm64, Windows x64 (MinGW), macOS arm64 +- [x] Linux: ctest + full smoke; Windows: self-check + ConPTY + HTTP/2 runtime smoke; + macOS: self-check + HTTP/2 runtime smoke +- [x] Performance baselines: `scripts/bench.sh` and `docs/PERFORMANCE.md` +- [x] Stable event schema and extension contract: `docs/EVENTS.md`, `docs/EXTENDING.md` +- [x] Documentation site (MkDocs Material on GitHub Pages, built in CI) +- [x] Supply chain: `SHA256SUMS`, Sigstore keyless signatures and an SPDX SBOM on every release + +## After 1.0 + +### Distribution and platform + +- [ ] Windows arm64 build and smoke (MSYS2 CLANGARM64 is still preview) +- [ ] Signed and notarized macOS `.app` bundle (the 1.x macOS asset is a Homebrew-linked tar.gz) +- [ ] More Linux package targets beyond tar.gz/deb/rpm +- [ ] Guided TLS setup for the control plane (currently `--tls-cert` / `--tls-key`) + +### Protocol depth + +- [ ] HTTP/3/QUIC: QPACK summaries, 0-RTT and connection-migration events; stabilize once the + OpenSSL 3.6+ requirement fits the default toolchain +- [ ] Dedicated WebSocket relay / pass-through listener mode +- [ ] Fully asynchronous proxy upstream client (replace the bounded blocking worker pool) +- [ ] Raw QUIC stream/datagram workbench parity with TCP/UDP + +### Operations and scale + +- [ ] Versioned SQLite migrations with explicit downgrade/rollback guidance +- [ ] Concurrent saturation benchmarks (latency at 1/10/100 connections) tracked in CI +- [ ] Optional per-child resource limits (CPU, memory, file descriptors) +- [ ] Restart budget / circuit breaker for permanently failing supervised services + +### Extensibility and UX + +- [ ] Native plugin ABI (the 1.x boundary is the HTTP API, scripts and the internal IPC contract) - [ ] Page-by-page light-theme color audit for every workbench -- [x] Raw / preview / hex switching for binary and truncated bodies - -## 0.3 — transport workbench - -- [x] Persistent TCP client sessions (send repeatedly, stream receives, close, reconnect) -- [x] TCP server listeners with session list, send and close -- [x] UDP server listeners with datagram timeline and replies -- [x] Listener persistence and optional auto-start -- [x] SOCKS5 client for HTTP/TCP/UDP with authentication and error classification -- [x] SOCKS5 server (CONNECT / BIND / UDP ASSOCIATE) with authentication and session logs -- [x] No silent direct fallback when a proxy is configured - -## 0.4 — protocol depth (0.4.0) - -0.4.0 ships HTTP/2 and the live certificate chain; the remaining items continue in 0.4.x. - -- [x] HTTP/2 over libuvcpp 1.8.2 (ALPN, stream IDs, GOAWAY, frame summary; Linux smoke) -- [ ] HTTP/2 runtime validation on the Windows and macOS CI jobs -- [ ] HTTP/3 / QUIC client with QPACK / 0-RTT / migration events (experimental; needs OpenSSL 3.2+) -- [x] HTTP/3 results expose ALPN, stream ID and peer GOAWAY state (experimental builds) -- [x] Linux CI builds OpenSSL 3.6 + QUIC SDK and runs a local HTTP/3 origin -- [~] Raw QUIC client probe and echo server (streams/datagrams; experimental build) -- [x] Compression decoding: gzip / deflate / brotli with a raw-body toggle -- [x] Large-response streaming download (512 MiB cap, progress, cancel) -- [x] Multipart upload and resumable Range downloads -- [x] Live peer certificate chain inspection -- [x] Backend WebSocket sessions (daemon-side connect, headers, ping/pong, close codes, frames) -- [ ] WebSocket backend relay / pass-through mode - -## 0.5 — gateway and team workflow - -- [x] Exact / prefix / regex routing with redirects and path rewrites -- [x] Header / method / host routing conditions -- [x] Multiple weighted upstreams and retry policy -- [x] WebSocket pass-through -- [x] Active upstream health checks -- [x] Certificate import / self-sign / selection -- [x] Per-instance QPS, latency and traffic metrics -- [x] Collections, environments and variable substitution -- [x] Pre-request / post-response scripts (browser-side, experimental) -- [x] cURL, HAR, OpenAPI and Postman import/export (HAR export is one entry) -- [x] Audit log and active session management -- [x] Backup download and staged restore (migrations still ad hoc) -- [x] Service supervisor with desired-state restarts (proxies, listeners, SOCKS5, terminals) -- [x] Proxy upstream I/O off the proxy loop (bounded per-instance worker pool) -- [x] Configurable upstream connect/request timeouts and worker count -- [x] Proxy config hot reload with runtime config versioning -- [x] Per-upstream circuit breaker with half-open recovery -- [~] Length-prefixed JSON IPC channel and child-process self-check (foundation) -- [x] Reverse-proxy child runtime with IPC start/stop/snapshot/logs -- [x] Per-proxy child isolation with supervisor restart and telemetry -- [x] SOCKS5 server child isolation with telemetry and manual restart -- [x] TCP/UDP listener child isolation with session send/close over IPC -- [x] Virtual-terminal PTY child isolation with streamed output over IPC -- [x] Process isolation + IPC + supervision for all long-lived services - -## 1.0 — stable - -- [ ] Linux x64/arm64, Windows x64/arm64 (MinGW), macOS arm64 -- [ ] Signed releases and SBOM -- [ ] Full test matrix and performance baselines -- [ ] Stable event schema and plugin boundary -- [ ] Documentation site - -## Post-0.4 additions - -- [x] Build against the libuvcpp 1.8.2 release SDK and retire the local upstream patches -- [x] Reverse proxy exact/prefix/regex redirect and rewrite rules -- [x] Live TLS peer-chain probe and managed certificate vault (generate/import/delete) -- [x] Unix virtual terminal with WebSocket, xterm.js, persistent sessions and admin control -- [x] Seeded SQLite command library with quick bar, search, custom snippets and quick pins -- [x] Windows ConPTY terminal backend -- [x] Local CA hierarchy and certificate renewal +- [ ] Broader documentation translations based on community demand +- [ ] Collections/environments sharing beyond a single workspace file + +## Shipped before 1.0 + +The 0.1–0.11 series delivered the workbench, transport, gateway, certificate and isolation +foundations. See the [changelog](CHANGELOG.md) for the release-by-release record and +[`docs/DEVELOPMENT_PLAN.zh-CN.md`](docs/DEVELOPMENT_PLAN.zh-CN.md) for the slice-level design and +verification notes (including S13–S19 for IPC, isolation and supervision). diff --git a/SECURITY.md b/SECURITY.md index 9a74d7c..7324ee6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,14 +14,24 @@ the maintainers through the repository security settings. Include: You will receive an acknowledgement as soon as possible. Do not include real credentials, production certificates or private customer data in a report. +## Supported versions + +Security fixes are provided for the latest 1.x release. The 0.x series is no longer supported. + ## Threat model uvscope is a **local developer tool**. Anyone who can authenticate to the workbench can: - make arbitrary HTTP(S), TCP and UDP requests from the host network; -- start HTTP reverse proxies on local ports; +- start HTTP reverse proxies, TCP/UDP listeners and SOCKS5 servers on local addresses; - inspect certificates and SOAP contracts; -- read stored request history. +- use the virtual terminal to run a shell as the daemon user when that module is enabled; +- read stored request history, workspace state and audit records. + +Child-process isolation is a **fault-containment boundary**, not a security sandbox: isolated +proxies, listeners, SOCKS5 servers and terminals run as the same OS user with the same filesystem +and network access. Use OS users, `ulimit`, containers or a dedicated host for untrusted +workloads. Treat access to uvscope as equivalent to code execution with the daemon user's network access. @@ -34,6 +44,12 @@ Treat access to uvscope as equivalent to code execution with the daemon user's n - Security headers include a restrictive CSP, `X-Frame-Options: DENY` and `nosniff`. - The TLS client verifies the peer certificate and hostname by default. The explicit `insecure` option disables verification for the current request only. +- The response preview is sandboxed and runs no scripts unless “Run scripts” is explicitly + enabled; remote resources are blocked in the default mode. +- Child processes are spawned from the same executable and communicate over a local IPC pipe; + there is no network-facing control channel for a child. +- Isolated children inherit the daemon user and environment. Resource limits are the operator's + responsibility. ## Hardening for shared machines @@ -48,5 +64,14 @@ Treat access to uvscope as equivalent to code execution with the daemon user's n ## Dependency vulnerabilities uvscope pins libuvcpp 1.8.2 (release SDK, plus the minimal WebSocket handshake-header patch kept -in `patches/upstream/`). If a dependency advisory applies, -open a normal issue for non-sensitive reports or use private reporting for exploit details. +in [`patches/upstream/`](patches/upstream/)). The release also publishes an SPDX SBOM, a +`SHA256SUMS` file and a Sigstore keyless signature over the checksums so consumers can audit and +verify what they run. If a dependency advisory applies, open a normal issue for non-sensitive +reports or use private reporting for exploit details. + +## Data handling + +The data directory contains the SQLite database (users, session hashes, history, workspace, +configuration and audit log), managed certificate private keys and staged downloads. Treat it as +sensitive, restrict its permissions (`0700`), and exclude it from backups that leave the machine +unless that is intended. Request and response bodies are not logged by default. diff --git a/docs/API.md b/docs/API.md index b8ac8e1..5796eb6 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,5 +1,9 @@ # HTTP API +This document describes the uvscope **1.x** HTTP API. Event payloads and their compatibility +policy are in [Event schema](EVENTS.md); extension points are in +[Extending uvscope](EXTENDING.md); deployment and recovery are in [Operations](OPERATIONS.md). + All endpoints are same-origin JSON. Authenticated endpoints use the `uvscope_session` HttpOnly cookie. Errors are `{ "ok": false, "error": "...", "code": "..." }`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a825cf4..bd9b195 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -37,7 +37,7 @@ persistence, protocol clients and the gateway runtimes. The SPA is a view over t HTTP(S) / TCP / UDP / TLS targets ``` -## Process model in 0.11.0 +## Process model - **Main daemon**: owns the UI server, SQLite, auth and all protocol clients. Long-lived services (proxies, listeners, SOCKS5 servers, isolated terminals) can move to child processes. @@ -307,14 +307,27 @@ not an HTTP 500 for the API; it is a successful probe request whose `ok` is fals - **Vue 3 / Vite / Pinia / Vue Router** — SPA. - **Inno Setup** — Windows installer. -## Known architectural gaps (0.11.0) +## Release verification matrix -- HTTP/2 works over TLS (ALPN `h2`, stream IDs, GOAWAY and frame summaries); the runtime matrix - still needs Windows and macOS coverage. HTTP/3/QUIC remain experimental and closed in release - builds. The QUIC SDK build reports H3 ALPN, stream IDs and peer GOAWAY; it does not expose an - H3 error code, QPACK summaries, 0-RTT or migration events. On Unix the SDK install carries the - custom OpenSSL lib directory in `libuvcpp.so`'s RUNPATH so the experimental daemon starts - without `LD_LIBRARY_PATH`. +| Platform | Build | Runtime verification | +| --- | --- | --- | +| Linux x64 | Release + ctest | `--self-check` and the full smoke suite (HTTP/2, listeners, SOCKS5, outbound proxy, preview, downloads, WebSocket, TLS/CA, audit, backup, workspace, supervisor) | +| Linux arm64 | Release package | `--self-check` | +| Windows x64 (MinGW) | Release package | `--self-check`, ConPTY terminal smoke, HTTP/2 runtime smoke | +| macOS arm64 | Release package | `--self-check`, HTTP/2 runtime smoke | + +Every release asset is published with `SHA256SUMS`, a Sigstore keyless signature over the +checksums (`SHA256SUMS.sigstore.json`) and a generated SPDX SBOM. Windows arm64 and the +experimental HTTP/3 build are outside the 1.x release matrix. + +## Known architectural gaps + +- HTTP/2 works over TLS (ALPN `h2`, stream IDs, GOAWAY and frame summaries) and is validated at + runtime on Linux, Windows MinGW and macOS arm64. HTTP/3/QUIC remain experimental and closed in + release builds. The QUIC SDK build reports H3 ALPN, stream IDs and peer GOAWAY; it does not + expose an H3 error code, QPACK summaries, 0-RTT or migration events. On Unix the SDK install + carries the custom OpenSSL lib directory in `libuvcpp.so`'s RUNPATH so the experimental daemon + starts without `LD_LIBRARY_PATH`. - Preview and probes still cap bodies at 4 MiB; larger responses can be streamed to a staging file and downloaded (512 MiB cap, HTTP/1.1, attachment-only). A failed transfer keeps its partial file and resumes in place with `Range`; a short body against a known `Content-Length` @@ -340,3 +353,7 @@ not an HTTP 500 for the API; it is a successful probe request whose `ok` is fals - The supervisor retries a desired service indefinitely with a 60 s backoff cap; there is no restart budget or circuit breaker for a permanently broken child. Terminal sessions do not survive a daemon restart. +- Windows arm64 is not built in 1.x, and there is no native plugin ABI: the stable extension + boundary is the HTTP API plus request scripts. +- The light theme has not had a page-by-page visual audit; both themes are functional, but a + dedicated polish pass is planned after 1.0. diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 4c48756..2ab4b79 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -188,17 +188,49 @@ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ cmake --build build -j ``` -macOS builds are source-oriented in 0.11.0. The install tree carries `share/uvscope/uvscope.icns`, -but a Finder-visible icon needs a signed/notarized `.app`, which is later work. +macOS builds are source-oriented in 1.0.x. The install tree carries `share/uvscope/uvscope.icns`, +but a Finder-visible icon needs a signed/notarized `.app`, which is later work. Windows arm64 is +not part of the 1.x matrix; MSYS2 CLANGARM64 is still preview. + +## Verification scripts + +```sh +./build/uvscope --self-check # offline crypto/IPC/CA/server checks +bash ./scripts/smoke-test.sh ./build/uvscope 18787 18788 +bash ./scripts/h2-smoke.sh ./build/uvscope 18820 18821 +./scripts/bench.sh ./build/uvscope --requests 500 +npm --prefix web run check:i18n +``` + +`h2-smoke.sh` starts a throwaway daemon and a local Node.js HTTP/2 server, then verifies a real +ALPN `h2` probe (version, stream id, frame summary). It requires `curl`, `openssl`, `node` and +`python3` (or `python`); set `OPENSSL_BIN` when Homebrew's keg-only OpenSSL is needed. ## CI -- `.github/workflows/ci.yml` builds the frontend and the C++ daemon on Linux, Windows (MinGW) and - macOS, runs `--self-check` and a real HTTP smoke test. +- `.github/workflows/ci.yml` builds the frontend and the daemon on Linux x64, Windows x64 + (MinGW) and macOS arm64. Linux runs ctest and the full smoke suite; Windows runs `--self-check`, + a ConPTY terminal smoke and the HTTP/2 runtime smoke; macOS runs `--self-check` and the HTTP/2 + runtime smoke. +- `.github/workflows/docs.yml` builds the MkDocs site and deploys it to GitHub Pages from `main`. - `.github/workflows/release.yml` runs on tags `v*` and publishes: - Linux x64 / arm64 tar.gz and deb/rpm where supported; - Windows x64 zip and Inno Setup installer; - - macOS arm64 zip. + - macOS arm64 tar.gz; + - `SHA256SUMS`, a Sigstore keyless signature over the checksums and a generated SPDX SBOM. + +## Documentation site + +The site is MkDocs Material with `docs/` as the source directory: + +```sh +python3 -m pip install mkdocs-material +mkdocs serve # http://127.0.0.1:8000 +mkdocs build # static site in site/ +``` + +Navigation is in `mkdocs.yml`; project-level files (roadmap, changelog, contributing, security) +are linked to GitHub because they live outside `docs/`. ## Troubleshooting diff --git a/docs/DEVELOPMENT_PLAN.zh-CN.md b/docs/DEVELOPMENT_PLAN.zh-CN.md index 451e370..05ee7bf 100644 --- a/docs/DEVELOPMENT_PLAN.zh-CN.md +++ b/docs/DEVELOPMENT_PLAN.zh-CN.md @@ -1,6 +1,6 @@ # uvscope 开发计划 -> 版本:0.11.0 发布 +> 版本:1.0.0 发布 > 技术底座:libuvcpp 1.8.2 + Vue 3 + SQLite + libuvcpp WSDL/SOAP > 目标:做一款覆盖 Web 调试、协议探测与可编程反向代理的本地瑞士军刀,并保持单机、可审计、可离线部署。 @@ -39,8 +39,9 @@ - 开源运营能力:关于页、GitHub 链接、star 数、版本检查、更新说明、帮助页。 - 跨平台构建基础:CMake、MinGW-w64、CPack、GitHub Actions 工作流、Windows Inno Setup 安装脚本。 - libuvcpp 上游修复:TLS SNI、同步 I/O 超时、install 完整性与 SOCKS5 失败阶段;这些改动 - 已在 libuvcpp v1.8.1 / v1.8.2 发布,uvscope 现直接使用 1.8.2 发布版 SDK,不再携带本地补丁。 - 历史复现记录见 [`LIBUVCPP_UPSTREAM.zh-CN.md`](LIBUVCPP_UPSTREAM.zh-CN.md)。 + 已在 libuvcpp v1.8.1 / v1.8.2 发布。当前只保留 `0005-ws-client-handshake-headers.patch` + (WebSocket 后备会话的握手 Header),其余补丁已随发布版 SDK 退役;历史复现记录见 + [`LIBUVCPP_UPSTREAM.zh-CN.md`](LIBUVCPP_UPSTREAM.zh-CN.md)。 ### 1.2 0.1.0 明确不承诺的能力 @@ -804,6 +805,40 @@ smoke kill 隔离 SOCKS5、listener、终端的子进程后,等待 supervisor 不做:重启次数上限/熔断、跨进程鉴权、资源限制、终端会话跨守护进程重启保留。 +### S20 — 1.0 稳定化与发布(已实现) + +边界:README/文档站、发布工作流、CI 矩阵、性能基线、版本号。 + +内容: + +- 文档重整:README 面向产品;新增 MkDocs Material 文档站(GitHub Pages,CI 构建)、 + `docs/EVENTS.md`(事件契约与 1.x 兼容策略)、`docs/EXTENDING.md`(变量/脚本/命令/IPC + 边界)、`docs/PERFORMANCE.md`、`docs/OPERATIONS.md`;API/ARCHITECTURE/BUILDING/SECURITY/ + CONTRIBUTING/AGENTS 与帮助页同步到 1.0; +- CI 矩阵:Linux 全量 smoke;Windows 自检 + ConPTY + HTTP/2 运行期冒烟;macOS 自检 + + HTTP/2 运行期冒烟;新增 `scripts/h2-smoke.sh`; +- 性能基线:`scripts/bench.sh` / `scripts/bench.py`,记录直连、进程内代理与隔离子进程代理的 + 顺序 keep-alive 延迟/吞吐; +- 供应链:Release 的 publish job 生成 SPDX SBOM、`SHA256SUMS` 与 Sigstore keyless 签名 + (`SHA256SUMS.sigstore.json`); +- 版本与矩阵:1.0.0;Windows arm64、HTTP/3 与原生插件 ABI 明确排除在 1.x 之外。 + +验收: + +```sh +./scripts/build.sh +./build/uvscope --self-check +bash ./scripts/smoke-test.sh ./build/uvscope 18787 18788 +bash ./scripts/h2-smoke.sh ./build/uvscope 18820 18821 +./scripts/bench.sh ./build/uvscope +mkdocs build +``` + +CI 三端绿;`v1.0.0` release 包含 Linux x64/arm64、Windows x64、macOS arm64 资产、校验和、 +签名与 SBOM;下载 Linux 产物复验 `--version` 与 `--self-check`。 + +不做:Windows arm64、HTTP/3 稳定化、原生插件 ABI、逐页浅色主题审计(列入 1.x 后续)。 + --- ### 风险与回退(本迭代) @@ -825,8 +860,8 @@ smoke kill 隔离 SOCKS5、listener、终端的子进程后,等待 supervisor - Linux x64 / arm64(deb、rpm、tar.gz) - Windows x64(MinGW-w64,zip + Inno Setup 安装 exe) -- Windows arm64(MinGW-w64 clang + libc++,实验) -- macOS arm64(源码构建 + zip,后续补签名) +- macOS arm64(源码构建 + tar.gz,后续补签名/公证) +- Windows arm64:1.x 不发布(MSYS2 CLANGARM64 仍为预览,列入 1.x 后续) ### 构建事实 @@ -841,15 +876,18 @@ smoke kill 隔离 SOCKS5、listener、终端的子进程后,等待 supervisor ### 发布产物 ```text -uvscope-0.11.0-linux-x64.tar.gz -uvscope-0.11.0-linux-arm64.tar.gz -uvscope_0.11.0_amd64.deb -uvscope_0.11.0_arm64.deb -uvscope-0.11.0-1.x86_64.rpm -uvscope-0.11.0-1.aarch64.rpm -uvscope-0.11.0-mingw-x64.zip -uvscope-0.11.0-setup.exe -uvscope-0.11.0-macos-arm64.tar.gz +uvscope-1.0.0-linux-x64.tar.gz +uvscope-1.0.0-linux-arm64.tar.gz +uvscope_1.0.0_amd64.deb +uvscope_1.0.0_arm64.deb +uvscope-1.0.0-1.x86_64.rpm +uvscope-1.0.0-1.aarch64.rpm +uvscope-1.0.0-mingw-x64.zip +uvscope-1.0.0-setup.exe +uvscope-1.0.0-macos-arm64.tar.gz +SHA256SUMS +SHA256SUMS.sigstore.json +uvscope-v1.0.0-sbom.spdx.json ``` --- diff --git a/docs/EVENTS.md b/docs/EVENTS.md new file mode 100644 index 0000000..05c32a4 --- /dev/null +++ b/docs/EVENTS.md @@ -0,0 +1,178 @@ +# Event schema + +uvscope exposes several bounded, sequenced event streams. The HTTP API reads them with +`?after=&limit=`; long-lived WebSocket terminals stream output separately. This page is the +1.x compatibility contract for those payloads. + +## Compatibility policy + +- Within 1.x, event fields may be **added**; existing field names, types and meanings do not + change without a major release. +- Consumers must ignore unknown fields and unknown `type` values. +- `seq` is monotonic per stream and starts at 1. It is the cursor for `after`. Gaps mean the + bounded buffer dropped older entries, not that the event was lost silently: the stream's + `firstSeq`/`nextSeq` (where exposed) describe the retained window. +- Timestamps are ISO-8601 UTC strings. Proxy logs use `time`; other streams use `at`. +- Payloads are truncated at a fixed byte budget. Truncation is always explicit in the payload + (`truncated: true`) and never changes the event type. +- No stream includes credentials, private keys or complete request/response bodies by default. + +## Common envelope + +Every stream event carries: + +```json +{ + "seq": 12, + "at": "2026-10-25T09:30:00Z", + "type": "connected" +} +``` + +Supervisor events add `id` (service id), `name` and an optional `message`. Proxy logs add +`method`, `path`, `status` and optional `rule`, `action`, `latencyMs`, `error`, `message`. + +## Service supervisor + +`GET /api/supervisor/events?after=&limit=` returns: + +| `type` | Meaning | Extra fields | +| --- | --- | --- | +| `registered` | A service was added to the watchdog | `message` describes desired state | +| `unregistered` | A service was removed (delete, module disable, reset) | — | +| `enabled` / `disabled` | `desired` changed | — | +| `restart-requested` | `POST /api/supervisor/:id/restart` | — | +| `fail-runtime` | `POST /api/supervisor/:id/stop-runtime` | — | +| `restart` | The watchdog started a desired service that was down | `message` | + +Snapshot rows (`GET /api/supervisor`) contain `id`, `kind` (`proxy`, `listener`, `socks5`, +`terminal`), `name`, `desired`, `running`, `restarts`, `lastError` and `updatedAt`. + +## Persistent TCP sessions + +`GET /api/tcp/sessions/:id/events?after=&limit=`: + +| `type` | Extra fields | +| --- | --- | +| `connected` | — | +| `recv` | `payload` (see below) | +| `send` | `payload` | +| `closed` | `reason` | +| `peer-closed` | `fin` | +| `error` | `error` | + +### Payload objects + +Binary-safe payloads use one shape in TCP/UDP/listener events: + +```json +{ + "length": 12, + "truncated": false, + "hex": "68 65 6c 6c 6f 0a ...", + "encoding": "utf8", + "text": "hello" +} +``` + +`encoding` is `utf8` or `base64` (binary payloads use `base64` instead of `text`). `length` is the +original byte count; `truncated` means the retained sample is shorter than `length`. + +## TCP / UDP listeners + +`GET /api/transport/listeners/:id/events?after=&limit=`: + +| `type` | Extra fields | +| --- | --- | +| `started` | — | +| `stopped` | — | +| `error` | `error` | +| `accepted` | `sessionId`, `peer` | +| `session-closed` | `sessionId`, `peer`, `reason` | +| `datagram` | `peer`, `partial`, `payload` | + +Per-session events (`.../sessions/:sid/events`) use the same envelope: + +| `type` | Extra fields | +| --- | --- | +| `accepted` | `peer` | +| `recv` | `payload` | +| `send` | `payload` | +| `closed` | `reason` | +| `peer-closed` | `fin` | +| `error` | `error` | + +`peer` is `"host:port"`. UDP datagrams also report `partial` when the datagram was clipped at the +receive buffer size. + +## SOCKS5 servers + +`GET /api/socks5/servers/:id/events?after=&limit=`: + +| `type` | Extra fields | +| --- | --- | +| `started` | `port` (actual bound port) | +| `stopped` | — | +| `error` | `error` | +| `session-end` | `sessionId`, `command`, `target`, `peer`, `endStatus`, `durationMs`, `bytesClientToTarget`, `bytesTargetToClient`, `udpDatagrams`, `udpBytes` | + +Session records (`.../sessions`) use the same fields plus `state`, `endedAt`, +`bytesAvailable` and `error` when the session failed. + +## Reverse proxy logs + +`GET /api/proxies/:id/logs?after=&limit=` returns entries with `seq`, `time`, `method`, `path`, +`status` and, when relevant, `rule`, `action`, `latencyMs`, `error` or `message`. `method` is +`SYSTEM` for lifecycle entries (started, stopped, hot reload, supervisor restart) and the request +method for traffic. A WebSocket upgrade is logged with `status: 101` and `action: "upgrade"`. + +## Backend WebSocket sessions + +`GET /api/ws/sessions/:id/events?after=&limit=`: + +| `type` | Extra fields | +| --- | --- | +| `connecting` | `url` | +| `open` | — | +| `error` | `error`, optional `detail`, `closeCode` | +| `sent` | `opcode` (`text`, `binary`, `ping`, `pong`), `size` | +| `text` | `text` or `base64` + `encoding`, `size`, `truncated` | +| `binary` | `base64`, `size`, `truncated` | +| `frame` | `opcode`, `opcodeId`, `fin`, `rsv1`, `masked`, `length`, `truncated` and a body field | +| `closing` | `code`, `reason` | +| `close` | `code`, `reason`, `clean` | + +## Virtual terminal output + +Terminal output is not an event log; it is a sequenced chunk stream exposed by both +`GET /api/terminal/sessions/:id/output?after=&limit=` and the terminal WebSocket: + +```json +{ + "ok": true, + "session": { "id": "...", "state": "running", "nextSeq": 42 }, + "chunks": [{ "seq": 41, "data": "base64-bytes" }], + "firstSeq": 40, + "nextSeq": 42, + "truncated": false, + "missed": false +} +``` + +`missed: true` means the client cursor is ahead of or older than the retained window; reset the +cursor to 0 (the WebSocket sends `{"type":"reset"}` and the UI clears xterm). A supervised +restart resets the sequence numbers because the new child starts a fresh stream. + +## Terminal WebSocket control frames + +| Direction | Message | +| --- | --- | +| Server → client | `{"type":"attached","id":"..."}` | +| Server → client | `{"type":"reset"}` | +| Server → client | `{"type":"exit","state":"exited","code":0}` | +| Server → client | `{"type":"error","error":"..."}` | +| Client → server | `{"type":"input","data":"...","encoding":"text\|hex\|base64"}` | +| Client → server | `{"type":"resize","cols":100,"rows":30}` | +| Client → server | `{"type":"ping"}` (server replies `{"type":"pong"}`) | + +Output is sent as binary WebSocket frames; terminal input may also be sent as raw binary frames. diff --git a/docs/EXTENDING.md b/docs/EXTENDING.md new file mode 100644 index 0000000..85fd846 --- /dev/null +++ b/docs/EXTENDING.md @@ -0,0 +1,133 @@ +# Extending uvscope + +uvscope does not load native plugins in 1.x. Extensibility is provided by four stable, documented +surfaces: environment variables, request scripts, the command library and the HTTP API. The +child-process IPC protocol is an internal boundary between uvscope processes, not a public ABI. + +## Environment variables + +Environments live in the HTTP workspace (`GET/PUT /api/http/workspace`) and are selected per +workspace. Variables use double braces and are substituted in the request URL, header names and +values, and the request body: + +```text +{{base_url}}/v1/users +Authorization: Bearer {{token}} +``` + +The substitution pattern is `{{ name }}` with `[\w.-]+` names. Unknown variables are left literal +so a typo is visible in the outgoing request. The active environment can be changed without +re-saving a request; the raw request text is preserved. + +## Pre-request and post-response scripts + +Scripts run in the browser as async functions with `"use strict"`. They are trusted local code: +unlike the response preview, they are not sandboxed and can call browser APIs. The workspace +request and response are passed through the `uvscope` argument. + +### Pre-request + +```js +const url = new URL(uvscope.request.url) +url.searchParams.set('trace', '1') +uvscope.request.url = url.toString() +uvscope.request.headers.push({ name: 'X-Debug', value: '1' }) +uvscope.env.set('last_trace', '1') +``` + +| Surface | Description | +| --- | --- | +| `uvscope.request.method` | Mutable method | +| `uvscope.request.url` | Mutable URL (variables already substituted) | +| `uvscope.request.headers` | Mutable `[{ name, value }]` array | +| `uvscope.request.body` | Mutable body text | +| `uvscope.env.get(name)` / `uvscope.env.set(name, value)` | Read or write the active environment | +| `uvscope.variables` | Read-only snapshot of the active environment | + +Returning normally sends the (possibly modified) request. A thrown error aborts the send and is +shown in the UI. The script may be async; `await` is supported. + +### Post-response + +```js +uvscope.test('status is 200', () => { + if (uvscope.response.status !== 200) throw new Error('unexpected status') +}) +uvscope.test('json has items', () => { + if (!Array.isArray(uvscope.response.json().items)) throw new Error('items missing') +}) +if (uvscope.response.status === 401) uvscope.env.set('token', '') +``` + +| Surface | Description | +| --- | --- | +| `uvscope.response.status` | Numeric status | +| `uvscope.response.headers` | `[{ name, value }]` | +| `uvscope.response.text` | Body text | +| `uvscope.response.contentType` | `Content-Type` header or `""` | +| `uvscope.response.json()` | Parsed JSON (throws on invalid JSON) | +| `uvscope.test(name, fn)` | Runs `fn`; a throw records a failed assertion | +| `uvscope.env.get/set`, `uvscope.variables` | Same as pre-request | + +Post-response scripts run only for successful requests (`res.ok`). Script source is stored in +browser `localStorage` per workspace; it is not written to SQLite and is not sent to the daemon. + +## Command library + +The terminal command catalog is seeded into SQLite on first migration. Custom entries are created +with `POST /api/terminal/commands`: + +```json +{ + "name": "Tail app log", + "command": "tail -f /var/log/app.log", + "category": "custom", + "platform": "linux", + "description": "Follow the application log", + "quick": true +} +``` + +`platform` is `linux`, `windows` or empty (both), `category` groups the list, `description` is +shown in the command detail, and `quick: true` pins the entry to the quick bar. Admins can edit +or delete any entry and restore deleted built-ins with +`POST /api/terminal/commands/restore`. Commands are plain text; uvscope never executes a command +without an explicit user action in a terminal session. + +## Proxy destination variables + +Rule destinations (`redirect` / `rewrite`) support the variables documented in the +[HTTP API](API.md#reverse-proxy-routing-rules): `$uri`, `$args`, `$request_uri`, `$rest`, +`$1`–`$9`, `$host` and `$scheme`. Unknown variables are left literal. This is the supported way to +express routing logic without a plugin. + +## HTTP API + +Every UI feature is backed by the same JSON API documented in [API.md](API.md). Responses use +`{ ok, ... }` with a typed `code` on errors, timestamps are ISO-8601 UTC, and event streams are +sequenced for incremental polling. Event and payload compatibility rules are in +[EVENTS.md](EVENTS.md). Integrations should use the API rather than driving the UI. + +## Child-process IPC (internal) + +Isolated services are launched with hidden flags (`--proxy-child`, `--listener-child`, +`--socks5-child`, `--terminal-child`, `--ipc-child`). The parent and child exchange frames over a +Unix domain socket (Windows named pipe): + +```text +4-byte big-endian length | UTF-8 JSON payload (maximum 4 MiB) +``` + +Each service defines its own message types (`start`, `input`, `resize`, `redraw`, `close`, `stop`, +`snapshot`, `output`, `events`, `send`, `session-events`, …) and an initial `ready`/`started` +handshake. The protocol is versioned with the daemon: a child is always the same executable and +build as its parent. It is documented here for operators and contributors, but it is **not** a +stable third-party plugin ABI and may change between minor releases. + +## What is not in 1.x + +- A native plugin ABI or dynamically loaded modules. +- User-authored server-side code execution. +- A remote API authentication mode other than the workbench session cookie. + +These are tracked as post-1.0 work in the [roadmap](https://github.com/Antruly/uvscope/blob/main/ROADMAP.md). diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 0000000..ae2ad35 --- /dev/null +++ b/docs/OPERATIONS.md @@ -0,0 +1,121 @@ +# Operations + +uvscope is a single-binary daemon with a SQLite database and a static UI bundle. This page covers +day-to-day operation, backup, upgrades and hardening. + +## Data directory + +The daemon prints the data directory at startup. Override it with `--data-dir` or +`UVSCOPE_DATA_DIR`; the default is `~/.local/share/uvscope` on Linux and the platform equivalent +elsewhere. + +| Path | Contents | +| --- | --- | +| `uvscope.db` | SQLite database: users, sessions, settings, history, workspace, proxied configuration, audit log | +| `certs/` | Managed TLS certificates and private keys (created by the certificate vault) | +| `downloads/` | Staged large responses and resumable partial files | +| `backup-.db` | Temporary snapshot used by the backup endpoint | + +The data directory is the entire persistent state. Child processes hold no state of their own; +their configuration is reconstructed from the parent and the database. + +## First run + +1. Start `uvscope --port 8787` (add `--open` to launch a browser). +2. Open ; the first visit creates the administrator account. +3. Enable the modules you need in **Settings → Modules**. The terminal is off by default. +4. Add users under **Users**; roles are `admin`, `operator` and `viewer`. + +`GET /api/health` is public and returns the version, `libuvcpp` version, uptime, setup state and +transport capabilities. Use it for supervision and smoke checks. It is not an authenticated +endpoint and exposes no user data. + +## Users, roles and sessions + +- `admin` — full access, including users, settings, reset and the supervisor. +- `operator` — debug modules, gateways, listeners and terminals. +- `viewer` — read-only access to dashboards and history surfaces. + +Sessions are server-side with a seven-day expiry. `GET /api/auth/sessions` lists active sessions; +`DELETE /api/auth/sessions/:id` revokes one. Changing a password invalidates that user's other +sessions. + +## Modules + +Module switches live in `PUT /api/settings` (`{"modules": {"terminal": true}}`). Disabling the +terminal closes all terminal sessions and unregisters their supervisor entries. Disabling other +modules blocks new API calls but does not stop running long-lived services; stop those explicitly +in their views. + +## Backup and restore + +- `GET /api/backup` returns a consistent SQLite snapshot as a download. +- `POST /api/backup/restore` stages an uploaded database as `uvscope.db.restore` and applies it on + the next start after validation. +- To back up manually while the daemon is stopped, copy `uvscope.db` (plus `certs/` if you want to + keep private keys). Back up `downloads/` only if you need staged files. + +Stop the daemon or use the API snapshot; copying a live SQLite file without the API can capture a +partial write. + +## Reset + +`POST /api/reset` with `{"module": "all"}` stops proxies, listeners, SOCKS5 servers and terminals, +unregisters their supervisor entries, removes the database, recreates the schema and returns +`setupRequired: true`. Module-scoped resets (`proxy`, `terminal`, `tcpudp`, `http`, `websocket`, +`soap`, `tls`) clear that module's configuration. + +## Long-lived services and isolation + +Reverse proxies, TCP/UDP listeners, SOCKS5 servers and virtual terminals accept +`isolated: true`. Isolated services run in a child process of the same binary: + +- the parent caches telemetry and proxies commands over IPC; +- a crash is restarted with exponential backoff (2–60 s) and a new PID; +- an explicit stop clears the desired state, so the watchdog leaves the service down; +- deleting a service unregisters it before the child is stopped and reaped; +- disabling the terminal module unregisters and closes all terminal sessions. + +Isolation is a fault boundary, not a security sandbox: children run as the same OS user with the +same filesystem and network access. Resource limits (`ulimit`, containers, a dedicated OS user) +must be applied by the operator. + +`GET /api/supervisor` and `GET /api/supervisor/events` show desired/running state, restart counts +and the event history. Terminal sessions do not survive a daemon restart; proxies, listeners and +SOCKS5 servers with `autoStart` are restored from SQLite. + +## Logs + +uvscope logs to stderr. Run it under your service manager to capture output: + +```sh +uvscope --port 8787 >>/var/log/uvscope.log 2>&1 +``` + +Per-service telemetry is not written to the log; use the API views (`/api/proxies/:id/logs`, +listener/SOCKS5 events, terminal output) and the audit log in the UI. Request bodies are not +logged. + +## Upgrades + +1. Stop the daemon. +2. Back up the data directory (or use `GET /api/backup` while running). +3. Replace the binary and the `share/uvscope/web` bundle with the new release. +4. Start the daemon; pending SQLite migrations apply automatically. + +Version rollback across a schema migration is not supported; restore the pre-upgrade database if +you need to go back. + +## Hardening for shared machines + +- Do not run as root. Use a dedicated OS user and a data directory with `0700` permissions. +- Keep the default `127.0.0.1` bind unless the service is behind an authenticating proxy. +- Serve the UI over TLS (`--tls-cert` / `--tls-key`) if it is reachable beyond localhost. +- Disable modules you do not use; keep the terminal off unless needed. A terminal grants a shell + as the daemon user. +- Isolated services still share the OS user; use OS-level limits for untrusted workloads. +- Review reverse proxy targets and listener bind addresses; the daemon can reach internal networks. +- Treat the data directory as sensitive: it contains session hashes, history and private keys. + +See [SECURITY.md](https://github.com/Antruly/uvscope/blob/main/SECURITY.md) for the threat model +and private vulnerability reporting. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md new file mode 100644 index 0000000..baac2fa --- /dev/null +++ b/docs/PERFORMANCE.md @@ -0,0 +1,66 @@ +# Performance baselines + +uvscope ships a dependency-free reference benchmark for the reverse proxy. The goal is a +repeatable **relative** measurement on one machine — not a cross-machine guarantee. Absolute +numbers depend on the CPU, kernel, Python client and the origin server. + +## What it measures + +`scripts/bench.sh` starts: + +1. a local HTTP/1.1 origin (Python `ThreadingHTTPServer`, Nagle disabled) on `--origin-port`; +2. a throwaway uvscope daemon on `--port` with a temporary data directory; +3. a reverse proxy on `--proxy-port` targeting the origin (in-process by default, or a child + process with `--isolated`). + +It then sends `--requests` sequential keep-alive `GET` requests to the origin directly and through +the proxy, and reports throughput plus mean/p50/p95/max latency. Both paths use the same Python +`http.client` connection and payload, so the difference is the proxy path itself. + +```sh +./scripts/bench.sh ./build/uvscope --requests 500 +./scripts/bench.sh ./build/uvscope --requests 500 --isolated +./scripts/bench.sh ./build/uvscope --requests 500 --json +``` + +The client is single-threaded: this is a latency/overhead measurement, not a saturation test. For +concurrency or saturation numbers, drive the listener with your own load generator and compare +release builds on the same host. + +## Reference run + +Recorded on 2026-10-25 with the v1.0.0 release build: + +- CPU / host: `Linux 6.14.0-37-generic x86_64` (glibc 2.39), development container +- Proxy: in-process reverse proxy, empty rule set +- Requests: 500, warmup 50, keep-alive, sequential + +| Path | req/s | mean ms | p50 ms | p95 ms | max ms | +| --- | ---: | ---: | ---: | ---: | ---: | +| Direct origin | 6209 | 0.16 | 0.15 | 0.23 | 0.34 | +| Reverse proxy (in-process) | 567 | 1.76 | 1.76 | 1.88 | 2.59 | +| Reverse proxy (isolated child) | 577 | 1.73 | 1.73 | 1.84 | 2.88 | + +Interpretation: + +- The proxy adds about **1.6 ms** per sequential request on this host. +- In-process and isolated proxies have effectively the same data path cost; isolation adds a + child process and IPC telemetry, not per-request IPC. Choose isolation for fault containment, + not for throughput. +- The direct-origin number is dominated by the Python client and loopback; use the ratio, not the + absolute rate. + +Record the command, host and commit alongside any baseline you publish. Numbers from different +hosts or Python versions are not comparable. + +## Release verification + +The release pipeline runs, per platform: + +- `--self-check` (IPC, crypto, SQLite, CA, HTTP server, proxy-child); +- the full smoke suite on Linux; +- a Windows ConPTY terminal smoke and the H2 runtime check; +- a macOS self-check and the H2 runtime check. + +The benchmark is a developer/operator tool; it is not a pass/fail CI gate because CI hosts are +shared and noisy. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..aeb273a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,77 @@ +# uvscope + +**A local-first, multi-protocol web debugging workbench and gateway.** + +uvscope puts HTTP, WebSocket, TCP, UDP, TLS, SOAP/WSDL and managed reverse proxies behind one +Vue 3 control plane. A C++ daemon owns the event loop, SQLite store, authentication and outbound +traffic; long-lived services can move to supervised child processes, and the browser only renders +state and sends commands. + +![uvscope dashboard](screenshots/dashboard.png) + +## Start here + +
+ +- **[Build and install](BUILDING.md)** — requirements, the libuvcpp SDK, packaging and the + Windows installer. +- **[HTTP API](API.md)** — every route, payload and state model, including terminals and the + service supervisor. +- **[Architecture](ARCHITECTURE.md)** — process model, request flows, SQLite schema and the + long-lived runtimes. +- **[Operations](OPERATIONS.md)** — data directory, backup/restore, logs, isolation and hardening. + +
+ +## What it does + +- **HTTP Studio** — HTTP/1.1 and HTTP/2 (ALPN, stream IDs, GOAWAY, frame summaries), body modes, + cURL/HAR import and export, environments, collections, scripts and assertions, compression, + streaming downloads and a sandboxed response preview. +- **Realtime consoles** — WebSocket with daemon-side backend sessions or browser-native mode, + one-shot TCP/UDP probes and persistent TCP sessions. +- **Gateway** — reverse proxies with exact/prefix/regex routing, rewrites, weighted upstreams, + retries, active health checks, per-upstream circuit breakers, metrics, hot reload and WebSocket + pass-through. +- **Long-lived services** — TCP/UDP listeners, SOCKS5 servers, reverse proxies and terminal + PTYs can run in isolated child processes under one desired-state supervisor. +- **Security tooling** — X.509 decode/probe, managed certificate vault, local CA, CA-signed + certificates and in-place renewal. +- **Operations** — SQLite persistence, first-run admin, RBAC, server-side sessions, audit log, + backup/restore, module toggles and one-command reset. + +## Isolation in one minute + +Set `isolated: true` when creating a reverse proxy, listener, SOCKS5 server, or terminal session. +The service runs in a `--*-child` process; the parent keeps the API state and talks to it over a +length-prefixed JSON pipe. + +All four kinds register with the same supervisor: + +- a crashed child is restarted with exponential backoff and a new PID; +- `GET /api/supervisor` reports `desired`, `running`, `restarts` and `lastError`; +- stopping a service clears its desired state; deleting it unregisters it first; +- a terminal shell `exit` is not a crash — the child stays alive for scrollback and exit status; + a killed child is respawned with a fresh shell and WebSocket clients get a `reset` frame. + +## Release + +v1.0.0 is the first stable release. Assets, `SHA256SUMS`, the Sigstore signature and the SPDX +SBOM are on the [Releases page](https://github.com/Antruly/uvscope/releases/tag/v1.0.0). + +| Platform | Assets | +| --- | --- | +| Linux x64 / arm64 | `.tar.gz`, `.deb`, `.rpm` | +| Windows x64 (MinGW) | `.zip`, `uvscope-1.0.0-setup.exe` | +| macOS arm64 | `.tar.gz` (Homebrew `libuv` and `openssl@3`) | + +HTTP/3 and raw QUIC remain experimental and are closed in release builds. Windows arm64 is +planned for a post-1.0 release. + +## Project links + +- [Repository](https://github.com/Antruly/uvscope) +- [Roadmap](https://github.com/Antruly/uvscope/blob/main/ROADMAP.md) +- [Changelog](https://github.com/Antruly/uvscope/blob/main/CHANGELOG.md) +- [Contributing](https://github.com/Antruly/uvscope/blob/main/CONTRIBUTING.md) +- [Security policy](https://github.com/Antruly/uvscope/blob/main/SECURITY.md) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..f49f97d --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,69 @@ +site_name: uvscope +site_description: A local-first multi-protocol web debugging workbench and gateway +site_url: https://antruly.github.io/uvscope/ +repo_url: https://github.com/Antruly/uvscope +repo_name: Antruly/uvscope +edit_uri: edit/main/docs/ +docs_dir: docs +use_directory_urls: true + +theme: + name: material + language: en + features: + - navigation.sections + - navigation.top + - content.code.copy + - search.suggest + - search.highlight + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + +nav: + - Home: index.md + - Guides: + - Building and packaging: BUILDING.md + - Operations: OPERATIONS.md + - Performance baselines: PERFORMANCE.md + - Reference: + - HTTP API: API.md + - Event schema: EVENTS.md + - Extending uvscope: EXTENDING.md + - Architecture: ARCHITECTURE.md + - Project: + - Roadmap: https://github.com/Antruly/uvscope/blob/main/ROADMAP.md + - Changelog: https://github.com/Antruly/uvscope/blob/main/CHANGELOG.md + - Contributing: https://github.com/Antruly/uvscope/blob/main/CONTRIBUTING.md + - Security policy: https://github.com/Antruly/uvscope/blob/main/SECURITY.md + - Development plan (中文): DEVELOPMENT_PLAN.zh-CN.md + - libuvcpp upstream notes (中文): LIBUVCPP_UPSTREAM.zh-CN.md + +markdown_extensions: + - admonition + - attr_list + - def_list + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.highlight + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + +plugins: + - search diff --git a/packaging/windows/uvscope.iss b/packaging/windows/uvscope.iss index 748dbcc..4a1da6f 100644 --- a/packaging/windows/uvscope.iss +++ b/packaging/windows/uvscope.iss @@ -3,7 +3,7 @@ ; Expects the CMake install tree under stage\ (bin/, share/uvscope/web/). #define MyAppName "uvscope" -#define MyAppVersion "0.11.0" +#define MyAppVersion "1.0.0" #define MyAppPublisher "Antruly" #define MyAppURL "https://github.com/Antruly/uvscope" #define MyAppExeName "uvscope.exe" diff --git a/scripts/bench.py b/scripts/bench.py new file mode 100644 index 0000000..f2a2821 --- /dev/null +++ b/scripts/bench.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""Small, dependency-free reverse-proxy benchmark for uvscope. + +Starts an in-process origin, a throwaway uvscope daemon and a reverse proxy, +then measures sequential keep-alive latency and throughput for the origin and +the proxy. The numbers are a reference for relative comparisons on the same +machine, not a cross-machine guarantee. + +Usage: + python3 scripts/bench.py ./build/uvscope [--requests 500] [--isolated] +""" + +import argparse +import http.client +import http.cookiejar +import http.server +import json +import os +import platform +import shutil +import socket +import subprocess +import sys +import tempfile +import threading +import time +import urllib.error +import urllib.request + +BODY = b'{"ok":true,"bench":"uvscope"}' + + +class OriginHandler(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + # Without this the header and body writes collide with delayed ACKs and + # every keep-alive response picks up ~40 ms of latency. + disable_nagle_algorithm = True + + def do_GET(self): + self.send_response(200) + self.send_header("content-type", "application/json") + self.send_header("content-length", str(len(BODY))) + self.end_headers() + self.wfile.write(BODY) + + def log_message(self, *_args): + pass + + +def wait_for_port(host, port, timeout=10.0): + deadline = time.time() + timeout + while time.time() < deadline: + try: + with socket.create_connection((host, port), timeout=0.5): + return True + except OSError: + time.sleep(0.05) + return False + + +def wait_for_health(base, timeout=15.0): + deadline = time.time() + timeout + while time.time() < deadline: + try: + with urllib.request.urlopen(base + "/api/health", timeout=1): + return True + except Exception: + time.sleep(0.1) + return False + + +class Client: + def __init__(self, base): + self.base = base + self.jar = http.cookiejar.CookieJar() + self.opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(self.jar)) + + def call(self, method, path, payload=None, timeout=30): + data = json.dumps(payload).encode() if payload is not None else None + request = urllib.request.Request( + self.base + path, data=data, method=method, + headers={"content-type": "application/json"} if data else {}) + try: + with self.opener.open(request, timeout=timeout) as response: + return response.status, json.loads(response.read() or b"{}") + except urllib.error.HTTPError as error: + return error.code, json.loads(error.read() or b"{}") + + +def measure(host, port, path, requests, warmup): + try: + connection = http.client.HTTPConnection(host, port, timeout=15) + for _ in range(warmup): + connection.request("GET", path) + response = connection.getresponse() + response.read() + if response.status != 200: + raise RuntimeError("warmup status %s" % response.status) + samples = [] + started = time.perf_counter() + for _ in range(requests): + begin = time.perf_counter() + connection.request("GET", path) + response = connection.getresponse() + body = response.read() + samples.append((time.perf_counter() - begin) * 1000.0) + if response.status != 200 or body != BODY: + raise RuntimeError("unexpected response: %s %r" % + (response.status, body[:80])) + elapsed = time.perf_counter() - started + connection.close() + except OSError as error: + raise RuntimeError("could not measure %s:%s: %s" % (host, port, error)) + samples.sort() + count = len(samples) + return { + "requests": count, + "seconds": elapsed, + "throughput": count / elapsed if elapsed else 0.0, + "meanMs": sum(samples) / count, + "p50Ms": samples[count // 2], + "p95Ms": samples[min(count - 1, int(count * 0.95))], + "maxMs": samples[-1], + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("binary", nargs="?", default="./build/uvscope") + parser.add_argument("--port", type=int, default=18830) + parser.add_argument("--proxy-port", type=int, default=18831) + parser.add_argument("--origin-port", type=int, default=18832) + parser.add_argument("--requests", type=int, default=500) + parser.add_argument("--warmup", type=int, default=50) + parser.add_argument("--isolated", action="store_true", + help="run the reverse proxy in a child process") + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + + binary = os.path.abspath(args.binary) + if not os.path.exists(binary): + raise SystemExit("bench: binary not found: %s" % binary) + + origin = http.server.ThreadingHTTPServer( + ("127.0.0.1", args.origin_port), OriginHandler) + origin_thread = threading.Thread(target=origin.serve_forever, daemon=True) + origin_thread.start() + + data_dir = tempfile.mkdtemp(prefix="uvscope-bench.") + daemon_log = os.path.join(data_dir, "daemon.log") + environment = dict(os.environ) + environment["UVSCOPE_DATA_DIR"] = data_dir + log_file = open(daemon_log, "wb") + daemon = subprocess.Popen( + [binary, "--host", "127.0.0.1", "--port", str(args.port)], + stdout=log_file, stderr=subprocess.STDOUT, env=environment) + base = "http://127.0.0.1:%d" % args.port + + try: + if not wait_for_health(base): + raise SystemExit("bench: daemon did not start\n" + + open(daemon_log, encoding="utf-8", + errors="replace").read()[-2000:]) + client = Client(base) + status, payload = client.call( + "POST", "/api/setup/init", + {"username": "bench-admin", "password": "bench-admin-password"}) + if status != 200: + raise SystemExit("bench: setup failed: %s" % payload) + + status, payload = client.call("POST", "/api/proxies", { + "name": "bench", + "listenHost": "127.0.0.1", + "listenPort": args.proxy_port, + "target": "http://127.0.0.1:%d" % args.origin_port, + "start": True, + "isolated": args.isolated, + "rules": [], + }) + if status != 200 or not payload.get("proxies"): + raise SystemExit("bench: proxy creation failed: %s" % payload) + proxy_id = next( + (item.get("id") for item in payload["proxies"] + if item.get("name") == "bench"), None) + if not proxy_id: + raise SystemExit("bench: proxy id missing: %s" % payload) + if not wait_for_port("127.0.0.1", args.proxy_port, timeout=15): + raise SystemExit("bench: proxy did not listen\n" + + open(daemon_log, encoding="utf-8", + errors="replace").read()[-2000:]) + + direct = measure("127.0.0.1", args.origin_port, "/bench", + args.requests, args.warmup) + proxied = measure("127.0.0.1", args.proxy_port, "/bench", + args.requests, args.warmup) + + status, payload = client.call("GET", "/api/health") + version = payload.get("version", "") if isinstance(payload, dict) else "" + mode = "isolated child" if args.isolated else "in-process" + + if args.json: + print(json.dumps({ + "version": version, + "platform": platform.platform(), + "mode": mode, + "direct": direct, + "proxy": proxied, + }, indent=2)) + else: + print("# uvscope proxy benchmark") + print() + print("- version: %s" % (version or "unknown")) + print("- platform: %s" % platform.platform()) + print("- proxy: %s" % mode) + print("- requests: %d, warmup: %d, keep-alive, sequential" + % (args.requests, args.warmup)) + print() + print("| Path | req/s | mean ms | p50 ms | p95 ms | max ms |") + print("| --- | ---: | ---: | ---: | ---: | ---: |") + for label, result in (("direct origin", direct), + ("reverse proxy", proxied)): + print("| %s | %.1f | %.2f | %.2f | %.2f | %.2f |" % ( + label, result["throughput"], result["meanMs"], + result["p50Ms"], result["p95Ms"], result["maxMs"])) + print() + print("Compare runs on the same machine; absolute numbers include " + "the Python client.") + finally: + try: + daemon.terminate() + daemon.wait(timeout=10) + except Exception: + daemon.kill() + log_file.close() + origin.shutdown() + origin.server_close() + shutil.rmtree(data_dir, ignore_errors=True) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + sys.exit(130) diff --git a/scripts/bench.sh b/scripts/bench.sh new file mode 100644 index 0000000..8c9a999 --- /dev/null +++ b/scripts/bench.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Reverse-proxy latency/throughput reference run. See docs/PERFORMANCE.md. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PY="${PYTHON_BIN:-}" +if [[ -z "${PY}" ]]; then + if command -v python3 >/dev/null 2>&1; then + PY=python3 + elif command -v python >/dev/null 2>&1; then + PY=python + else + echo "bench: python3 (or python) is required" >&2 + exit 1 + fi +fi +exec "${PY}" "${ROOT}/scripts/bench.py" "$@" diff --git a/scripts/h2-smoke.sh b/scripts/h2-smoke.sh new file mode 100644 index 0000000..ab6364a --- /dev/null +++ b/scripts/h2-smoke.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# HTTP/2 over TLS runtime check used by CI on Linux, macOS and Windows (MSYS2). +# +# Usage: h2-smoke.sh [daemon-port] [h2-port] +# +# Starts a throwaway daemon and a local Node.js h2 server, then asks the +# daemon's HTTP probe for a real ALPN h2 request. Requires curl, openssl, node +# and python3 (or python on systems without the versioned name). +set -euo pipefail + +BIN="${1:-./build/uvscope}" +PORT="${2:-18820}" +H2_PORT="${3:-18821}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +PY="${PYTHON_BIN:-}" +if [[ -z "${PY}" ]]; then + if command -v python3 >/dev/null 2>&1; then + PY=python3 + elif command -v python >/dev/null 2>&1; then + PY=python + else + echo "h2-smoke: python3 (or python) is required" >&2 + exit 1 + fi +fi +OPENSSL="${OPENSSL_BIN:-openssl}" +if ! command -v "${OPENSSL}" >/dev/null 2>&1; then + echo "h2-smoke: openssl (or OPENSSL_BIN) is required" >&2 + exit 1 +fi +for tool in curl node; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo "h2-smoke: ${tool} is required" >&2 + exit 1 + fi +done + +DATA_DIR="$(mktemp -d "${TMPDIR:-/tmp}/uvscope-h2.XXXXXX")" +DATA_ARG="${DATA_DIR}" +if command -v cygpath >/dev/null 2>&1; then + DATA_ARG="$(cygpath -m "${DATA_DIR}")" +fi + +H2_PID="" +DAEMON_PID="" +cleanup() { + if [[ -n "${H2_PID}" ]] && kill -0 "${H2_PID}" 2>/dev/null; then + kill "${H2_PID}" 2>/dev/null || true + wait "${H2_PID}" 2>/dev/null || true + fi + if [[ -n "${DAEMON_PID}" ]] && kill -0 "${DAEMON_PID}" 2>/dev/null; then + kill "${DAEMON_PID}" 2>/dev/null || true + for _ in $(seq 1 30); do + kill -0 "${DAEMON_PID}" 2>/dev/null || break + sleep 0.1 + done + kill -9 "${DAEMON_PID}" 2>/dev/null || true + fi + rm -rf "${DATA_DIR}" +} +trap cleanup EXIT + +"${OPENSSL}" req -x509 -newkey rsa:2048 -nodes \ + -keyout "${DATA_DIR}/key.pem" -out "${DATA_DIR}/cert.pem" \ + -days 2 -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" >/dev/null 2>&1 + +H2_SERVER="${ROOT}/scripts/h2-test-server.mjs" +if command -v cygpath >/dev/null 2>&1; then + H2_SERVER="$(cygpath -m "${H2_SERVER}")" + H2_KEY="$(cygpath -m "${DATA_DIR}/key.pem")" + H2_CERT="$(cygpath -m "${DATA_DIR}/cert.pem")" +else + H2_KEY="${DATA_DIR}/key.pem" + H2_CERT="${DATA_DIR}/cert.pem" +fi +node "${H2_SERVER}" "${H2_KEY}" "${H2_CERT}" "${H2_PORT}" \ + >"${DATA_DIR}/h2.log" 2>&1 & +H2_PID=$! +for _ in $(seq 1 80); do + grep -q "H2 LISTENING" "${DATA_DIR}/h2.log" 2>/dev/null && break + sleep 0.1 +done +if ! grep -q "H2 LISTENING" "${DATA_DIR}/h2.log" 2>/dev/null; then + echo "h2-smoke: the local h2 server did not start" >&2 + cat "${DATA_DIR}/h2.log" >&2 + exit 1 +fi + +UVSCOPE_DATA_DIR="${DATA_ARG}" "${BIN}" --host 127.0.0.1 --port "${PORT}" \ + >"${DATA_DIR}/daemon.log" 2>&1 & +DAEMON_PID=$! +for _ in $(seq 1 100); do + if curl -fsS --max-time 1 "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then + break + fi + if ! kill -0 "${DAEMON_PID}" 2>/dev/null; then + echo "h2-smoke: the daemon exited during startup" >&2 + cat "${DATA_DIR}/daemon.log" >&2 + exit 1 + fi + sleep 0.1 +done +if ! curl -fsS --max-time 2 "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then + echo "h2-smoke: the daemon did not become ready" >&2 + cat "${DATA_DIR}/daemon.log" >&2 + exit 1 +fi + +JAR="${DATA_DIR}/cookies.txt" +curl -fsS -c "${JAR}" -X POST "http://127.0.0.1:${PORT}/api/setup/init" \ + -H 'content-type: application/json' \ + -d '{"username":"h2-smoke","password":"h2-smoke-password"}' >/dev/null + +RESULT="$(curl -fsS -b "${JAR}" -X POST "http://127.0.0.1:${PORT}/api/http/send" \ + -H 'content-type: application/json' \ + -d "{\"url\":\"https://127.0.0.1:${H2_PORT}/h2\",\"method\":\"GET\",\"protocol\":\"http2\",\"headers\":[],\"body\":\"\",\"timeoutMs\":8000,\"insecure\":true}")" + +printf '%s' "${RESULT}" | "${PY}" -c ' +import json +import sys + +result = json.load(sys.stdin) +if result.get("ok") is not True: + error = result.get("error") or "" + if "no HTTP/2 support" in error: + raise SystemExit("h2-smoke: this libuvcpp build has no HTTP/2 support") + raise SystemExit("h2-smoke: probe failed: %s" % (error or result,)) +version = result.get("httpVersion") +alpn = result.get("alpn") +body = result.get("bodyText") +if version != "HTTP/2": + raise SystemExit("h2-smoke: version was %r" % (version,)) +if alpn != "h2": + raise SystemExit("h2-smoke: ALPN was %r" % (alpn,)) +if body != "h2-ok": + raise SystemExit("h2-smoke: body was %r" % (body,)) +stream_id = int(result.get("streamId", 0)) +if stream_id <= 0: + raise SystemExit("h2-smoke: no stream id") +frames = (result.get("h2") or {}).get("frames", []) +types = {frame.get("type") for frame in frames} +if "HEADERS" not in types or "DATA" not in types: + raise SystemExit("h2-smoke: frame summary is incomplete: %r" % (sorted(types),)) +print("H2 smoke OK: HTTP/2, ALPN h2, stream", stream_id, "frames", len(frames)) +' diff --git a/web/package-lock.json b/web/package-lock.json index 4f96f86..34d7c79 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "uvscope-web", - "version": "0.11.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "uvscope-web", - "version": "0.11.0", + "version": "1.0.0", "dependencies": { "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", diff --git a/web/package.json b/web/package.json index 6091e95..8be24da 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "uvscope-web", "private": true, - "version": "0.11.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/web/src/i18n/help.ts b/web/src/i18n/help.ts index 6ca9b9f..be2ac5f 100644 --- a/web/src/i18n/help.ts +++ b/web/src/i18n/help.ts @@ -22,16 +22,16 @@ const en: Record = { 'Send any method, tune headers and body, and read status, timing and raw bytes. Ctrl+Enter sends from anywhere in the request pane.', 'help.modules.websocket.title': 'WebSocket console', 'help.modules.websocket.text': - 'Connects from the browser, so the URL must be reachable by this machine. Text and hex frames are captured in the tab while it is cached.', + 'Use a daemon-side backend session to send handshake headers, subprotocols and ping/pong frames, or switch to browser-native mode when origin, mixed-content or certificate prompts are part of the test.', 'help.modules.listeners.title': 'TCP / UDP servers', 'help.modules.listeners.text': 'Bind a local TCP or UDP port, inspect accepted sessions or datagrams, and send replies from the workbench. Listener configs persist and can auto-start with the daemon.', 'help.modules.terminal.title': 'Virtual terminal', 'help.modules.terminal.text': - 'A real server-side PTY per user. Sessions keep running when you refresh or switch menus; use Ctrl+Shift+C/V for copy and paste. The quick bar and command library provide searchable Linux/Windows snippets that can be pinned, edited or extended. Admins can list and close every user terminal. The module is off by default because it grants shell access.', + 'A real server-side PTY per user. Sessions keep running when you refresh or switch menus; use Ctrl+Shift+C/V for copy and paste. “Run the shell in a child process” isolates the PTY; a crashed child is restarted with a fresh shell and the output stream resets. The quick bar and command library provide searchable Linux/Windows snippets that can be pinned, edited or extended. Admins can list and close every user terminal. The module is off by default because it grants shell access.', 'help.modules.socks5.title': 'SOCKS5 server', 'help.modules.socks5.text': - 'Run a local SOCKS5 proxy with optional username/password authentication. Each server has its own loop; sessions show the command, target, peer and rejection reason. Byte counters are not available from the runtime, so none are shown.', + 'Run a local SOCKS5 proxy with optional username/password authentication. Each server has its own loop; sessions show the command, target, peer, byte counters, datagrams and rejection reason. Servers can run in a supervised child process.', 'help.modules.tcpudp.title': 'TCP / UDP bench', 'help.modules.tcpudp.text': 'One-shot probes through libuvcpp, plus persistent TCP sessions with repeated sends and a per-session event log. Hex mode accepts spaced, colon-separated or plain hex payloads.', @@ -46,7 +46,7 @@ const en: Record = { 'Decodes a PEM bundle certificate by certificate, or one DER certificate. You can also fetch an https:// URL directly to capture the live peer chain, ALPN and verification result, and manage server certificates: generate self-signed or local-CA-signed RSA/ECDSA certificates with SANs, import a certificate/key pair for the reverse proxy, and renew generated certificates in place. Install the local root certificate on clients that should trust your proxies.', 'help.modules.proxy.title': 'Reverse proxy', 'help.modules.proxy.text': - 'Each listener owns a host:port and forwards HTTP/1.1 requests to one upstream. Ordered exact/prefix/regex rules can redirect or rewrite paths before forwarding; logs stream into the gateway panel every few seconds.', + 'Each proxy owns a host:port and forwards HTTP requests to a target or a weighted upstream pool, with retries, health checks, a circuit breaker and WebSocket pass-through. Ordered exact/prefix/regex rules can redirect or rewrite paths first. Proxies can run in a supervised child process; logs and metrics stream into the gateway panel.', 'help.keyboard': 'Keyboard', 'help.shortcuts': 'Shortcuts', @@ -62,7 +62,11 @@ const en: Record = { 'help.note.sessions': 'Sessions', 'help.note.sessionsText': 'HttpOnly cookie, seven-day expiry, server-side lookup.', 'help.note.network': 'Network', - 'help.note.networkText': 'All requests originate from the uvscope daemon, not the browser.', + 'help.note.networkText': + 'Probes and backend sessions originate from the uvscope daemon; the WebSocket view also offers a browser-native mode.', + 'help.note.isolation': 'Isolation', + 'help.note.isolationText': + 'Proxies, listeners, SOCKS5 servers and terminals can run in supervised child processes. A crashed child restarts with a new PID; stopping a service clears its desired state.', 'help.note.preview': 'Preview', 'help.note.previewText': 'Sandboxed HTML never runs scripts. Remote images and media load only after you tick the option.', @@ -121,16 +125,16 @@ const zh: Record = { '发送任意方法,调整 Header 与请求体,查看状态、耗时与原始字节。在请求面板任意位置按 Ctrl+Enter 即可发送。', 'help.modules.websocket.title': 'WebSocket 控制台', 'help.modules.websocket.text': - '由浏览器发起连接,因此 URL 必须能从本机访问。标签页缓存期间会捕获文本与十六进制帧。', + '使用守护进程侧后端会话可自定义握手 Header、子协议并发送 ping/pong;也可切换到浏览器原生模式,用于验证 Origin、混合内容或证书提示。', 'help.modules.listeners.title': 'TCP / UDP 服务端', 'help.modules.listeners.text': '绑定本地 TCP 或 UDP 端口,检查接入会话或数据报,并在工作台内直接回复。监听配置会持久化,并随守护进程自动启动。', 'help.modules.terminal.title': '虚拟终端', 'help.modules.terminal.text': - '每个用户一个真实的服务端 PTY。刷新或切换菜单不会终止会话;复制粘贴使用 Ctrl+Shift+C/V。快捷栏与命令库提供可搜索的 Linux/Windows 命令,可固定、编辑或扩展。管理员可列出并关闭所有用户的终端。该模块默认关闭,因为开启等于授予 Shell 权限。', + '每个用户一个真实的服务端 PTY。刷新或切换菜单不会终止会话;复制粘贴使用 Ctrl+Shift+C/V。勾选“在子进程中运行 Shell”可隔离 PTY;子进程崩溃后会自动拉起新的 Shell,输出流随之重置。快捷栏与命令库提供可搜索的 Linux/Windows 命令,可固定、编辑或扩展。管理员可列出并关闭所有用户的终端。该模块默认关闭,因为开启等于授予 Shell 权限。', 'help.modules.socks5.title': 'SOCKS5 服务端', 'help.modules.socks5.text': - '运行本地 SOCKS5 代理,可选用户名密码认证。每个服务端独占一条事件循环;会话显示命令、目标、来源与拒绝原因。运行时没有字节计数,因此界面不显示。', + '运行本地 SOCKS5 代理,可选用户名密码认证。每个服务端独占一条事件循环;会话显示命令、目标、来源、字节计数、数据报与拒绝原因。服务端可在受监督的子进程中运行。', 'help.modules.tcpudp.title': 'TCP / UDP 调试台', 'help.modules.tcpudp.text': '通过 libuvcpp 执行一次性探测,并支持可连续发送、带逐会话事件日志的持久 TCP 会话。十六进制模式支持带空格、冒号分隔或纯十六进制负载。', @@ -145,7 +149,7 @@ const zh: Record = { '逐张解析 PEM 证书包,或解析单张 DER 证书。也可以直接抓取 https:// 地址,获取实时证书链、ALPN 与校验结果,并管理服务端证书:生成带 SAN 的自签名或本地 CA 签发的 RSA/ECDSA 证书、导入证书/私钥供反向代理使用,以及原地续期已生成证书。需要信任本地代理的客户端应安装本地根证书。', 'help.modules.proxy.title': '反向代理', 'help.modules.proxy.text': - '每个监听器占用一个 host:port,并将 HTTP/1.1 请求转发到一个上游。按顺序执行的精确/前缀/正则规则可以在转发前跳转或改写路径;日志每隔几秒流入网关面板。', + '每个代理占用一个 host:port,将 HTTP 请求转发到目标或加权上游池,并支持重试、健康检查、熔断与 WebSocket 透传。按顺序执行的精确/前缀/正则规则可以在转发前跳转或改写路径。代理可在受监督的子进程中运行;日志与指标持续流入网关面板。', 'help.keyboard': '键盘', 'help.shortcuts': '快捷键', @@ -161,7 +165,11 @@ const zh: Record = { 'help.note.sessions': '会话', 'help.note.sessionsText': 'HttpOnly Cookie,七天过期,服务端查询。', 'help.note.network': '网络', - 'help.note.networkText': '所有请求均由 uvscope 守护进程发起,而非浏览器。', + 'help.note.networkText': + '探测与后端会话由 uvscope 守护进程发起;WebSocket 视图另提供浏览器原生模式。', + 'help.note.isolation': '隔离', + 'help.note.isolationText': + '反向代理、监听器、SOCKS5 服务端与终端可在受监督的子进程中运行。子进程崩溃后会以新 PID 重启;停止服务会清除期望状态。', 'help.note.preview': '预览', 'help.note.previewText': '沙箱 HTML 从不执行脚本。远程图片与媒体仅在勾选选项后加载。', 'help.note.language': '语言与主题', diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts index 9a8a173..234bbd1 100644 --- a/web/src/stores/auth.ts +++ b/web/src/stores/auth.ts @@ -21,7 +21,7 @@ export const useAuthStore = defineStore('auth', () => { const setupRequired = ref(false) const initialized = ref(false) const loading = ref(false) - const version = ref('0.11.0') + const version = ref('1.0.0') const libuvcpp = ref('') const dataDir = ref('') const statusError = ref('') diff --git a/web/src/views/HelpView.vue b/web/src/views/HelpView.vue index c547f78..22a63a3 100644 --- a/web/src/views/HelpView.vue +++ b/web/src/views/HelpView.vue @@ -88,6 +88,7 @@ const shortcuts = [
{{ t('help.note.storage') }}{{ t('help.note.storageText') }}
{{ t('help.note.sessions') }}{{ t('help.note.sessionsText') }}
{{ t('help.note.network') }}{{ t('help.note.networkText') }}
+
{{ t('help.note.isolation') }}{{ t('help.note.isolationText') }}
{{ t('help.note.preview') }}{{ t('help.note.previewText') }}
{{ t('help.note.language') }}{{ t('help.note.languageText') }}
{{ t('help.note.exposure') }}{{ t('help.note.exposureText') }}
From eb1423bb1d8b9b0a77e428d3fec9221bf5d406ac Mon Sep 17 00:00:00 2001 From: Antruly Date: Sun, 25 Oct 2026 03:28:09 +0800 Subject: [PATCH 2/7] fix(ci): keep the H2 smoke POSIX-portable for macOS macOS does not ship seq(1); use arithmetic while loops so the macOS CI and release jobs can run the script. --- scripts/h2-smoke.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/h2-smoke.sh b/scripts/h2-smoke.sh index ab6364a..f40e6aa 100644 --- a/scripts/h2-smoke.sh +++ b/scripts/h2-smoke.sh @@ -51,9 +51,11 @@ cleanup() { fi if [[ -n "${DAEMON_PID}" ]] && kill -0 "${DAEMON_PID}" 2>/dev/null; then kill "${DAEMON_PID}" 2>/dev/null || true - for _ in $(seq 1 30); do + local i=0 + while [[ "${i}" -lt 30 ]]; do kill -0 "${DAEMON_PID}" 2>/dev/null || break sleep 0.1 + i=$((i + 1)) done kill -9 "${DAEMON_PID}" 2>/dev/null || true fi @@ -78,9 +80,11 @@ fi node "${H2_SERVER}" "${H2_KEY}" "${H2_CERT}" "${H2_PORT}" \ >"${DATA_DIR}/h2.log" 2>&1 & H2_PID=$! -for _ in $(seq 1 80); do +i=0 +while [[ "${i}" -lt 80 ]]; do grep -q "H2 LISTENING" "${DATA_DIR}/h2.log" 2>/dev/null && break sleep 0.1 + i=$((i + 1)) done if ! grep -q "H2 LISTENING" "${DATA_DIR}/h2.log" 2>/dev/null; then echo "h2-smoke: the local h2 server did not start" >&2 @@ -91,7 +95,8 @@ fi UVSCOPE_DATA_DIR="${DATA_ARG}" "${BIN}" --host 127.0.0.1 --port "${PORT}" \ >"${DATA_DIR}/daemon.log" 2>&1 & DAEMON_PID=$! -for _ in $(seq 1 100); do +i=0 +while [[ "${i}" -lt 100 ]]; do if curl -fsS --max-time 1 "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then break fi @@ -101,6 +106,7 @@ for _ in $(seq 1 100); do exit 1 fi sleep 0.1 + i=$((i + 1)) done if ! curl -fsS --max-time 2 "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then echo "h2-smoke: the daemon did not become ready" >&2 From 36676f14386a019f25c678b858bb4c1b7df53c52 Mon Sep 17 00:00:00 2001 From: Antruly Date: Sun, 25 Oct 2026 03:28:22 +0800 Subject: [PATCH 3/7] docs(site): render Markdown inside the landing-page grid --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index f49f97d..79bae40 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,7 @@ markdown_extensions: - admonition - attr_list - def_list + - md_in_html - tables - toc: permalink: true From 8d9da53301bcc309d67101157c129bb5b37f915f Mon Sep 17 00:00:00 2001 From: Antruly Date: Sun, 25 Oct 2026 03:29:38 +0800 Subject: [PATCH 4/7] docs(events): match listener session-closed fields to the implementation --- docs/EVENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/EVENTS.md b/docs/EVENTS.md index 05c32a4..0453f08 100644 --- a/docs/EVENTS.md +++ b/docs/EVENTS.md @@ -88,7 +88,7 @@ original byte count; `truncated` means the retained sample is shorter than `leng | `stopped` | — | | `error` | `error` | | `accepted` | `sessionId`, `peer` | -| `session-closed` | `sessionId`, `peer`, `reason` | +| `session-closed` | `sessionId` (see the per-session `closed`/`error` event for the reason) | | `datagram` | `peer`, `partial`, `payload` | Per-session events (`.../sessions/:sid/events`) use the same envelope: From 4b26f559e244b2f87fa6bbc005e8a3946fe4d927 Mon Sep 17 00:00:00 2001 From: Antruly Date: Sun, 25 Oct 2026 03:30:00 +0800 Subject: [PATCH 5/7] ci(release): sanitize the SBOM name for branch dry-runs --- .github/workflows/release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1a89de..47e8752 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -240,12 +240,17 @@ jobs: with: name: macos-arm64 path: artifacts/macos-arm64 + - name: Version label + id: version + # A branch dispatch has slashes in ref_name; a tag does not. Keep the + # SBOM path valid for both. + run: echo "name=${GITHUB_REF_NAME//\//-}" >> "$GITHUB_OUTPUT" - name: Generate SBOM uses: anchore/sbom-action@v0 with: path: . format: spdx-json - output-file: artifacts/uvscope-${{ github.ref_name }}-sbom.spdx.json + output-file: artifacts/uvscope-${{ steps.version.outputs.name }}-sbom.spdx.json upload-artifact: false upload-release-assets: false - uses: sigstore/cosign-installer@v3 From 405f57220f60409cdedd4ed9773e135521f804da Mon Sep 17 00:00:00 2001 From: Antruly Date: Sun, 25 Oct 2026 03:31:46 +0800 Subject: [PATCH 6/7] ci(windows): install Node from the UCRT64 repo for the H2 smoke The MINGW64 pacman repo has no nodejs package, so the job failed at setup. Install mingw-w64-ucrt-x86_64-nodejs and put /ucrt64/bin on PATH for the HTTP/2 runtime check. --- .github/workflows/ci.yml | 9 +++++---- .github/workflows/release.yml | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01ba156..8f730a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,7 @@ jobs: mingw-w64-x86_64-zlib curl openssl - nodejs + mingw-w64-ucrt-x86_64-nodejs python - name: Build libuvcpp and uvscope shell: msys2 {0} @@ -251,9 +251,10 @@ jobs: shell: msys2 {0} run: | set -e - # Prefer the MSYS toolchain here: its curl/openssl/node understand - # POSIX paths, unlike the native MinGW binaries earlier on PATH. - export PATH="/usr/bin:$PWD/.deps/libuvcpp/bin:$MINGW_PREFIX/bin:$PATH" + # Prefer the MSYS tools here: curl/openssl understand POSIX paths. + # Node comes from the UCRT64 repo (the MINGW64 repo has no nodejs + # package); the script converts the paths it passes to Windows form. + export PATH="/usr/bin:/ucrt64/bin:$PWD/.deps/libuvcpp/bin:$MINGW_PREFIX/bin:$PATH" bash ./scripts/h2-smoke.sh ./build-mingw/uvscope.exe 18820 18821 macos: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47e8752..51c278f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -107,7 +107,7 @@ jobs: mingw-w64-x86_64-curl curl openssl - nodejs + mingw-w64-ucrt-x86_64-nodejs - name: Build and stage shell: msys2 {0} run: | @@ -125,8 +125,9 @@ jobs: cp -f "$MINGW_PREFIX/bin/"*.dll build-mingw/ 2>/dev/null || true find .deps/libuvcpp-src/build-uvscope -name 'libllhttp.dll' -type f -exec cp -f {} build-mingw/ \; 2>/dev/null || true timeout 90 ./build-mingw/uvscope.exe --self-check - # Same HTTP/2 runtime check as CI, using the MSYS tools. - PATH="/usr/bin:$PWD/.deps/libuvcpp/bin:$MINGW_PREFIX/bin:$PATH" \ + # Same HTTP/2 runtime check as CI. Node comes from the UCRT64 repo; + # curl/openssl come from MSYS and understand POSIX paths. + PATH="/usr/bin:/ucrt64/bin:$PWD/.deps/libuvcpp/bin:$MINGW_PREFIX/bin:$PATH" \ bash ./scripts/h2-smoke.sh ./build-mingw/uvscope.exe 18820 18821 cmake --install build-mingw --prefix "$(cygpath -m "$PWD/stage")" mkdir -p stage/bin From 286533e69523297e9916dcbeba5b651b96dbe840 Mon Sep 17 00:00:00 2001 From: Antruly Date: Sun, 25 Oct 2026 03:40:27 +0800 Subject: [PATCH 7/7] ci(release): sanitize package labels for branch dry-runs The dry run used the branch name in tar/zip paths, which contains a slash. Tags are unaffected (v1.0.0 -> 1.0.0), but the sanitized label lets the dry run validate the full packaging and supply-chain pipeline. --- .github/workflows/release.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51c278f..8b3626a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,11 +63,18 @@ jobs: cmake --build build -j"$(nproc)" ./build/uvscope --self-check bash ./scripts/smoke-test.sh ./build/uvscope 18787 18788 + - name: Resolve version + id: version + # Tag: v1.0.0 -> 1.0.0. Branch dispatch: chore/release-1.0.0 -> + # chore-release-1.0.0, so a dry run cannot create slashed paths. + run: | + label="${GITHUB_REF_NAME#v}" + echo "label=${label//\//-}" >> "$GITHUB_OUTPUT" - name: Package run: | mkdir -p packages stage cmake --install build --prefix "$PWD/stage" - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${{ steps.version.outputs.label }}" tar -C stage -czf "packages/uvscope-${VERSION}-linux-${{ matrix.arch }}.tar.gz" . cpack --config build/CPackConfig.cmake -G DEB -B packages || true cpack --config build/CPackConfig.cmake -G RPM -B packages || true @@ -139,11 +146,17 @@ jobs: done curl -fsSL https://curl.se/ca/cacert.pem -o stage/bin/cacert.pem || \ cp "$MINGW_PREFIX/ssl/certs/ca-bundle.crt" stage/bin/cacert.pem || true + - name: Resolve version + id: version + shell: pwsh + run: | + $name = "${{ github.ref_name }}".TrimStart('v').Replace('/', '-') + "label=$name" >> $env:GITHUB_OUTPUT - name: Create zip shell: pwsh run: | New-Item -ItemType Directory -Force packages | Out-Null - $name = "${{ github.ref_name }}".TrimStart('v') + $name = "${{ steps.version.outputs.label }}" Compress-Archive -Path stage\* -DestinationPath "packages\uvscope-$name-mingw-x64.zip" -Force - name: Install Inno Setup shell: pwsh @@ -189,6 +202,11 @@ jobs: ./build/uvscope --self-check OPENSSL_BIN="$(brew --prefix openssl@3)/bin/openssl" \ bash ./scripts/h2-smoke.sh ./build/uvscope 18820 18821 + - name: Resolve version + id: version + run: | + label="${GITHUB_REF_NAME#v}" + echo "label=${label//\//-}" >> "$GITHUB_OUTPUT" - name: Bundle and package run: | mkdir -p packages stage @@ -198,7 +216,7 @@ jobs: # diagnostics and ad-hoc sign the executable. otool -L stage/bin/uvscope || true codesign --force --deep -s - stage/bin/uvscope || true - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${{ steps.version.outputs.label }}" tar -C stage -czf "packages/uvscope-${VERSION}-macos-arm64.tar.gz" . ls -lh packages - uses: actions/upload-artifact@v4